Skip to content

Commit

Permalink
🎨 minor adjustments for compatibility purposes (#128)
Browse files Browse the repository at this point in the history
2.8.901 (2024-06-27)
====================

- Improved compatibility with httplib exception for ``IncompleteRead``
that did not behave exactly like expected (repr/str format over it).
- The metric TLS handshake delay was wrongfully set when using HTTP/2
over cleartext.
- Fixed compatibility with some third-party mocking library that are
injecting io.BytesIO in HTTPResponse.
  In some cases, ``IncompleteRead`` might not be raised like expected.
  • Loading branch information
Ousret committed Jun 27, 2024
1 parent 39f656d commit b8037d0
Show file tree
Hide file tree
Showing 9 changed files with 59 additions and 18 deletions.
8 changes: 8 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
2.8.901 (2024-06-27)
====================

- Improved compatibility with httplib exception for ``IncompleteRead`` that did not behave exactly like expected (repr/str format over it).
- The metric TLS handshake delay was wrongfully set when using HTTP/2 over cleartext.
- Fixed compatibility with some third-party mocking library that are injecting io.BytesIO in HTTPResponse.
In some cases, ``IncompleteRead`` might not be raised like expected.

2.8.900 (2024-06-24)
====================

Expand Down
18 changes: 16 additions & 2 deletions src/urllib3/_async/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,20 @@ async def _raw_read( # type: ignore[override]

async with self._error_catcher():
data = (await self._fp_read(amt)) if not fp_closed else b""
if amt is not None and amt != 0 and not data:

# Mocking library often use io.BytesIO
# which does not auto-close when reading data
# with amt=None.
is_foreign_fp_unclosed = (
amt is None and getattr(self._fp, "closed", False) is False
)

if (amt is not None and amt != 0 and not data) or is_foreign_fp_unclosed:
if is_foreign_fp_unclosed:
self._fp_bytes_read += len(data)
if self.length_remaining is not None:
self.length_remaining -= len(data)

# Platform-specific: Buggy versions of Python.
# Close the connection when no data is returned
#
Expand All @@ -308,10 +321,11 @@ async def _raw_read( # type: ignore[override]
# Content-Length are caught.
raise IncompleteRead(self._fp_bytes_read, self.length_remaining)

if data:
if data and not is_foreign_fp_unclosed:
self._fp_bytes_read += len(data)
if self.length_remaining is not None:
self.length_remaining -= len(data)

return data

async def read( # type: ignore[override]
Expand Down
2 changes: 1 addition & 1 deletion src/urllib3/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# This file is protected via CODEOWNERS
from __future__ import annotations

__version__ = "2.8.900"
__version__ = "2.8.901"
6 changes: 5 additions & 1 deletion src/urllib3/backend/_async/hface.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,11 @@ async def _post_conn(self) -> None: # type: ignore[override]
# save the quic ticket for session resumption
self.__session_ticket = self._protocol.session_ticket

if hasattr(self, "_connect_timings"):
if (
self.conn_info.certificate_der
and hasattr(self, "_connect_timings")
and not self.conn_info.tls_handshake_latency
):
self.conn_info.tls_handshake_latency = (
datetime.now(tz=timezone.utc) - self._connect_timings[-1]
)
Expand Down
6 changes: 5 additions & 1 deletion src/urllib3/backend/hface.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,11 @@ def _post_conn(self) -> None:
# save the quic ticket for session resumption
self.__session_ticket = self._protocol.session_ticket

if hasattr(self, "_connect_timings"):
if (
self.conn_info.certificate_der
and hasattr(self, "_connect_timings")
and not self.conn_info.tls_handshake_latency
):
self.conn_info.tls_handshake_latency = (
datetime.now(tz=timezone.utc) - self._connect_timings[-1]
)
Expand Down
14 changes: 6 additions & 8 deletions src/urllib3/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,18 +265,16 @@ class IncompleteRead(ProtocolError):
for ``partial`` to avoid creating large objects on streamed reads.
"""

def __init__(self, partial: int, expected: int) -> None:
super().__init__(
f"peer closed connection without sending complete message body (received {partial} bytes, expected {expected} more)"
)
def __init__(self, partial: int, expected: int | None = None) -> None:
self.partial = partial
self.expected = expected

def __repr__(self) -> str:
return "IncompleteRead(%i bytes read, %i more expected)" % (
self.partial,
self.expected,
)
if self.expected is not None:
return f"IncompleteRead({self.partial} bytes read, {self.expected} more expected)"
return f"IncompleteRead({self.partial} bytes read)"

__str__ = object.__str__


class InvalidChunkLength(ProtocolError):
Expand Down
17 changes: 15 additions & 2 deletions src/urllib3/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -760,7 +760,19 @@ def _raw_read(

with self._error_catcher():
data = self._fp_read(amt) if not fp_closed else b""
if amt is not None and amt != 0 and not data:

# Mocking library often use io.BytesIO
# which does not auto-close when reading data
# with amt=None.
is_foreign_fp_unclosed = (
amt is None and getattr(self._fp, "closed", False) is False
)

if (amt is not None and amt != 0 and not data) or is_foreign_fp_unclosed:
if is_foreign_fp_unclosed:
self._fp_bytes_read += len(data)
if self.length_remaining is not None:
self.length_remaining -= len(data)
# Platform-specific: Buggy versions of Python.
# Close the connection when no data is returned
#
Expand All @@ -782,10 +794,11 @@ def _raw_read(
# Content-Length are caught.
raise IncompleteRead(self._fp_bytes_read, self.length_remaining)

if data:
if data and not is_foreign_fp_unclosed:
self._fp_bytes_read += len(data)
if self.length_remaining is not None:
self.length_remaining -= len(data)

return data

def read(
Expand Down
4 changes: 2 additions & 2 deletions test/test_response.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ def test_preload(self) -> None:

r = HTTPResponse(fp, preload_content=True)

assert fp.tell() == len(b"foo")
assert fp.closed is True
assert r.data == b"foo"

def test_no_preload(self) -> None:
Expand All @@ -148,7 +148,7 @@ def test_no_preload(self) -> None:

assert fp.tell() == 0
assert r.data == b"foo"
assert fp.tell() == len(b"foo")
assert fp.closed is True

def test_decode_bad_data(self) -> None:
fp = BytesIO(b"\x00" * 10)
Expand Down
2 changes: 1 addition & 1 deletion test/with_dummyserver/test_socketlevel.py
Original file line number Diff line number Diff line change
Expand Up @@ -2047,7 +2047,7 @@ def socket_handler(listener: socket.socket) -> None:
)
data = get_response.stream(100)
with pytest.raises(
IncompleteRead, match="received 12 bytes, expected 10 more"
IncompleteRead, match=r"12 bytes read\, 10 more expected"
):
next(data)
done_event.set()
Expand Down

0 comments on commit b8037d0

Please sign in to comment.