From 268832608a101059fd6606a24e39700295ef4bd9 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Thu, 23 Jun 2022 08:57:29 -0700 Subject: [PATCH 01/15] allow setting an explicit multipart boundary via headers --- httpx/_models.py | 11 ++++++++++- httpx/_multipart.py | 10 ++++++++++ tests/test_multipart.py | 24 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/httpx/_models.py b/httpx/_models.py index 8879532c81..d3ab9fa630 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -27,6 +27,7 @@ StreamConsumed, request_context, ) +from ._multipart import get_multipart_boundary_from_content_type from ._status_codes import codes from ._types import ( AsyncByteStream, @@ -332,7 +333,15 @@ def __init__( Cookies(cookies).set_cookie_header(self) if stream is None: - headers, stream = encode_request(content, data, files, json) + if "content-type" in self.headers: + boundary = get_multipart_boundary_from_content_type( + self.headers["content-type"] + ) + else: + boundary = None + headers, stream = encode_request( + content, data, files, json, boundary=boundary + ) self._prepare(headers) self.stream = stream # Load the request body, except for streaming content. diff --git a/httpx/_multipart.py b/httpx/_multipart.py index d42f5cb31b..a9651a4d7c 100644 --- a/httpx/_multipart.py +++ b/httpx/_multipart.py @@ -20,6 +20,16 @@ ) +def get_multipart_boundary_from_content_type( + content_type: str, +) -> typing.Optional[bytes]: + if ";" in content_type: + for section in content_type.split(";"): + if section.strip().startswith("boundary="): + return section.strip().split("boundary=")[-1].encode("latin-1") + return None + + class DataField: """ A single form field item, within a multipart form field. diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 46ad0e01a1..1509457a80 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -42,6 +42,30 @@ def test_multipart(value, output): assert multipart["file"] == [b""] +def test_multipart_explicit_boundary() -> None: + client = httpx.Client(transport=httpx.MockTransport(echo_request_content)) + + files = {"file": io.BytesIO(b"")} + headers = {"content-type": "multipart/form-data; boundary=+++"} + response = client.post("http://127.0.0.1:8000/", files=files, headers=headers) + assert response.status_code == 200 + + # We're using the cgi module to verify the behavior here, which is a + # bit grungy, but sufficient just for our testing purposes. + boundary = response.request.headers["Content-Type"].split("boundary=")[-1] + assert boundary == "+++" + content_length = response.request.headers["Content-Length"] + pdict: dict = { + "boundary": boundary.encode("ascii"), + "CONTENT-LENGTH": content_length, + } + multipart = cgi.parse_multipart(io.BytesIO(response.content), pdict) + + # Note that the expected return type for text fields + # appears to differs from 3.6 to 3.7+ + assert multipart["file"] == [b""] + + @pytest.mark.parametrize(("key"), (b"abc", 1, 2.3, None)) def test_multipart_invalid_key(key): client = httpx.Client(transport=httpx.MockTransport(echo_request_content)) From 5dd17fa3a18d05f13d674ab0b716910339da1ead Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Thu, 23 Jun 2022 09:23:45 -0700 Subject: [PATCH 02/15] more tests --- tests/test_multipart.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 1509457a80..37223d77df 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -42,21 +42,29 @@ def test_multipart(value, output): assert multipart["file"] == [b""] -def test_multipart_explicit_boundary() -> None: +@pytest.mark.parametrize( + "header", + [ + "multipart/form-data; boundary=+++; charset=utf-8", + "multipart/form-data; charset=utf-8; boundary=+++", + "multipart/form-data; boundary=+++", + "multipart/form-data; boundary=+++ ;", + ], +) +def test_multipart_explicit_boundary(header: str) -> None: client = httpx.Client(transport=httpx.MockTransport(echo_request_content)) files = {"file": io.BytesIO(b"")} - headers = {"content-type": "multipart/form-data; boundary=+++"} + headers = {"content-type": header} response = client.post("http://127.0.0.1:8000/", files=files, headers=headers) assert response.status_code == 200 # We're using the cgi module to verify the behavior here, which is a # bit grungy, but sufficient just for our testing purposes. - boundary = response.request.headers["Content-Type"].split("boundary=")[-1] - assert boundary == "+++" + assert response.request.headers["Content-Type"] == header content_length = response.request.headers["Content-Length"] pdict: dict = { - "boundary": boundary.encode("ascii"), + "boundary": b"+++", "CONTENT-LENGTH": content_length, } multipart = cgi.parse_multipart(io.BytesIO(response.content), pdict) From c6e4cdf80cd5d59d8c92a1a274d3423ba43cf891 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Thu, 23 Jun 2022 09:30:55 -0700 Subject: [PATCH 03/15] add failing test --- tests/test_multipart.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 37223d77df..e3f1e07ab5 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -69,8 +69,34 @@ def test_multipart_explicit_boundary(header: str) -> None: } multipart = cgi.parse_multipart(io.BytesIO(response.content), pdict) - # Note that the expected return type for text fields - # appears to differs from 3.6 to 3.7+ + assert multipart["file"] == [b""] + + +@pytest.mark.parametrize( + "header", + [ + "multipart/form-data; charset=utf-8", + "multipart/form-data; charset=utf-8; ", + ], +) +def test_multipart_header_without_boundary(header: str) -> None: + client = httpx.Client(transport=httpx.MockTransport(echo_request_content)) + + files = {"file": io.BytesIO(b"")} + headers = {"content-type": header} + response = client.post("http://127.0.0.1:8000/", files=files, headers=headers) + assert response.status_code == 200 + + # We're using the cgi module to verify the behavior here, which is a + # bit grungy, but sufficient just for our testing purposes. + boundary = response.request.headers["Content-Type"].split("boundary=")[-1] + content_length = response.request.headers["Content-Length"] + pdict: dict = { + "boundary": boundary.encode("ascii"), + "CONTENT-LENGTH": content_length, + } + multipart = cgi.parse_multipart(io.BytesIO(response.content), pdict) + assert multipart["file"] == [b""] From 20050f0db31fac177ef59d8ba1c336ce283ef378 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Thu, 23 Jun 2022 09:44:36 -0700 Subject: [PATCH 04/15] error if missing boundary --- httpx/_content.py | 22 +++++++++++++++++----- httpx/_models.py | 12 +++++------- httpx/_multipart.py | 4 ++-- tests/test_multipart.py | 16 ++-------------- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/httpx/_content.py b/httpx/_content.py index 24a967d506..cf3462a7c0 100644 --- a/httpx/_content.py +++ b/httpx/_content.py @@ -8,6 +8,7 @@ Dict, Iterable, Iterator, + Mapping, Optional, Tuple, Union, @@ -15,7 +16,7 @@ from urllib.parse import urlencode from ._exceptions import StreamClosed, StreamConsumed -from ._multipart import MultipartStream +from ._multipart import MultipartStream, get_multipart_boundary_from_content_type from ._types import ( AsyncByteStream, RequestContent, @@ -150,11 +151,21 @@ def encode_urlencoded_data( def encode_multipart_data( - data: dict, files: RequestFiles, boundary: Optional[bytes] = None + data: dict, + files: RequestFiles, + boundary: Optional[bytes] = None, + headers: Optional[Mapping[str, str]] = None, ) -> Tuple[Dict[str, str], MultipartStream]: + if headers and boundary is None and "content-type" in headers: + content_type = headers["content-type"] + if not content_type.startswith("multipart/form-data"): + raise ValueError( + f"Invalid content-type header for multipart request: {content_type}" + ) + boundary = get_multipart_boundary_from_content_type(content_type) multipart = MultipartStream(data=data, files=files, boundary=boundary) - headers = multipart.get_headers() - return headers, multipart + new_headers = multipart.get_headers() + return new_headers, multipart def encode_text(text: str) -> Tuple[Dict[str, str], ByteStream]: @@ -187,6 +198,7 @@ def encode_request( files: Optional[RequestFiles] = None, json: Optional[Any] = None, boundary: Optional[bytes] = None, + headers: Optional[Mapping[str, str]] = None, ) -> Tuple[Dict[str, str], Union[SyncByteStream, AsyncByteStream]]: """ Handles encoding the given `content`, `data`, `files`, and `json`, @@ -207,7 +219,7 @@ def encode_request( if content is not None: return encode_content(content) elif files: - return encode_multipart_data(data or {}, files, boundary) + return encode_multipart_data(data or {}, files, boundary, headers) elif data: return encode_urlencoded_data(data) elif json is not None: diff --git a/httpx/_models.py b/httpx/_models.py index d3ab9fa630..f0eec3de4d 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -333,14 +333,12 @@ def __init__( Cookies(cookies).set_cookie_header(self) if stream is None: - if "content-type" in self.headers: - boundary = get_multipart_boundary_from_content_type( - self.headers["content-type"] - ) - else: - boundary = None headers, stream = encode_request( - content, data, files, json, boundary=boundary + content=content, + data=data, + files=files, + json=json, + headers=self.headers, ) self._prepare(headers) self.stream = stream diff --git a/httpx/_multipart.py b/httpx/_multipart.py index a9651a4d7c..90d6903910 100644 --- a/httpx/_multipart.py +++ b/httpx/_multipart.py @@ -22,12 +22,12 @@ def get_multipart_boundary_from_content_type( content_type: str, -) -> typing.Optional[bytes]: +) -> bytes: if ";" in content_type: for section in content_type.split(";"): if section.strip().startswith("boundary="): return section.strip().split("boundary=")[-1].encode("latin-1") - return None + raise ValueError("Missing boundary in multipart/form-data content-type header") class DataField: diff --git a/tests/test_multipart.py b/tests/test_multipart.py index e3f1e07ab5..d9baef3efc 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -84,20 +84,8 @@ def test_multipart_header_without_boundary(header: str) -> None: files = {"file": io.BytesIO(b"")} headers = {"content-type": header} - response = client.post("http://127.0.0.1:8000/", files=files, headers=headers) - assert response.status_code == 200 - - # We're using the cgi module to verify the behavior here, which is a - # bit grungy, but sufficient just for our testing purposes. - boundary = response.request.headers["Content-Type"].split("boundary=")[-1] - content_length = response.request.headers["Content-Length"] - pdict: dict = { - "boundary": boundary.encode("ascii"), - "CONTENT-LENGTH": content_length, - } - multipart = cgi.parse_multipart(io.BytesIO(response.content), pdict) - - assert multipart["file"] == [b""] + with pytest.raises(ValueError, match=r"Missing boundary"): + client.post("http://127.0.0.1:8000/", files=files, headers=headers) @pytest.mark.parametrize(("key"), (b"abc", 1, 2.3, None)) From e4275c1d562685ecbe173a4b9ba7fbe203c40f3b Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Thu, 23 Jun 2022 09:45:10 -0700 Subject: [PATCH 05/15] remove unused import --- httpx/_models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/httpx/_models.py b/httpx/_models.py index f0eec3de4d..25b05a6e9f 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -27,7 +27,6 @@ StreamConsumed, request_context, ) -from ._multipart import get_multipart_boundary_from_content_type from ._status_codes import codes from ._types import ( AsyncByteStream, From aa48d29d7996b9d75a1259a528d1e77a8835f4c0 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Sat, 25 Jun 2022 16:08:41 -0700 Subject: [PATCH 06/15] pass content_type instaed of all headers --- httpx/_content.py | 10 ++++------ httpx/_models.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/httpx/_content.py b/httpx/_content.py index cf3462a7c0..df5cbfdaee 100644 --- a/httpx/_content.py +++ b/httpx/_content.py @@ -8,7 +8,6 @@ Dict, Iterable, Iterator, - Mapping, Optional, Tuple, Union, @@ -154,10 +153,9 @@ def encode_multipart_data( data: dict, files: RequestFiles, boundary: Optional[bytes] = None, - headers: Optional[Mapping[str, str]] = None, + content_type: Optional[str] = None, ) -> Tuple[Dict[str, str], MultipartStream]: - if headers and boundary is None and "content-type" in headers: - content_type = headers["content-type"] + if content_type: if not content_type.startswith("multipart/form-data"): raise ValueError( f"Invalid content-type header for multipart request: {content_type}" @@ -198,7 +196,7 @@ def encode_request( files: Optional[RequestFiles] = None, json: Optional[Any] = None, boundary: Optional[bytes] = None, - headers: Optional[Mapping[str, str]] = None, + content_type: Optional[str] = None, ) -> Tuple[Dict[str, str], Union[SyncByteStream, AsyncByteStream]]: """ Handles encoding the given `content`, `data`, `files`, and `json`, @@ -219,7 +217,7 @@ def encode_request( if content is not None: return encode_content(content) elif files: - return encode_multipart_data(data or {}, files, boundary, headers) + return encode_multipart_data(data or {}, files, boundary, content_type) elif data: return encode_urlencoded_data(data) elif json is not None: diff --git a/httpx/_models.py b/httpx/_models.py index 25b05a6e9f..3239dc1226 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -337,7 +337,7 @@ def __init__( data=data, files=files, json=json, - headers=self.headers, + content_type=self.headers.get("content-type"), ) self._prepare(headers) self.stream = stream From fabb04999057b99f12aaff7d00b69377c85fd46b Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Sat, 25 Jun 2022 16:17:38 -0700 Subject: [PATCH 07/15] be more lax --- httpx/_content.py | 4 ---- httpx/_multipart.py | 4 ++-- tests/test_multipart.py | 6 ++++-- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/httpx/_content.py b/httpx/_content.py index df5cbfdaee..38c5c2779e 100644 --- a/httpx/_content.py +++ b/httpx/_content.py @@ -156,10 +156,6 @@ def encode_multipart_data( content_type: Optional[str] = None, ) -> Tuple[Dict[str, str], MultipartStream]: if content_type: - if not content_type.startswith("multipart/form-data"): - raise ValueError( - f"Invalid content-type header for multipart request: {content_type}" - ) boundary = get_multipart_boundary_from_content_type(content_type) multipart = MultipartStream(data=data, files=files, boundary=boundary) new_headers = multipart.get_headers() diff --git a/httpx/_multipart.py b/httpx/_multipart.py index 90d6903910..a9651a4d7c 100644 --- a/httpx/_multipart.py +++ b/httpx/_multipart.py @@ -22,12 +22,12 @@ def get_multipart_boundary_from_content_type( content_type: str, -) -> bytes: +) -> typing.Optional[bytes]: if ";" in content_type: for section in content_type.split(";"): if section.strip().startswith("boundary="): return section.strip().split("boundary=")[-1].encode("latin-1") - raise ValueError("Missing boundary in multipart/form-data content-type header") + return None class DataField: diff --git a/tests/test_multipart.py b/tests/test_multipart.py index d9baef3efc..d3e69bfb80 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -84,8 +84,10 @@ def test_multipart_header_without_boundary(header: str) -> None: files = {"file": io.BytesIO(b"")} headers = {"content-type": header} - with pytest.raises(ValueError, match=r"Missing boundary"): - client.post("http://127.0.0.1:8000/", files=files, headers=headers) + response = client.post("http://127.0.0.1:8000/", files=files, headers=headers) + + assert response.status_code == 200 + assert response.request.headers["Content-Type"] == header @pytest.mark.parametrize(("key"), (b"abc", 1, 2.3, None)) From 004a81d670e00473c9f4ad11f984e5c645823559 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Sat, 13 Aug 2022 15:01:29 -0500 Subject: [PATCH 08/15] add comment --- httpx/_content.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/httpx/_content.py b/httpx/_content.py index 38c5c2779e..043bb4bd17 100644 --- a/httpx/_content.py +++ b/httpx/_content.py @@ -152,9 +152,12 @@ def encode_urlencoded_data( def encode_multipart_data( data: dict, files: RequestFiles, - boundary: Optional[bytes] = None, - content_type: Optional[str] = None, + boundary: Optional[bytes], + content_type: Optional[str], ) -> Tuple[Dict[str, str], MultipartStream]: + # note: we are the only ones calling into this function + # (not users) so there should never be a situation where + # both content_type and boundary are set if content_type: boundary = get_multipart_boundary_from_content_type(content_type) multipart = MultipartStream(data=data, files=files, boundary=boundary) From 2a1e04371b55b22ca9bb3bc734a955bdcc777655 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Sat, 13 Aug 2022 15:15:04 -0500 Subject: [PATCH 09/15] move multipart parsing --- httpx/_content.py | 10 ++-------- httpx/_models.py | 5 ++++- httpx/_multipart.py | 4 +++- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/httpx/_content.py b/httpx/_content.py index 043bb4bd17..6db2d9f74e 100644 --- a/httpx/_content.py +++ b/httpx/_content.py @@ -15,7 +15,7 @@ from urllib.parse import urlencode from ._exceptions import StreamClosed, StreamConsumed -from ._multipart import MultipartStream, get_multipart_boundary_from_content_type +from ._multipart import MultipartStream from ._types import ( AsyncByteStream, RequestContent, @@ -153,13 +153,7 @@ def encode_multipart_data( data: dict, files: RequestFiles, boundary: Optional[bytes], - content_type: Optional[str], ) -> Tuple[Dict[str, str], MultipartStream]: - # note: we are the only ones calling into this function - # (not users) so there should never be a situation where - # both content_type and boundary are set - if content_type: - boundary = get_multipart_boundary_from_content_type(content_type) multipart = MultipartStream(data=data, files=files, boundary=boundary) new_headers = multipart.get_headers() return new_headers, multipart @@ -216,7 +210,7 @@ def encode_request( if content is not None: return encode_content(content) elif files: - return encode_multipart_data(data or {}, files, boundary, content_type) + return encode_multipart_data(data or {}, files, boundary) elif data: return encode_urlencoded_data(data) elif json is not None: diff --git a/httpx/_models.py b/httpx/_models.py index 3239dc1226..fa2315c67f 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -27,6 +27,7 @@ StreamConsumed, request_context, ) +from ._multipart import get_multipart_boundary_from_content_type from ._status_codes import codes from ._types import ( AsyncByteStream, @@ -337,7 +338,9 @@ def __init__( data=data, files=files, json=json, - content_type=self.headers.get("content-type"), + boundary=get_multipart_boundary_from_content_type( + self.headers.get("content-type") + ), ) self._prepare(headers) self.stream = stream diff --git a/httpx/_multipart.py b/httpx/_multipart.py index a9651a4d7c..bc79a59c9a 100644 --- a/httpx/_multipart.py +++ b/httpx/_multipart.py @@ -21,8 +21,10 @@ def get_multipart_boundary_from_content_type( - content_type: str, + content_type: typing.Optional[str], ) -> typing.Optional[bytes]: + if not content_type or not content_type.startswith("multipart/form-data"): + return None if ";" in content_type: for section in content_type.split(";"): if section.strip().startswith("boundary="): From 5c301ccc000d40bed965871e2c5d04938127aa8d Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Sat, 13 Aug 2022 15:31:11 -0500 Subject: [PATCH 10/15] minimize changes --- httpx/_content.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/httpx/_content.py b/httpx/_content.py index 6db2d9f74e..eb7a7aef17 100644 --- a/httpx/_content.py +++ b/httpx/_content.py @@ -150,13 +150,11 @@ def encode_urlencoded_data( def encode_multipart_data( - data: dict, - files: RequestFiles, - boundary: Optional[bytes], + data: dict, files: RequestFiles, boundary: Optional[bytes] ) -> Tuple[Dict[str, str], MultipartStream]: multipart = MultipartStream(data=data, files=files, boundary=boundary) - new_headers = multipart.get_headers() - return new_headers, multipart + headers = multipart.get_headers() + return headers, multipart def encode_text(text: str) -> Tuple[Dict[str, str], ByteStream]: @@ -189,7 +187,6 @@ def encode_request( files: Optional[RequestFiles] = None, json: Optional[Any] = None, boundary: Optional[bytes] = None, - content_type: Optional[str] = None, ) -> Tuple[Dict[str, str], Union[SyncByteStream, AsyncByteStream]]: """ Handles encoding the given `content`, `data`, `files`, and `json`, From 38d535728ce9add762cb8b529bc8387fbe2a3845 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Sat, 13 Aug 2022 18:55:34 -0500 Subject: [PATCH 11/15] handle quoted boundary --- httpx/_models.py | 5 ++++- httpx/_multipart.py | 19 +++++++++++++------ tests/test_multipart.py | 4 ++++ 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/httpx/_models.py b/httpx/_models.py index fa2315c67f..fd1d7fe9a1 100644 --- a/httpx/_models.py +++ b/httpx/_models.py @@ -333,13 +333,16 @@ def __init__( Cookies(cookies).set_cookie_header(self) if stream is None: + content_type: typing.Optional[str] = self.headers.get("content-type") headers, stream = encode_request( content=content, data=data, files=files, json=json, boundary=get_multipart_boundary_from_content_type( - self.headers.get("content-type") + content_type=content_type.encode(self.headers.encoding) + if content_type + else None ), ) self._prepare(headers) diff --git a/httpx/_multipart.py b/httpx/_multipart.py index bc79a59c9a..76c77590dd 100644 --- a/httpx/_multipart.py +++ b/httpx/_multipart.py @@ -21,14 +21,21 @@ def get_multipart_boundary_from_content_type( - content_type: typing.Optional[str], + content_type: typing.Optional[bytes], ) -> typing.Optional[bytes]: - if not content_type or not content_type.startswith("multipart/form-data"): + if not content_type or not content_type.startswith(b"multipart/form-data"): return None - if ";" in content_type: - for section in content_type.split(";"): - if section.strip().startswith("boundary="): - return section.strip().split("boundary=")[-1].encode("latin-1") + # parse boundary according to + # https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1 + if b";" in content_type: + for section in content_type.split(b";"): + if section.strip().startswith(b"boundary="): + return ( + section.strip() + .split(b"boundary=")[-1] + .strip(b'"') + .strip(b"'") + ) return None diff --git a/tests/test_multipart.py b/tests/test_multipart.py index d3e69bfb80..38ab1da8e8 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -49,6 +49,10 @@ def test_multipart(value, output): "multipart/form-data; charset=utf-8; boundary=+++", "multipart/form-data; boundary=+++", "multipart/form-data; boundary=+++ ;", + "multipart/form-data; boundary=\"+++\"; charset=utf-8", + "multipart/form-data; charset=utf-8; boundary=\"+++\"", + "multipart/form-data; boundary=\"+++\"", + "multipart/form-data; boundary=\"+++\" ;", ], ) def test_multipart_explicit_boundary(header: str) -> None: From 424beb3d0f31e795ea9081e5c3171b97f17b789e Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Sat, 13 Aug 2022 22:20:51 -0500 Subject: [PATCH 12/15] lint --- httpx/_multipart.py | 7 +------ tests/test_multipart.py | 8 ++++---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/httpx/_multipart.py b/httpx/_multipart.py index 76c77590dd..8607115723 100644 --- a/httpx/_multipart.py +++ b/httpx/_multipart.py @@ -30,12 +30,7 @@ def get_multipart_boundary_from_content_type( if b";" in content_type: for section in content_type.split(b";"): if section.strip().startswith(b"boundary="): - return ( - section.strip() - .split(b"boundary=")[-1] - .strip(b'"') - .strip(b"'") - ) + return section.strip().split(b"boundary=")[-1].strip(b'"').strip(b"'") return None diff --git a/tests/test_multipart.py b/tests/test_multipart.py index 38ab1da8e8..dc93d26505 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -49,10 +49,10 @@ def test_multipart(value, output): "multipart/form-data; charset=utf-8; boundary=+++", "multipart/form-data; boundary=+++", "multipart/form-data; boundary=+++ ;", - "multipart/form-data; boundary=\"+++\"; charset=utf-8", - "multipart/form-data; charset=utf-8; boundary=\"+++\"", - "multipart/form-data; boundary=\"+++\"", - "multipart/form-data; boundary=\"+++\" ;", + 'multipart/form-data; boundary="+++"; charset=utf-8', + 'multipart/form-data; charset=utf-8; boundary="+++"', + 'multipart/form-data; boundary="+++"', + 'multipart/form-data; boundary="+++" ;', ], ) def test_multipart_explicit_boundary(header: str) -> None: From c78fa10344b92d4a651ccb15d764de96fd07e562 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Mon, 15 Aug 2022 08:21:15 -0500 Subject: [PATCH 13/15] remove support for ' --- httpx/_multipart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpx/_multipart.py b/httpx/_multipart.py index 8607115723..5d0e792a16 100644 --- a/httpx/_multipart.py +++ b/httpx/_multipart.py @@ -30,7 +30,7 @@ def get_multipart_boundary_from_content_type( if b";" in content_type: for section in content_type.split(b";"): if section.strip().startswith(b"boundary="): - return section.strip().split(b"boundary=")[-1].strip(b'"').strip(b"'") + return section.strip().split(b"boundary=")[-1].strip(b'"') return None From 899c8206b323512f80b686874926184c8982ee68 Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Mon, 15 Aug 2022 09:45:42 -0500 Subject: [PATCH 14/15] Update httpx/_multipart.py Co-authored-by: Jean Hominal --- httpx/_multipart.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpx/_multipart.py b/httpx/_multipart.py index 5d0e792a16..eb6be7dd1d 100644 --- a/httpx/_multipart.py +++ b/httpx/_multipart.py @@ -29,8 +29,8 @@ def get_multipart_boundary_from_content_type( # https://www.rfc-editor.org/rfc/rfc2046#section-5.1.1 if b";" in content_type: for section in content_type.split(b";"): - if section.strip().startswith(b"boundary="): - return section.strip().split(b"boundary=")[-1].strip(b'"') + if section.strip().lower().startswith(b"boundary="): + return section.strip()[len(b"boundary="):].strip(b'"') return None From 74de49482f0fdf49fb112d118bf580d764d18ccd Mon Sep 17 00:00:00 2001 From: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com> Date: Mon, 15 Aug 2022 10:15:48 -0500 Subject: [PATCH 15/15] lint --- httpx/_multipart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpx/_multipart.py b/httpx/_multipart.py index eb6be7dd1d..8bd7a17c9b 100644 --- a/httpx/_multipart.py +++ b/httpx/_multipart.py @@ -30,7 +30,7 @@ def get_multipart_boundary_from_content_type( if b";" in content_type: for section in content_type.split(b";"): if section.strip().lower().startswith(b"boundary="): - return section.strip()[len(b"boundary="):].strip(b'"') + return section.strip()[len(b"boundary=") :].strip(b'"') return None