Skip to content

Commit 74f3429

Browse files
authored
Release 2.15.900 (#300)
2.15.900 (2025-12-16) ===================== - Improved pre-check for socket liveness probe before connection reuse from pool. - Backported "HTTPHeaderDict bytes key handling" from upstream urllib3#3653 - Backported "Expand environment variable of SSLKEYLOGFILE" from upstream urllib3#3705 - Backported "Fix redirect handling when an integer is passed to a pool manager" from upstream urllib3#3655 - Backported "Improved the performance of content decoding by optimizing ``BytesQueueBuffer`` class." from upstream urllib3#3711 - Backported "GHSA-gm62-xv2j-4w53" security patch for "attacker could compose an HTTP response with virtually unlimited links in the ``Content-Encoding`` header" from upstream urllib3@24d7b67
2 parents 0bac688 + 7cba58e commit 74f3429

File tree

17 files changed

+174
-50
lines changed

17 files changed

+174
-50
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
matrix:
4545
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]
4646
os:
47-
- macos-13
47+
- macos-14-large
4848
- windows-2025
4949
- ubuntu-24.04
5050
nox-session: ['']
@@ -122,10 +122,12 @@ jobs:
122122
- python-version: "3.8"
123123
os: ubuntu-24.04
124124
- python-version: "3.7"
125-
os: macos-13
125+
os: macos-14-large
126+
- python-version: "3.8"
127+
os: macos-14-large
126128

127129
runs-on: ${{ matrix.os }}
128-
name: ${{ fromJson('{"macos-13":"macOS","windows-2025":"Windows","ubuntu-24.04":"Ubuntu"}')[matrix.os] }} ${{ matrix.python-version }} ${{ matrix.nox-session }}
130+
name: ${{ fromJson('{"macos-14-large":"macOS","windows-2025":"Windows","ubuntu-24.04":"Ubuntu"}')[matrix.os] }} ${{ matrix.python-version }} ${{ matrix.nox-session }}
129131
continue-on-error: ${{ matrix.experimental }}
130132
timeout-minutes: 35
131133
steps:

.github/workflows/integration.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
- name: "Setup Python"
2525
uses: "actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c"
2626
with:
27-
python-version: "3.11"
27+
python-version: "3.12"
2828

2929
- name: "Install dependencies"
3030
run: python -m pip install --upgrade nox

.github/workflows/packaging.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@ jobs:
2121
matrix:
2222
python-version: [ "3.7", "3.13" ]
2323
os:
24-
- macos-13
24+
- macos-15
2525
- windows-2022
2626
- ubuntu-22.04
27+
exclude:
28+
- python-version: "3.7"
29+
os: macos-15
2730

2831
steps:
2932
- name: "Checkout repository"
@@ -58,9 +61,12 @@ jobs:
5861
matrix:
5962
python-version: [ "3.7", "3.13" ]
6063
os:
61-
- macos-13
64+
- macos-15
6265
- windows-2022
6366
- ubuntu-22.04
67+
exclude:
68+
- python-version: "3.7"
69+
os: macos-15
6470

6571
steps:
6672
- name: "Checkout repository"

CHANGES.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
2.15.900 (2025-12-16)
2+
=====================
3+
4+
- Improved pre-check for socket liveness probe before connection reuse from pool.
5+
- Backported "HTTPHeaderDict bytes key handling" from upstream https://github.com/urllib3/urllib3/pull/3653
6+
- Backported "Expand environment variable of SSLKEYLOGFILE" from upstream https://github.com/urllib3/urllib3/pull/3705
7+
- Backported "Fix redirect handling when an integer is passed to a pool manager" from upstream https://github.com/urllib3/urllib3/pull/3655
8+
- Backported "Improved the performance of content decoding by optimizing ``BytesQueueBuffer`` class." from upstream https://github.com/urllib3/urllib3/pull/3711
9+
- Backported "GHSA-gm62-xv2j-4w53" security patch for "attacker could compose an HTTP response with virtually unlimited links in the ``Content-Encoding`` header" from upstream https://github.com/urllib3/urllib3/commit/24d7b67eac89f94e11003424bcf0d8f7b72222a8
10+
111
2.14.908 (2025-11-27)
212
=====================
313

src/urllib3/_async/poolmanager.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -105,22 +105,18 @@ def __init__(
105105
) -> None:
106106
super().__init__(headers)
107107

108+
# PoolManager handles redirects itself in PoolManager.urlopen().
109+
# It always passes redirect=False to the underlying connection pool to
110+
# suppress per-pool redirect handling. If the user supplied a non-Retry
111+
# value (int/bool/etc) for retries and we let the pool normalize it
112+
# while redirect=False, the resulting Retry object would have redirect
113+
# handling disabled, which can interfere with PoolManager's own
114+
# redirect logic. Normalize here so redirects remain governed solely by
115+
# PoolManager logic.
108116
if "retries" in connection_pool_kw:
109117
retries = connection_pool_kw["retries"]
110118
if not isinstance(retries, Retry):
111-
# When Retry is initialized, raise_on_redirect is based
112-
# on a redirect boolean value.
113-
# But requests made via a pool manager always set
114-
# redirect to False, and raise_on_redirect always ends
115-
# up being False consequently.
116-
# Here we fix the issue by setting raise_on_redirect to
117-
# a value needed by the pool manager without considering
118-
# the redirect boolean.
119-
120-
raise_on_redirect = retries is not False
121-
122-
retries = Retry.from_int(retries, redirect=False)
123-
retries.raise_on_redirect = raise_on_redirect
119+
retries = Retry.from_int(retries)
124120

