Don't log defect warnings on multipart/* responses
httplib expects the response body to be available to the HTTP header parser but that's not the case for `parse_headers()` so the following defects are always added to multipart/* responses. We simply ignore these defects at this stage as they are a known issue.
MODIFY
src/urllib3/util/response.py
src/urllib3/util/response.py
from __future__ import absolute_import
+from email.errors import StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect
from ..packages.six.moves import http_client as httplib
from ..exceptions import HeaderParsingError
@@ -66,6 +67,25 @@ def assert_header_parsing(headers):
if isinstance(payload, (bytes, str)):
unparsed_data = payload
+ if defects:
+ # httplib is assuming a response body is available
+ # when parsing headers even when httplib only sends
+ # header data to parse_headers() This results in
+ # defects on multipart responses in particular.
+ # See: https://github.com/urllib3/urllib3/issues/800
+
+ # So we ignore the following defects:
+ # - StartBoundaryNotFoundDefect:
+ # The claimed start boundary was never found.
+ # - MultipartInvariantViolationDefect:
+ # A message claimed to be a multipart but no subparts were found.
+ defects = [
+ defect
+ for defect in defects
+ if not isinstance(
+ defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect)
+ )
+ ]
if defects or unparsed_data:
raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data)

MODIFY
test/test_util.py
test/test_util.py
@@ -748,6 +748,20 @@ class TestUtil(object):
with pytest.raises(TypeError):
assert_header_parsing(headers)
+ @onlyPy3
+ def test_assert_header_parsing_no_error_on_multipart(self):
+ from http import client
+
+ header_msg = io.BytesIO()
+ header_msg.write(
+ b'Content-Type: multipart/encrypted;protocol="application/'
+ b'HTTP-SPNEGO-session-encrypted";boundary="Encrypted Boundary"'
+ b"\nServer: Microsoft-HTTPAPI/2.0\nDate: Fri, 16 Aug 2019 19:28:01 GMT"
+ b"\nContent-Length: 1895\n\n\n"
+ )
+ header_msg.seek(0)
+ assert_header_parsing(client.parse_headers(header_msg))
+
class TestUtilSSL(object):
"""Test utils that use an SSL backend."""

MODIFY
test/with_dummyserver/test_socketlevel.py
test/with_dummyserver/test_socketlevel.py
@@ -1808,3 +1808,40 @@ class TestRetryPoolSizeDrainFail(SocketDummyServerTestCase):
) as pool:
pool.urlopen("GET", "/not_found", preload_content=False)
assert pool.num_connections == 1
+
+
+class TestMultipartResponse(SocketDummyServerTestCase):
+ def test_multipart_assert_header_parsing_no_defects(self):
+ def socket_handler(listener):
+ for _ in range(2):
+ sock = listener.accept()[0]
+ while not sock.recv(65536).endswith(b"\r\n\r\n"):
+ pass
+
+ sock.sendall(
+ b"HTTP/1.1 404 Not Found\r\n"
+ b"Server: example.com\r\n"
+ b"Content-Type: multipart/mixed; boundary=36eeb8c4e26d842a\r\n"
+ b"Content-Length: 73\r\n"
+ b"\r\n"
+ b"--36eeb8c4e26d842a\r\n"
+ b"Content-Type: text/plain\r\n"
+ b"\r\n"
+ b"1\r\n"
+ b"--36eeb8c4e26d842a--\r\n",
+ )
+ sock.close()
+
+ self._start_server(socket_handler)
+ from urllib3.connectionpool import log
+
+ with mock.patch.object(log, "warning") as log_warning:
+ with HTTPConnectionPool(self.host, self.port, timeout=3) as pool:
+ resp = pool.urlopen("GET", "/")
+ assert resp.status == 404
+ assert (
+ resp.headers["content-type"]
+ == "multipart/mixed; boundary=36eeb8c4e26d842a"
+ )
+ assert len(resp.data) == 73
+ log_warning.assert_not_called()

Don't log defect warnings on multipart/* responses

httplib expects the response body to be available to the HTTP header parser but that's not the case for `parse_headers()` so the following defects are always added to multipart/* responses. We simply ignore these defects at this stage as they are a known issue.