diff --git a/src/requests/models.py b/src/requests/models.py index 2d043f59cf..14e87407f1 100644 --- a/src/requests/models.py +++ b/src/requests/models.py @@ -575,9 +575,13 @@ def prepare_content_length(self, body): """Prepare Content-Length header based on request method and body""" if body is not None: length = super_len(body) - if length: - # If length exists, set it. Otherwise, we fallback - # to Transfer-Encoding: chunked. + if length is not None and "Transfer-Encoding" not in self.headers: + # Set Content-Length for any known length, including 0. + # A length of 0 is valid (e.g. data={'foo': None} encodes + # to an empty body) and must be sent to avoid falling back + # to Transfer-Encoding: chunked, which can cause servers + # to misinterpret the terminating chunk as a new request. + # Skip if Transfer-Encoding is already set (e.g. for streams). self.headers["Content-Length"] = builtin_str(length) elif ( self.method not in ("GET", "HEAD") diff --git a/tests/test_requests.py b/tests/test_requests.py index 257d9d7ab1..12379a9282 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -2229,6 +2229,24 @@ def test_chunked_upload_does_not_set_content_length_header(self, httpbin): assert "Transfer-Encoding" in prepared_request.headers assert "Content-Length" not in prepared_request.headers + def test_empty_body_from_none_values_sets_content_length_zero(self, httpbin): + """Ensure that a request body consisting only of None values sets + Content-Length: 0 instead of falling back to Transfer-Encoding: chunked. + + When data={'foo': None}, the body encodes to an empty string. + Without Content-Length: 0, the adapter falls back to chunked encoding + and sends a terminating chunk ('0\\r\\n\\r\\n') that servers may + misinterpret as a second, malformed request. + + See: https://github.com/psf/requests/issues/6122 + """ + url = httpbin("post") + r = requests.Request("POST", url, data={"foo": None}) + prepared_request = r.prepare() + assert prepared_request.body == "" + assert prepared_request.headers["Content-Length"] == "0" + assert "Transfer-Encoding" not in prepared_request.headers + def test_custom_redirect_mixin(self, httpbin): """Tests a custom mixin to overwrite ``get_redirect_target``.