125121
connection_pool_kw = connection_pool_kw.copy()
126122
connection_pool_kw["retries"] = retries

src/urllib3/_collections.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,19 @@ def __setitem__(self, key: str, val: str) -> None:
260260
self._container[_lower_wrapper(key)] = [key, val]
261261

262262
def __getitem__(self, key: str) -> str:
263+
if isinstance(key, bytes):
264+
key = key.decode("latin-1")
263265
val = self._container[_lower_wrapper(key)]
264266
return ", ".join(val[1:])
265267

266268
def __delitem__(self, key: str) -> None:
269+
if isinstance(key, bytes):
270+
key = key.decode("latin-1")
267271
del self._container[_lower_wrapper(key)]
268272

269273
def __contains__(self, key: object) -> bool:
274+
if isinstance(key, bytes):
275+
key = key.decode("latin-1")
270276
if isinstance(key, str):
271277
return _lower_wrapper(key) in self._container
272278
return False
@@ -377,6 +383,8 @@ def getlist(
377383
) -> list[str] | _DT:
378384
"""Returns a list of all the values for the named field. Returns an
379385
empty list if the key doesn't exist."""
386+
if isinstance(key, bytes):
387+
key = key.decode("latin-1")
380388
try:
381389
vals = self._container[_lower_wrapper(key)]
382390
except KeyError:

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.14.908"
4+
__version__ = "2.15.900"

src/urllib3/poolmanager.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -242,22 +242,18 @@ def __init__(
242242
) -> None:
243243
super().__init__(headers)
244244

245+
# PoolManager handles redirects itself in PoolManager.urlopen().
246+
# It always passes redirect=False to the underlying connection pool to
247+
# suppress per-pool redirect handling. If the user supplied a non-Retry
248+
# value (int/bool/etc) for retries and we let the pool normalize it
249+
# while redirect=False, the resulting Retry object would have redirect
250+
# handling disabled, which can interfere with PoolManager's own
251+
# redirect logic. Normalize here so redirects remain governed solely by
252+
# PoolManager logic.
245253
if "retries" in connection_pool_kw:
246254
retries = connection_pool_kw["retries"]
247255
if not isinstance(retries, Retry):
248-
# When Retry is initialized, raise_on_redirect is based
249-
# on a redirect boolean value.
250-
# But requests made via a pool manager always set
251-
# redirect to False, and raise_on_redirect always ends
252-
# up being False consequently.
253-
# Here we fix the issue by setting raise_on_redirect to
254-
# a value needed by the pool manager without considering
255-
# the redirect boolean.
256-
257-
raise_on_redirect = retries is not False
258-
259-
retries = Retry.from_int(retries, redirect=False)
260-
retries.raise_on_redirect = raise_on_redirect
256+
retries = Retry.from_int(retries)
261257

262258
connection_pool_kw = connection_pool_kw.copy()
263259
connection_pool_kw["retries"] = retries

src/urllib3/response.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,8 +206,20 @@ class MultiDecoder(ContentDecoder):
206206
they were applied.
207207
"""
208208

209+
# Maximum allowed number of chained HTTP encodings in the
210+
# Content-Encoding header.
211+
max_decode_links = 5
212+
209213
def __init__(self, modes: str) -> None:
210-
self._decoders = [_get_decoder(m.strip()) for m in modes.split(",")]
214+
encodings = [m.strip() for m in modes.split(",")]
215+
216+
if len(encodings) > self.max_decode_links:
217+
raise DecodeError(
218+
"Too many content encodings in the chain: "
219+
f"{len(encodings)} > {self.max_decode_links}"
220+
)
221+
222+
self._decoders = [_get_decoder(e) for e in encodings]
211223

212224
def flush(self) -> bytes:
213225
return self._decoders[0].flush()

src/urllib3/util/response.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,10 @@ class BytesQueueBuffer:
6363
6464
* self.buffer, which contains the full data
6565
* the largest chunk that we will copy in get()
66-
67-
The worst case scenario is a single chunk, in which case we'll make a full copy of
68-
the data inside get().
6966
"""
7067

7168
def __init__(self) -> None:
72-
self.buffer: typing.Deque[bytes] = collections.deque()
69+
self.buffer: typing.Deque[bytes | memoryview[bytes]] = collections.deque()
7370
self._size: int = 0
7471

7572
def __len__(self) -> int:
@@ -87,13 +84,18 @@ def get(self, n: int) -> bytes:
8784
elif n < 0:
8885
raise ValueError("n should be > 0")
8986

87+
if len(self.buffer[0]) == n and isinstance(self.buffer[0], bytes):
88+
self._size -= n
89+
return self.buffer.popleft()
90+
9091
fetched = 0
9192
ret = io.BytesIO()
9293
while fetched < n:
9394
remaining = n - fetched
9495
chunk = self.buffer.popleft()
9596
chunk_length = len(chunk)
9697
if remaining < chunk_length:
98+
chunk = memoryview(chunk)
9799
left_chunk, right_chunk = chunk[:remaining], chunk[remaining:]
98100
ret.write(left_chunk)
99101
self.buffer.appendleft(right_chunk)

0 commit comments

Comments
 (0)