Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-python-multipart for
openSUSE:Factory checked in at 2026-05-16 19:24:41
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-python-multipart (Old)
and /work/SRC/openSUSE:Factory/.python-python-multipart.new.1966 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-python-multipart"
Sat May 16 19:24:41 2026 rev:15 rq:1353179 version:0.0.28
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-python-multipart/python-python-multipart.changes
2026-04-21 12:42:16.648087570 +0200
+++
/work/SRC/openSUSE:Factory/.python-python-multipart.new.1966/python-python-multipart.changes
2026-05-16 19:25:46.605112193 +0200
@@ -1,0 +2,10 @@
+Thu May 14 12:49:47 UTC 2026 - Daniel Garcia <[email protected]>
+
+- Update to 0.0.28 (CVE-2026-42561, bsc#1265250):
+ * Speed up partial-boundary tail scan via bytes.find by @Kludex in #281
+ * Cap multipart boundary length at 256 bytes by @Kludex in #282
+- 0.0.27:
+ * Pass parse offsets via constructors by @Kludex in #268
+ * Add multipart header limits by @Kludex in #267
+
+-------------------------------------------------------------------
Old:
----
python_multipart-0.0.26.tar.gz
New:
----
python_multipart-0.0.28.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-python-multipart.spec ++++++
--- /var/tmp/diff_new_pack.vTIDfU/_old 2026-05-16 19:25:47.141134172 +0200
+++ /var/tmp/diff_new_pack.vTIDfU/_new 2026-05-16 19:25:47.145134335 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-python-multipart
-Version: 0.0.26
+Version: 0.0.28
Release: 0
License: Apache-2.0
Summary: Python streaming multipart parser
++++++ python_multipart-0.0.26.tar.gz -> python_multipart-0.0.28.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_multipart-0.0.26/CHANGELOG.md
new/python_multipart-0.0.28/CHANGELOG.md
--- old/python_multipart-0.0.26/CHANGELOG.md 2020-02-02 01:00:00.000000000
+0100
+++ new/python_multipart-0.0.28/CHANGELOG.md 2020-02-02 01:00:00.000000000
+0100
@@ -1,5 +1,15 @@
# Changelog
+## 0.0.28 (2026-05-10)
+
+* Speed up partial-boundary tail scan via `bytes.find`
[#281](https://github.com/Kludex/python-multipart/pull/281).
+* Cap multipart boundary length at 256 bytes
[#282](https://github.com/Kludex/python-multipart/pull/282).
+
+## 0.0.27 (2026-04-27)
+
+* Add multipart header limits
[#267](https://github.com/Kludex/python-multipart/pull/267).
+* Pass parse offsets via constructors
[#268](https://github.com/Kludex/python-multipart/pull/268).
+
## 0.0.26 (2026-04-10)
* Skip preamble before the first multipart boundary more efficiently
[#262](https://github.com/Kludex/python-multipart/pull/262).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_multipart-0.0.26/PKG-INFO
new/python_multipart-0.0.28/PKG-INFO
--- old/python_multipart-0.0.26/PKG-INFO 2020-02-02 01:00:00.000000000
+0100
+++ new/python_multipart-0.0.28/PKG-INFO 2020-02-02 01:00:00.000000000
+0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: python-multipart
-Version: 0.0.26
+Version: 0.0.28
Summary: A streaming multipart parser for Python
Project-URL: Homepage, https://github.com/Kludex/python-multipart
Project-URL: Documentation, https://kludex.github.io/python-multipart/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_multipart-0.0.26/pyproject.toml
new/python_multipart-0.0.28/pyproject.toml
--- old/python_multipart-0.0.26/pyproject.toml 2020-02-02 01:00:00.000000000
+0100
+++ new/python_multipart-0.0.28/pyproject.toml 2020-02-02 01:00:00.000000000
+0100
@@ -37,16 +37,17 @@
"atomicwrites==1.4.1",
"attrs==26.1.0",
"coverage==7.13.5",
- "more-itertools==11.0.1",
+ "more-itertools==11.0.2",
"pbr==7.0.3",
"pluggy==1.6.0",
"py==1.11.0",
- "pytest==9.0.2",
+ "pytest==9.0.3",
"pytest-cov==7.1.0",
+ "pytest-codspeed>=4.1.1",
"PyYAML==6.0.3",
- "invoke==2.2.1",
+ "invoke==3.0.3",
"pytest-timeout==2.4.0",
- "ruff==0.15.9",
+ "ruff==0.15.11",
"mypy",
"types-PyYAML",
"atheris==2.3.0; python_version <= '3.11'",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_multipart-0.0.26/python_multipart/__init__.py
new/python_multipart-0.0.28/python_multipart/__init__.py
--- old/python_multipart-0.0.26/python_multipart/__init__.py 2020-02-02
01:00:00.000000000 +0100
+++ new/python_multipart-0.0.28/python_multipart/__init__.py 2020-02-02
01:00:00.000000000 +0100
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-__version__ = "0.0.26"
+__version__ = "0.0.28"
from .multipart import (
BaseParser,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_multipart-0.0.26/python_multipart/exceptions.py
new/python_multipart-0.0.28/python_multipart/exceptions.py
--- old/python_multipart-0.0.26/python_multipart/exceptions.py 2020-02-02
01:00:00.000000000 +0100
+++ new/python_multipart-0.0.28/python_multipart/exceptions.py 2020-02-02
01:00:00.000000000 +0100
@@ -7,9 +7,9 @@
parsing something.
"""
- #: This is the offset in the input data chunk (*NOT* the overall stream) in
- #: which the parse error occurred. It will be -1 if not specified.
- offset = -1
+ def __init__(self, message: str, *, offset: int = -1) -> None:
+ super().__init__(message)
+ self.offset = offset
class MultipartParseError(ParseError):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/python_multipart-0.0.26/python_multipart/multipart.py
new/python_multipart-0.0.28/python_multipart/multipart.py
--- old/python_multipart-0.0.26/python_multipart/multipart.py 2020-02-02
01:00:00.000000000 +0100
+++ new/python_multipart-0.0.28/python_multipart/multipart.py 2020-02-02
01:00:00.000000000 +0100
@@ -54,6 +54,8 @@
class FormParserConfig(FileConfig):
UPLOAD_ERROR_ON_BAD_CTE: bool
MAX_BODY_SIZE: float
+ MAX_HEADER_COUNT: int
+ MAX_HEADER_SIZE: int
CallbackName: TypeAlias = Literal[
"start",
@@ -140,6 +142,20 @@
b"!#$%&'*+-.^_`|~")
# fmt: on
+DEFAULT_MAX_HEADER_COUNT = 8
+"""Default maximum number of headers allowed per multipart part."""
+
+DEFAULT_MAX_HEADER_SIZE = 4096 + 128
+"""Default maximum size of a single multipart header line, including syntax
overhead."""
+
+MAX_BOUNDARY_LENGTH = 256
+"""Maximum allowed length of a multipart boundary.
+
+[RFC 2046 ยง5.1.1](https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.1)
+recommends boundaries be at most 70 bytes. 256 bytes is generous headroom over
+every HTTP client.
+"""
+
def parse_options_header(value: str | bytes | None) -> tuple[bytes,
dict[bytes, bytes]]:
"""Parses a Content-Type header into a value in the following format:
(content_type, {parameters})."""
@@ -820,9 +836,7 @@
if found_sep:
# If we're parsing strictly, we disallow blank chunks.
if strict_parsing:
- e = QuerystringParseError("Skipping duplicate
ampersand/semicolon at %d" % i)
- e.offset = i
- raise e
+ raise QuerystringParseError("Skipping duplicate
ampersand/semicolon at %d" % i, offset=i)
else:
self.logger.debug("Skipping duplicate
ampersand/semicolon at %d", i)
else:
@@ -886,13 +900,12 @@
# We're parsing strictly. If we find a separator,
# this is an error - we require an equals sign.
if sep_pos != -1:
- e = QuerystringParseError(
+ raise QuerystringParseError(
"When strict_parsing is True, we require an "
"equals sign in all field chunks. Did not "
- "find one in the chunk that starts at %d" %
(i,)
+ "find one in the chunk that starts at %d" %
(i,),
+ offset=i,
)
- e.offset = i
- raise e
# No separator in the rest of this chunk, so it's just
# a field name.
@@ -927,9 +940,7 @@
else: # pragma: no cover (error case)
msg = "Reached an unknown state %d at %d" % (state, i)
self.logger.warning(msg)
- e = QuerystringParseError(msg)
- e.offset = i
- raise e
+ raise QuerystringParseError(msg, offset=i)
i += 1
@@ -972,10 +983,18 @@
boundary: The multipart boundary. This is required, and must match
what is given in the HTTP request - usually in the Content-Type header.
callbacks: A dictionary of callbacks. See the documentation for
[`BaseParser`][python_multipart.BaseParser].
max_size: The maximum size of body to parse. Defaults to infinity -
i.e. unbounded.
+ max_header_count: The maximum number of headers allowed per part.
+ max_header_size: The maximum size of a single header line (excluding
the trailing CRLF).
""" # noqa: E501
def __init__(
- self, boundary: bytes | str, callbacks: MultipartCallbacks = {},
max_size: float = float("inf")
+ self,
+ boundary: bytes | str,
+ callbacks: MultipartCallbacks = {},
+ max_size: float = float("inf"),
+ *,
+ max_header_count: int = DEFAULT_MAX_HEADER_COUNT,
+ max_header_size: int = DEFAULT_MAX_HEADER_SIZE,
) -> None:
# Initialize parser state.
super().__init__()
@@ -989,12 +1008,20 @@
self.max_size = max_size
self._current_size = 0
+ self.max_header_count = max_header_count
+ self._current_header_count = 0
+
+ self.max_header_size = max_header_size
+ self._current_header_size = 0
+
# Setup marks. These are used to track the state of data received.
self.marks: dict[str, int] = {}
# Save our boundary.
if isinstance(boundary, str): # pragma: no cover
boundary = boundary.encode("latin-1")
+ if len(boundary) > MAX_BOUNDARY_LENGTH:
+ raise FormParserError(f"Boundary length {len(boundary)} exceeds
maximum of {MAX_BOUNDARY_LENGTH}")
self.boundary = b"\r\n--" + boundary
def write(self, data: bytes) -> int:
@@ -1042,10 +1069,18 @@
state = self.state
index = self.index
flags = self.flags
+ current_header_count = self._current_header_count
+ current_header_size = self._current_header_size
# Our index defaults to 0.
i = 0
+ def advance_header_size(amount: int = 1) -> None:
+ nonlocal current_header_size
+ current_header_size += amount
+ if current_header_size > self.max_header_size:
+ raise MultipartParseError("Maximum header size exceeded",
offset=i)
+
# Set a mark.
def set_mark(name: str) -> None:
self.marks[name] = i
@@ -1131,9 +1166,7 @@
# Error!
msg = "Did not find CR at end of boundary (%d)" % (i,)
self.logger.warning(msg)
- e = MultipartParseError(msg)
- e.offset = i
- raise e
+ raise MultipartParseError(msg, offset=i)
index += 1
@@ -1141,15 +1174,15 @@
if c != LF:
msg = "Did not find LF at end of boundary (%d)" % (i,)
self.logger.warning(msg)
- e = MultipartParseError(msg)
- e.offset = i
- raise e
+ raise MultipartParseError(msg, offset=i)
# The index is now used for indexing into our boundary.
index = 0
# Callback for the start of a part.
self.callback("part_begin")
+ current_header_count = 0
+ current_header_size = 0
# Move to the next character and state.
state = MultipartState.HEADER_FIELD_START
@@ -1159,9 +1192,7 @@
if c != boundary[index + 2]:
msg = "Expected boundary character %r, got %r at index
%d" % (boundary[index + 2], c, index + 2)
self.logger.warning(msg)
- e = MultipartParseError(msg)
- e.offset = i
- raise e
+ raise MultipartParseError(msg, offset=i)
# Increment index into boundary and continue.
index += 1
@@ -1171,6 +1202,12 @@
# continue parsing our header field.
index = 0
+ if c != CR:
+ current_header_count += 1
+ if current_header_count > self.max_header_count:
+ raise MultipartParseError("Maximum header count
exceeded", offset=i)
+ current_header_size = 0
+
# Set a mark of our header field.
set_mark("header_field")
@@ -1200,13 +1237,12 @@
# If we've reached a colon, we're done with this header.
if c == COLON:
+ advance_header_size()
# A 0-length header is an error.
if index == 1:
msg = "Found 0-length header at %d" % (i,)
self.logger.warning(msg)
- e = MultipartParseError(msg)
- e.offset = i
- raise e
+ raise MultipartParseError(msg, offset=i)
# Call our callback with the header field.
data_callback("header_field", i)
@@ -1217,13 +1253,14 @@
elif c not in TOKEN_CHARS_SET:
msg = "Found invalid character %r in header at %d" % (c, i)
self.logger.warning(msg)
- e = MultipartParseError(msg)
- e.offset = i
- raise e
+ raise MultipartParseError(msg, offset=i)
+ else:
+ advance_header_size()
elif state == MultipartState.HEADER_VALUE_START:
# Skip leading spaces.
if c == SPACE:
+ advance_header_size()
i += 1
continue
@@ -1240,16 +1277,17 @@
if c == CR:
data_callback("header_value", i)
self.callback("header_end")
+ current_header_size = 0
state = MultipartState.HEADER_VALUE_ALMOST_DONE
+ else:
+ advance_header_size()
elif state == MultipartState.HEADER_VALUE_ALMOST_DONE:
# The last character should be a LF. If not, it's an error.
if c != LF:
msg = f"Did not find LF character at end of header (found
{c!r})"
self.logger.warning(msg)
- e = MultipartParseError(msg)
- e.offset = i
- raise e
+ raise MultipartParseError(msg, offset=i)
# Move back to the start of another header. Note that if that
# state detects ANOTHER newline, it'll trigger the end of our
@@ -1263,9 +1301,7 @@
if c != LF:
msg = f"Did not find LF at end of headers (found {c!r})"
self.logger.warning(msg)
- e = MultipartParseError(msg)
- e.offset = i
- raise e
+ raise MultipartParseError(msg, offset=i)
self.callback("headers_finished")
state = MultipartState.PART_DATA_START
@@ -1306,14 +1342,12 @@
# No match found for whole string.
# There may be a partial boundary at the end of the
# data, which the find will not match.
- # Since the length should to be searched is limited to
- # the boundary length, just perform a naive search.
+ # Since the length to be searched is limited to the
+ # boundary length, scan the tail for boundary[0] via
+ # bytes.find (C-level) to keep cost off the Python
loop.
i = max(i, data_length - boundary_length)
-
- # Search forward until we either hit the end of our
buffer,
- # or reach a potential start of the boundary.
- while i < data_length - 1 and data[i] != boundary[0]:
- i += 1
+ j = data.find(boundary[:1], i, data_length - 1)
+ i = j if j >= 0 else data_length - 1
c = data[i]
@@ -1364,6 +1398,8 @@
# a part, and are starting a new one.
self.callback("part_end")
self.callback("part_begin")
+ current_header_count = 0
+ current_header_size = 0
# Move to parsing new headers.
index = 0
@@ -1409,9 +1445,7 @@
if c != HYPHEN:
msg = "Did not find - at end of boundary (%d)" % (i,)
self.logger.warning(msg)
- e = MultipartParseError(msg)
- e.offset = i
- raise e
+ raise MultipartParseError(msg, offset=i)
index += 1
self.callback("end")
state = MultipartState.END
@@ -1426,9 +1460,7 @@
# We got into a strange state somehow! Just stop processing.
msg = "Reached an unknown state %d at %d" % (state, i)
self.logger.warning(msg)
- e = MultipartParseError(msg)
- e.offset = i
- raise e
+ raise MultipartParseError(msg, offset=i)
# Move to the next byte.
i += 1
@@ -1449,6 +1481,8 @@
self.state = state
self.index = index
self.flags = flags
+ self._current_header_count = current_header_count
+ self._current_header_size = current_header_size
# Return our data length to indicate no errors, and that we processed
# all of it.
@@ -1494,6 +1528,8 @@
#: Note: all file sizes should be in bytes.
DEFAULT_CONFIG: FormParserConfig = {
"MAX_BODY_SIZE": float("inf"),
+ "MAX_HEADER_COUNT": DEFAULT_MAX_HEADER_COUNT,
+ "MAX_HEADER_SIZE": DEFAULT_MAX_HEADER_SIZE,
"MAX_MEMORY_FILE_SIZE": 1 * 1024 * 1024,
"UPLOAD_DIR": None,
"UPLOAD_DELETE_TMP": True,
@@ -1733,6 +1769,8 @@
"on_end": _on_end,
},
max_size=self.config["MAX_BODY_SIZE"],
+ max_header_count=self.config["MAX_HEADER_COUNT"],
+ max_header_size=self.config["MAX_HEADER_SIZE"],
)
else:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_multipart-0.0.26/tests/test_benchmarks.py
new/python_multipart-0.0.28/tests/test_benchmarks.py
--- old/python_multipart-0.0.26/tests/test_benchmarks.py 1970-01-01
01:00:00.000000000 +0100
+++ new/python_multipart-0.0.28/tests/test_benchmarks.py 2020-02-02
01:00:00.000000000 +0100
@@ -0,0 +1,117 @@
+from __future__ import annotations
+
+import string
+from collections.abc import Iterator
+from typing import TYPE_CHECKING
+
+import pytest
+
+from python_multipart import MultipartParser, QuerystringParser
+
+if TYPE_CHECKING:
+ from python_multipart.multipart import MultipartCallbacks,
QuerystringCallbacks
+
+pytestmark = pytest.mark.benchmark
+
+BOUNDARY = b"------------------------WqclBHaXe8KIsoSum4zfZ6"
+CHUNK_SIZE = 64 * 1024
+
+
+def on_event() -> None:
+ pass
+
+
+def on_data(_data: bytes, _start: int, _end: int) -> None:
+ pass
+
+
+MULTIPART_CALLBACKS: MultipartCallbacks = {
+ "on_part_begin": on_event,
+ "on_part_data": on_data,
+ "on_part_end": on_event,
+ "on_header_field": on_data,
+ "on_header_value": on_data,
+ "on_header_end": on_event,
+ "on_headers_finished": on_event,
+ "on_end": on_event,
+}
+
+QUERYSTRING_CALLBACKS: QuerystringCallbacks = {
+ "on_field_start": on_event,
+ "on_field_name": on_data,
+ "on_field_data": on_data,
+ "on_field_end": on_event,
+ "on_end": on_event,
+}
+
+
+def pattern(pat: bytes, size: int) -> bytes:
+ return (pat * (size // len(pat) + 1))[:size]
+
+
+def build_part(name: bytes, body: bytes, *, filename: bytes | None = None) ->
bytes:
+ disposition = b'form-data; name="' + name + b'"'
+ if filename is not None:
+ disposition += b'; filename="' + filename + b'"'
+ headers = b"Content-Disposition: " + disposition + b"\r\n"
+ if filename is not None:
+ headers += b"Content-Type: application/octet-stream\r\n"
+ return b"--" + BOUNDARY + b"\r\n" + headers + b"\r\n" + body
+
+
+def build_form(parts: list[bytes]) -> bytes:
+ return b"\r\n".join(parts) + b"\r\n--" + BOUNDARY + b"--\r\n"
+
+
+def split(data: bytes, chunk_size: int = CHUNK_SIZE) -> list[bytes]:
+ return [data[i : i + chunk_size] for i in range(0, len(data), chunk_size)]
+
+
+PRINTABLE = string.printable.encode("ascii")
+
+SIMPLE_FORM = build_form(
+ [build_part(b"email", pattern(PRINTABLE, 24)), build_part(b"password",
pattern(PRINTABLE, 16))]
+)
+LARGE_FORM = build_form([build_part(f"field{i}".encode(), pattern(PRINTABLE,
i)) for i in range(100)])
+FILE_UPLOAD = build_form([build_part(b"file", pattern(PRINTABLE, 8 * 1024 *
1024), filename=b"file.bin")])
+FILE_UPLOAD_CHUNKS = split(FILE_UPLOAD)
+WORSTCASE_BCHAR = build_form([build_part(b"file", pattern(b"\r\n", 1024 *
1024), filename=b"file.bin")])
+WORSTCASE_BCHAR_CHUNKS = split(WORSTCASE_BCHAR)
+
+URLENCODED_LARGE = b"&".join(f"field{i}={'v' * 64}".encode() for i in
range(100))
+
+
[email protected]
+def multipart_parser() -> Iterator[MultipartParser]:
+ parser = MultipartParser(BOUNDARY, MULTIPART_CALLBACKS)
+ yield parser
+ parser.finalize()
+
+
[email protected]
+def querystring_parser() -> Iterator[QuerystringParser]:
+ parser = QuerystringParser(QUERYSTRING_CALLBACKS)
+ yield parser
+ parser.finalize()
+
+
+def test_multipart_simple_form(multipart_parser: MultipartParser) -> None:
+ multipart_parser.write(SIMPLE_FORM)
+
+
+def test_multipart_large_form(multipart_parser: MultipartParser) -> None:
+ multipart_parser.write(LARGE_FORM)
+
+
+def test_multipart_file_upload(multipart_parser: MultipartParser) -> None:
+ for chunk in FILE_UPLOAD_CHUNKS:
+ multipart_parser.write(chunk)
+
+
+def test_multipart_worstcase_boundary_chars(multipart_parser: MultipartParser)
-> None:
+ for chunk in WORSTCASE_BCHAR_CHUNKS:
+ multipart_parser.write(chunk)
+
+
+def test_querystring_large_form(querystring_parser: QuerystringParser) -> None:
+ querystring_parser.write(URLENCODED_LARGE)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/python_multipart-0.0.26/tests/test_multipart.py
new/python_multipart-0.0.28/tests/test_multipart.py
--- old/python_multipart-0.0.26/tests/test_multipart.py 2020-02-02
01:00:00.000000000 +0100
+++ new/python_multipart-0.0.28/tests/test_multipart.py 2020-02-02
01:00:00.000000000 +0100
@@ -1182,6 +1182,18 @@
):
self.f.write(data)
+ def test_multipart_header_count_limit(self) -> None:
+ self.make("poc")
+ payload = b'--poc\r\nContent-Disposition: form-data; name="x"\r\n' +
(b"X-A: 1\r\n" * 8)
+ with self.assertRaisesRegex(MultipartParseError, "Maximum header count
exceeded"):
+ self.f.write(payload)
+
+ def test_multipart_header_size_limit(self) -> None:
+ self.make("poc")
+ payload = b'--poc\r\nContent-Disposition: form-data; name="x"\r\n' +
b"X-A: " + (b"a" * (4096 + 124))
+ with self.assertRaisesRegex(MultipartParseError, "Maximum header size
exceeded"):
+ self.f.write(payload)
+
def test_octet_stream(self) -> None:
files: list[File] = []
@@ -1502,6 +1514,12 @@
with self.assertRaises(ValueError):
MultipartParser(b"bound", max_size="foo") # type: ignore[arg-type]
+ def test_boundary_too_long(self) -> None:
+ with self.assertRaisesRegex(FormParserError, "Boundary length 257
exceeds maximum of 256"):
+ MultipartParser(b"x" * 257)
+ # 256 should be accepted.
+ MultipartParser(b"x" * 256)
+
def test_header_begin_callback(self) -> None:
"""
This test verifies we call the `on_header_begin` callback.