Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug/6294 zero bytes files are chunked #6568

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/requests/models.py
Expand Up @@ -545,7 +545,7 @@ def prepare_body(self, data, files, json=None):
"Streamed bodies and files are mutually exclusive."
)

if length:
if length is not None:
self.headers["Content-Length"] = builtin_str(length)
else:
self.headers["Transfer-Encoding"] = "chunked"
Expand Down Expand Up @@ -573,7 +573,7 @@ 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 is not None:
# If length exists, set it. Otherwise, we fallback
# to Transfer-Encoding: chunked.
self.headers["Content-Length"] = builtin_str(length)
Expand Down
27 changes: 24 additions & 3 deletions src/requests/utils.py
Expand Up @@ -12,6 +12,7 @@
import os
import re
import socket
import stat
import struct
import sys
import tempfile
Expand Down Expand Up @@ -131,7 +132,14 @@ def dict_to_sequence(d):


def super_len(o):
"""Returns the length of the object or None if the length cannot be measured.

Tries looking for length attributes, file handles and seek/tell in order
to figure out the object length. If the length cannot be found, None
is returned.
"""
total_length = None
length_undefined = False
current_position = 0

if isinstance(o, str):
Expand All @@ -146,11 +154,18 @@ def super_len(o):
elif hasattr(o, "fileno"):
try:
fileno = o.fileno()
if not stat.S_ISREG(os.fstat(fileno).st_mode):
raise BufferError("Cannot tell size of non regular file")
except (io.UnsupportedOperation, AttributeError):
# AttributeError is a surprising exception, seeing as how we've just checked
# that `hasattr(o, 'fileno')`. It happens for objects obtained via
# `Tarfile.extractfile()`, per issue 5229.
pass
except BufferError:
# Telling size of non regular files does is not realiable
# if it is a socket or a pipe, they need to be handled as
# streams with no known end.
length_undefined = True
else:
total_length = os.fstat(fileno).st_size

Expand All @@ -169,7 +184,13 @@ def super_len(o):
FileModeWarning,
)

if hasattr(o, "tell"):
if hasattr(o, "tell") and not length_undefined:
# pipes have tell/seek, tell() will fail on Linux/MacOS,
# but not in windows, causing super_len to report the
# length as the number of bytes currently written to
# the pipe, which will be wrong if the pipe is continuously
# being written to. The length_underfined check will
# prevent trying to use tell/seek for a pipe.
try:
current_position = o.tell()
except OSError:
Expand All @@ -191,10 +212,10 @@ def super_len(o):
# partially read file-like objects
o.seek(current_position or 0)
except OSError:
total_length = 0
total_length = None

if total_length is None:
total_length = 0
return total_length

return max(0, total_length - current_position)

Expand Down
24 changes: 21 additions & 3 deletions tests/test_requests.py
Expand Up @@ -130,6 +130,22 @@ def test_empty_content_length(self, httpbin, method):
req = requests.Request(method, httpbin(method.lower()), data="").prepare()
assert req.headers["Content-Length"] == "0"

@pytest.mark.parametrize("method", ("POST", "PUT", "PATCH", "OPTIONS"))
def test_empty_file_content_length(self, httpbin, method):
data = io.BytesIO(b"")
req = requests.Request(method, httpbin(method.lower()), data=data).prepare()
assert req.headers["Content-Length"] == "0"

@pytest.mark.parametrize("method", ("POST", "PUT", "PATCH", "OPTIONS"))
def test_not_readable_content_length_pipe(self, httpbin, method):
pipe_r, pipe_w = os.pipe()
pipe_rf = os.fdopen(pipe_r, "rb")
pipe_wf = os.fdopen(pipe_w, "wb")
pipe_wf.write(b"hello")
req = requests.Request(method, httpbin(method.lower()), data=pipe_rf).prepare()
assert req.headers.get("Content-Length") is None
assert req.headers["Transfer-Encoding"] == "chunked"

def test_override_content_length(self, httpbin):
headers = {"Content-Length": "not zero"}
r = requests.Request("POST", httpbin("post"), headers=headers).prepare()
Expand Down Expand Up @@ -2151,7 +2167,9 @@ def test_response_without_release_conn(self):
resp.close()
assert resp.raw.closed

def test_empty_stream_with_auth_does_not_set_content_length_header(self, httpbin):
def test_empty_stream_with_auth_does_not_set_transfer_encoding_header(
self, httpbin
):
"""Ensure that a byte stream with size 0 will not set both a Content-Length
and Transfer-Encoding header.
"""
Expand All @@ -2160,8 +2178,8 @@ def test_empty_stream_with_auth_does_not_set_content_length_header(self, httpbin
file_obj = io.BytesIO(b"")
r = requests.Request("POST", url, auth=auth, data=file_obj)
prepared_request = r.prepare()
assert "Transfer-Encoding" in prepared_request.headers
assert "Content-Length" not in prepared_request.headers
assert "Transfer-Encoding" not in prepared_request.headers
assert "Content-Length" in prepared_request.headers

def test_stream_with_auth_does_not_set_transfer_encoding_header(self, httpbin):
"""Ensure that a byte stream with size > 0 will not set both a Content-Length
Expand Down
12 changes: 9 additions & 3 deletions tests/test_utils.py
Expand Up @@ -92,7 +92,7 @@ def tell(self):
def seek(self, offset, whence):
pass

assert super_len(NoLenBoomFile()) == 0
assert super_len(NoLenBoomFile()) is None

def test_string(self):
assert super_len("Test") == 4
Expand Down Expand Up @@ -148,8 +148,14 @@ def test_super_len_with_fileno(self):
assert length == len(file_data)

def test_super_len_with_no_matches(self):
"""Ensure that objects without any length methods default to 0"""
assert super_len(object()) == 0
"""Ensure that objects without any length methods default to None"""
assert super_len(object()) is None

def test_super_len_with_pipe(self):
"""Ensure that ojects with a fileno that are not regular files default to length None"""
r, w = os.pipe()
rf = os.fdopen(r, "rb")
assert super_len(rf) is None


class TestToKeyValList:
Expand Down