Skip to content

Commit e05c705

Browse files
authored
Release 2.17.900 (#319)
2.17.900 (2026-03-03) ===================== - Added GRO/GSO support for Linux users in order to leverage UDP coalescing when available. This should increase QUIC/UDP performances in high throughput networking environment (very-fast NIC). Expect no more than 10 to 20% improvement usually. - Changed timings in our ``AsyncPoliceTraffic`` scheduler implementation in order to not penalize fast servers. - Fixed passing non-str/non-bytes as an header value. It was previously allowed by accident from urllib3. (#318)
2 parents 9af6d00 + 90cdeae commit e05c705

File tree

20 files changed

+1264
-262
lines changed

20 files changed

+1264
-262
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ jobs:
208208
run: |
209209
python -m coverage combine
210210
python -m coverage html --skip-covered --skip-empty
211-
python -m coverage report --ignore-errors --show-missing --fail-under=86
211+
python -m coverage report --ignore-errors --show-missing --fail-under=80
212212
213213
- name: "Upload report"
214214
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f

CHANGES.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
2.17.900 (2026-03-03)
2+
=====================
3+
4+
- Added GRO/GSO support for Linux users in order to leverage UDP coalescing when available.
5+
This should increase QUIC/UDP performances in high throughput networking environment (very-fast NIC).
6+
Expect no more than 10 to 20% improvement usually.
7+
- Changed timings in our ``AsyncPoliceTraffic`` scheduler implementation in order to not penalize fast servers.
8+
- Fixed passing non-str/non-bytes as an header value. It was previously allowed by accident from urllib3. (#318)
9+
110
2.16.900 (2026-02-22)
211
=====================
312

src/urllib3/_constant.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,9 @@ def __new__(
240240

241241
TCP_DEFAULT_BLOCKSIZE: int = DEFAULT_BLOCKSIZE
242242

243+
UDP_LINUX_GRO: int = 104
244+
UDP_LINUX_SEGMENT: int = 103
245+
243246
# Mozilla TLS recommendations for ciphers
244247
# General-purpose servers with a variety of clients, recommended for almost all systems.
245248
MOZ_INTERMEDIATE_CIPHERS: str = "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305"

src/urllib3/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# This file is protected via CODEOWNERS
22
from __future__ import annotations
33

4-
__version__ = "2.16.900"
4+
__version__ = "2.17.900"

src/urllib3/backend/_async/hface.py

Lines changed: 98 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -780,7 +780,11 @@ async def peek_and_react(self, expect_frame: bool = False) -> bool:
780780
return False
781781

782782
try:
783-
self._protocol.bytes_received(peek_data)
783+
if isinstance(peek_data, list):
784+
for gro_segment in peek_data:
785+
self._protocol.bytes_received(gro_segment)
786+
else:
787+
self._protocol.bytes_received(peek_data)
784788
except self._protocol.exceptions():
785789
return False
786790

@@ -819,7 +823,7 @@ async def __exchange_until(
819823
maximal_data_in_read = None
820824

821825
data_out: bytes
822-
data_in: bytes
826+
data_in: bytes | list[bytes]
823827

824828
data_in_len: int = 0
825829

@@ -839,13 +843,21 @@ async def __exchange_until(
839843
reach_socket: bool = False
840844
if not self._protocol.has_pending_event(stream_id=stream_id):
841845
if receive_first is False:
846+
tbs = []
847+
842848
while True:
843849
data_out = self._protocol.bytes_to_send()
844850

845851
if not data_out:
846852
break
847853

848-
await self.sock.sendall(data_out)
854+
tbs.append(data_out)
855+
856+
if self._svn is HttpVersion.h3 and len(tbs) > 1:
857+
await self.sock.sendall(tbs)
858+
else:
859+
for chunk in tbs:
860+
await self.sock.sendall(chunk)
849861

850862
try:
851863
data_in = await self.sock.recv(self.blocksize)
@@ -880,30 +892,62 @@ async def __exchange_until(
880892
else:
881893
self._protocol.connection_lost()
882894
else:
883-
if data_in_len_from is None:
884-
data_in_len += len(data_in)
895+
if isinstance(data_in, list):
896+
for udp_gro_segment in data_in:
897+
if data_in_len_from is None:
898+
data_in_len += len(udp_gro_segment)
885899

886-
try:
887-
self._protocol.bytes_received(data_in)
888-
except self._protocol.exceptions() as e:
889-
# h2 has a dedicated exception for IncompleteRead (InvalidBodyLengthError)
890-
# we convert the exception to our "IncompleteRead" instead.
891-
if hasattr(e, "expected_length") and hasattr(
892-
e, "actual_length"
893-
):
894-
raise IncompleteRead(
895-
partial=e.actual_length, expected=e.expected_length
896-
) from e # Defensive:
897-
raise ProtocolError(e) from e # Defensive:
900+
try:
901+
self._protocol.bytes_received(udp_gro_segment)
902+
except self._protocol.exceptions() as e:
903+
# h2 has a dedicated exception for IncompleteRead (InvalidBodyLengthError)
904+
# we convert the exception to our "IncompleteRead" instead.
905+
if hasattr(e, "expected_length") and hasattr(
906+
e, "actual_length"
907+
):
908+
raise IncompleteRead(
909+
partial=e.actual_length,
910+
expected=e.expected_length,
911+
) from e # Defensive:
912+
raise ProtocolError(e) from e # Defensive:
913+
else:
914+
incoming_buffer_size = len(data_in)
915+
self._recv_size_ema = (
916+
self._recv_size_ema * 0.7 + incoming_buffer_size * 0.3
917+
)
918+
919+
if data_in_len_from is None:
920+
data_in_len += incoming_buffer_size
921+
922+
try:
923+
self._protocol.bytes_received(data_in)
924+
except self._protocol.exceptions() as e:
925+
# h2 has a dedicated exception for IncompleteRead (InvalidBodyLengthError)
926+
# we convert the exception to our "IncompleteRead" instead.
927+
if hasattr(e, "expected_length") and hasattr(
928+
e, "actual_length"
929+
):
930+
raise IncompleteRead(
931+
partial=e.actual_length, expected=e.expected_length
932+
) from e # Defensive:
933+
raise ProtocolError(e) from e # Defensive:
898934

899935
if receive_first is True:
936+
tbs = []
937+
900938
while True:
901939
data_out = self._protocol.bytes_to_send()
902940

903941
if not data_out:
904942
break
905943

906-
await self.sock.sendall(data_out)
944+
tbs.append(data_out)
945+
946+
if self._svn is HttpVersion.h3 and len(tbs) > 1:
947+
await self.sock.sendall(tbs)
948+
else:
949+
for chunk in tbs:
950+
await self.sock.sendall(chunk)
907951

908952
for event in self._protocol.events(stream_id=stream_id): # type: Event
909953
stream_related_event: bool = hasattr(event, "stream_id")
@@ -1157,15 +1201,21 @@ def putheader(self, header: str, *values: str) -> None:
11571201
f"Invalid content-length set. Given '{values[0]}' when only digits are allowed."
11581202
)
11591203
elif self.__legacy_host_entry is None and encoded_header == b"host":
1160-
self.__legacy_host_entry = (
1161-
values[0].encode("idna") if isinstance(values[0], str) else values[0]
1162-
)
1204+
if isinstance(values[0], str):
1205+
self.__legacy_host_entry = values[0].encode("idna")
1206+
elif isinstance(values[0], bytes):
1207+
self.__legacy_host_entry = values[0]
1208+
else:
1209+
self.__legacy_host_entry = str(values[0]).encode("iso-8859-1")
11631210
return
11641211

11651212
for value in values:
1166-
encoded_value = (
1167-
value.encode("iso-8859-1") if isinstance(value, str) else value
1168-
)
1213+
if isinstance(value, str):
1214+
encoded_value = value.encode("iso-8859-1")
1215+
elif isinstance(value, bytes):
1216+
encoded_value = value
1217+
else: # best effort branch
1218+
encoded_value = str(value).encode("iso-8859-1")
11691219

11701220
if encoded_header.startswith(b":"):
11711221
if encoded_header == b":protocol":
@@ -1256,11 +1306,19 @@ async def endheaders( # type: ignore[override]
12561306
raise ProtocolError(e) from e # Defensive:
12571307

12581308
try:
1309+
tbs = []
1310+
12591311
while True:
12601312
buf = self._protocol.bytes_to_send()
12611313
if not buf:
12621314
break
1263-
await self.sock.sendall(buf)
1315+
tbs.append(buf)
1316+
1317+
if self._svn is HttpVersion.h3 and len(tbs) > 1:
1318+
await self.sock.sendall(tbs)
1319+
else:
1320+
for chunk in tbs:
1321+
await self.sock.sendall(chunk)
12641322
except BrokenPipeError as e:
12651323
rp = ResponsePromise(self, self._stream_id, self.__headers)
12661324
self._promises[rp.uid] = rp
@@ -1294,7 +1352,13 @@ async def __write_st(
12941352
self._protocol.should_wait_remote_flow_control(__stream_id, len(__buf))
12951353
is True
12961354
):
1297-
self._protocol.bytes_received(await self.sock.recv(self.blocksize))
1355+
data_in = await self.sock.recv(self.blocksize)
1356+
1357+
if isinstance(data_in, list):
1358+
for gro_segment in data_in:
1359+
self._protocol.bytes_received(gro_segment)
1360+
else:
1361+
self._protocol.bytes_received(data_in)
12981362

12991363
while True:
13001364
data_out = self._protocol.bytes_to_send()
@@ -1612,7 +1676,13 @@ async def send( # type: ignore[override]
16121676
)
16131677
is True
16141678
):
1615-
self._protocol.bytes_received(await self.sock.recv(self.blocksize))
1679+
data_in = await self.sock.recv(self.blocksize)
1680+
1681+
if not isinstance(data_in, list):
1682+
self._protocol.bytes_received(data_in)
1683+
else:
1684+
for gro_segment in data_in:
1685+
self._protocol.bytes_received(gro_segment)
16161686

16171687
# this is a bad sign. we should stop sending and instead retrieve the response.
16181688
if self._protocol.has_pending_event(
@@ -1758,3 +1828,4 @@ async def close(self) -> None: # type: ignore[override]
17581828
self._cached_http_vsn = None
17591829
self._connected_at = None
17601830
self._last_used_at = time.monotonic()
1831+
self._recv_size_ema = 0.0

src/urllib3/backend/_base.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,9 +535,17 @@ def __init__(
535535
self._connected_at: float | None = None
536536
self._last_used_at: float = time.monotonic()
537537

538+
self._recv_size_ema: float = 0.0
539+
538540
def __contains__(self, item: ResponsePromise) -> bool:
539541
return item.uid in self._promises
540542

543+
@property
544+
def _fast_recv_mode(self) -> bool:
545+
if len(self._promises) <= 1 or self._svn is HttpVersion.h3:
546+
return True
547+
return self._recv_size_ema >= 1450
548+
541549
@property
542550
def last_used_at(self) -> float:
543551
return self._last_used_at

0 commit comments

Comments
 (0)