Prefer Content-Length over Transfer-Encoding: chunked for content=<file-like> cases. (#1619)

* Add failing test case for 'content=io.BytesIO(...)'

* Refactor peek_filelike_length to return an Optional[int]

* Peek filelength on file-like objects when rendering 'content=...'
This commit is contained in:
Tom Christie 2021-04-30 10:40:42 +01:00 committed by GitHub
parent 54f4194c38
commit 2129a9789a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 31 additions and 16 deletions

View File

@ -17,7 +17,7 @@ from ._exceptions import StreamClosed, StreamConsumed
from ._multipart import MultipartStream
from ._transports.base import AsyncByteStream, SyncByteStream
from ._types import RequestContent, RequestData, RequestFiles, ResponseContent
from ._utils import primitive_value_to_str
from ._utils import peek_filelike_length, primitive_value_to_str
class ByteStream(AsyncByteStream, SyncByteStream):
@ -82,12 +82,17 @@ def encode_content(
if isinstance(content, (bytes, str)):
body = content.encode("utf-8") if isinstance(content, str) else content
content_length = str(len(body))
headers = {"Content-Length": content_length} if body else {}
content_length = len(body)
headers = {"Content-Length": str(content_length)} if body else {}
return headers, ByteStream(body)
elif isinstance(content, Iterable):
headers = {"Transfer-Encoding": "chunked"}
content_length_or_none = peek_filelike_length(content)
if content_length_or_none is None:
headers = {"Transfer-Encoding": "chunked"}
else:
headers = {"Content-Length": str(content_length_or_none)}
return headers, IteratorByteStream(content) # type: ignore
elif isinstance(content, AsyncIterable):

View File

@ -93,9 +93,8 @@ class FileField:
return len(headers) + len(to_bytes(self.file))
# Let's do our best not to read `file` into memory.
try:
file_length = peek_filelike_length(self.file)
except OSError:
file_length = peek_filelike_length(self.file)
if file_length is None:
# As a last resort, read file and cache contents for later.
assert not hasattr(self, "_data")
self._data = to_bytes(self.file.read())

View File

@ -342,7 +342,7 @@ def guess_content_type(filename: typing.Optional[str]) -> typing.Optional[str]:
return None
def peek_filelike_length(stream: typing.IO) -> int:
def peek_filelike_length(stream: typing.Any) -> typing.Optional[int]:
"""
Given a file-like stream object, return its length in number of bytes
without reading it into memory.
@ -350,7 +350,9 @@ def peek_filelike_length(stream: typing.IO) -> int:
try:
# Is it an actual file?
fd = stream.fileno()
except OSError:
# Yup, seems to be an actual file.
length = os.fstat(fd).st_size
except (AttributeError, OSError):
# No... Maybe it's something that supports random access, like `io.BytesIO`?
try:
# Assuming so, go to end of stream to figure out its length,
@ -358,14 +360,11 @@ def peek_filelike_length(stream: typing.IO) -> int:
offset = stream.tell()
length = stream.seek(0, os.SEEK_END)
stream.seek(offset)
except OSError:
except (AttributeError, OSError):
# Not even that? Sorry, we're doomed...
raise
else:
return length
else:
# Yup, seems to be an actual file.
return os.fstat(fd).st_size
return None
return length
class Timer:

View File

@ -48,6 +48,18 @@ async def test_bytes_content():
assert async_content == b"Hello, world!"
@pytest.mark.asyncio
async def test_bytesio_content():
headers, stream = encode_request(content=io.BytesIO(b"Hello, world!"))
assert isinstance(stream, typing.Iterable)
assert not isinstance(stream, typing.AsyncIterable)
content = b"".join([part for part in stream])
assert headers == {"Content-Length": "13"}
assert content == b"Hello, world!"
@pytest.mark.asyncio
async def test_iterator_content():
def hello_world():