From 83a85fa69d82966bbdbd6bb79d24fa74ede6bbd6 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Tue, 31 Mar 2026 12:07:33 -1000
Subject: [PATCH 001/191] Incremnet version to 3.13.5.dev0
---
aiohttp/__init__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py
index 404f9267748..bc8bcea47ed 100644
--- a/aiohttp/__init__.py
+++ b/aiohttp/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "3.13.5"
+__version__ = "3.13.5.dev0"
from typing import TYPE_CHECKING, Tuple
From 5b8c540dfcbd473de9f7f18f499561c0cb8e09c5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 3 Apr 2026 10:51:35 +0000
Subject: [PATCH 002/191] Bump pypa/cibuildwheel from 3.4.0 to 3.4.1 (#12314)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from
3.4.0 to 3.4.1.
Release notes
Sourced from pypa/cibuildwheel's
releases.
v3.4.1
- ⚠️ Building for the experimental CPython 3.13 free-threading variant
is now deprecated. That functionality will be removed in the next minor
release. The
enable
option cpython-freethreading is therefore also deprecated.
Builds specifying enable = "all" no longer select
cpython-freethreading. CPython 3.14 free-threading support
remains available without the enable flag. (#2787)
- 🐛 iOS builds will no longer skip
repair-wheel-command
if it's defined in config (#2761)
- 🐛 Fix bug causing
uv to fail when environments define
PYTHON_VERSION or UV_PYTHON, conflicting with our venvs (#2795)
- ✨ cibuildwheel prints the selected build identifiers at the start of
the build. (#2785)
- 🔐 The GitHub Action now references other actions with a full SHA (#2744)
Changelog
Sourced from pypa/cibuildwheel's
changelog.
v3.4.1
2 April 2026
- ⚠️ Building for the experimental CPython 3.13 free-threading variant
is now deprecated. That functionality will be removed in the next minor
release. The
enable
option cpython-freethreading is therefore also deprecated.
Builds specifying enable = "all" no longer select
cpython-freethreading. CPython 3.14 free-threading support
remains available without the enable flag. (#2787)
- 🐛 iOS builds will no longer skip
repair-wheel-command
if it's defined in config (#2761)
- 🐛 Fix bug causing
uv to fail when environments define
PYTHON_VERSION or UV_PYTHON, conflicting with our venvs (#2795)
- ✨ cibuildwheel prints the selected build identifiers at the start of
the build. (#2785)
- 🔐 The GitHub Action now references other actions with a full SHA (#2744)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/ci-cd.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 0e513530162..7443b5061ae 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -552,7 +552,7 @@ jobs:
run: |
make cythonize
- name: Build wheels
- uses: pypa/cibuildwheel@v3.4.0
+ uses: pypa/cibuildwheel@v3.4.1
env:
CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }}
CIBW_ARCHS_MACOS: x86_64 arm64 universal2
From d20f8d64619256177e9c1a3bd184aaccebd160e1 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Mon, 6 Apr 2026 22:03:54 +0100
Subject: [PATCH 003/191] [PR #12307/f4dc0dee backport][3.14] Fix spurious
"Future exception was never retrieved" warning on disconnect during
backpressure (#12326)
**This is a backport of PR #12307 as merged into master
(f4dc0deedbae67a94b0ab86b6a0e748249f26d83).**
Co-authored-by: availov <51930102+availov@users.noreply.github.com>
---
CHANGES/12281.bugfix.rst | 1 +
CONTRIBUTORS.txt | 1 +
aiohttp/base_protocol.py | 2 +-
tests/test_web_server.py | 53 ++++++++++++++++++++++++++++++++++++++--
4 files changed, 54 insertions(+), 3 deletions(-)
create mode 100644 CHANGES/12281.bugfix.rst
diff --git a/CHANGES/12281.bugfix.rst b/CHANGES/12281.bugfix.rst
new file mode 100644
index 00000000000..63521a73b1c
--- /dev/null
+++ b/CHANGES/12281.bugfix.rst
@@ -0,0 +1 @@
+Fixed spurious ``Future exception was never retrieved`` warning on disconnect during back-pressure -- by :user:`availov`.
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 27d04363b7f..530b6683d54 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -410,6 +410,7 @@ Yegor Roganov
Yifei Kong
Young-Ho Cha
Yuriy Shatrov
+Yury Novikov
Yury Pliner
Yury Selivanov
Yusuke Tsutsumi
diff --git a/aiohttp/base_protocol.py b/aiohttp/base_protocol.py
index 7f01830f4e9..d7d83425b88 100644
--- a/aiohttp/base_protocol.py
+++ b/aiohttp/base_protocol.py
@@ -97,4 +97,4 @@ async def _drain_helper(self) -> None:
if waiter is None:
waiter = self._loop.create_future()
self._drain_waiter = waiter
- await asyncio.shield(waiter)
+ await waiter
diff --git a/tests/test_web_server.py b/tests/test_web_server.py
index 09b7d0bc71b..59c5da744a6 100644
--- a/tests/test_web_server.py
+++ b/tests/test_web_server.py
@@ -1,14 +1,15 @@
import asyncio
+import gc
import socket
from contextlib import suppress
-from typing import NoReturn
+from typing import Any, NoReturn
from unittest import mock
import pytest
from aiohttp import client, web
from aiohttp.http_exceptions import BadHttpMethod, BadStatusLine
-from aiohttp.pytest_plugin import AiohttpClient, AiohttpRawServer
+from aiohttp.pytest_plugin import AiohttpClient, AiohttpRawServer, AiohttpServer
async def test_simple_server(aiohttp_raw_server, aiohttp_client) -> None:
@@ -417,3 +418,51 @@ async def on_request(_: web.Request) -> web.Response:
assert done_event.is_set()
finally:
await asyncio.gather(runner.shutdown(), site.stop())
+
+
+async def test_no_future_warning_on_disconnect_during_backpressure(
+ aiohttp_server: AiohttpServer,
+) -> None:
+ loop = asyncio.get_running_loop()
+ exc_handler_calls: list[dict[str, Any]] = []
+ original_handler = loop.get_exception_handler()
+ loop.set_exception_handler(lambda _loop, ctx: exc_handler_calls.append(ctx))
+ protocol = None
+
+ async def handler(request: web.Request) -> NoReturn:
+ nonlocal protocol
+ protocol = request.protocol
+ resp = web.StreamResponse()
+ await resp.prepare(request)
+ while True:
+ await resp.write(b"x" * 65536)
+
+ app = web.Application()
+ app.router.add_route("GET", "/", handler)
+ # aiohttp_server enables handler_cancellation by default so the handler
+ # task is cancelled when connection_lost() fires.
+ server = await aiohttp_server(app)
+
+ # Open a raw asyncio connection so we control exactly when the client
+ # side closes.
+ reader, writer = await asyncio.open_connection(server.host, server.port)
+ writer.write(b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
+ await writer.drain()
+
+ try:
+ # Poll until the server protocol reports that writing is paused.
+ async def wait_for_backpressure() -> None:
+ while protocol is None or not protocol.writing_paused:
+ await asyncio.sleep(0.01)
+
+ await asyncio.wait_for(wait_for_backpressure(), timeout=5.0)
+
+ writer.close()
+ await asyncio.sleep(0.1)
+
+ gc.collect()
+ await asyncio.sleep(0)
+ finally:
+ loop.set_exception_handler(original_handler)
+
+ assert not exc_handler_calls
From 2acea2d7b383f40780d9d8188b260dcea124020e Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 7 Apr 2026 00:56:47 +0100
Subject: [PATCH 004/191] [PR #12328/522439ac backport][3.14] Fix
GunicornWebWorker failing to reset SIGCHLD handler (#12329)
**This is a backport of PR #12328 as merged into master
(522439ace1c695c5b73d4c27f2d67bba37277fa7).**
Co-authored-by: bahtyar <34988899+Bahtya@users.noreply.github.com>
---
aiohttp/worker.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/aiohttp/worker.py b/aiohttp/worker.py
index 27e8a8a002c..a86573b1d45 100644
--- a/aiohttp/worker.py
+++ b/aiohttp/worker.py
@@ -191,8 +191,12 @@ def init_signals(self) -> None:
# by interrupting system calls
signal.siginterrupt(signal.SIGTERM, False)
signal.siginterrupt(signal.SIGUSR1, False)
- # Reset signals so Gunicorn doesn't swallow subprocess return codes
- # See: https://github.com/aio-libs/aiohttp/issues/6130
+
+ # Reset SIGCHLD to default so Gunicorn doesn't swallow subprocess
+ # return codes. Without this, workers inherit the master arbiter's
+ # SIGCHLD handler, causing spurious "Worker exited" errors when
+ # application code spawns subprocesses.
+ signal.signal(signal.SIGCHLD, signal.SIG_DFL)
def handle_quit(self, sig: int, frame: FrameType | None) -> None:
self.alive = False
From c68f7712713edcb0f38e8845c33c7c68bf938ca6 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 7 Apr 2026 13:27:51 +0100
Subject: [PATCH 005/191] [PR #12330/fc67cfdf backport][3.14] Increase some
size in client benchmark tests (#12333)
**This is a backport of PR #12330 as merged into master
(fc67cfdfd7d4bbf53ef76515fae69726626fe256).**
Co-authored-by: Sam Bull
---
tests/test_benchmarks_client.py | 29 +++++++++++++++--------------
1 file changed, 15 insertions(+), 14 deletions(-)
diff --git a/tests/test_benchmarks_client.py b/tests/test_benchmarks_client.py
index 5e205549e9c..8e75ef9c040 100644
--- a/tests/test_benchmarks_client.py
+++ b/tests/test_benchmarks_client.py
@@ -177,14 +177,14 @@ def _run() -> None:
loop.run_until_complete(run_client_benchmark())
-def test_one_hundred_get_requests_with_512kib_chunked_payload(
+def test_one_hundred_get_requests_with_10mb_chunked_payload(
loop: asyncio.AbstractEventLoop,
aiohttp_client: AiohttpClient,
benchmark: BenchmarkFixture,
) -> None:
- """Benchmark 100 GET requests with a payload of 512KiB using read."""
+ """Benchmark 100 GET requests with a payload of 10 MiB using read."""
message_count = 100
- payload = b"a" * (2**19)
+ payload = b"a" * (10 * 2**20)
async def handler(request: web.Request) -> web.Response:
resp = web.Response(body=payload)
@@ -206,14 +206,14 @@ def _run() -> None:
loop.run_until_complete(run_client_benchmark())
-def test_one_hundred_get_requests_iter_chunks_on_512kib_chunked_payload(
+def test_one_hundred_get_requests_iter_chunks_on_10mb_chunked_payload(
loop: asyncio.AbstractEventLoop,
aiohttp_client: AiohttpClient,
benchmark: BenchmarkFixture,
) -> None:
- """Benchmark 100 GET requests with a payload of 512KiB using iter_chunks."""
+ """Benchmark 100 GET requests with a payload of 10 MiB using iter_chunks."""
message_count = 100
- payload = b"a" * (2**19)
+ payload = b"a" * (10 * 2**20)
async def handler(request: web.Request) -> web.Response:
resp = web.Response(body=payload)
@@ -327,14 +327,14 @@ def _run() -> None:
loop.run_until_complete(run_client_benchmark())
-def test_one_hundred_get_requests_with_512kib_content_length_payload(
+def test_one_hundred_get_requests_with_10mb_content_length_payload(
loop: asyncio.AbstractEventLoop,
aiohttp_client: AiohttpClient,
benchmark: BenchmarkFixture,
) -> None:
- """Benchmark 100 GET requests with a payload of 512KiB."""
+ """Benchmark 100 GET requests with a payload of 10 MiB."""
message_count = 100
- payload = b"a" * (2**19)
+ payload = b"a" * (10 * 2**20)
headers = {hdrs.CONTENT_LENGTH: str(len(payload))}
async def handler(request: web.Request) -> web.Response:
@@ -471,14 +471,15 @@ def _run() -> None:
loop.run_until_complete(run_client_benchmark())
-def test_ten_streamed_responses_iter_chunked_65536(
+def test_ten_streamed_responses_iter_chunked_1mb(
loop: asyncio.AbstractEventLoop,
aiohttp_client: AiohttpClient,
benchmark: BenchmarkFixture,
) -> None:
- """Benchmark 10 streamed responses using iter_chunked 65536."""
+ """Benchmark 10 streamed responses using iter_chunked 1 MiB."""
message_count = 10
- data = b"x" * 65536 # 64 KiB chunk size, 64 KiB iter_chunked
+ MB = 2**20
+ data = b"x" * 10 * MB
async def handler(request: web.Request) -> web.StreamResponse:
resp = web.StreamResponse()
@@ -494,7 +495,7 @@ async def run_client_benchmark() -> None:
client = await aiohttp_client(app)
for _ in range(message_count):
resp = await client.get("/")
- async for _ in resp.content.iter_chunked(65536):
+ async for _ in resp.content.iter_chunked(MB):
pass
await client.close()
@@ -510,7 +511,7 @@ def test_ten_streamed_responses_iter_chunks(
) -> None:
"""Benchmark 10 streamed responses using iter_chunks."""
message_count = 10
- data = b"x" * 65536 # 64 KiB chunk size
+ data = b"x" * 2**20
async def handler(request: web.Request) -> web.StreamResponse:
resp = web.StreamResponse()
From 4cad2570d7c46868f2da48ec9dac915a700cd25c Mon Sep 17 00:00:00 2001
From: digiscrypt
Date: Tue, 7 Apr 2026 18:43:55 +0530
Subject: [PATCH 006/191] [3.14] Harden cookie file permissions in
CookieJar.save() (#12335)
---
CHANGES/12312.bugfix.rst | 1 +
aiohttp/cookiejar.py | 13 +++++++++++--
tests/test_cookiejar.py | 36 ++++++++++++++++++++++++++++++++++++
3 files changed, 48 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12312.bugfix.rst
diff --git a/CHANGES/12312.bugfix.rst b/CHANGES/12312.bugfix.rst
new file mode 100644
index 00000000000..a7d240ad79c
--- /dev/null
+++ b/CHANGES/12312.bugfix.rst
@@ -0,0 +1 @@
+``Cookiejar.save()`` now uses ``0x600`` permissions to better protect them from being read by other users -- by :user:`digiscrypt`.
diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py
index 098e7ee33a5..a0330ec901a 100644
--- a/aiohttp/cookiejar.py
+++ b/aiohttp/cookiejar.py
@@ -5,7 +5,7 @@
import heapq
import itertools
import json
-import os # noqa
+import os
import pathlib
import pickle
import re
@@ -174,7 +174,16 @@ def save(self, file_path: PathLike) -> None:
if attr_val:
morsel_data[attr] = attr_val
data[key][name] = morsel_data
- with file_path.open(mode="w", encoding="utf-8") as f:
+
+ # Cookie persistence may include authentication/session tokens.
+ # Use 0o600 at creation time to avoid umask-dependent overexposure
+ # and enforce least-privilege access to sensitive credential data.
+ with open(
+ file_path,
+ mode="w",
+ encoding="utf-8",
+ opener=lambda path, flags: os.open(path, flags, 0o600),
+ ) as f:
json.dump(data, f, indent=2)
def load(self, file_path: PathLike) -> None:
diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py
index 00586145524..d3001191fbd 100644
--- a/tests/test_cookiejar.py
+++ b/tests/test_cookiejar.py
@@ -3,8 +3,10 @@
import heapq
import itertools
import logging
+import os
import pathlib
import pickle
+import stat
import sys
import unittest
from http.cookies import BaseCookie, Morsel, SimpleCookie
@@ -1812,6 +1814,40 @@ async def test_save_load_json_secure_cookies(tmp_path: Path) -> None:
assert cookie["domain"] == "example.com"
+@pytest.mark.skipif(
+ os.name != "posix", reason="POSIX permission bits are required for this test"
+)
+def test_save_creates_private_cookie_file(tmp_path: Path, loop) -> None:
+ file_path = tmp_path / "private-cookies.json"
+ jar = CookieJar(loop=loop)
+ jar.update_cookies_from_headers(
+ ["token=abc123; Path=/"], URL("https://example.com/")
+ )
+
+ jar.save(file_path=file_path)
+
+ assert file_path.exists()
+ assert stat.S_IMODE(file_path.stat().st_mode) == 0o600
+
+
+@pytest.mark.skipif(
+ os.name != "posix", reason="POSIX permission bits are required for this test"
+)
+def test_save_preserves_existing_cookie_file_permissions(tmp_path: Path, loop) -> None:
+ file_path = tmp_path / "existing-cookies.json"
+ file_path.write_text("{}", encoding="utf-8")
+ file_path.chmod(0o644)
+
+ jar = CookieJar(loop=loop)
+ jar.update_cookies_from_headers(
+ ["token=abc123; Path=/"], URL("https://example.com/")
+ )
+
+ jar.save(file_path=file_path)
+
+ assert stat.S_IMODE(file_path.stat().st_mode) == 0o644
+
+
async def test_cookie_jar_unsafe_property() -> None:
jar_safe = CookieJar()
assert jar_safe.unsafe is False
From f2c0280fe7d9857378add2619abd4661b1b13e1c Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 11 Apr 2026 00:19:42 +0100
Subject: [PATCH 007/191] [PR #12350/7eb05774 backport][3.14] Fix octal tests
(#12351)
**This is a backport of PR #12350 as merged into master
(7eb057747aa8ac6b0031fd032b8e2bda53d98b53).**
Co-authored-by: Sam Bull
---
tests/test_cookie_helpers.py | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/tests/test_cookie_helpers.py b/tests/test_cookie_helpers.py
index 767e1eaa34a..fead869d6f3 100644
--- a/tests/test_cookie_helpers.py
+++ b/tests/test_cookie_helpers.py
@@ -1095,13 +1095,13 @@ def test_parse_set_cookie_headers_date_formats_with_attributes() -> None:
@pytest.mark.parametrize(
("header", "expected_name", "expected_value", "expected_coded"),
[
- # Test cookie values with octal escape sequences
- (r'name="\012newline\012"', "name", "\nnewline\n", r'"\012newline\012"'),
+ # Test cookie values with octal escape sequences (printable chars only)
+ (r'name="\050parens\051"', "name", "(parens)", r'"\050parens\051"'),
(
- r'tab="\011separated\011values"',
- "tab",
- "\tseparated\tvalues",
- r'"\011separated\011values"',
+ r'punct="\053plus\053values"',
+ "punct",
+ "+plus+values",
+ r'"\053plus\053values"',
),
(
r'mixed="hello\040world\041"',
@@ -1110,10 +1110,10 @@ def test_parse_set_cookie_headers_date_formats_with_attributes() -> None:
r'"hello\040world\041"',
),
(
- r'complex="\042quoted\042 text with \012 newline"',
+ r'complex="\042quoted\042 text with \055 hyphen"',
"complex",
- '"quoted" text with \n newline',
- r'"\042quoted\042 text with \012 newline"',
+ '"quoted" text with - hyphen',
+ r'"\042quoted\042 text with \055 hyphen"',
),
],
)
From 0373f7c7a28cb7f36e9a4996cce42bec3d81811d Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sun, 12 Apr 2026 16:50:05 +0100
Subject: [PATCH 008/191] [PR #12349/b5f1aa53 backport][3.14] Add Cython
coverage (#12354)
**This is a backport of PR #12349 as merged into master
(b5f1aa53fdcd01b621990ec7e4da152a3de72fb5).**
Co-authored-by: Sam Bull
---
.coveragerc-cython.toml | 17 +++++++++++
.github/workflows/ci-cd.yml | 58 +++++++++++++++++++++++++++++++++++++
CHANGES/12349.contrib.rst | 1 +
Makefile | 7 +++--
setup.py | 23 +++++++++++++--
5 files changed, 100 insertions(+), 6 deletions(-)
create mode 100644 .coveragerc-cython.toml
create mode 100644 CHANGES/12349.contrib.rst
diff --git a/.coveragerc-cython.toml b/.coveragerc-cython.toml
new file mode 100644
index 00000000000..586dc937786
--- /dev/null
+++ b/.coveragerc-cython.toml
@@ -0,0 +1,17 @@
+[run]
+branch = true
+plugins = [
+ 'Cython.Coverage',
+]
+omit = [
+ 'site-packages',
+]
+
+[report]
+exclude_also = [
+ 'if TYPE_CHECKING',
+ 'assert False',
+ ': \.\.\.(\s*#.*)?$',
+ '^ +\.\.\.$',
+ 'pytest.fail\('
+]
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 7443b5061ae..423f2ff5001 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -401,6 +401,64 @@ jobs:
run: python -Im pytest --no-cov --numprocesses=0 -vvvvv --codspeed
+ cython-coverage:
+ permissions:
+ contents: read # to fetch code (actions/checkout)
+
+ name: Cython coverage
+ needs: gen_llhttp
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ submodules: true
+ - name: Setup Python
+ id: python-install
+ uses: actions/setup-python@v6
+ with:
+ python-version: '3.12'
+ - name: Update pip, wheel, setuptools, build, twine
+ run: |
+ python -m pip install -U pip wheel setuptools build twine
+ - name: Install dependencies
+ run: |
+ python -Im pip install -r requirements/test.in -c requirements/test.txt
+ - name: Uninstall blocbuster
+ run: python -m pip uninstall blockbuster -y
+ - name: Restore llhttp generated files
+ uses: actions/download-artifact@v8
+ with:
+ name: llhttp
+ path: vendor/llhttp/build/
+ - name: Cythonize with linetrace
+ run: |
+ make cythonize CYTHON_EXTRA="-X linetrace=True"
+ - name: Install self
+ env:
+ AIOHTTP_CYTHON_TRACE: 1
+ run: python -m pip install -e .
+ - name: Run tests with Cython tracing
+ env:
+ COLOR: yes
+ PIP_USER: 1
+ run: >-
+ pytest tests/test_client_functional.py tests/test_http_parser.py tests/test_http_writer.py tests/test_web_functional.py tests/test_web_response.py tests/test_websocket_parser.py
+ --cov-config=.coveragerc-cython.toml
+ -m 'not dev_mode and not autobahn'
+ shell: bash
+ - name: Turn coverage into xml
+ run: |
+ python -m coverage xml -o cython-coverage.xml --rcfile=.coveragerc-cython.toml
+ - name: Upload coverage
+ uses: codecov/codecov-action@v6
+ with:
+ files: ./cython-coverage.xml
+ disable_search: true
+ flags: cython-coverage
+ token: ${{ secrets.CODECOV_TOKEN }}
+ fail_ci_if_error: true
+
check: # This job does nothing and is only used for the branch protection
if: always()
diff --git a/CHANGES/12349.contrib.rst b/CHANGES/12349.contrib.rst
new file mode 100644
index 00000000000..12aeb069354
--- /dev/null
+++ b/CHANGES/12349.contrib.rst
@@ -0,0 +1 @@
+Added a CI job to measure Cython coverage -- by :user:`Dreamsorcerer`.
diff --git a/Makefile b/Makefile
index 29dd75cd53c..1d85f8cdb23 100644
--- a/Makefile
+++ b/Makefile
@@ -3,6 +3,7 @@
to-hash-one = $(dir $1).hash/$(addsuffix .hash,$(notdir $1))
to-hash = $(foreach fname,$1,$(call to-hash-one,$(fname)))
+CYTHON_EXTRA ?=
CYS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/*.pyi) $(wildcard aiohttp/*.pxd) $(wildcard aiohttp/_websocket/*.pyx) $(wildcard aiohttp/_websocket/*.pyi) $(wildcard aiohttp/_websocket/*.pxd)
PYXS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/_websocket/*.pyx)
CS := $(wildcard aiohttp/*.c) $(wildcard aiohttp/_websocket/*.c)
@@ -59,14 +60,14 @@ aiohttp/_find_header.c: $(call to-hash,aiohttp/hdrs.py ./tools/gen.py)
# Special case for reader since we want to be able to disable
# the extension with AIOHTTP_NO_EXTENSIONS
aiohttp/_websocket/reader_c.c: aiohttp/_websocket/reader_c.py
- cython -3 -X freethreading_compatible=True -o $@ $< -I aiohttp -Werror
+ cython -3 -X freethreading_compatible=True $(CYTHON_EXTRA) -o $@ $< -I aiohttp -Werror
# _find_headers generator creates _headers.pyi as well
aiohttp/%.c: aiohttp/%.pyx $(call to-hash,$(CYS)) aiohttp/_find_header.c
- cython -3 -X freethreading_compatible=True -o $@ $< -I aiohttp -Werror
+ cython -3 -X freethreading_compatible=True $(CYTHON_EXTRA) -o $@ $< -I aiohttp -Werror
aiohttp/_websocket/%.c: aiohttp/_websocket/%.pyx $(call to-hash,$(CYS))
- cython -3 -X freethreading_compatible=True -o $@ $< -I aiohttp -Werror
+ cython -3 -X freethreading_compatible=True $(CYTHON_EXTRA) -o $@ $< -I aiohttp -Werror
vendor/llhttp/node_modules: vendor/llhttp/package.json
cd vendor/llhttp; npm ci
diff --git a/setup.py b/setup.py
index e88566b435e..9f910fa823a 100644
--- a/setup.py
+++ b/setup.py
@@ -12,6 +12,7 @@
os.environ.get("AIOHTTP_USE_SYSTEM_DEPS", os.environ.get("USE_SYSTEM_DEPS"))
)
NO_EXTENSIONS: bool = bool(os.environ.get("AIOHTTP_NO_EXTENSIONS"))
+CYTHON_TRACING: bool = bool(os.environ.get("AIOHTTP_CYTHON_TRACE"))
HERE = pathlib.Path(__file__).parent
IS_GIT_REPO = (HERE / ".git").exists()
@@ -54,8 +55,16 @@
"include_dirs": ["vendor/llhttp/build"],
}
+cython_trace_macros = [("CYTHON_TRACE", 1)] if CYTHON_TRACING else []
+if cython_trace_macros:
+ llhttp_kwargs.setdefault("define_macros", []).extend(cython_trace_macros)
+
extensions = [
- Extension("aiohttp._websocket.mask", ["aiohttp/_websocket/mask.c"]),
+ Extension(
+ "aiohttp._websocket.mask",
+ ["aiohttp/_websocket/mask.c"],
+ define_macros=cython_trace_macros,
+ ),
Extension(
"aiohttp._http_parser",
[
@@ -65,8 +74,16 @@
],
**llhttp_kwargs,
),
- Extension("aiohttp._http_writer", ["aiohttp/_http_writer.c"]),
- Extension("aiohttp._websocket.reader_c", ["aiohttp/_websocket/reader_c.c"]),
+ Extension(
+ "aiohttp._http_writer",
+ ["aiohttp/_http_writer.c"],
+ define_macros=cython_trace_macros,
+ ),
+ Extension(
+ "aiohttp._websocket.reader_c",
+ ["aiohttp/_websocket/reader_c.c"],
+ define_macros=cython_trace_macros,
+ ),
]
From b502ae655c8788b469dcc832923a85d661719699 Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Sun, 12 Apr 2026 21:35:00 +0100
Subject: [PATCH 009/191] Allow decompression to continue after exceeding
max_length (#11966) (#12355)
(cherry picked from commit 0c7ce3485dbcda196e125b4ea913a67e6b54f26c)
---
.github/workflows/ci-cd.yml | 5 +-
CHANGES/11966.feature.rst | 8 +
aiohttp/_cparser.pxd | 1 +
aiohttp/_http_parser.pyx | 121 ++++++--
aiohttp/_websocket/writer.py | 2 +-
aiohttp/base_protocol.py | 33 +-
aiohttp/client.py | 4 +-
aiohttp/client_proto.py | 19 +-
aiohttp/compression_utils.py | 43 ++-
aiohttp/http_exceptions.py | 2 +-
aiohttp/http_parser.py | 217 +++++++++-----
aiohttp/multipart.py | 27 +-
aiohttp/payload.py | 2 +-
aiohttp/streams.py | 40 ++-
aiohttp/web_exceptions.py | 8 +-
aiohttp/web_protocol.py | 55 ++--
aiohttp/web_request.py | 16 +-
aiohttp/web_ws.py | 2 +-
setup.cfg | 2 +
tests/test_base_protocol.py | 22 +-
tests/test_benchmarks_http_websocket.py | 4 +-
tests/test_client_functional.py | 42 +--
tests/test_client_proto.py | 5 +-
tests/test_flowcontrol_streams.py | 7 +-
tests/test_http_parser.py | 382 ++++++++++++++++++++----
tests/test_http_writer.py | 8 +-
tests/test_multipart.py | 70 ++++-
tests/test_payload.py | 4 +-
tests/test_streams.py | 40 +--
tests/test_web_functional.py | 44 ++-
tests/test_web_protocol.py | 44 +--
tests/test_web_request.py | 23 +-
tests/test_websocket_parser.py | 5 +-
tests/test_websocket_writer.py | 4 +-
34 files changed, 937 insertions(+), 374 deletions(-)
create mode 100644 CHANGES/11966.feature.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 423f2ff5001..34488f65681 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -407,7 +407,10 @@ jobs:
name: Cython coverage
needs: gen_llhttp
- runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ os: [ubuntu, windows]
+ runs-on: ${{ matrix.os }}-latest
steps:
- name: Checkout
uses: actions/checkout@v6
diff --git a/CHANGES/11966.feature.rst b/CHANGES/11966.feature.rst
new file mode 100644
index 00000000000..9f298f98e12
--- /dev/null
+++ b/CHANGES/11966.feature.rst
@@ -0,0 +1,8 @@
+Large overhaul of parser/decompression code.
+
+The zip bomb security fix in 3.13 stopped highly compressed payloads
+from being decompressed, regardless of validity. Now aiohttp will
+decompress such payloads in chunks of 256+ KiB, allowing safe decompression
+of such payloads.
+
+-- by :user:`Dreamsorcerer`.
diff --git a/aiohttp/_cparser.pxd b/aiohttp/_cparser.pxd
index 1b3be6d4efb..cc7ef58d664 100644
--- a/aiohttp/_cparser.pxd
+++ b/aiohttp/_cparser.pxd
@@ -145,6 +145,7 @@ cdef extern from "llhttp.h":
int llhttp_should_keep_alive(const llhttp_t* parser)
+ void llhttp_resume(llhttp_t* parser)
void llhttp_resume_after_upgrade(llhttp_t* parser)
llhttp_errno_t llhttp_get_errno(const llhttp_t* parser)
diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx
index 5da835bc642..e5e3bec8689 100644
--- a/aiohttp/_http_parser.pyx
+++ b/aiohttp/_http_parser.pyx
@@ -46,7 +46,8 @@ include "_headers.pxi"
from aiohttp cimport _find_header
-ALLOWED_UPGRADES = frozenset({"websocket"})
+
+cdef frozenset ALLOWED_UPGRADES = frozenset({"websocket"})
DEF DEFAULT_FREELIST_SIZE = 250
cdef extern from "Python.h":
@@ -69,7 +70,7 @@ cdef object CONTENT_ENCODING = hdrs.CONTENT_ENCODING
cdef object EMPTY_PAYLOAD = _EMPTY_PAYLOAD
cdef object StreamReader = _StreamReader
cdef object DeflateBuffer = _DeflateBuffer
-cdef bytes EMPTY_BYTES = b""
+cdef tuple EMPTY_FEED_DATA_RESULT = ((), False, b"")
# RFC 9110 singleton headers — duplicates are rejected in strict mode.
# In lax mode (response parser default), the check is skipped entirely
@@ -298,7 +299,7 @@ cdef class HttpParser:
bint _has_value
int _header_name_size
- object _protocol
+ readonly object protocol
object _loop
object _timer
@@ -309,6 +310,7 @@ cdef class HttpParser:
bint _read_until_eof
bint _lax
+ bytes _tail
bint _started
object _url
bytearray _buf
@@ -319,6 +321,9 @@ cdef class HttpParser:
list _raw_headers
bint _upgraded
list _messages
+ bint _more_data_available
+ bint _paused
+ bint _eof_pending
object _payload
bint _payload_error
object _payload_exception
@@ -359,18 +364,22 @@ cdef class HttpParser:
self._cparser.data = self
self._cparser.content_length = 0
- self._protocol = protocol
+ self.protocol = protocol
self._loop = loop
self._timer = timer
self._buf = bytearray()
+ self._more_data_available = False
+ self._paused = False
+ self._eof_pending = False
self._payload = None
self._payload_error = 0
self._payload_exception = payload_exception
self._messages = []
- self._raw_name = EMPTY_BYTES
- self._raw_value = EMPTY_BYTES
+ self._raw_name = b""
+ self._raw_value = b""
+ self._tail = b""
self._has_value = False
self._header_name_size = 0
@@ -401,7 +410,7 @@ cdef class HttpParser:
cdef _process_header(self):
cdef str value
- if self._raw_name is not EMPTY_BYTES:
+ if self._raw_name != b"":
name = find_header(self._raw_name)
value = self._raw_value.decode('utf-8', 'surrogateescape')
@@ -426,20 +435,20 @@ cdef class HttpParser:
self._has_value = False
self._header_name_size = 0
self._raw_headers.append((self._raw_name, self._raw_value))
- self._raw_name = EMPTY_BYTES
- self._raw_value = EMPTY_BYTES
+ self._raw_name = b""
+ self._raw_value = b""
cdef _on_header_field(self, char* at, size_t length):
if self._has_value:
self._process_header()
- if self._raw_name is EMPTY_BYTES:
+ if self._raw_name == b"":
self._raw_name = at[:length]
else:
self._raw_name += at[:length]
cdef _on_header_value(self, char* at, size_t length):
- if self._raw_value is EMPTY_BYTES:
+ if self._raw_value == b"":
self._raw_value = at[:length]
else:
self._raw_value += at[:length]
@@ -495,14 +504,14 @@ cdef class HttpParser:
self._read_until_eof)
):
payload = StreamReader(
- self._protocol, timer=self._timer, loop=self._loop,
+ self.protocol, timer=self._timer, loop=self._loop,
limit=self._limit)
else:
payload = EMPTY_PAYLOAD
self._payload = payload
if encoding is not None and self._auto_decompress:
- self._payload = DeflateBuffer(payload, encoding)
+ self._payload = DeflateBuffer(payload, encoding, max_decompress_size=self._limit)
if not self._response_with_body:
payload = EMPTY_PAYLOAD
@@ -535,6 +544,10 @@ cdef class HttpParser:
### Public API ###
+ def pause_reading(self):
+ assert self._payload is not None
+ self._paused = True
+
def feed_eof(self):
cdef bytes desc
@@ -549,18 +562,52 @@ cdef class HttpParser:
desc = cparser.llhttp_get_error_reason(self._cparser)
raise PayloadEncodingError(desc.decode('latin-1'))
else:
+ self._eof_pending = True
+ while self._more_data_available:
+ if self._paused:
+ self._paused = False
+ return # Will resume via feed_data(b"") later
+ self._more_data_available = self._payload.feed_data(b"", 0)
self._payload.feed_eof()
+ self._payload = None
+ self._more_data_available = False
+ self._eof_pending = False
elif self._started:
self._on_headers_complete()
if self._messages:
return self._messages[-1][0]
- def feed_data(self, data):
+ def feed_data(self, incoming_data):
cdef:
size_t data_len
size_t nb
char* base
cdef cparser.llhttp_errno_t errno
+ cdef bytes data
+
+ # Proactor loop sends bytearray.
+ # Ensure cython sees `data` as bytes
+ if type(incoming_data) is not bytes:
+ data = bytes(incoming_data)
+ else:
+ data = incoming_data
+
+ if self._tail:
+ data, self._tail = self._tail + data, b""
+
+ if self._more_data_available:
+ result = cb_on_body(self._cparser, b"", 0)
+ if result is cparser.HPE_PAUSED:
+ self._tail = data
+ return EMPTY_FEED_DATA_RESULT
+
+ if self._eof_pending:
+ self._payload.feed_eof()
+ self._payload = None
+ self._eof_pending = False
+ # We can't have new messages here, otherwise we wouldn't have
+ # received EOF.
+ return EMPTY_FEED_DATA_RESULT
PyObject_GetBuffer(data, &self.py_buf, PyBUF_SIMPLE)
# Cache buffer pointer before PyBuffer_Release to avoid use-after-release.
@@ -574,12 +621,15 @@ cdef class HttpParser:
if errno is cparser.HPE_PAUSED_UPGRADE:
cparser.llhttp_resume_after_upgrade(self._cparser)
-
nb = cparser.llhttp_get_error_pos(self._cparser) - base
+ elif errno is cparser.HPE_PAUSED:
+ cparser.llhttp_resume(self._cparser)
+ pos = cparser.llhttp_get_error_pos(self._cparser) - base
+ self._tail = data[pos:]
PyBuffer_Release(&self.py_buf)
- if errno not in (cparser.HPE_OK, cparser.HPE_PAUSED_UPGRADE):
+ if errno not in (cparser.HPE_OK, cparser.HPE_PAUSED, cparser.HPE_PAUSED_UPGRADE):
if self._payload_error == 0:
if self._last_error is not None:
ex = self._last_error
@@ -603,8 +653,9 @@ cdef class HttpParser:
if self._upgraded:
return messages, True, data[nb:]
- else:
- return messages, False, b""
+ if not messages: # Shortcut to reduce Python overhead
+ return EMPTY_FEED_DATA_RESULT
+ return messages, False, b""
def set_upgraded(self, val):
self._upgraded = val
@@ -799,19 +850,27 @@ cdef int cb_on_body(cparser.llhttp_t* parser,
const char *at, size_t length) except -1:
cdef HttpParser pyparser = parser.data
cdef bytes body = at[:length]
- try:
- pyparser._payload.feed_data(body, length)
- except BaseException as underlying_exc:
- reraised_exc = underlying_exc
- if pyparser._payload_exception is not None:
- reraised_exc = pyparser._payload_exception(str(underlying_exc))
-
- set_exception(pyparser._payload, reraised_exc, underlying_exc)
-
- pyparser._payload_error = 1
- return -1
- else:
- return 0
+ while body or pyparser._more_data_available:
+ try:
+ pyparser._more_data_available = pyparser._payload.feed_data(body, length)
+ except BaseException as underlying_exc:
+ reraised_exc = underlying_exc
+ if pyparser._payload_exception is not None:
+ reraised_exc = pyparser._payload_exception(str(underlying_exc))
+
+ set_exception(pyparser._payload, reraised_exc, underlying_exc)
+
+ pyparser._payload_error = 1
+ pyparser._paused = False
+ return -1
+ body = b""
+ length = 0
+
+ if pyparser._paused:
+ pyparser._paused = False
+ return cparser.HPE_PAUSED
+ pyparser._paused = False
+ return 0
cdef int cb_on_message_complete(cparser.llhttp_t* parser) except -1:
diff --git a/aiohttp/_websocket/writer.py b/aiohttp/_websocket/writer.py
index 0d5f56f4b81..5e4bbf62f90 100644
--- a/aiohttp/_websocket/writer.py
+++ b/aiohttp/_websocket/writer.py
@@ -21,7 +21,7 @@
)
from .models import WS_DEFLATE_TRAILING, WSMsgType
-DEFAULT_LIMIT: Final[int] = 2**16
+DEFAULT_LIMIT: Final[int] = 2**18
# WebSocket opcode boundary: opcodes 0-7 are data frames, 8-15 are control frames
# Control frames (ping, pong, close) are never compressed
diff --git a/aiohttp/base_protocol.py b/aiohttp/base_protocol.py
index d7d83425b88..f1f6edc3836 100644
--- a/aiohttp/base_protocol.py
+++ b/aiohttp/base_protocol.py
@@ -1,26 +1,35 @@
import asyncio
-from typing import cast
+from typing import TYPE_CHECKING, Any, cast
from .client_exceptions import ClientConnectionResetError
from .helpers import set_exception
from .tcp_helpers import tcp_nodelay
+if TYPE_CHECKING:
+ from .http_parser import HttpParser
+
class BaseProtocol(asyncio.Protocol):
__slots__ = (
"_loop",
"_paused",
+ "_parser",
"_drain_waiter",
"_connection_lost",
"_reading_paused",
+ "_upgraded",
"transport",
)
- def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
+ def __init__(
+ self, loop: asyncio.AbstractEventLoop, parser: "HttpParser[Any] | None" = None
+ ) -> None:
self._loop: asyncio.AbstractEventLoop = loop
self._paused = False
self._drain_waiter: asyncio.Future[None] | None = None
self._reading_paused = False
+ self._parser = parser
+ self._upgraded = False
self.transport: asyncio.Transport | None = None
@@ -48,15 +57,27 @@ def resume_writing(self) -> None:
waiter.set_result(None)
def pause_reading(self) -> None:
- if not self._reading_paused and self.transport is not None:
+ self._reading_paused = True
+ # Parser shouldn't be paused on websockets.
+ if not self._upgraded:
+ assert self._parser is not None
+ self._parser.pause_reading()
+ if self.transport is not None:
try:
self.transport.pause_reading()
except (AttributeError, NotImplementedError, RuntimeError):
pass
- self._reading_paused = True
- def resume_reading(self) -> None:
- if self._reading_paused and self.transport is not None:
+ def resume_reading(self, resume_parser: bool = True) -> None:
+ self._reading_paused = False
+
+ # This will resume parsing any unprocessed data from the last pause.
+ if not self._upgraded and resume_parser:
+ self.data_received(b"")
+
+ # Reading may have been paused again in the above call if there was a lot of
+ # compressed data still pending.
+ if not self._reading_paused and self.transport is not None:
try:
self.transport.resume_reading()
except (AttributeError, NotImplementedError, RuntimeError):
diff --git a/aiohttp/client.py b/aiohttp/client.py
index d797aa31021..890555e5783 100644
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -333,7 +333,7 @@ def __init__(
trust_env: bool = False,
requote_redirect_url: bool = True,
trace_configs: list[TraceConfig] | None = None,
- read_bufsize: int = 2**16,
+ read_bufsize: int = 2**18,
max_line_size: int = 8190,
max_field_size: int = 8190,
max_headers: int = 128,
@@ -1294,7 +1294,7 @@ async def _ws_connect(
transport = conn.transport
assert transport is not None
- reader = WebSocketDataQueue(conn_proto, 2**16, loop=self._loop)
+ reader = WebSocketDataQueue(conn_proto, 2**18, loop=self._loop)
writer = WebSocketWriter(
conn_proto,
transport,
diff --git a/aiohttp/client_proto.py b/aiohttp/client_proto.py
index dedde857704..011fc7217fd 100644
--- a/aiohttp/client_proto.py
+++ b/aiohttp/client_proto.py
@@ -26,7 +26,7 @@ class ResponseHandler(BaseProtocol, DataQueue[tuple[RawResponseMessage, StreamRe
"""Helper class to adapt between Protocol and StreamReader."""
def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
- BaseProtocol.__init__(self, loop=loop)
+ BaseProtocol.__init__(self, loop=loop, parser=None)
DataQueue.__init__(self, loop)
self._should_close = False
@@ -37,10 +37,7 @@ def __init__(self, loop: asyncio.AbstractEventLoop) -> None:
self._data_received_cb: Callable[[], None] | None = None
self._timer = None
-
self._tail = b""
- self._upgraded = False
- self._parser: HttpResponseParser | None = None
self._read_timeout: float | None = None
self._read_timeout_handle: asyncio.TimerHandle | None = None
@@ -191,8 +188,8 @@ def pause_reading(self) -> None:
super().pause_reading()
self._drop_timeout()
- def resume_reading(self) -> None:
- super().resume_reading()
+ def resume_reading(self, resume_parser: bool = True) -> None:
+ super().resume_reading(resume_parser)
self._reschedule_timeout()
def set_exception(
@@ -233,7 +230,7 @@ def set_response_params(
read_until_eof: bool = False,
auto_decompress: bool = True,
read_timeout: float | None = None,
- read_bufsize: int = 2**16,
+ read_bufsize: int = 2**18,
timeout_ceil_threshold: float = 5,
max_line_size: int = 8190,
max_field_size: int = 8190,
@@ -298,10 +295,10 @@ def _on_read_timeout(self) -> None:
set_exception(self._payload, exc)
def data_received(self, data: bytes) -> None:
- self._reschedule_timeout()
-
- if not data:
- return
+ # If no data, then we are resuming decompression. We haven't received
+ # data from the socket, so we can avoid the reschedule overhead.
+ if data:
+ self._reschedule_timeout()
# custom payload parser - currently always WebSocketReader
if self._payload_parser is not None:
diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py
index 562b2813401..92c44da53c8 100644
--- a/aiohttp/compression_utils.py
+++ b/aiohttp/compression_utils.py
@@ -34,7 +34,9 @@
MAX_SYNC_CHUNK_SIZE = 4096
-DEFAULT_MAX_DECOMPRESS_SIZE = 2**25 # 32MiB
+# Matches the max size we receive from sockets:
+# https://github.com/python/cpython/blob/1857a40807daeae3a1bf5efb682de9c9ae6df845/Lib/asyncio/selector_events.py#L766
+DEFAULT_MAX_DECOMPRESS_SIZE = 256 * 1024
# Unlimited decompression constants - different libraries use different conventions
ZLIB_MAX_LENGTH_UNLIMITED = 0 # zlib uses 0 to mean unlimited
@@ -53,6 +55,9 @@ def flush(self, length: int = ..., /) -> bytes: ...
@property
def eof(self) -> bool: ...
+ @property
+ def unconsumed_tail(self) -> bytes: ...
+
class ZLibBackendProtocol(Protocol):
MAX_WBITS: int
@@ -179,6 +184,11 @@ async def decompress(
)
return self.decompress_sync(data, max_length)
+ @property
+ @abstractmethod
+ def data_available(self) -> bool:
+ """Return True if more output is available by passing b""."""
+
class ZLibCompressor:
def __init__(
@@ -267,11 +277,17 @@ def __init__(
self._mode = encoding_to_mode(encoding, suppress_deflate_header)
self._zlib_backend: Final = ZLibBackendWrapper(ZLibBackend._zlib_backend)
self._decompressor = self._zlib_backend.decompressobj(wbits=self._mode)
+ self._last_empty = False
def decompress_sync(
self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
) -> bytes:
- return self._decompressor.decompress(data, max_length)
+ result = self._decompressor.decompress(
+ self._decompressor.unconsumed_tail + data, max_length
+ )
+ # Only way to know that isal has no further data is checking we get no output
+ self._last_empty = result == b""
+ return result
def flush(self, length: int = 0) -> bytes:
return (
@@ -280,6 +296,10 @@ def flush(self, length: int = 0) -> bytes:
else self._decompressor.flush()
)
+ @property
+ def data_available(self) -> bool:
+ return bool(self._decompressor.unconsumed_tail) or not self._last_empty
+
@property
def eof(self) -> bool:
return self._decompressor.eof
@@ -301,6 +321,7 @@ def __init__(
"Please install `Brotli` module"
)
self._obj = brotli.Decompressor()
+ self._last_empty = False
super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size)
def decompress_sync(
@@ -308,8 +329,12 @@ def decompress_sync(
) -> bytes:
"""Decompress the given data."""
if hasattr(self._obj, "decompress"):
- return cast(bytes, self._obj.decompress(data, max_length))
- return cast(bytes, self._obj.process(data, max_length))
+ result = cast(bytes, self._obj.decompress(data, max_length))
+ else:
+ result = cast(bytes, self._obj.process(data, max_length))
+ # Only way to know that brotli has no further data is checking we get no output
+ self._last_empty = result == b""
+ return result
def flush(self) -> bytes:
"""Flush the decompressor."""
@@ -317,6 +342,10 @@ def flush(self) -> bytes:
return cast(bytes, self._obj.flush())
return b""
+ @property
+ def data_available(self) -> bool:
+ return not self._obj.is_finished() and not self._last_empty
+
class ZSTDDecompressor(DecompressionBaseHandler):
def __init__(
@@ -373,3 +402,9 @@ def decompress_sync(
def flush(self) -> bytes:
return b""
+
+ @property
+ def data_available(self) -> bool:
+ return (
+ not self._obj.needs_input and not self._obj.eof
+ ) or self._pending_unused_data is not None
diff --git a/aiohttp/http_exceptions.py b/aiohttp/http_exceptions.py
index daa8da81a3c..f21a97f0170 100644
--- a/aiohttp/http_exceptions.py
+++ b/aiohttp/http_exceptions.py
@@ -74,7 +74,7 @@ class ContentLengthError(PayloadEncodingError):
class DecompressSizeError(PayloadEncodingError):
- """Decompressed size exceeds the configured limit."""
+ """Deprecated. Removed in v4."""
class LineTooLong(BadHttpMessage):
diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py
index 161b474ac38..3fb5368e2bf 100644
--- a/aiohttp/http_parser.py
+++ b/aiohttp/http_parser.py
@@ -5,7 +5,16 @@
from contextlib import suppress
from enum import IntEnum
from re import Pattern
-from typing import Any, ClassVar, Final, Generic, Literal, NamedTuple, TypeVar
+from typing import (
+ TYPE_CHECKING,
+ Any,
+ ClassVar,
+ Final,
+ Generic,
+ Literal,
+ NamedTuple,
+ TypeVar,
+)
from multidict import CIMultiDict, CIMultiDictProxy, istr
from yarl import URL
@@ -35,7 +44,6 @@
BadStatusLine,
ContentEncodingError,
ContentLengthError,
- DecompressSizeError,
InvalidHeader,
InvalidURLError,
LineTooLong,
@@ -45,6 +53,9 @@
from .streams import EMPTY_PAYLOAD, StreamReader
from .typedefs import RawHeaders
+if TYPE_CHECKING:
+ from .client_proto import ResponseHandler
+
__all__ = (
"HeadersParser",
"HttpParser",
@@ -124,6 +135,12 @@ class RawResponseMessage(NamedTuple):
_MsgT = TypeVar("_MsgT", RawRequestMessage, RawResponseMessage)
+class PayloadState(IntEnum):
+ PAYLOAD_COMPLETE = 0
+ PAYLOAD_NEEDS_INPUT = 1
+ PAYLOAD_HAS_PENDING_INPUT = 2
+
+
class ParseState(IntEnum):
PARSE_NONE = 0
@@ -276,6 +293,7 @@ def __init__(
self._upgraded = False
self._payload = None
self._payload_parser: HttpPayloadParser | None = None
+ self._payload_has_more_data = False
self._auto_decompress = auto_decompress
self._limit = limit
self._headers_parser = HeadersParser(
@@ -288,10 +306,15 @@ def parse_message(self, lines: list[bytes]) -> _MsgT: ...
@abc.abstractmethod
def _is_chunked_te(self, te: str) -> bool: ...
+ def pause_reading(self) -> None:
+ assert self._payload_parser is not None
+ self._payload_parser.pause_reading()
+
def feed_eof(self) -> _MsgT | None:
if self._payload_parser is not None:
self._payload_parser.feed_eof()
- self._payload_parser = None
+ if self._payload_parser.done:
+ self._payload_parser = None
else:
# try to extract partial message
if self._tail:
@@ -325,8 +348,7 @@ def feed_data(
max_line_length = self.max_line_size
should_close = False
- while start_pos < data_len:
-
+ while start_pos < data_len or self._payload_has_more_data:
# read HTTP message (request/response line + headers), \r\n\r\n
# and split by lines
if self._payload_parser is None and not self._upgraded:
@@ -420,6 +442,7 @@ def get_content_length() -> int | None:
max_line_size=self.max_line_size,
max_field_size=self.max_field_size,
max_trailers=max_trailers,
+ limit=self._limit,
)
if not payload_parser.done:
self._payload_parser = payload_parser
@@ -442,6 +465,7 @@ def get_content_length() -> int | None:
max_line_size=self.max_line_size,
max_field_size=self.max_field_size,
max_trailers=max_trailers,
+ limit=self._limit,
)
elif not empty_body and length is None and self.read_until_eof:
payload = StreamReader(
@@ -464,6 +488,7 @@ def get_content_length() -> int | None:
max_line_size=self.max_line_size,
max_field_size=self.max_field_size,
max_trailers=max_trailers,
+ limit=self._limit,
)
if not payload_parser.done:
self._payload_parser = payload_parser
@@ -485,11 +510,13 @@ def get_content_length() -> int | None:
break
# feed payload
- elif data and start_pos < data_len:
+ else:
assert not self._lines
assert self._payload_parser is not None
try:
- eof, data = self._payload_parser.feed_data(data[start_pos:], SEP)
+ payload_state, data = self._payload_parser.feed_data(
+ data[start_pos:], SEP
+ )
except Exception as underlying_exc:
reraised_exc: BaseException = underlying_exc
if self.payload_exception is not None:
@@ -501,20 +528,25 @@ def get_content_length() -> int | None:
underlying_exc,
)
- eof = True
+ payload_state = PayloadState.PAYLOAD_COMPLETE
data = b""
if isinstance(
underlying_exc, (InvalidHeader, TransferEncodingError)
):
raise
- if eof:
- start_pos = 0
- data_len = len(data)
- self._payload_parser = None
- continue
- else:
- break
+ self._payload_has_more_data = (
+ payload_state == PayloadState.PAYLOAD_HAS_PENDING_INPUT
+ )
+
+ if payload_state is not PayloadState.PAYLOAD_COMPLETE:
+ # We've either consumed all available data, or we're pausing
+ # until the reader buffer is freed up.
+ break
+
+ start_pos = 0
+ data_len = len(data)
+ self._payload_parser = None
if data and start_pos < data_len:
data = data[start_pos:]
@@ -689,6 +721,8 @@ class HttpResponseParser(HttpParser[RawResponseMessage]):
Returns RawResponseMessage.
"""
+ protocol: "ResponseHandler"
+
# Lax mode should only be enabled on response parser.
lax = not DEBUG
@@ -783,8 +817,10 @@ def __init__(
max_line_size: int = 8190,
max_field_size: int = 8190,
max_trailers: int = 128,
+ limit: int = DEFAULT_MAX_DECOMPRESS_SIZE,
) -> None:
self._length = 0
+ self._paused = False
self._type = ParseState.PARSE_UNTIL_EOF
self._chunk = ChunkState.PARSE_CHUNKED_SIZE
self._chunk_size = 0
@@ -795,13 +831,15 @@ def __init__(
self._max_line_size = max_line_size
self._max_field_size = max_field_size
self._max_trailers = max_trailers
+ self._more_data_available = False
self._trailer_lines: list[bytes] = []
self.done = False
+ self._eof_pending = False
# payload decompression wrapper
if response_with_body and compression and self._auto_decompress:
real_payload: StreamReader | DeflateBuffer = DeflateBuffer(
- payload, compression
+ payload, compression, max_decompress_size=limit
)
else:
real_payload = payload
@@ -823,9 +861,20 @@ def __init__(
self.payload = real_payload
+ def pause_reading(self) -> None:
+ self._paused = True
+
def feed_eof(self) -> None:
if self._type == ParseState.PARSE_UNTIL_EOF:
+ self._eof_pending = True
+ while self._more_data_available:
+ if self._paused:
+ self._paused = False
+ return # Will resume via feed_data(b"") later
+ self._more_data_available = self.payload.feed_data(b"", 0)
self.payload.feed_eof()
+ self.done = True
+ self._eof_pending = False
elif self._type == ParseState.PARSE_LENGTH:
raise ContentLengthError(
"Not enough data to satisfy content length header."
@@ -837,41 +886,54 @@ def feed_eof(self) -> None:
def feed_data(
self, chunk: bytes, SEP: _SEP = b"\r\n", CHUNK_EXT: bytes = b";"
- ) -> tuple[bool, bytes]:
+ ) -> tuple[PayloadState, bytes]:
+ """Receive a chunk of data to process.
+
+ Return:
+ PayloadState - The current state of payload processing.
+ This function may be called with empty bytes after returning
+ PAYLOAD_HAS_PENDING_INPUT to continue processing after a pause.
+ bytes - If payload is complete, this is the unconsumed bytes intended for the
+ next message/payload, b"" otherwise.
+ """
# Read specified amount of bytes
if self._type == ParseState.PARSE_LENGTH:
+ if self._chunk_tail:
+ chunk = self._chunk_tail + chunk
+ self._chunk_tail = b""
+
required = self._length
- chunk_len = len(chunk)
-
- if required >= chunk_len:
- self._length = required - chunk_len
- self.payload.feed_data(chunk, chunk_len)
- if self._length == 0:
- self.payload.feed_eof()
- return True, b""
- else:
- self._length = 0
- self.payload.feed_data(chunk[:required], required)
- self.payload.feed_eof()
- return True, chunk[required:]
+ self._length = max(required - len(chunk), 0)
+ self._more_data_available = self.payload.feed_data(
+ chunk[:required], required
+ )
+ while self._more_data_available:
+ if self._paused:
+ self._paused = False
+ self._chunk_tail = chunk[required:]
+ return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b""
+ self._more_data_available = self.payload.feed_data(b"", 0)
+ if self._length == 0:
+ self.payload.feed_eof()
+ return PayloadState.PAYLOAD_COMPLETE, chunk[required:]
# Chunked transfer encoding parser
elif self._type == ParseState.PARSE_CHUNKED:
if self._chunk_tail:
- # We should never have a tail if we're inside the payload body.
- assert self._chunk != ChunkState.PARSE_CHUNKED_CHUNK
- # We should check the length is sane.
- max_line_length = self._max_line_size
- if self._chunk == ChunkState.PARSE_TRAILERS:
- max_line_length = self._max_field_size
- if len(self._chunk_tail) > max_line_length:
- raise LineTooLong(self._chunk_tail[:100] + b"...", max_line_length)
+ # We should check the length is sane when not processing payload body.
+ if self._chunk != ChunkState.PARSE_CHUNKED_CHUNK:
+ max_line_length = self._max_line_size
+ if self._chunk == ChunkState.PARSE_TRAILERS:
+ max_line_length = self._max_field_size
+ if len(self._chunk_tail) > max_line_length:
+ raise LineTooLong(
+ self._chunk_tail[:100] + b"...", max_line_length
+ )
chunk = self._chunk_tail + chunk
self._chunk_tail = b""
- while chunk:
-
+ while chunk or self._more_data_available:
# read next chunk size
if self._chunk == ChunkState.PARSE_CHUNKED_SIZE:
pos = chunk.find(SEP)
@@ -911,23 +973,30 @@ def feed_data(
self.payload.begin_http_chunk_receiving()
else:
self._chunk_tail = chunk
- return False, b""
+ return PayloadState.PAYLOAD_NEEDS_INPUT, b""
# read chunk and feed buffer
if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK:
+ if self._paused:
+ self._paused = False
+ self._chunk_tail = chunk
+ return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b""
+
required = self._chunk_size
- chunk_len = len(chunk)
+ self._chunk_size = max(required - len(chunk), 0)
+ self._more_data_available = self.payload.feed_data(
+ chunk[:required], required
+ )
+ chunk = chunk[required:]
- if required > chunk_len:
- self._chunk_size = required - chunk_len
- self.payload.feed_data(chunk, chunk_len)
- return False, b""
- else:
- self._chunk_size = 0
- self.payload.feed_data(chunk[:required], required)
- chunk = chunk[required:]
- self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF
- self.payload.end_http_chunk_receiving()
+ if self._more_data_available:
+ continue
+
+ if self._chunk_size:
+ self._paused = False
+ return PayloadState.PAYLOAD_NEEDS_INPUT, b""
+ self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF
+ self.payload.end_http_chunk_receiving()
# toss the CRLF at the end of the chunk
if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK_EOF:
@@ -944,13 +1013,13 @@ def feed_data(
raise exc
else:
self._chunk_tail = chunk
- return False, b""
+ return PayloadState.PAYLOAD_NEEDS_INPUT, b""
if self._chunk == ChunkState.PARSE_TRAILERS:
pos = chunk.find(SEP)
if pos < 0: # No line found
self._chunk_tail = chunk
- return False, b""
+ return PayloadState.PAYLOAD_NEEDS_INPUT, b""
line = chunk[:pos]
chunk = chunk[pos + len(SEP) :]
@@ -976,13 +1045,24 @@ def feed_data(
finally:
self._trailer_lines.clear()
self.payload.feed_eof()
- return True, chunk
+ return PayloadState.PAYLOAD_COMPLETE, chunk
# Read all bytes until eof
elif self._type == ParseState.PARSE_UNTIL_EOF:
- self.payload.feed_data(chunk, len(chunk))
+ self._more_data_available = self.payload.feed_data(chunk, len(chunk))
+ while self._more_data_available:
+ if self._paused:
+ self._paused = False
+ return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b""
+ self._more_data_available = self.payload.feed_data(b"", 0)
+
+ if self._eof_pending:
+ self.payload.feed_eof()
+ self.done = True
+ self._eof_pending = False
+ return PayloadState.PAYLOAD_COMPLETE, b""
- return False, b""
+ return PayloadState.PAYLOAD_NEEDS_INPUT, b""
class DeflateBuffer:
@@ -1029,10 +1109,7 @@ def set_exception(
) -> None:
set_exception(self.out, exc, exc_cause)
- def feed_data(self, chunk: bytes, size: int) -> None:
- if not size:
- return
-
+ def feed_data(self, chunk: bytes, size: int) -> bool:
self.size += size
self.out.total_compressed_bytes = self.size
@@ -1051,9 +1128,8 @@ def feed_data(self, chunk: bytes, size: int) -> None:
)
try:
- # Decompress with limit + 1 so we can detect if output exceeds limit
chunk = self.decompressor.decompress_sync(
- chunk, max_length=self._max_decompress_size + 1
+ chunk, max_length=self._max_decompress_size
)
except Exception:
raise ContentEncodingError(
@@ -1062,21 +1138,18 @@ def feed_data(self, chunk: bytes, size: int) -> None:
self._started_decoding = True
- # Check if decompression limit was exceeded
- if len(chunk) > self._max_decompress_size:
- raise DecompressSizeError(
- "Decompressed data exceeds the configured limit of %d bytes"
- % self._max_decompress_size
- )
-
if chunk:
self.out.feed_data(chunk, len(chunk))
+ return self.decompressor.data_available # type: ignore[no-any-return]
def feed_eof(self) -> None:
chunk = self.decompressor.flush()
+ # This should never contain data as we defer the call until exhausting
+ # the decompression. If .flush() is returning data, this may indicate a
+ # zip bomb vulnerability as it will decompress all remaining data at once.
+ assert not chunk
- if chunk or self.size > 0:
- self.out.feed_data(chunk, len(chunk))
+ if self.size > 0:
if self.encoding == "deflate" and not self.decompressor.eof:
raise ContentEncodingError("deflate")
diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py
index 5a4f5742fcb..cf990ded777 100644
--- a/aiohttp/multipart.py
+++ b/aiohttp/multipart.py
@@ -268,6 +268,8 @@ def __init__(
subtype: str = "mixed",
default_charset: str | None = None,
max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE,
+ client_max_size: int = sys.maxsize,
+ max_size_error_cls: type[Exception] = ValueError,
) -> None:
self.headers = headers
self._boundary = boundary
@@ -285,6 +287,8 @@ def __init__(
self._content_eof = 0
self._cache: dict[str, Any] = {}
self._max_decompress_size = max_decompress_size
+ self._client_max_size = client_max_size
+ self._max_size_error_cls = max_size_error_cls
def __aiter__(self: Self) -> Self:
return self
@@ -313,10 +317,14 @@ async def read(self, *, decode: bool = False) -> bytes:
data = bytearray()
while not self._at_eof:
data.extend(await self.read_chunk(self.chunk_size))
+ if len(data) > self._client_max_size:
+ raise self._max_size_error_cls(self._client_max_size)
if decode:
decoded_data = bytearray()
async for d in self.decode_iter(data):
decoded_data.extend(d)
+ if len(decoded_data) > self._client_max_size:
+ raise self._max_size_error_cls(self._client_max_size)
return decoded_data
return data
@@ -558,6 +566,8 @@ async def _decode_content_async(self, data: bytes) -> AsyncIterator[bytes]:
suppress_deflate_header=True,
)
yield await d.decompress(data, max_length=self._max_decompress_size)
+ while d.data_available:
+ yield await d.decompress(b"", max_length=self._max_decompress_size)
else:
raise RuntimeError(f"unknown content encoding: {encoding}")
@@ -651,8 +661,10 @@ def __init__(
headers: Mapping[str, str],
content: StreamReader,
*,
+ client_max_size: int = sys.maxsize,
max_field_size: int = 8190,
max_headers: int = 128,
+ max_size_error_cls: type[Exception] = ValueError,
) -> None:
self._mimetype = parse_mimetype(headers[CONTENT_TYPE])
assert self._mimetype.type == "multipart", "multipart/* content type expected"
@@ -663,11 +675,13 @@ def __init__(
self.headers = headers
self._boundary = ("--" + self._get_boundary()).encode()
+ self._client_max_size = client_max_size
self._content = content
self._default_charset: str | None = None
self._last_part: MultipartReader | BodyPartReader | None = None
self._max_field_size = max_field_size
self._max_headers = max_headers
+ self._max_size_error_cls = max_size_error_cls
self._at_eof = False
self._at_bof = True
self._unread: list[bytes] = []
@@ -766,12 +780,21 @@ def _get_part_reader(
if mimetype.type == "multipart":
if self.multipart_reader_cls is None:
- return type(self)(headers, self._content)
+ return type(self)(
+ headers,
+ self._content,
+ client_max_size=self._client_max_size,
+ max_field_size=self._max_field_size,
+ max_headers=self._max_headers,
+ max_size_error_cls=self._max_size_error_cls,
+ )
return self.multipart_reader_cls(
headers,
self._content,
+ client_max_size=self._client_max_size,
max_field_size=self._max_field_size,
max_headers=self._max_headers,
+ max_size_error_cls=self._max_size_error_cls,
)
else:
return self.part_reader_cls(
@@ -780,6 +803,8 @@ def _get_part_reader(
self._content,
subtype=self._mimetype.subtype,
default_charset=self._default_charset,
+ client_max_size=self._client_max_size,
+ max_size_error_cls=self._max_size_error_cls,
)
def _get_boundary(self) -> str:
diff --git a/aiohttp/payload.py b/aiohttp/payload.py
index 5ea00039be7..5b4cf94800c 100644
--- a/aiohttp/payload.py
+++ b/aiohttp/payload.py
@@ -43,7 +43,7 @@
)
TOO_LARGE_BYTES_BODY: Final[int] = 2**20 # 1 MB
-READ_SIZE: Final[int] = 2**16 # 64 KB
+READ_SIZE: Final[int] = 2**18 # 256 KiB
_CLOSE_FUTURES: set[asyncio.Future[None]] = set()
diff --git a/aiohttp/streams.py b/aiohttp/streams.py
index 33cd2113d84..bf26b59d49e 100644
--- a/aiohttp/streams.py
+++ b/aiohttp/streams.py
@@ -140,11 +140,11 @@ def __init__(
self._high_water = limit * 2
if loop is None:
loop = asyncio.get_event_loop()
- # Ensure high_water_chunks >= 3 so it's always > low_water_chunks.
- self._high_water_chunks = max(3, limit // 4)
- # Use max(2, ...) because there's always at least 1 chunk split remaining
+ # Use max(4, ...) because there's always at least 1 chunk split remaining
# (the current position), so we need low_water >= 2 to allow resume.
- self._low_water_chunks = max(2, self._high_water_chunks // 2)
+ # limit // 16 gets us a reasonable value of 16k with default 256KiB limit.
+ self._high_water_chunks = max(4, limit // 16)
+ self._low_water_chunks = self._high_water_chunks // 2
self._loop = loop
self._size = 0
self._cursor = 0
@@ -167,7 +167,7 @@ def __repr__(self) -> str:
info.append("%d bytes" % self._size)
if self._eof:
info.append("eof")
- if self._low_water != 2**16: # default limit
+ if self._low_water != 2**18: # default limit
info.append("low=%d high=%d" % (self._low_water, self._high_water))
if self._waiter:
info.append("w=%r" % self._waiter)
@@ -221,8 +221,8 @@ def feed_eof(self) -> None:
self._eof_waiter = None
set_result(waiter, None)
- if self._protocol._reading_paused:
- self._protocol.resume_reading()
+ # At EOF the parser is done, there won't be unprocessed data.
+ self._protocol.resume_reading(resume_parser=False)
for cb in self._eof_callbacks:
try:
@@ -277,11 +277,11 @@ def unread_data(self, data: bytes) -> None:
self._eof_counter = 0
# TODO: size is ignored, remove the param later
- def feed_data(self, data: bytes, size: int = 0) -> None:
+ def feed_data(self, data: bytes, size: int = 0) -> bool:
assert not self._eof, "feed_data after feed_eof"
if not data:
- return
+ return False
data_len = len(data)
self._size += data_len
@@ -293,8 +293,9 @@ def feed_data(self, data: bytes, size: int = 0) -> None:
self._waiter = None
set_result(waiter, None)
- if self._size > self._high_water and not self._protocol._reading_paused:
+ if self._size > self._high_water:
self._protocol.pause_reading()
+ return False
def begin_http_chunk_receiving(self) -> None:
if self._http_chunk_splits is None:
@@ -331,10 +332,7 @@ def end_http_chunk_receiving(self) -> None:
# If we get too many small chunks before self._high_water is reached, then any
# .read() call becomes computationally expensive, and could block the event loop
# for too long, hence an additional self._high_water_chunks here.
- if (
- len(self._http_chunk_splits) > self._high_water_chunks
- and not self._protocol._reading_paused
- ):
+ if len(self._http_chunk_splits) > self._high_water_chunks:
self._protocol.pause_reading()
# wake up readchunk when end of http chunk received
@@ -548,13 +546,9 @@ def _read_nowait_chunk(self, n: int) -> bytes:
while chunk_splits and chunk_splits[0] < self._cursor:
chunk_splits.popleft()
- if (
- self._protocol._reading_paused
- and self._size < self._low_water
- and (
- self._http_chunk_splits is None
- or len(self._http_chunk_splits) < self._low_water_chunks
- )
+ if self._size < self._low_water and (
+ self._http_chunk_splits is None
+ or len(self._http_chunk_splits) < self._low_water_chunks
):
self._protocol.resume_reading()
return data
@@ -614,8 +608,8 @@ def at_eof(self) -> bool:
async def wait_eof(self) -> None:
return
- def feed_data(self, data: bytes, n: int = 0) -> None:
- pass
+ def feed_data(self, data: bytes, n: int = 0) -> bool:
+ return False
async def readline(self, *, max_line_length: int | None = None) -> bytes:
return b""
diff --git a/aiohttp/web_exceptions.py b/aiohttp/web_exceptions.py
index 5f065a86add..b5b3b30cb7d 100644
--- a/aiohttp/web_exceptions.py
+++ b/aiohttp/web_exceptions.py
@@ -315,12 +315,8 @@ class HTTPPreconditionFailed(HTTPClientError):
class HTTPRequestEntityTooLarge(HTTPClientError):
status_code = 413
- def __init__(self, max_size: float, actual_size: float, **kwargs: Any) -> None:
- kwargs.setdefault(
- "text",
- f"Maximum request body size {max_size} exceeded, "
- f"actual body size {actual_size}",
- )
+ def __init__(self, max_size: float, actual_size: float = 0, **kwargs: Any) -> None:
+ kwargs.setdefault("text", f"Maximum request body size {max_size} exceeded.")
super().__init__(**kwargs)
diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py
index 68fb266c786..be63b824b45 100644
--- a/aiohttp/web_protocol.py
+++ b/aiohttp/web_protocol.py
@@ -24,6 +24,7 @@
HttpVersion10,
RawRequestMessage,
StreamWriter,
+ WebSocketReader,
)
from .http_exceptions import BadHttpMethod
from .log import access_logger, server_logger
@@ -149,11 +150,8 @@ class RequestHandler(BaseProtocol):
"_handler_waiter",
"_waiter",
"_task_handler",
- "_upgrade",
"_payload_parser",
"_data_received_cb",
- "_request_parser",
- "_reading_paused",
"logger",
"debug",
"access_log",
@@ -184,11 +182,21 @@ def __init__(
max_headers: int = 128,
max_field_size: int = 8190,
lingering_time: float = 10.0,
- read_bufsize: int = 2**16,
+ read_bufsize: int = 2**18,
auto_decompress: bool = True,
timeout_ceil_threshold: float = 5,
):
- super().__init__(loop)
+ parser = HttpRequestParser(
+ self,
+ loop,
+ read_bufsize,
+ max_line_size=max_line_size,
+ max_field_size=max_field_size,
+ max_headers=max_headers,
+ payload_exception=RequestPayloadError,
+ auto_decompress=auto_decompress,
+ )
+ super().__init__(loop, parser)
# _request_count is the number of requests processed with the same connection.
self._request_count = 0
@@ -216,19 +224,7 @@ def __init__(
self._waiter: asyncio.Future[None] | None = None
self._handler_waiter: asyncio.Future[None] | None = None
self._task_handler: asyncio.Task[None] | None = None
-
- self._upgrade = False
self._payload_parser: Any = None
- self._request_parser: HttpRequestParser | None = HttpRequestParser(
- self,
- loop,
- read_bufsize,
- max_line_size=max_line_size,
- max_field_size=max_field_size,
- max_headers=max_headers,
- payload_exception=RequestPayloadError,
- auto_decompress=auto_decompress,
- )
self._timeout_ceil_threshold: float = 5
try:
@@ -363,7 +359,7 @@ def connection_lost(self, exc: BaseException | None) -> None:
self._manager = None
self._request_factory = None
self._request_handler = None
- self._request_parser = None
+ self._parser = None
if self._keepalive_handle is not None:
self._keepalive_handle.cancel()
@@ -383,9 +379,10 @@ def connection_lost(self, exc: BaseException | None) -> None:
self._payload_parser = None
def set_parser(
- self, parser: Any, data_received_cb: Callable[[], None] | None = None
+ self,
+ parser: WebSocketReader,
+ data_received_cb: Callable[[], None] | None = None,
) -> None:
- # Actual type is WebReader
assert self._payload_parser is None
self._payload_parser = parser
@@ -403,10 +400,10 @@ def data_received(self, data: bytes) -> None:
return
# parse http messages
messages: Sequence[_MsgType]
- if self._payload_parser is None and not self._upgrade:
- assert self._request_parser is not None
+ if self._payload_parser is None and not self._upgraded:
+ assert self._parser is not None
try:
- messages, upgraded, tail = self._request_parser.feed_data(data)
+ messages, upgraded, tail = self._parser.feed_data(data)
except HttpProcessingError as exc:
messages = [
(_ErrInfo(status=400, exc=exc, message=exc.message), EMPTY_PAYLOAD)
@@ -423,12 +420,12 @@ def data_received(self, data: bytes) -> None:
# don't set result twice
waiter.set_result(None)
- self._upgrade = upgraded
+ self._upgraded = upgraded
if upgraded and tail:
self._message_tail = tail
# no parser, just store
- elif self._payload_parser is None and self._upgrade and data:
+ elif self._payload_parser is None and self._upgraded and data:
self._message_tail += data
# feed payload
@@ -691,11 +688,11 @@ async def finish_response(
prematurely.
"""
request._finish()
- if self._request_parser is not None:
- self._request_parser.set_upgraded(False)
- self._upgrade = False
+ if self._parser is not None:
+ self._parser.set_upgraded(False)
+ self._upgraded = False
if self._message_tail:
- self._request_parser.feed_data(self._message_tail)
+ self._parser.feed_data(self._message_tail)
self._message_tail = b""
try:
prepare_meth = resp.prepare
diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py
index 4bdd7aff606..826a88337a0 100644
--- a/aiohttp/web_request.py
+++ b/aiohttp/web_request.py
@@ -683,10 +683,8 @@ async def read(self) -> bytes:
body.extend(chunk)
if self._client_max_size:
body_size = len(body)
- if body_size >= self._client_max_size:
- raise HTTPRequestEntityTooLarge(
- max_size=self._client_max_size, actual_size=body_size
- )
+ if body_size > self._client_max_size:
+ raise HTTPRequestEntityTooLarge(self._client_max_size)
if not chunk:
break
self._read_bytes = bytes(body)
@@ -708,8 +706,10 @@ async def multipart(self) -> MultipartReader:
return MultipartReader(
self._headers,
self._payload,
+ client_max_size=self._client_max_size,
max_field_size=self._protocol.max_field_size,
max_headers=self._protocol.max_headers,
+ max_size_error_cls=HTTPRequestEntityTooLarge,
)
async def post(self) -> "MultiDictProxy[str | bytes | FileField]":
@@ -760,9 +760,7 @@ async def post(self) -> "MultiDictProxy[str | bytes | FileField]":
size += len(decoded_chunk)
if 0 < max_size < size:
await self._loop.run_in_executor(None, tmp.close)
- raise HTTPRequestEntityTooLarge(
- max_size=max_size, actual_size=size
- )
+ raise HTTPRequestEntityTooLarge(max_size)
await self._loop.run_in_executor(None, tmp.seek, 0)
if field_ct is None:
@@ -782,9 +780,7 @@ async def post(self) -> "MultiDictProxy[str | bytes | FileField]":
while chunk := await field.read_chunk():
size += len(chunk)
if 0 < max_size < size:
- raise HTTPRequestEntityTooLarge(
- max_size=max_size, actual_size=size
- )
+ raise HTTPRequestEntityTooLarge(max_size)
raw_data.extend(chunk)
value = bytearray()
diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py
index 4c540eea422..74811fdd283 100644
--- a/aiohttp/web_ws.py
+++ b/aiohttp/web_ws.py
@@ -380,7 +380,7 @@ def _post_start(
loop = self._loop
assert loop is not None
- self._reader = WebSocketDataQueue(request._protocol, 2**16, loop=loop)
+ self._reader = WebSocketDataQueue(request._protocol, 2**18, loop=loop)
parser = WebSocketReader(
self._reader,
self._max_msg_size,
diff --git a/setup.cfg b/setup.cfg
index 5f02a07c14f..01e43450d95 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -86,6 +86,8 @@ filterwarnings =
# https://github.com/spulec/freezegun/issues/508
# https://github.com/spulec/freezegun/pull/511
ignore:datetime.*utcnow\(\) is deprecated and scheduled for removal:DeprecationWarning:freezegun.api
+ # Weird issue in Python 3.13+ triggered in test_multipart.py
+ ignore:coroutine method 'aclose' of 'BodyPartReader._decode_content_async' was never awaited:RuntimeWarning
junit_suite_name = aiohttp_test_suite
norecursedirs = dist docs build .tox .eggs
minversion = 3.8.2
diff --git a/tests/test_base_protocol.py b/tests/test_base_protocol.py
index 4866ea37576..20b9a41d788 100644
--- a/tests/test_base_protocol.py
+++ b/tests/test_base_protocol.py
@@ -5,6 +5,7 @@
import pytest
from aiohttp.base_protocol import BaseProtocol
+from aiohttp.http_parser import HttpParser
async def test_loop() -> None:
@@ -26,33 +27,28 @@ async def test_pause_writing() -> None:
async def test_pause_reading_no_transport() -> None:
loop = asyncio.get_event_loop()
- pr = BaseProtocol(loop)
- assert not pr._reading_paused
+ parser = mock.create_autospec(HttpParser, spec_set=True, instance=True)
+ pr = BaseProtocol(loop, parser=parser)
pr.pause_reading()
- assert not pr._reading_paused
+ parser.pause_reading.assert_called_once()
async def test_pause_reading_stub_transport() -> None:
loop = asyncio.get_event_loop()
- pr = BaseProtocol(loop)
+ parser = mock.create_autospec(HttpParser, spec_set=True, instance=True)
+ pr = BaseProtocol(loop, parser=parser)
tr = asyncio.Transport()
pr.transport = tr
assert not pr._reading_paused
pr.pause_reading()
assert pr._reading_paused
-
-
-async def test_resume_reading_no_transport() -> None:
- loop = asyncio.get_event_loop()
- pr = BaseProtocol(loop)
- pr._reading_paused = True
- pr.resume_reading()
- assert pr._reading_paused
+ parser.pause_reading.assert_called_once() # type: ignore[unreachable]
async def test_resume_reading_stub_transport() -> None:
loop = asyncio.get_event_loop()
- pr = BaseProtocol(loop)
+ parser = mock.create_autospec(HttpParser, spec_set=True, instance=True)
+ pr = BaseProtocol(loop, parser=parser)
tr = asyncio.Transport()
pr.transport = tr
pr._reading_paused = True
diff --git a/tests/test_benchmarks_http_websocket.py b/tests/test_benchmarks_http_websocket.py
index d2761b3687e..f40db171919 100644
--- a/tests/test_benchmarks_http_websocket.py
+++ b/tests/test_benchmarks_http_websocket.py
@@ -36,8 +36,8 @@ def test_read_one_hundred_websocket_text_messages(
loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture
) -> None:
"""Benchmark reading 100 WebSocket text messages."""
- queue = WebSocketDataQueue(BaseProtocol(loop), 2**16, loop=loop)
- reader = WebSocketReader(queue, max_msg_size=2**16)
+ queue = WebSocketDataQueue(BaseProtocol(loop), 2**18, loop=loop)
+ reader = WebSocketReader(queue, max_msg_size=2**18)
raw_message = (
b'\x81~\x01!{"id":1,"src":"shellyplugus-c049ef8c30e4","dst":"aios-1453812500'
b'8","result":{"name":null,"id":"shellyplugus-c049ef8c30e4","mac":"C049EF8C30E'
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
index 524b712d4b8..05e6574b89f 100644
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -53,7 +53,6 @@
from aiohttp.client_reqrep import ClientRequest
from aiohttp.compression_utils import DEFAULT_MAX_DECOMPRESS_SIZE
from aiohttp.connector import Connection
-from aiohttp.http_exceptions import DecompressSizeError
from aiohttp.http_writer import StreamWriter
from aiohttp.payload import (
AsyncIterablePayload,
@@ -2454,10 +2453,9 @@ async def test_payload_decompress_size_limit(aiohttp_client: AiohttpClient) -> N
When a compressed payload expands beyond the configured limit,
we raise DecompressSizeError.
"""
- # Create a highly compressible payload that exceeds the decompression limit.
- # 64MiB of repeated bytes compresses to ~32KB but expands beyond the
- # 32MiB per-call limit.
- original = b"A" * (64 * 2**20)
+ # Create a highly compressible payload.
+ payload_size = 64 * 2**20
+ original = b"A" * payload_size
compressed = zlib.compress(original)
assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE
@@ -2474,11 +2472,11 @@ async def handler(request: web.Request) -> web.Response:
async with client.get("/") as resp:
assert resp.status == 200
- with pytest.raises(aiohttp.ClientPayloadError) as exc_info:
- await resp.read()
+ received = 0
+ async for chunk in resp.content.iter_chunked(1024):
+ received += len(chunk)
- assert isinstance(exc_info.value.__cause__, DecompressSizeError)
- assert "Decompressed data exceeds" in str(exc_info.value.__cause__)
+ assert received == payload_size
@pytest.mark.skipif(brotli is None, reason="brotli is not installed")
@@ -2487,8 +2485,9 @@ async def test_payload_decompress_size_limit_brotli(
) -> None:
"""Test that brotli decompression size limit triggers DecompressSizeError."""
assert brotli is not None
- # Create a highly compressible payload that exceeds the decompression limit.
- original = b"A" * (64 * 2**20)
+ # Create a highly compressible payload
+ payload_size = 64 * 2**20
+ original = b"A" * payload_size
compressed = brotli.compress(original)
assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE
@@ -2504,11 +2503,11 @@ async def handler(request: web.Request) -> web.Response:
async with client.get("/") as resp:
assert resp.status == 200
- with pytest.raises(aiohttp.ClientPayloadError) as exc_info:
- await resp.read()
+ received = 0
+ async for chunk in resp.content.iter_chunked(1024):
+ received += len(chunk)
- assert isinstance(exc_info.value.__cause__, DecompressSizeError)
- assert "Decompressed data exceeds" in str(exc_info.value.__cause__)
+ assert received == payload_size
@pytest.mark.skipif(ZstdCompressor is None, reason="backports.zstd is not installed")
@@ -2517,8 +2516,9 @@ async def test_payload_decompress_size_limit_zstd(
) -> None:
"""Test that zstd decompression size limit triggers DecompressSizeError."""
assert ZstdCompressor is not None
- # Create a highly compressible payload that exceeds the decompression limit.
- original = b"A" * (64 * 2**20)
+ # Create a highly compressible payload.
+ payload_size = 64 * 2**20
+ original = b"A" * payload_size
compressor = ZstdCompressor()
compressed = compressor.compress(original) + compressor.flush()
assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE
@@ -2535,11 +2535,11 @@ async def handler(request: web.Request) -> web.Response:
async with client.get("/") as resp:
assert resp.status == 200
- with pytest.raises(aiohttp.ClientPayloadError) as exc_info:
- await resp.read()
+ received = 0
+ async for chunk in resp.content.iter_chunked(1024):
+ received += len(chunk)
- assert isinstance(exc_info.value.__cause__, DecompressSizeError)
- assert "Decompressed data exceeds" in str(exc_info.value.__cause__)
+ assert received == payload_size
async def test_bad_payload_chunked_encoding(aiohttp_client: AiohttpClient) -> None:
diff --git a/tests/test_client_proto.py b/tests/test_client_proto.py
index b75ebae1137..2936aab021a 100644
--- a/tests/test_client_proto.py
+++ b/tests/test_client_proto.py
@@ -8,6 +8,7 @@
from aiohttp.client_proto import ResponseHandler
from aiohttp.client_reqrep import ClientResponse
from aiohttp.helpers import TimerNoop
+from aiohttp.http_parser import HttpParser
async def test_force_close(loop: asyncio.AbstractEventLoop) -> None:
@@ -31,8 +32,10 @@ async def test_oserror(loop: asyncio.AbstractEventLoop) -> None:
assert isinstance(proto.exception(), ClientOSError)
-async def test_pause_resume_on_error(loop) -> None:
+async def test_pause_resume_on_error(loop: asyncio.AbstractEventLoop) -> None:
+ parser = mock.create_autospec(HttpParser, spec_set=True, instance=True)
proto = ResponseHandler(loop=loop)
+ proto._parser = parser
transport = mock.Mock()
proto.connection_made(transport)
diff --git a/tests/test_flowcontrol_streams.py b/tests/test_flowcontrol_streams.py
index 9874cc2511e..e71107b0e00 100644
--- a/tests/test_flowcontrol_streams.py
+++ b/tests/test_flowcontrol_streams.py
@@ -5,6 +5,7 @@
from aiohttp import streams
from aiohttp.base_protocol import BaseProtocol
+from aiohttp.http_parser import HttpParser
@pytest.fixture
@@ -43,7 +44,6 @@ async def test_readline(self, stream) -> None:
stream.feed_data(b"d\n", 5)
res = await stream.readline()
assert res == b"d\n"
- assert not stream._protocol.resume_reading.called
async def test_readline_resume_paused(self, stream) -> None:
stream._protocol._reading_paused = True
@@ -56,7 +56,6 @@ async def test_readany(self, stream) -> None:
stream.feed_data(b"data", 4)
res = await stream.readany()
assert res == b"data"
- assert not stream._protocol.resume_reading.called
async def test_readany_resume_paused(self, stream) -> None:
stream._protocol._reading_paused = True
@@ -70,7 +69,6 @@ async def test_readchunk(self, stream) -> None:
res, end_of_http_chunk = await stream.readchunk()
assert res == b"data"
assert not end_of_http_chunk
- assert not stream._protocol.resume_reading.called
async def test_readchunk_resume_paused(self, stream) -> None:
stream._protocol._reading_paused = True
@@ -194,7 +192,8 @@ async def test_flow_control_data_queue_read_eof(
async def test_stream_reader_eof_when_full() -> None:
loop = asyncio.get_event_loop()
- protocol = BaseProtocol(loop=loop)
+ parser = mock.create_autospec(HttpParser, spec_set=True, instance=True)
+ protocol = BaseProtocol(loop=loop, parser=parser)
protocol.transport = asyncio.Transport()
stream = streams.StreamReader(protocol, 1024, loop=loop)
diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
index 16e0ab1f558..6f9caa2fe55 100644
--- a/tests/test_http_parser.py
+++ b/tests/test_http_parser.py
@@ -1,9 +1,11 @@
# Tests for aiohttp/protocol.py
import asyncio
+import platform
import re
import sys
import zlib
+from collections.abc import Iterator
from contextlib import nullcontext
from typing import Any
from unittest import mock
@@ -16,6 +18,7 @@
import aiohttp
from aiohttp import http_exceptions, streams
from aiohttp.base_protocol import BaseProtocol
+from aiohttp.client_proto import ResponseHandler
from aiohttp.http_parser import (
NO_EXTENSIONS,
DeflateBuffer,
@@ -25,8 +28,11 @@
HttpRequestParserPy,
HttpResponseParser,
HttpResponseParserPy,
- HttpVersion,
+ PayloadState,
)
+from aiohttp.http_writer import HttpVersion
+from aiohttp.web_protocol import RequestHandler
+from aiohttp.web_server import Server
try:
try:
@@ -57,8 +63,22 @@
@pytest.fixture
-def protocol():
- return mock.Mock()
+def server() -> Any:
+ return mock.create_autospec(
+ Server,
+ request_factory=mock.Mock(),
+ request_handler=mock.AsyncMock(),
+ instance=True,
+ )
+
+
+@pytest.fixture
+def protocol() -> Any:
+ return mock.create_autospec(
+ BaseProtocol,
+ spec_set=True,
+ instance=True,
+ )
def _gen_ids(parsers: list[Any]) -> list[str]:
@@ -69,16 +89,26 @@ def _gen_ids(parsers: list[Any]) -> list[str]:
@pytest.fixture(params=REQUEST_PARSERS, ids=_gen_ids(REQUEST_PARSERS))
-def parser(loop: Any, protocol: Any, request: Any):
+def parser(
+ loop: asyncio.AbstractEventLoop,
+ server: Server,
+ request: pytest.FixtureRequest,
+) -> Iterator[HttpRequestParser]:
+ protocol = RequestHandler(server, loop=loop)
+
# Parser implementations
- return request.param(
+ parser = request.param(
protocol,
loop,
- 2**16,
+ 2**18,
max_line_size=8190,
max_headers=128,
max_field_size=8190,
)
+ protocol._force_close = False
+ protocol._parser = parser
+ with mock.patch.object(protocol, "transport", True):
+ yield parser
@pytest.fixture(params=REQUEST_PARSERS, ids=_gen_ids(REQUEST_PARSERS))
@@ -88,17 +118,24 @@ def request_cls(request: Any):
@pytest.fixture(params=RESPONSE_PARSERS, ids=_gen_ids(RESPONSE_PARSERS))
-def response(loop: Any, protocol: Any, request: Any):
+def response(
+ loop: asyncio.AbstractEventLoop,
+ request: pytest.FixtureRequest,
+) -> HttpResponseParser:
+ protocol = ResponseHandler(loop)
+
# Parser implementations
- return request.param(
+ parser = request.param(
protocol,
loop,
- 2**16,
+ 2**18,
max_line_size=8190,
max_headers=128,
max_field_size=8190,
read_until_eof=True,
)
+ protocol._parser = parser
+ return parser # type: ignore[no-any-return]
@pytest.fixture(params=RESPONSE_PARSERS, ids=_gen_ids(RESPONSE_PARSERS))
@@ -149,7 +186,13 @@ def test_reject_obsolete_line_folding(parser: Any) -> None:
@pytest.mark.skipif(NO_EXTENSIONS, reason="Only tests C parser.")
-def test_invalid_character(loop: Any, protocol: Any, request: Any) -> None:
+def test_invalid_character(
+ loop: asyncio.AbstractEventLoop,
+ server: Server,
+ request: pytest.FixtureRequest,
+) -> None:
+ protocol = RequestHandler(server, loop=loop)
+
parser = HttpRequestParserC(
protocol,
loop,
@@ -157,6 +200,7 @@ def test_invalid_character(loop: Any, protocol: Any, request: Any) -> None:
max_line_size=8190,
max_field_size=8190,
)
+ protocol._parser = parser
text = b"POST / HTTP/1.1\r\nHost: localhost:8080\r\nSet-Cookie: abc\x01def\r\n\r\n"
error_detail = re.escape(
r""":
@@ -169,7 +213,13 @@ def test_invalid_character(loop: Any, protocol: Any, request: Any) -> None:
@pytest.mark.skipif(NO_EXTENSIONS, reason="Only tests C parser.")
-def test_invalid_linebreak(loop: Any, protocol: Any, request: Any) -> None:
+def test_invalid_linebreak(
+ loop: asyncio.AbstractEventLoop,
+ server: Server,
+ request: pytest.FixtureRequest,
+) -> None:
+ protocol = RequestHandler(server, loop=loop)
+
parser = HttpRequestParserC(
protocol,
loop,
@@ -177,6 +227,7 @@ def test_invalid_linebreak(loop: Any, protocol: Any, request: Any) -> None:
max_line_size=8190,
max_field_size=8190,
)
+ protocol._parser = parser
text = b"GET /world HTTP/1.1\r\nHost: 127.0.0.1\n\r\n"
error_detail = re.escape(
r""":
@@ -239,7 +290,11 @@ def test_ctl_host_header_bad_characters(parser: HttpRequestParser) -> None:
parser.feed_data(text)
-def test_unpaired_surrogate_in_header_py(loop: Any, protocol: Any) -> None:
+def test_unpaired_surrogate_in_header_py(
+ loop: asyncio.AbstractEventLoop, server: Server
+) -> None:
+ protocol = RequestHandler(server, loop=loop)
+
parser = HttpRequestParserPy(
protocol,
loop,
@@ -247,6 +302,7 @@ def test_unpaired_surrogate_in_header_py(loop: Any, protocol: Any) -> None:
max_line_size=8190,
max_field_size=8190,
)
+ protocol._parser = parser
text = b"POST / HTTP/1.1\r\n\xff\r\n\r\n"
message = None
try:
@@ -987,6 +1043,203 @@ def test_max_header_value_size_under_limit(parser: HttpRequestParser) -> None:
assert msg.url == URL("/test")
+async def test_chunk_splits_after_pause(parser: HttpRequestParser) -> None:
+ text = (
+ b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
+ + b"1\r\nb\r\n" * 50000
+ + b"0\r\n\r\n"
+ )
+
+ messages, upgrade, tail = parser.feed_data(text)
+ payload = messages[0][-1]
+ # Payload should have paused reading and stopped receiving new chunks after 16k.
+ assert payload._http_chunk_splits is not None
+ assert len(payload._http_chunk_splits) == 16385
+ # We should still get the full result after read(), as it will continue processing.
+ result = await payload.read()
+ assert len(result) == 50000 # Compare len first, as it's easier to debug in diff.
+ assert result == b"b" * 50000
+
+
+async def test_compressed_with_tail(response: HttpResponseParser) -> None:
+ """Test compressed content-length body followed by a second response.
+
+ With 2 responses arriving in one call and the first compressed, this should
+ trigger decompression pausing with the second response being saved as the tail.
+ Verify that the second response is resumed from the tail.
+ """
+ # Must be large enough to exceed high water mark.
+ original = b"x" * 1024 * 1024
+ compressed = zlib.compress(original)
+ resp1 = (
+ b"HTTP/1.1 200 OK\r\n"
+ b"Content-Length: " + str(len(compressed)).encode() + b"\r\n"
+ b"Content-Encoding: deflate\r\n"
+ b"\r\n"
+ ) + compressed
+ resp2 = b"HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nok"
+
+ msgs, upgrade, tail = response.feed_data(resp1 + resp2)
+ payload = msgs[0][-1]
+ result = await payload.read()
+ assert len(result) == len(original)
+ assert result == original
+
+ payload = response.protocol._buffer[0][0][-1]
+ result = await payload.read()
+ assert result == b"ok"
+
+
+async def test_two_content_length_responses_in_one_call(
+ response: HttpResponseParser,
+) -> None:
+ """Two complete responses in a single feed_data call.
+
+ The first payload completes with tail data for the second, hitting the
+ PAYLOAD_COMPLETE branch that resets the parser for the next message.
+ """
+ resp1 = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello"
+ resp2 = b"HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nworld"
+
+ msgs, upgrade, tail = response.feed_data(resp1 + resp2)
+ assert len(msgs) == 2
+ assert await msgs[0][-1].read() == b"hello"
+ assert await msgs[1][-1].read() == b"world"
+
+
+@pytest.mark.usefixtures("parametrize_zlib_backend")
+async def test_compressed_zlib_64kb(response_cls: type[HttpResponseParser]) -> None:
+ loop = asyncio.get_running_loop()
+ protocol = ResponseHandler(loop)
+ response = response_cls(
+ protocol,
+ loop,
+ # 64KiB limit triggered a bug with isal implementation not returning all data.
+ 2**16,
+ max_line_size=8190,
+ max_headers=128,
+ max_field_size=8190,
+ )
+ protocol._parser = response
+
+ original = b"".join(
+ bytes((*range(0, i), *range(i, 0, -1))) for _ in range(255) for i in range(255)
+ )
+ compressed = zlib.compress(original)
+ headers = (
+ b"HTTP/1.1 200 OK\r\n"
+ b"Content-Length: " + str(len(compressed)).encode() + b"\r\n"
+ b"Content-Encoding: deflate\r\n"
+ b"\r\n"
+ )
+
+ msgs, upgrade, tail = response.feed_data(headers + compressed)
+ payload = msgs[0][-1]
+ result = await payload.read()
+ assert len(result) == len(original)
+ assert result == original
+
+
+async def test_compressed_chunked_with_pending(response: HttpResponseParser) -> None:
+ """Test chunked + compressed where the decompressor needs to resume from pause.
+
+ We need to verify that chunked messages continue parsing correctly after
+ a pause and resume in the decompression.
+ """
+ # Must be large enough to exceed high water mark.
+ original = b"A" * 1024 * 1024
+ compressed = zlib.compress(original)
+ chunk_data = hex(len(compressed))[2:].encode() + b"\r\n" + compressed + b"\r\n"
+ headers = (
+ b"HTTP/1.1 200 OK\r\n"
+ b"Transfer-Encoding: chunked\r\n"
+ b"Content-Encoding: deflate\r\n"
+ b"\r\n"
+ )
+ data = headers + chunk_data + b"0\r\n\r\n"
+
+ msgs, upgrade, tail = response.feed_data(data)
+ payload = msgs[0][-1]
+ result = await payload.read()
+ assert len(result) == len(original)
+ assert result == original
+
+
+async def test_compressed_until_eof_with_pending(response: HttpResponseParser) -> None:
+ """Test read-until-eof + compressed with pause."""
+ # Must be large enough to exceed high water mark.
+ original = b"B" * 5 * 1024 * 1024
+ compressed = zlib.compress(original)
+ # No Content-Length or Transfer-Encoding means the parser must parse until EOF.
+ headers = b"HTTP/1.1 200 OK\r\nContent-Encoding: deflate\r\n\r\n"
+
+ msgs, upgrade, tail = response.feed_data(headers + compressed)
+ response.feed_eof()
+ payload = msgs[0][-1]
+
+ # Check that .feed_eof() hasn't decompressed entire payload into memory.
+ assert sum(len(b) for b in payload._buffer) <= (1024 * 1024)
+
+ result = await payload.read()
+ assert len(result) == len(original)
+ assert result == original
+
+
+async def test_compressed_until_eof_high_water(
+ response_cls: type[HttpResponseParser],
+) -> None:
+ """Test read-until-eof + compressed with higher limit."""
+ loop = asyncio.get_running_loop()
+ protocol = ResponseHandler(loop)
+ response = response_cls(
+ protocol,
+ loop,
+ 2**19, # 512 KiB limit
+ max_line_size=8190,
+ max_headers=128,
+ max_field_size=8190,
+ read_until_eof=True,
+ )
+ protocol._parser = response
+
+ # Must be large enough to exceed high water mark.
+ original = b"B" * 5 * 1024 * 1024
+ compressed = zlib.compress(original)
+ # No Content-Length or Transfer-Encoding means the parser must parse until EOF.
+ headers = b"HTTP/1.1 200 OK\r\nContent-Encoding: deflate\r\n\r\n"
+
+ msgs, upgrade, tail = response.feed_data(headers + compressed)
+ response.feed_eof()
+ payload = msgs[0][-1]
+
+ # Check that .feed_eof() hasn't decompressed entire payload into memory.
+ assert sum(len(b) for b in payload._buffer) <= (2 * 1024 * 1024)
+ # Individual chunks should have been decompressed at limit amount.
+ assert all(len(b) == 512 * 1024 for b in payload._buffer)
+
+ result = await payload.read()
+ assert len(result) == len(original)
+ assert result == original
+
+
+async def test_compressed_256kb(response: HttpResponseParser) -> None:
+ original = b"x" * 256 * 1024
+ compressed = zlib.compress(original)
+ headers = (
+ b"HTTP/1.1 200 OK\r\n"
+ b"Content-Length: " + str(len(compressed)).encode() + b"\r\n"
+ b"Content-Encoding: deflate\r\n"
+ b"\r\n"
+ )
+
+ messages, upgrade, tail = response.feed_data(headers + compressed)
+ assert len(messages) == 1
+ payload = messages[0][-1]
+ result = await payload.read()
+ assert len(result) == len(original)
+ assert result == original
+
+
@pytest.mark.parametrize("size", [40965, 8191])
def test_max_header_value_size_continuation(response, size) -> None:
name = b"T" * (size - 5)
@@ -1412,14 +1665,19 @@ async def test_http_response_parser_bad_chunked_lax(response) -> None:
@pytest.mark.dev_mode
-async def test_http_response_parser_bad_chunked_strict_py(loop, protocol) -> None:
+async def test_http_response_parser_bad_chunked_strict_py(
+ loop: asyncio.AbstractEventLoop,
+) -> None:
+ protocol = ResponseHandler(loop)
+
response = HttpResponseParserPy(
protocol,
loop,
- 2**16,
+ 2**18,
max_line_size=8190,
max_field_size=8190,
)
+ protocol._parser = response
text = (
b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5 \r\nabcde\r\n0\r\n\r\n"
)
@@ -1432,7 +1690,11 @@ async def test_http_response_parser_bad_chunked_strict_py(loop, protocol) -> Non
"HttpRequestParserC" not in dir(aiohttp.http_parser),
reason="C based HTTP parser not available",
)
-async def test_http_response_parser_bad_chunked_strict_c(loop, protocol) -> None:
+async def test_http_response_parser_bad_chunked_strict_c(
+ loop: asyncio.AbstractEventLoop,
+) -> None:
+ protocol = ResponseHandler(loop)
+
response = HttpResponseParserC(
protocol,
loop,
@@ -1440,6 +1702,7 @@ async def test_http_response_parser_bad_chunked_strict_c(loop, protocol) -> None
max_line_size=8190,
max_field_size=8190,
)
+ protocol._parser = response
text = (
b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5 \r\nabcde\r\n0\r\n\r\n"
)
@@ -1582,18 +1845,25 @@ async def test_request_chunked_reject_bad_trailer(parser: HttpRequestParser) ->
def test_parse_no_length_or_te_on_post(
loop: asyncio.AbstractEventLoop,
- protocol: BaseProtocol,
+ server: Server,
request_cls: type[HttpRequestParser],
) -> None:
- parser = request_cls(protocol, loop, limit=2**16)
+ protocol = RequestHandler(server, loop=loop)
+ parser = request_cls(protocol, loop, limit=2**18)
+ protocol._parser = parser
text = b"POST /test HTTP/1.1\r\n\r\n"
msg, payload = parser.feed_data(text)[0][0]
assert payload.is_eof()
-def test_parse_payload_response_without_body(loop, protocol, response_cls) -> None:
+def test_parse_payload_response_without_body(
+ loop: asyncio.AbstractEventLoop,
+ response_cls: type[HttpResponseParser],
+) -> None:
+ protocol = ResponseHandler(loop)
parser = response_cls(protocol, loop, 2**16, response_with_body=False)
+ protocol._parser = parser
text = b"HTTP/1.1 200 Ok\r\ncontent-length: 10\r\n\r\n"
msg, payload = parser.feed_data(text)[0][0]
@@ -1849,16 +2119,21 @@ def test_parse_uri_utf8_percent_encoded(parser) -> None:
"HttpRequestParserC" not in dir(aiohttp.http_parser),
reason="C based HTTP parser not available",
)
-def test_parse_bad_method_for_c_parser_raises(loop, protocol):
+def test_parse_bad_method_for_c_parser_raises(
+ loop: asyncio.AbstractEventLoop, server: Server
+) -> None:
+ protocol = RequestHandler(server, loop=loop)
+
payload = b"GET1 /test HTTP/1.1\r\n\r\n"
parser = HttpRequestParserC(
protocol,
loop,
- 2**16,
+ 2**18,
max_line_size=8190,
max_headers=128,
max_field_size=8190,
)
+ protocol._parser = parser
with pytest.raises(aiohttp.http_exceptions.BadStatusLine):
messages, upgrade, tail = parser.feed_data(payload)
@@ -1875,7 +2150,7 @@ async def test_parse_eof_payload(self, protocol: BaseProtocol) -> None:
assert [(bytearray(b"data"))] == list(out._buffer)
async def test_parse_length_payload_eof(self, protocol: BaseProtocol) -> None:
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(out, length=4, headers_parser=HeadersParser())
p.feed_data(b"da")
@@ -1899,7 +2174,7 @@ async def test_parse_chunked_payload_size_data_mismatch(
Regression test for #10596.
"""
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
# Declared chunk-size is 4 but actual data is "Hello" (5 bytes).
# After consuming 4 bytes, remaining starts with "o" not "\r\n".
@@ -1914,7 +2189,7 @@ async def test_parse_chunked_payload_size_data_mismatch_too_short(
Regression test for #10596.
"""
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
# Declared chunk-size is 6 but actual data before CRLF is "Hello" (5 bytes).
# Parser reads 6 bytes: "Hello\r", then expects \r\n but sees "\n0\r\n..."
@@ -1936,7 +2211,7 @@ async def test_parse_chunked_payload_split_end(
async def test_parse_chunked_payload_split_end2(
self, protocol: BaseProtocol
) -> None:
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
p.feed_data(b"4\r\nasdf\r\n0\r\n\r")
p.feed_data(b"\n")
@@ -1947,7 +2222,7 @@ async def test_parse_chunked_payload_split_end2(
async def test_parse_chunked_payload_split_end_trailers(
self, protocol: BaseProtocol
) -> None:
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
p.feed_data(b"4\r\nasdf\r\n0\r\n")
p.feed_data(b"Content-MD5: 912ec803b2ce49e4a541068d495ab570\r\n")
@@ -1959,7 +2234,7 @@ async def test_parse_chunked_payload_split_end_trailers(
async def test_parse_chunked_payload_split_end_trailers2(
self, protocol: BaseProtocol
) -> None:
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
p.feed_data(b"4\r\nasdf\r\n0\r\n")
p.feed_data(b"Content-MD5: 912ec803b2ce49e4a541068d495ab570\r\n\r")
@@ -1991,10 +2266,10 @@ async def test_parse_chunked_payload_split_end_trailers4(
assert b"asdf" == b"".join(out._buffer)
async def test_http_payload_parser_length(self, protocol: BaseProtocol) -> None:
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(out, length=2, headers_parser=HeadersParser())
- eof, tail = p.feed_data(b"1245")
- assert eof
+ state, tail = p.feed_data(b"1245")
+ assert state is PayloadState.PAYLOAD_COMPLETE
assert b"12" == out._buffer[0]
assert b"45" == tail
@@ -2004,7 +2279,7 @@ async def test_http_payload_parser_deflate(self, protocol: BaseProtocol) -> None
COMPRESSED = b"x\x9cKI,I\x04\x00\x04\x00\x01\x9b"
length = len(COMPRESSED)
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(
out, length=length, compression="deflate", headers_parser=HeadersParser()
)
@@ -2075,7 +2350,7 @@ async def test_http_payload_parser_deflate_split_err(
async def test_http_payload_parser_length_zero(
self, protocol: BaseProtocol
) -> None:
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(out, length=0, headers_parser=HeadersParser())
assert p.done
assert out.is_eof()
@@ -2083,7 +2358,7 @@ async def test_http_payload_parser_length_zero(
@pytest.mark.skipif(brotli is None, reason="brotli is not installed")
async def test_http_payload_brotli(self, protocol: BaseProtocol) -> None:
compressed = brotli.compress(b"brotli data")
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(
out,
length=len(compressed),
@@ -2097,7 +2372,7 @@ async def test_http_payload_brotli(self, protocol: BaseProtocol) -> None:
@pytest.mark.skipif(zstandard is None, reason="zstandard is not installed")
async def test_http_payload_zstandard(self, protocol: BaseProtocol) -> None:
compressed = zstandard.compress(b"zstd data")
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(
out,
length=len(compressed),
@@ -2115,7 +2390,7 @@ async def test_http_payload_zstandard_multi_frame(
frame1 = zstandard.compress(b"first")
frame2 = zstandard.compress(b"second")
payload = frame1 + frame2
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(
out,
length=len(payload),
@@ -2132,7 +2407,7 @@ async def test_http_payload_zstandard_multi_frame_chunked(
) -> None:
frame1 = zstandard.compress(b"chunk1")
frame2 = zstandard.compress(b"chunk2")
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(
out,
length=len(frame1) + len(frame2),
@@ -2152,7 +2427,7 @@ async def test_http_payload_zstandard_frame_split_mid_chunk(
frame2 = zstandard.compress(b"BBBB")
combined = frame1 + frame2
split_point = len(frame1) + 3 # 3 bytes into frame2
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(
out,
length=len(combined),
@@ -2170,7 +2445,7 @@ async def test_http_payload_zstandard_many_small_frames(
) -> None:
parts = [f"part{i}".encode() for i in range(10)]
payload = b"".join(zstandard.compress(p) for p in parts)
- out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
p = HttpPayloadParser(
out,
length=len(payload),
@@ -2184,7 +2459,7 @@ async def test_http_payload_zstandard_many_small_frames(
class TestDeflateBuffer:
async def test_feed_data(self, protocol: BaseProtocol) -> None:
- buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_event_loop())
+ buf = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
dbuf = DeflateBuffer(buf, "deflate")
dbuf.decompressor = mock.Mock()
@@ -2212,10 +2487,10 @@ async def test_feed_eof(self, protocol: BaseProtocol) -> None:
dbuf = DeflateBuffer(buf, "deflate")
dbuf.decompressor = mock.Mock()
- dbuf.decompressor.flush.return_value = b"line"
+ dbuf.decompressor.data_available = False
+ dbuf.decompressor.flush.return_value = b""
dbuf.feed_eof()
- assert [b"line"] == list(buf._buffer)
assert buf._eof
async def test_feed_eof_err_deflate(self, protocol: BaseProtocol) -> None:
@@ -2223,8 +2498,10 @@ async def test_feed_eof_err_deflate(self, protocol: BaseProtocol) -> None:
dbuf = DeflateBuffer(buf, "deflate")
dbuf.decompressor = mock.Mock()
- dbuf.decompressor.flush.return_value = b"line"
+ dbuf.decompressor.data_available = False
+ dbuf.decompressor.flush.return_value = b""
dbuf.decompressor.eof = False
+ dbuf.size = 1 # Simulate that data was previously fed
with pytest.raises(http_exceptions.ContentEncodingError):
dbuf.feed_eof()
@@ -2234,22 +2511,24 @@ async def test_feed_eof_no_err_gzip(self, protocol: BaseProtocol) -> None:
dbuf = DeflateBuffer(buf, "gzip")
dbuf.decompressor = mock.Mock()
- dbuf.decompressor.flush.return_value = b"line"
+ dbuf.decompressor.data_available = False
+ dbuf.decompressor.flush.return_value = b""
dbuf.decompressor.eof = False
dbuf.feed_eof()
- assert [b"line"] == list(buf._buffer)
+ assert buf._eof
async def test_feed_eof_no_err_brotli(self, protocol: BaseProtocol) -> None:
buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
dbuf = DeflateBuffer(buf, "br")
dbuf.decompressor = mock.Mock()
- dbuf.decompressor.flush.return_value = b"line"
+ dbuf.decompressor.data_available = False
+ dbuf.decompressor.flush.return_value = b""
dbuf.decompressor.eof = False
dbuf.feed_eof()
- assert [b"line"] == list(buf._buffer)
+ assert buf._eof
@pytest.mark.skipif(zstandard is None, reason="zstandard is not installed")
async def test_feed_eof_no_err_zstandard(self, protocol: BaseProtocol) -> None:
@@ -2257,19 +2536,21 @@ async def test_feed_eof_no_err_zstandard(self, protocol: BaseProtocol) -> None:
dbuf = DeflateBuffer(buf, "zstd")
dbuf.decompressor = mock.Mock()
- dbuf.decompressor.flush.return_value = b"line"
+ dbuf.decompressor.data_available = False
+ dbuf.decompressor.flush.return_value = b""
dbuf.decompressor.eof = False
dbuf.feed_eof()
- assert [b"line"] == list(buf._buffer)
+ assert buf._eof
async def test_empty_body(self, protocol: BaseProtocol) -> None:
- buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ buf = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
dbuf = DeflateBuffer(buf, "deflate")
dbuf.feed_eof()
assert buf.at_eof()
+ @pytest.mark.skipif(platform.python_implementation() == "PyPy", reason="Broken")
@pytest.mark.parametrize(
"chunk_size",
[1024, 2**14, 2**16], # 1KB, 16KB, 64KB
@@ -2288,13 +2569,14 @@ async def test_streaming_decompress_large_payload(
original = b"A" * (3 * 2**20)
compressed = zlib.compress(original)
- buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ buf = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
dbuf = DeflateBuffer(buf, "deflate")
# Feed compressed data in chunks (simulating network streaming)
for i in range(0, len(compressed), chunk_size): # pragma: no branch
chunk = compressed[i : i + chunk_size]
- dbuf.feed_data(chunk, len(chunk))
+ while dbuf.feed_data(chunk, len(chunk)):
+ chunk = b""
dbuf.feed_eof()
diff --git a/tests/test_http_writer.py b/tests/test_http_writer.py
index 0032f040cf5..251645f2398 100644
--- a/tests/test_http_writer.py
+++ b/tests/test_http_writer.py
@@ -1394,7 +1394,7 @@ async def test_write_drain_condition_with_small_buffer(
protocol._drain_helper.reset_mock() # type: ignore[attr-defined]
# Write small amount of data with drain=True but buffer under limit
- small_data = b"x" * 100 # Much less than LIMIT (2**16)
+ small_data = b"x" * 100 # Much less than LIMIT (2**18)
await msg.write(small_data, drain=True)
# Drain should NOT be called because buffer_size <= LIMIT
@@ -1423,7 +1423,7 @@ async def test_write_drain_condition_with_large_buffer(
protocol._drain_helper.reset_mock() # type: ignore[attr-defined]
# Write large amount of data with drain=True
- large_data = b"x" * (2**16 + 1) # Just over LIMIT
+ large_data = b"x" * (2**18 + 1) # Just over LIMIT
await msg.write(large_data, drain=True)
# Drain should be called because drain=True AND buffer_size > LIMIT
@@ -1452,12 +1452,12 @@ async def test_write_no_drain_with_large_buffer(
protocol._drain_helper.reset_mock() # type: ignore[attr-defined]
# Write large amount of data with drain=False
- large_data = b"x" * (2**16 + 1) # Just over LIMIT
+ large_data = b"x" * (2**18 + 1) # Just over LIMIT
await msg.write(large_data, drain=False)
# Drain should NOT be called because drain=False
assert not protocol._drain_helper.called # type: ignore[attr-defined]
- assert msg.buffer_size == (2**16 + 1) # Buffer not reset
+ assert msg.buffer_size == (2**18 + 1) # Buffer not reset
assert large_data in buf
diff --git a/tests/test_multipart.py b/tests/test_multipart.py
index 34e745830a9..0d01bf6a5ae 100644
--- a/tests/test_multipart.py
+++ b/tests/test_multipart.py
@@ -1,7 +1,9 @@
import asyncio
+import gzip
import io
import json
import pathlib
+import sys
from unittest import mock
import pytest
@@ -24,6 +26,7 @@
MultipartResponseWrapper,
)
from aiohttp.streams import StreamReader
+from aiohttp.web_exceptions import HTTPRequestEntityTooLarge
BOUNDARY = b"--:"
@@ -345,17 +348,17 @@ async def test_read_with_content_encoding_gzip(self) -> None:
result = await obj.read(decode=True)
assert b"Time to Relax!" == result
+ @pytest.mark.skipif(sys.version_info < (3, 11), reason="wbits not available")
async def test_read_with_content_encoding_deflate(self) -> None:
- data = b"\x0b\xc9\xccMU(\xc9W\x08J\xcdI\xacP\x04\x00"
- tail = b"%s--:--" % newline
- with Stream(data + tail) as stream:
- obj = aiohttp.BodyPartReader(
- BOUNDARY,
- {CONTENT_ENCODING: "deflate"},
- stream,
- )
+ content = b"A" * 1_000_000 # Large enough to exceed max_length.
+ compressed = ZLibBackend.compress(content, wbits=-ZLibBackend.MAX_WBITS)
+
+ h = CIMultiDictProxy(CIMultiDict({CONTENT_ENCODING: "deflate"}))
+ with Stream(compressed + b"\r\n--:--") as stream:
+ obj = aiohttp.BodyPartReader(BOUNDARY, h, stream)
result = await obj.read(decode=True)
- assert b"Time to Relax!" == result
+ assert len(result) == len(content) # Simplifies diff on failure
+ assert result == content
async def test_read_with_content_encoding_identity(self) -> None:
thing = (
@@ -381,6 +384,22 @@ async def test_read_with_content_encoding_unknown(self) -> None:
with pytest.raises(RuntimeError):
await obj.read(decode=True)
+ async def test_read_decode_compressed_exceeds_max_size(self) -> None:
+ # Compressed data is small, but decompresses beyond client_max_size.
+ original = b"A" * 1024
+ compressed = gzip.compress(original)
+ h = CIMultiDictProxy(CIMultiDict({CONTENT_ENCODING: "gzip"}))
+ with Stream(compressed + b"\r\n--:--") as stream:
+ obj = aiohttp.BodyPartReader(
+ BOUNDARY,
+ h,
+ stream,
+ client_max_size=256,
+ max_size_error_cls=HTTPRequestEntityTooLarge,
+ )
+ with pytest.raises(HTTPRequestEntityTooLarge):
+ await obj.read(decode=True)
+
async def test_read_with_content_transfer_encoding_base64(self) -> None:
with Stream(b"VGltZSB0byBSZWxheCE=%s--:--" % newline) as stream:
obj = aiohttp.BodyPartReader(
@@ -720,9 +739,9 @@ async def test_filename(self) -> None:
assert "foo.html" == part.filename
async def test_reading_long_part(self) -> None:
- size = 2 * 2**16
+ size = 2 * 2**18
protocol = mock.Mock(_reading_paused=False)
- stream = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop())
+ stream = StreamReader(protocol, 2**18, loop=asyncio.get_event_loop())
stream.feed_data(b"0" * size + b"\r\n--:--")
stream.feed_eof()
obj = aiohttp.BodyPartReader(BOUNDARY, {}, stream)
@@ -1805,6 +1824,35 @@ async def test_body_part_reader_payload_as_bytes() -> None:
payload.decode()
+@pytest.mark.skipif(sys.version_info < (3, 11), reason="No wbits parameter")
+async def test_body_part_reader_payload_write() -> None:
+ content = b"A" * 1_000_000 # Large enough to exceed max_length.
+ compressed = ZLibBackend.compress(content, wbits=-ZLibBackend.MAX_WBITS)
+ output = b""
+
+ async def write(inp: bytes) -> None:
+ nonlocal output
+ output += inp
+
+ h = CIMultiDictProxy(CIMultiDict({CONTENT_ENCODING: "deflate"}))
+ if sys.version_info >= (3, 12):
+ writer = mock.create_autospec(
+ AbstractStreamWriter, write=write, spec_set=True, instance=True
+ )
+ else:
+ writer = mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ )
+ writer.write.side_effect = write
+ with Stream(compressed + b"\r\n--:--") as stream:
+ body_part = aiohttp.BodyPartReader(BOUNDARY, h, stream)
+ payload = BodyPartReaderPayload(body_part)
+ await payload.write(writer)
+
+ assert len(output) == len(content) # Simplifies diff on failure
+ assert output == content
+
+
async def test_multipart_writer_close_with_exceptions() -> None:
"""Test that MultipartWriter.close() continues closing all parts even if one raises."""
writer = aiohttp.MultipartWriter()
diff --git a/tests/test_payload.py b/tests/test_payload.py
index 6fcc00449b8..f5454ec46f1 100644
--- a/tests/test_payload.py
+++ b/tests/test_payload.py
@@ -331,7 +331,7 @@ def mock_read(size: int | None = None) -> bytes:
async def test_bytesio_payload_large_data_multiple_chunks() -> None:
"""Test BytesIOPayload with large data requiring multiple read chunks."""
- chunk_size = 2**16 # 64KB (READ_SIZE)
+ chunk_size = 2**18 # 256KiB (READ_SIZE)
data = b"x" * (chunk_size + 1000) # Slightly larger than READ_SIZE
payload_bytesio = payload.BytesIOPayload(io.BytesIO(data))
writer = MockStreamWriter()
@@ -355,7 +355,7 @@ async def test_bytesio_payload_remaining_bytes_exhausted() -> None:
async def test_iobase_payload_exact_chunk_size_limit() -> None:
"""Test IOBasePayload with content length matching exactly one read chunk."""
- chunk_size = 2**16 # 65536 bytes (READ_SIZE)
+ chunk_size = 2**18 # 256KiB (READ_SIZE)
data = b"x" * chunk_size + b"extra" # Slightly larger than one read chunk
p = payload.IOBasePayload(io.BytesIO(data))
writer = MockStreamWriter()
diff --git a/tests/test_streams.py b/tests/test_streams.py
index 93686746ee0..a16b991e0ce 100644
--- a/tests/test_streams.py
+++ b/tests/test_streams.py
@@ -25,7 +25,7 @@ def chunkify(seq, n):
async def create_stream():
loop = asyncio.get_event_loop()
protocol = mock.Mock(_reading_paused=False)
- stream = streams.StreamReader(protocol, 2**16, loop=loop)
+ stream = streams.StreamReader(protocol, 2**18, loop=loop)
stream.feed_data(DATA)
stream.feed_eof()
return stream
@@ -73,7 +73,7 @@ class TestStreamReader:
DATA = b"line1\nline2\nline3\n"
def _make_one(self, *args, **kwargs):
- kwargs.setdefault("limit", 2**16)
+ kwargs.setdefault("limit", 2**18)
return streams.StreamReader(mock.Mock(_reading_paused=False), *args, **kwargs)
async def test_create_waiter(self) -> None:
@@ -1131,7 +1131,7 @@ async def test_empty_stream_reader() -> None:
assert s.set_exception(ValueError()) is None
assert s.exception() is None
assert s.feed_eof() is None
- assert s.feed_data(b"data") is None
+ assert s.feed_data(b"data") is False
assert s.at_eof()
assert (await s.wait_eof()) is None
assert await s.read() == b""
@@ -1293,7 +1293,7 @@ async def set_err():
async def test_feed_data_waiters(protocol) -> None:
loop = asyncio.get_event_loop()
- reader = streams.StreamReader(protocol, 2**16, loop=loop)
+ reader = streams.StreamReader(protocol, 2**18, loop=loop)
waiter = reader._waiter = loop.create_future()
eof_waiter = reader._eof_waiter = loop.create_future()
@@ -1321,7 +1321,7 @@ async def test_feed_data_completed_waiters(protocol) -> None:
async def test_feed_eof_waiters(protocol) -> None:
loop = asyncio.get_event_loop()
- reader = streams.StreamReader(protocol, 2**16, loop=loop)
+ reader = streams.StreamReader(protocol, 2**18, loop=loop)
waiter = reader._waiter = loop.create_future()
eof_waiter = reader._eof_waiter = loop.create_future()
@@ -1353,7 +1353,7 @@ async def test_feed_eof_cancelled(protocol) -> None:
async def test_on_eof(protocol) -> None:
loop = asyncio.get_event_loop()
- reader = streams.StreamReader(protocol, 2**16, loop=loop)
+ reader = streams.StreamReader(protocol, 2**18, loop=loop)
on_eof = mock.Mock()
reader.on_eof(on_eof)
@@ -1374,7 +1374,7 @@ async def test_on_eof_empty_reader() -> None:
async def test_on_eof_exc_in_callback(protocol) -> None:
loop = asyncio.get_event_loop()
- reader = streams.StreamReader(protocol, 2**16, loop=loop)
+ reader = streams.StreamReader(protocol, 2**18, loop=loop)
on_eof = mock.Mock()
on_eof.side_effect = ValueError
@@ -1409,7 +1409,7 @@ async def test_on_eof_eof_is_set(protocol) -> None:
async def test_on_eof_eof_is_set_exception(protocol) -> None:
loop = asyncio.get_event_loop()
- reader = streams.StreamReader(protocol, 2**16, loop=loop)
+ reader = streams.StreamReader(protocol, 2**18, loop=loop)
reader.feed_eof()
on_eof = mock.Mock()
@@ -1455,7 +1455,7 @@ async def test_set_exception_cancelled(protocol) -> None:
async def test_set_exception_eof_callbacks(protocol) -> None:
loop = asyncio.get_event_loop()
- reader = streams.StreamReader(protocol, 2**16, loop=loop)
+ reader = streams.StreamReader(protocol, 2**18, loop=loop)
on_eof = mock.Mock()
reader.on_eof(on_eof)
@@ -1560,8 +1560,8 @@ async def test_stream_reader_pause_on_high_water_chunks(
) -> None:
"""Test that reading is paused when chunk count exceeds high water mark."""
loop = asyncio.get_event_loop()
- # Use small limit so high_water_chunks is small: limit // 4 = 10
- stream = streams.StreamReader(protocol, limit=40, loop=loop)
+ # Use small limit so high_water_chunks is small: limit // 16 = 10
+ stream = streams.StreamReader(protocol, limit=160, loop=loop)
assert stream._high_water_chunks == 10
assert stream._low_water_chunks == 5
@@ -1581,8 +1581,8 @@ async def test_stream_reader_resume_on_low_water_chunks(
) -> None:
"""Test that reading resumes when chunk count drops below low water mark."""
loop = asyncio.get_event_loop()
- # Use small limit so high_water_chunks is small: limit // 4 = 10
- stream = streams.StreamReader(protocol, limit=40, loop=loop)
+ # Use small limit so high_water_chunks is small: limit // 16 = 10
+ stream = streams.StreamReader(protocol, limit=160, loop=loop)
assert stream._high_water_chunks == 10
assert stream._low_water_chunks == 5
@@ -1676,14 +1676,14 @@ async def test_stream_reader_resume_non_chunked_when_paused(
protocol.resume_reading.assert_called()
-@pytest.mark.parametrize("limit", [1, 2, 4])
+@pytest.mark.parametrize("limit", (1, 4, 7, 16))
async def test_stream_reader_small_limit_resumes_reading(
protocol: mock.Mock,
limit: int,
) -> None:
"""Test that small limits still allow resume_reading to be called.
- Even with very small limits, high_water_chunks should be at least 3
+ Even with very small limits, high_water_chunks should be at least 4
and low_water_chunks should be at least 2, with high > low to ensure
proper flow control.
"""
@@ -1691,8 +1691,8 @@ async def test_stream_reader_small_limit_resumes_reading(
stream = streams.StreamReader(protocol, limit=limit, loop=loop)
# Verify minimum thresholds are enforced and high > low
- assert stream._high_water_chunks >= 3
- assert stream._low_water_chunks >= 2
+ assert stream._high_water_chunks == 4
+ assert stream._low_water_chunks == 2
assert stream._high_water_chunks > stream._low_water_chunks
# Set up pause/resume side effects
@@ -1706,8 +1706,8 @@ def resume_reading() -> None:
protocol.resume_reading.side_effect = resume_reading
- # Feed 4 chunks (triggers pause at > high_water_chunks which is >= 3)
- for char in b"abcd":
+ # Feed 5 chunks (triggers pause at > high_water_chunks which is 4)
+ for char in b"abcde":
stream.begin_http_chunk_receiving()
stream.feed_data(bytes([char]))
stream.end_http_chunk_receiving()
@@ -1718,7 +1718,7 @@ def resume_reading() -> None:
# Read all data - should resume (chunk count drops below low_water_chunks)
data = stream.read_nowait()
- assert data == b"abcd"
+ assert data == b"abcde"
assert stream._size == 0
protocol.resume_reading.assert_called()
diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py
index 0d9ab86f036..d429e7ceaa0 100644
--- a/tests/test_web_functional.py
+++ b/tests/test_web_functional.py
@@ -322,7 +322,28 @@ async def handler(request):
await resp.release()
-async def test_multipart_empty(aiohttp_client) -> None:
+async def test_multipart_client_max_size(aiohttp_client: AiohttpClient) -> None:
+ with multipart.MultipartWriter() as writer:
+ writer.append("A" * 1020)
+
+ async def handler(request: web.Request) -> web.Response:
+ reader = await request.multipart()
+ assert isinstance(reader, multipart.MultipartReader)
+
+ part = await reader.next()
+ assert isinstance(part, multipart.BodyPartReader)
+ await part.text() # Should raise HttpRequestEntityTooLarge
+ assert False
+
+ app = web.Application(client_max_size=1000)
+ app.router.add_post("/", handler)
+ client = await aiohttp_client(app)
+
+ async with client.post("/", data=writer) as resp:
+ assert resp.status == 413
+
+
+async def test_multipart_empty(aiohttp_client: AiohttpClient) -> None:
with multipart.MultipartWriter() as writer:
pass
@@ -1698,12 +1719,9 @@ async def handler(request):
resp = await client.post("/", data=data)
assert 413 == resp.status
resp_text = await resp.text()
- assert "Maximum request body size 1048576 exceeded, actual body size" in resp_text
- # Maximum request body size X exceeded, actual body size X
- body_size = int(resp_text.split()[-1])
- assert body_size >= max_size
+ assert "Maximum request body size 1048576 exceeded" in resp_text
- await resp.release()
+ resp.release()
async def test_app_max_client_size_adjusted(aiohttp_client: AiohttpClient) -> None:
@@ -1730,10 +1748,7 @@ async def handler(request: web.Request) -> web.Response:
resp = await client.post("/", data=too_large_data)
assert 413 == resp.status
resp_text = await resp.text()
- assert "Maximum request body size 2097152 exceeded, actual body size" in resp_text
- # Maximum request body size X exceeded, actual body size X
- body_size = int(resp_text.split()[-1])
- assert body_size >= custom_max_size
+ assert "Maximum request body size 2097152 exceeded" in resp_text
await resp.release()
@@ -1781,11 +1796,10 @@ async def handler(request):
assert 413 == resp.status
resp_text = await resp.text()
- assert (
- "Maximum request body size 10 exceeded, "
- "actual body size 1024" in resp_text
- )
- data["file"].close()
+ assert "Maximum request body size 10 exceeded" in resp_text
+ data_file = data["file"]
+ assert isinstance(data_file, io.BytesIO)
+ data_file.close()
await resp.release()
diff --git a/tests/test_web_protocol.py b/tests/test_web_protocol.py
index 5ae1e5dd756..f823986b7b2 100644
--- a/tests/test_web_protocol.py
+++ b/tests/test_web_protocol.py
@@ -1,48 +1,50 @@
import asyncio
-from typing import Any, cast
from unittest import mock
-from aiohttp.web_protocol import RequestHandler
+import pytest
+from aiohttp.http import WebSocketReader
+from aiohttp.web_protocol import RequestHandler
+from aiohttp.web_server import Server
-class _DummyManager:
- def __init__(self) -> None:
- self.request_handler = mock.Mock()
- self.request_factory = mock.Mock()
+@pytest.fixture
+def dummy_manager() -> Server:
+ return mock.create_autospec(Server, request_handler=mock.Mock(), request_factory=mock.Mock(), instance=True) # type: ignore[no-any-return]
-class _DummyParser:
- def __init__(self) -> None:
- self.received: list[bytes] = []
- def feed_data(self, data: bytes) -> tuple[bool, bytes]:
- self.received.append(data)
- return False, b""
+@pytest.fixture
+def dummy_reader() -> tuple[WebSocketReader, mock.Mock]:
+ m = mock.create_autospec(WebSocketReader, spec_set=True, instance=True)
+ m.feed_data.return_value = False, b""
+ return m, m
def test_set_parser_does_not_call_data_received_cb_for_tail(
loop: asyncio.AbstractEventLoop,
+ dummy_manager: Server,
+ dummy_reader: tuple[WebSocketReader, mock.Mock],
) -> None:
- handler: RequestHandler[Any] = RequestHandler(cast(Any, _DummyManager()), loop=loop)
+ handler = RequestHandler(dummy_manager, loop=loop)
handler._message_tail = b"tail"
cb = mock.Mock()
- parser = _DummyParser()
- handler.set_parser(parser, data_received_cb=cb)
+ handler.set_parser(dummy_reader[0], data_received_cb=cb)
cb.assert_not_called()
- assert parser.received == [b"tail"]
+ dummy_reader[1].feed_data.assert_called_once_with(b"tail")
def test_data_received_calls_data_received_cb(
loop: asyncio.AbstractEventLoop,
+ dummy_manager: Server,
+ dummy_reader: tuple[WebSocketReader, mock.Mock],
) -> None:
- handler: RequestHandler[Any] = RequestHandler(cast(Any, _DummyManager()), loop=loop)
+ handler = RequestHandler(dummy_manager, loop=loop)
cb = mock.Mock()
- parser = _DummyParser()
- handler.set_parser(parser, data_received_cb=cb)
+ handler.set_parser(dummy_reader[0], data_received_cb=cb)
handler.data_received(b"x")
- assert cb.call_count == 1
- assert parser.received == [b"x"]
+ cb.assert_called_once()
+ dummy_reader[1].feed_data.assert_called_once_with(b"x")
diff --git a/tests/test_web_request.py b/tests/test_web_request.py
index 027e68ca746..fdbc3456d18 100644
--- a/tests/test_web_request.py
+++ b/tests/test_web_request.py
@@ -876,8 +876,8 @@ def test_clone_headers_dict() -> None:
assert req2.raw_headers == ((b"B", b"C"),)
-async def test_cannot_clone_after_read(protocol) -> None:
- payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop())
+async def test_cannot_clone_after_read(protocol: BaseProtocol) -> None:
+ payload = StreamReader(protocol, 2**18, loop=asyncio.get_event_loop())
payload.feed_data(b"data")
payload.feed_eof()
req = make_mocked_request("GET", "/path", payload=payload)
@@ -899,7 +899,18 @@ async def test_make_too_big_request(protocol) -> None:
assert err.value.status_code == 413
-async def test_make_too_big_request_adjust_limit(protocol) -> None:
+async def test_make_too_big_request_same_size_to_max(protocol: BaseProtocol) -> None:
+ payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop())
+ large_file = 1024**2 * b"x"
+ payload.feed_data(large_file)
+ payload.feed_eof()
+ req = make_mocked_request("POST", "/", payload=payload)
+ resp_text = await req.read()
+
+ assert resp_text == large_file
+
+
+async def test_make_too_big_request_adjust_limit(protocol: BaseProtocol) -> None:
payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop())
large_file = 1024**2 * b"x"
too_large_file = large_file + b"x"
@@ -937,7 +948,7 @@ async def test_multipart_formdata(protocol) -> None:
async def test_multipart_formdata_field_missing_name(protocol: BaseProtocol) -> None:
# Ensure ValueError is raised when Content-Disposition has no name
- payload = StreamReader(protocol, 2**16, loop=asyncio.get_event_loop())
+ payload = StreamReader(protocol, 2**18, loop=asyncio.get_event_loop())
payload.feed_data(
b"-----------------------------326931944431359\r\n"
b"Content-Disposition: form-data\r\n" # Missing name!
@@ -989,7 +1000,7 @@ async def test_multipart_formdata_headers_too_many(protocol: BaseProtocol) -> No
b"--b--\r\n"
)
content_type = "multipart/form-data; boundary=b"
- payload = StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ payload = StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
payload.feed_data(body)
payload.feed_eof()
req = make_mocked_request(
@@ -1016,7 +1027,7 @@ async def test_multipart_formdata_header_too_long(protocol: BaseProtocol) -> Non
b"--b--\r\n"
)
content_type = "multipart/form-data; boundary=b"
- payload = StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+ payload = StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
payload.feed_data(body)
payload.feed_eof()
req = make_mocked_request(
diff --git a/tests/test_websocket_parser.py b/tests/test_websocket_parser.py
index d98ceb4b19c..01e786787f7 100644
--- a/tests/test_websocket_parser.py
+++ b/tests/test_websocket_parser.py
@@ -19,7 +19,7 @@
from aiohttp._websocket.reader import WebSocketDataQueue
from aiohttp.base_protocol import BaseProtocol
from aiohttp.compression_utils import ZLibBackend
-from aiohttp.http import WebSocketError, WSCloseCode, WSMessage, WSMsgType
+from aiohttp.http import HttpParser, WebSocketError, WSCloseCode, WSMessage, WSMsgType
from aiohttp.http_websocket import WebSocketReader
@@ -106,8 +106,9 @@ def build_close_frame(code=1000, message=b"", noheader=False):
@pytest.fixture()
def protocol(loop: asyncio.AbstractEventLoop) -> BaseProtocol:
+ parser = mock.create_autospec(HttpParser, spec_set=True, instance=True)
transport = mock.Mock(spec_set=asyncio.Transport)
- protocol = BaseProtocol(loop)
+ protocol = BaseProtocol(loop, parser=parser)
protocol.connection_made(transport)
return protocol
diff --git a/tests/test_websocket_writer.py b/tests/test_websocket_writer.py
index d0b1b972a58..272374c7187 100644
--- a/tests/test_websocket_writer.py
+++ b/tests/test_websocket_writer.py
@@ -152,7 +152,7 @@ async def test_send_compress_cancelled(
monkeypatch.setattr("aiohttp._websocket.writer.WEBSOCKET_MAX_SYNC_CHUNK_SIZE", 1024)
writer = WebSocketWriter(protocol, transport, compress=15)
loop = asyncio.get_running_loop()
- queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**16, loop=loop)
+ queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**18, loop=loop)
reader = WebSocketReader(queue, 50000)
# Replace executor with slow one to make race condition reproducible
@@ -299,7 +299,7 @@ async def test_concurrent_messages(
):
writer = WebSocketWriter(protocol, transport, compress=15)
loop = asyncio.get_running_loop()
- queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**16, loop=loop)
+ queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**18, loop=loop)
reader = WebSocketReader(queue, 50000)
writers = []
payloads = []
From 04ed4bd019c395a374a0e9e2eac43f53c921fa65 Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Mon, 13 Apr 2026 19:16:31 +0100
Subject: [PATCH 010/191] Use DEFAULT_CHUNK_SIZE global (#12356) (#12365)
(cherry picked from commit bec74bb4bdfe81cc76cd9a96f6e0b01e9c1c590a)
---
aiohttp/_websocket/writer.py | 5 +-
aiohttp/client.py | 5 +-
aiohttp/client_proto.py | 3 +-
aiohttp/compression_utils.py | 3 -
aiohttp/helpers.py | 4 ++
aiohttp/http_parser.py | 6 +-
aiohttp/multipart.py | 12 ++--
aiohttp/payload.py | 32 ++++++----
aiohttp/streams.py | 3 +-
aiohttp/web_fileresponse.py | 4 +-
aiohttp/web_protocol.py | 4 +-
aiohttp/web_request.py | 3 +-
aiohttp/web_urldispatcher.py | 6 +-
aiohttp/web_ws.py | 14 +++--
tests/test_benchmarks_http_websocket.py | 9 +--
tests/test_client_functional.py | 8 +--
tests/test_http_parser.py | 83 ++++++++++++++++++-------
tests/test_http_writer.py | 7 ++-
tests/test_multipart.py | 8 ++-
tests/test_payload.py | 68 ++++++++++----------
tests/test_streams.py | 15 ++---
tests/test_web_request.py | 13 ++--
tests/test_websocket_writer.py | 9 ++-
23 files changed, 194 insertions(+), 130 deletions(-)
diff --git a/aiohttp/_websocket/writer.py b/aiohttp/_websocket/writer.py
index 5e4bbf62f90..d293171e38b 100644
--- a/aiohttp/_websocket/writer.py
+++ b/aiohttp/_websocket/writer.py
@@ -9,6 +9,7 @@
from ..base_protocol import BaseProtocol
from ..client_exceptions import ClientConnectionResetError
from ..compression_utils import ZLibBackend, ZLibCompressor
+from ..helpers import DEFAULT_CHUNK_SIZE
from .helpers import (
MASK_LEN,
MSG_SIZE,
@@ -21,8 +22,6 @@
)
from .models import WS_DEFLATE_TRAILING, WSMsgType
-DEFAULT_LIMIT: Final[int] = 2**18
-
# WebSocket opcode boundary: opcodes 0-7 are data frames, 8-15 are control frames
# Control frames (ping, pong, close) are never compressed
WS_CONTROL_FRAME_OPCODE: Final[int] = 8
@@ -52,7 +51,7 @@ def __init__(
transport: asyncio.Transport,
*,
use_mask: bool = False,
- limit: int = DEFAULT_LIMIT,
+ limit: int = DEFAULT_CHUNK_SIZE,
random: random.Random = random.Random(),
compress: int = 0,
notakeover: bool = False,
diff --git a/aiohttp/client.py b/aiohttp/client.py
index 890555e5783..09aa4d154de 100644
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -91,6 +91,7 @@
from .helpers import (
_SENTINEL,
DEBUG,
+ DEFAULT_CHUNK_SIZE,
EMPTY_BODY_METHODS,
BasicAuth,
TimeoutHandle,
@@ -333,7 +334,7 @@ def __init__(
trust_env: bool = False,
requote_redirect_url: bool = True,
trace_configs: list[TraceConfig] | None = None,
- read_bufsize: int = 2**18,
+ read_bufsize: int = DEFAULT_CHUNK_SIZE,
max_line_size: int = 8190,
max_field_size: int = 8190,
max_headers: int = 128,
@@ -1294,7 +1295,7 @@ async def _ws_connect(
transport = conn.transport
assert transport is not None
- reader = WebSocketDataQueue(conn_proto, 2**18, loop=self._loop)
+ reader = WebSocketDataQueue(conn_proto, DEFAULT_CHUNK_SIZE, loop=self._loop)
writer = WebSocketWriter(
conn_proto,
transport,
diff --git a/aiohttp/client_proto.py b/aiohttp/client_proto.py
index 011fc7217fd..e996d43eb2e 100644
--- a/aiohttp/client_proto.py
+++ b/aiohttp/client_proto.py
@@ -12,6 +12,7 @@
)
from .helpers import (
_EXC_SENTINEL,
+ DEFAULT_CHUNK_SIZE,
EMPTY_BODY_STATUS_CODES,
BaseTimerContext,
set_exception,
@@ -230,7 +231,7 @@ def set_response_params(
read_until_eof: bool = False,
auto_decompress: bool = True,
read_timeout: float | None = None,
- read_bufsize: int = 2**18,
+ read_bufsize: int = DEFAULT_CHUNK_SIZE,
timeout_ceil_threshold: float = 5,
max_line_size: int = 8190,
max_field_size: int = 8190,
diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py
index 92c44da53c8..c836b6a3da4 100644
--- a/aiohttp/compression_utils.py
+++ b/aiohttp/compression_utils.py
@@ -34,9 +34,6 @@
MAX_SYNC_CHUNK_SIZE = 4096
-# Matches the max size we receive from sockets:
-# https://github.com/python/cpython/blob/1857a40807daeae3a1bf5efb682de9c9ae6df845/Lib/asyncio/selector_events.py#L766
-DEFAULT_MAX_DECOMPRESS_SIZE = 256 * 1024
# Unlimited decompression constants - different libraries use different conventions
ZLIB_MAX_LENGTH_UNLIMITED = 0 # zlib uses 0 to mean unlimited
diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py
index a399c1c20ef..ebc73e19205 100644
--- a/aiohttp/helpers.py
+++ b/aiohttp/helpers.py
@@ -58,6 +58,10 @@
PY_311 = sys.version_info >= (3, 11)
+# This is the default size/limit for several operations.
+# Matches the max size we receive from sockets:
+# https://github.com/python/cpython/blob/1857a40807daeae3a1bf5efb682de9c9ae6df845/Lib/asyncio/selector_events.py#L766
+DEFAULT_CHUNK_SIZE = 2**18 # 256 KiB
_T = TypeVar("_T")
_S = TypeVar("_S")
diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py
index 3fb5368e2bf..dce38f1c058 100644
--- a/aiohttp/http_parser.py
+++ b/aiohttp/http_parser.py
@@ -22,7 +22,6 @@
from . import hdrs
from .base_protocol import BaseProtocol
from .compression_utils import (
- DEFAULT_MAX_DECOMPRESS_SIZE,
HAS_BROTLI,
HAS_ZSTD,
BrotliDecompressor,
@@ -32,6 +31,7 @@
from .helpers import (
_EXC_SENTINEL,
DEBUG,
+ DEFAULT_CHUNK_SIZE,
EMPTY_BODY_METHODS,
EMPTY_BODY_STATUS_CODES,
NO_EXTENSIONS,
@@ -817,7 +817,7 @@ def __init__(
max_line_size: int = 8190,
max_field_size: int = 8190,
max_trailers: int = 128,
- limit: int = DEFAULT_MAX_DECOMPRESS_SIZE,
+ limit: int = DEFAULT_CHUNK_SIZE,
) -> None:
self._length = 0
self._paused = False
@@ -1074,7 +1074,7 @@ def __init__(
self,
out: StreamReader,
encoding: str | None,
- max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE,
+ max_decompress_size: int = DEFAULT_CHUNK_SIZE,
) -> None:
self.out = out
self.size = 0
diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py
index cf990ded777..5cea890d343 100644
--- a/aiohttp/multipart.py
+++ b/aiohttp/multipart.py
@@ -14,11 +14,7 @@
from multidict import CIMultiDict, CIMultiDictProxy
from .abc import AbstractStreamWriter
-from .compression_utils import (
- DEFAULT_MAX_DECOMPRESS_SIZE,
- ZLibCompressor,
- ZLibDecompressor,
-)
+from .compression_utils import ZLibCompressor, ZLibDecompressor
from .hdrs import (
CONTENT_DISPOSITION,
CONTENT_ENCODING,
@@ -26,7 +22,7 @@
CONTENT_TRANSFER_ENCODING,
CONTENT_TYPE,
)
-from .helpers import CHAR, TOKEN, parse_mimetype, reify
+from .helpers import CHAR, DEFAULT_CHUNK_SIZE, TOKEN, parse_mimetype, reify
from .http import HeadersParser
from .http_exceptions import BadHttpMessage
from .log import internal_logger
@@ -267,7 +263,7 @@ def __init__(
*,
subtype: str = "mixed",
default_charset: str | None = None,
- max_decompress_size: int = DEFAULT_MAX_DECOMPRESS_SIZE,
+ max_decompress_size: int = DEFAULT_CHUNK_SIZE,
client_max_size: int = sys.maxsize,
max_size_error_cls: type[Exception] = ValueError,
) -> None:
@@ -640,7 +636,7 @@ async def as_bytes(self, encoding: str = "utf-8", errors: str = "strict") -> byt
async def write(self, writer: AbstractStreamWriter) -> None:
field = self._value
- while chunk := await field.read_chunk(size=2**18):
+ while chunk := await field.read_chunk(size=DEFAULT_CHUNK_SIZE):
async for d in field.decode_iter(chunk):
await writer.write(d)
diff --git a/aiohttp/payload.py b/aiohttp/payload.py
index 5b4cf94800c..dfc51831dad 100644
--- a/aiohttp/payload.py
+++ b/aiohttp/payload.py
@@ -17,6 +17,7 @@
from .abc import AbstractStreamWriter
from .helpers import (
_SENTINEL,
+ DEFAULT_CHUNK_SIZE,
content_disposition_header,
guess_filename,
parse_mimetype,
@@ -43,7 +44,6 @@
)
TOO_LARGE_BYTES_BODY: Final[int] = 2**20 # 1 MB
-READ_SIZE: Final[int] = 2**18 # 256 KiB
_CLOSE_FUTURES: set[asyncio.Future[None]] = set()
@@ -491,7 +491,7 @@ def _read_and_available_len(
Args:
remaining_content_len: Optional limit on how many bytes to read in this operation.
- If None, READ_SIZE will be used as the default chunk size.
+ If None, DEFAULT_CHUNK_SIZE will be used as the default chunk size.
Returns:
A tuple containing:
@@ -506,7 +506,11 @@ def _read_and_available_len(
self._set_or_restore_start_position()
size = self.size # Call size only once since it does I/O
return size, self._value.read(
- min(READ_SIZE, size or READ_SIZE, remaining_content_len or READ_SIZE)
+ min(
+ DEFAULT_CHUNK_SIZE,
+ size or DEFAULT_CHUNK_SIZE,
+ remaining_content_len or DEFAULT_CHUNK_SIZE,
+ )
)
def _read(self, remaining_content_len: int | None) -> bytes:
@@ -515,7 +519,7 @@ def _read(self, remaining_content_len: int | None) -> bytes:
Args:
remaining_content_len: Optional maximum number of bytes to read.
- If None, READ_SIZE will be used as the default chunk size.
+ If None, DEFAULT_CHUNK_SIZE will be used as the default chunk size.
Returns:
A chunk of bytes read from the file object, respecting the
@@ -525,7 +529,7 @@ def _read(self, remaining_content_len: int | None) -> bytes:
the initial _read_and_available_len call has been made.
"""
- return self._value.read(remaining_content_len or READ_SIZE) # type: ignore[no-any-return]
+ return self._value.read(remaining_content_len or DEFAULT_CHUNK_SIZE) # type: ignore[no-any-return]
@property
def size(self) -> int | None:
@@ -628,9 +632,9 @@ async def write_with_length(
None,
self._read,
(
- min(READ_SIZE, remaining_content_len)
+ min(DEFAULT_CHUNK_SIZE, remaining_content_len)
if remaining_content_len is not None
- else READ_SIZE
+ else DEFAULT_CHUNK_SIZE
),
)
@@ -756,7 +760,7 @@ def _read_and_available_len(
Args:
remaining_content_len: Optional limit on how many bytes to read in this operation.
- If None, READ_SIZE will be used as the default chunk size.
+ If None, DEFAULT_CHUNK_SIZE will be used as the default chunk size.
Returns:
A tuple containing:
@@ -775,7 +779,11 @@ def _read_and_available_len(
self._set_or_restore_start_position()
size = self.size
chunk = self._value.read(
- min(READ_SIZE, size or READ_SIZE, remaining_content_len or READ_SIZE)
+ min(
+ DEFAULT_CHUNK_SIZE,
+ size or DEFAULT_CHUNK_SIZE,
+ remaining_content_len or DEFAULT_CHUNK_SIZE,
+ )
)
return size, chunk.encode(self._encoding) if self._encoding else chunk.encode()
@@ -785,7 +793,7 @@ def _read(self, remaining_content_len: int | None) -> bytes:
Args:
remaining_content_len: Optional maximum number of bytes to read.
- If None, READ_SIZE will be used as the default chunk size.
+ If None, DEFAULT_CHUNK_SIZE will be used as the default chunk size.
Returns:
A chunk of bytes read from the file object and encoded using the payload's
@@ -797,7 +805,7 @@ def _read(self, remaining_content_len: int | None) -> bytes:
the specified encoding (or UTF-8 if none was provided).
"""
- chunk = self._value.read(remaining_content_len or READ_SIZE)
+ chunk = self._value.read(remaining_content_len or DEFAULT_CHUNK_SIZE)
return chunk.encode(self._encoding) if self._encoding else chunk.encode()
def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str:
@@ -881,7 +889,7 @@ async def write_with_length(
self._set_or_restore_start_position()
loop_count = 0
remaining_bytes = content_length
- while chunk := self._value.read(READ_SIZE):
+ while chunk := self._value.read(DEFAULT_CHUNK_SIZE):
if loop_count > 0:
# Avoid blocking the event loop
# if they pass a large BytesIO object
diff --git a/aiohttp/streams.py b/aiohttp/streams.py
index bf26b59d49e..42b427ee0f0 100644
--- a/aiohttp/streams.py
+++ b/aiohttp/streams.py
@@ -7,6 +7,7 @@
from .base_protocol import BaseProtocol
from .helpers import (
_EXC_SENTINEL,
+ DEFAULT_CHUNK_SIZE,
BaseTimerContext,
TimerNoop,
set_exception,
@@ -167,7 +168,7 @@ def __repr__(self) -> str:
info.append("%d bytes" % self._size)
if self._eof:
info.append("eof")
- if self._low_water != 2**18: # default limit
+ if self._low_water != DEFAULT_CHUNK_SIZE:
info.append("low=%d high=%d" % (self._low_water, self._high_water))
if self._waiter:
info.append("w=%r" % self._waiter)
diff --git a/aiohttp/web_fileresponse.py b/aiohttp/web_fileresponse.py
index d0a6972fdea..8aa67cec7c1 100644
--- a/aiohttp/web_fileresponse.py
+++ b/aiohttp/web_fileresponse.py
@@ -26,7 +26,7 @@
from . import hdrs
from .abc import AbstractStreamWriter
-from .helpers import ETAG_ANY, ETag, must_be_empty_body
+from .helpers import DEFAULT_CHUNK_SIZE, ETAG_ANY, ETag, must_be_empty_body
from .typedefs import LooseHeaders, PathLike
from .web_exceptions import (
HTTPForbidden,
@@ -95,7 +95,7 @@ class FileResponse(StreamResponse):
def __init__(
self,
path: PathLike,
- chunk_size: int = 256 * 1024,
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
status: int = 200,
reason: str | None = None,
headers: LooseHeaders | None = None,
diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py
index be63b824b45..f170c8e5de6 100644
--- a/aiohttp/web_protocol.py
+++ b/aiohttp/web_protocol.py
@@ -17,7 +17,7 @@
from .abc import AbstractAccessLogger, AbstractStreamWriter
from .base_protocol import BaseProtocol
-from .helpers import ceil_timeout
+from .helpers import DEFAULT_CHUNK_SIZE, ceil_timeout
from .http import (
HttpProcessingError,
HttpRequestParser,
@@ -182,7 +182,7 @@ def __init__(
max_headers: int = 128,
max_field_size: int = 8190,
lingering_time: float = 10.0,
- read_bufsize: int = 2**18,
+ read_bufsize: int = DEFAULT_CHUNK_SIZE,
auto_decompress: bool = True,
timeout_ceil_threshold: float = 5,
):
diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py
index 826a88337a0..47d50a12d5c 100644
--- a/aiohttp/web_request.py
+++ b/aiohttp/web_request.py
@@ -29,6 +29,7 @@
from .helpers import (
_SENTINEL,
DEBUG,
+ DEFAULT_CHUNK_SIZE,
ETAG_ANY,
LIST_QUOTED_ETAG_RE,
ChainMapProxy,
@@ -752,7 +753,7 @@ async def post(self) -> "MultiDictProxy[str | bytes | FileField]":
tmp = await self._loop.run_in_executor(
None, tempfile.TemporaryFile
)
- while chunk := await field.read_chunk(size=2**18):
+ while chunk := await field.read_chunk(size=DEFAULT_CHUNK_SIZE):
async for decoded_chunk in field.decode_iter(chunk):
await self._loop.run_in_executor(
None, tmp.write, decoded_chunk
diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py
index a725f7d792c..74351eecb06 100644
--- a/aiohttp/web_urldispatcher.py
+++ b/aiohttp/web_urldispatcher.py
@@ -31,7 +31,7 @@
from . import hdrs
from .abc import AbstractMatchInfo, AbstractRouter, AbstractView
-from .helpers import DEBUG
+from .helpers import DEBUG, DEFAULT_CHUNK_SIZE
from .http import HttpVersion11
from .typedefs import Handler, PathLike
from .web_exceptions import (
@@ -536,7 +536,7 @@ def __init__(
*,
name: str | None = None,
expect_handler: _ExpectHandler | None = None,
- chunk_size: int = 256 * 1024,
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
show_index: bool = False,
follow_symlinks: bool = False,
append_version: bool = False,
@@ -1166,7 +1166,7 @@ def add_static(
*,
name: str | None = None,
expect_handler: _ExpectHandler | None = None,
- chunk_size: int = 256 * 1024,
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
show_index: bool = False,
follow_symlinks: bool = False,
append_version: bool = False,
diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py
index 74811fdd283..5aa75b715a0 100644
--- a/aiohttp/web_ws.py
+++ b/aiohttp/web_ws.py
@@ -12,10 +12,14 @@
from . import hdrs
from ._websocket.reader import WebSocketDataQueue
-from ._websocket.writer import DEFAULT_LIMIT
from .abc import AbstractStreamWriter
from .client_exceptions import WSMessageTypeError
-from .helpers import calculate_timeout_when, set_exception, set_result
+from .helpers import (
+ DEFAULT_CHUNK_SIZE,
+ calculate_timeout_when,
+ set_exception,
+ set_result,
+)
from .http import (
WS_CLOSED_MESSAGE,
WS_CLOSING_MESSAGE,
@@ -104,7 +108,7 @@ def __init__(
protocols: Iterable[str] = (),
compress: bool = True,
max_msg_size: int = 4 * 1024 * 1024,
- writer_limit: int = DEFAULT_LIMIT,
+ writer_limit: int = DEFAULT_CHUNK_SIZE,
decode_text: bool = True,
) -> None:
super().__init__(status=101)
@@ -380,7 +384,9 @@ def _post_start(
loop = self._loop
assert loop is not None
- self._reader = WebSocketDataQueue(request._protocol, 2**18, loop=loop)
+ self._reader = WebSocketDataQueue(
+ request._protocol, DEFAULT_CHUNK_SIZE, loop=loop
+ )
parser = WebSocketReader(
self._reader,
self._max_msg_size,
diff --git a/tests/test_benchmarks_http_websocket.py b/tests/test_benchmarks_http_websocket.py
index f40db171919..61b23125460 100644
--- a/tests/test_benchmarks_http_websocket.py
+++ b/tests/test_benchmarks_http_websocket.py
@@ -8,6 +8,7 @@
from aiohttp._websocket.helpers import MSG_SIZE, PACK_LEN3
from aiohttp._websocket.reader import WebSocketDataQueue
from aiohttp.base_protocol import BaseProtocol
+from aiohttp.helpers import DEFAULT_CHUNK_SIZE
from aiohttp.http_websocket import WebSocketReader, WebSocketWriter, WSMsgType
@@ -15,8 +16,8 @@ def test_read_large_binary_websocket_messages(
loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture
) -> None:
"""Read one hundred large binary websocket messages."""
- queue = WebSocketDataQueue(BaseProtocol(loop), 2**16, loop=loop)
- reader = WebSocketReader(queue, max_msg_size=2**18)
+ queue = WebSocketDataQueue(BaseProtocol(loop), DEFAULT_CHUNK_SIZE, loop=loop)
+ reader = WebSocketReader(queue, max_msg_size=DEFAULT_CHUNK_SIZE)
# PACK3 has a minimum message length of 2**16 bytes.
message = b"x" * ((2**16) + 1)
@@ -36,8 +37,8 @@ def test_read_one_hundred_websocket_text_messages(
loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture
) -> None:
"""Benchmark reading 100 WebSocket text messages."""
- queue = WebSocketDataQueue(BaseProtocol(loop), 2**18, loop=loop)
- reader = WebSocketReader(queue, max_msg_size=2**18)
+ queue = WebSocketDataQueue(BaseProtocol(loop), DEFAULT_CHUNK_SIZE, loop=loop)
+ reader = WebSocketReader(queue, max_msg_size=DEFAULT_CHUNK_SIZE)
raw_message = (
b'\x81~\x01!{"id":1,"src":"shellyplugus-c049ef8c30e4","dst":"aios-1453812500'
b'8","result":{"name":null,"id":"shellyplugus-c049ef8c30e4","mac":"C049EF8C30E'
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
index 05e6574b89f..6d17172457b 100644
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -51,8 +51,8 @@
TooManyRedirects,
)
from aiohttp.client_reqrep import ClientRequest
-from aiohttp.compression_utils import DEFAULT_MAX_DECOMPRESS_SIZE
from aiohttp.connector import Connection
+from aiohttp.helpers import DEFAULT_CHUNK_SIZE
from aiohttp.http_writer import StreamWriter
from aiohttp.payload import (
AsyncIterablePayload,
@@ -2457,7 +2457,7 @@ async def test_payload_decompress_size_limit(aiohttp_client: AiohttpClient) -> N
payload_size = 64 * 2**20
original = b"A" * payload_size
compressed = zlib.compress(original)
- assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE
+ assert len(original) > DEFAULT_CHUNK_SIZE
async def handler(request: web.Request) -> web.Response:
# Send compressed data with Content-Encoding header
@@ -2489,7 +2489,7 @@ async def test_payload_decompress_size_limit_brotli(
payload_size = 64 * 2**20
original = b"A" * payload_size
compressed = brotli.compress(original)
- assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE
+ assert len(original) > DEFAULT_CHUNK_SIZE
async def handler(request: web.Request) -> web.Response:
resp = web.Response(body=compressed)
@@ -2521,7 +2521,7 @@ async def test_payload_decompress_size_limit_zstd(
original = b"A" * payload_size
compressor = ZstdCompressor()
compressed = compressor.compress(original) + compressor.flush()
- assert len(original) > DEFAULT_MAX_DECOMPRESS_SIZE
+ assert len(original) > DEFAULT_CHUNK_SIZE
async def handler(request: web.Request) -> web.Response:
resp = web.Response(body=compressed)
diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
index 6f9caa2fe55..9c65fb95257 100644
--- a/tests/test_http_parser.py
+++ b/tests/test_http_parser.py
@@ -19,6 +19,7 @@
from aiohttp import http_exceptions, streams
from aiohttp.base_protocol import BaseProtocol
from aiohttp.client_proto import ResponseHandler
+from aiohttp.helpers import DEFAULT_CHUNK_SIZE
from aiohttp.http_parser import (
NO_EXTENSIONS,
DeflateBuffer,
@@ -100,7 +101,7 @@ def parser(
parser = request.param(
protocol,
loop,
- 2**18,
+ DEFAULT_CHUNK_SIZE,
max_line_size=8190,
max_headers=128,
max_field_size=8190,
@@ -128,7 +129,7 @@ def response(
parser = request.param(
protocol,
loop,
- 2**18,
+ DEFAULT_CHUNK_SIZE,
max_line_size=8190,
max_headers=128,
max_field_size=8190,
@@ -1673,7 +1674,7 @@ async def test_http_response_parser_bad_chunked_strict_py(
response = HttpResponseParserPy(
protocol,
loop,
- 2**18,
+ DEFAULT_CHUNK_SIZE,
max_line_size=8190,
max_field_size=8190,
)
@@ -1849,7 +1850,7 @@ def test_parse_no_length_or_te_on_post(
request_cls: type[HttpRequestParser],
) -> None:
protocol = RequestHandler(server, loop=loop)
- parser = request_cls(protocol, loop, limit=2**18)
+ parser = request_cls(protocol, loop, limit=DEFAULT_CHUNK_SIZE)
protocol._parser = parser
text = b"POST /test HTTP/1.1\r\n\r\n"
msg, payload = parser.feed_data(text)[0][0]
@@ -2128,7 +2129,7 @@ def test_parse_bad_method_for_c_parser_raises(
parser = HttpRequestParserC(
protocol,
loop,
- 2**18,
+ DEFAULT_CHUNK_SIZE,
max_line_size=8190,
max_headers=128,
max_field_size=8190,
@@ -2150,7 +2151,9 @@ async def test_parse_eof_payload(self, protocol: BaseProtocol) -> None:
assert [(bytearray(b"data"))] == list(out._buffer)
async def test_parse_length_payload_eof(self, protocol: BaseProtocol) -> None:
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(out, length=4, headers_parser=HeadersParser())
p.feed_data(b"da")
@@ -2174,7 +2177,9 @@ async def test_parse_chunked_payload_size_data_mismatch(
Regression test for #10596.
"""
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
# Declared chunk-size is 4 but actual data is "Hello" (5 bytes).
# After consuming 4 bytes, remaining starts with "o" not "\r\n".
@@ -2189,7 +2194,9 @@ async def test_parse_chunked_payload_size_data_mismatch_too_short(
Regression test for #10596.
"""
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
# Declared chunk-size is 6 but actual data before CRLF is "Hello" (5 bytes).
# Parser reads 6 bytes: "Hello\r", then expects \r\n but sees "\n0\r\n..."
@@ -2211,7 +2218,9 @@ async def test_parse_chunked_payload_split_end(
async def test_parse_chunked_payload_split_end2(
self, protocol: BaseProtocol
) -> None:
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
p.feed_data(b"4\r\nasdf\r\n0\r\n\r")
p.feed_data(b"\n")
@@ -2222,7 +2231,9 @@ async def test_parse_chunked_payload_split_end2(
async def test_parse_chunked_payload_split_end_trailers(
self, protocol: BaseProtocol
) -> None:
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
p.feed_data(b"4\r\nasdf\r\n0\r\n")
p.feed_data(b"Content-MD5: 912ec803b2ce49e4a541068d495ab570\r\n")
@@ -2234,7 +2245,9 @@ async def test_parse_chunked_payload_split_end_trailers(
async def test_parse_chunked_payload_split_end_trailers2(
self, protocol: BaseProtocol
) -> None:
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(out, chunked=True, headers_parser=HeadersParser())
p.feed_data(b"4\r\nasdf\r\n0\r\n")
p.feed_data(b"Content-MD5: 912ec803b2ce49e4a541068d495ab570\r\n\r")
@@ -2266,7 +2279,9 @@ async def test_parse_chunked_payload_split_end_trailers4(
assert b"asdf" == b"".join(out._buffer)
async def test_http_payload_parser_length(self, protocol: BaseProtocol) -> None:
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(out, length=2, headers_parser=HeadersParser())
state, tail = p.feed_data(b"1245")
assert state is PayloadState.PAYLOAD_COMPLETE
@@ -2279,7 +2294,9 @@ async def test_http_payload_parser_deflate(self, protocol: BaseProtocol) -> None
COMPRESSED = b"x\x9cKI,I\x04\x00\x04\x00\x01\x9b"
length = len(COMPRESSED)
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(
out, length=length, compression="deflate", headers_parser=HeadersParser()
)
@@ -2350,7 +2367,9 @@ async def test_http_payload_parser_deflate_split_err(
async def test_http_payload_parser_length_zero(
self, protocol: BaseProtocol
) -> None:
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(out, length=0, headers_parser=HeadersParser())
assert p.done
assert out.is_eof()
@@ -2358,7 +2377,9 @@ async def test_http_payload_parser_length_zero(
@pytest.mark.skipif(brotli is None, reason="brotli is not installed")
async def test_http_payload_brotli(self, protocol: BaseProtocol) -> None:
compressed = brotli.compress(b"brotli data")
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(
out,
length=len(compressed),
@@ -2372,7 +2393,9 @@ async def test_http_payload_brotli(self, protocol: BaseProtocol) -> None:
@pytest.mark.skipif(zstandard is None, reason="zstandard is not installed")
async def test_http_payload_zstandard(self, protocol: BaseProtocol) -> None:
compressed = zstandard.compress(b"zstd data")
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(
out,
length=len(compressed),
@@ -2390,7 +2413,9 @@ async def test_http_payload_zstandard_multi_frame(
frame1 = zstandard.compress(b"first")
frame2 = zstandard.compress(b"second")
payload = frame1 + frame2
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(
out,
length=len(payload),
@@ -2407,7 +2432,9 @@ async def test_http_payload_zstandard_multi_frame_chunked(
) -> None:
frame1 = zstandard.compress(b"chunk1")
frame2 = zstandard.compress(b"chunk2")
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(
out,
length=len(frame1) + len(frame2),
@@ -2427,7 +2454,9 @@ async def test_http_payload_zstandard_frame_split_mid_chunk(
frame2 = zstandard.compress(b"BBBB")
combined = frame1 + frame2
split_point = len(frame1) + 3 # 3 bytes into frame2
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(
out,
length=len(combined),
@@ -2445,7 +2474,9 @@ async def test_http_payload_zstandard_many_small_frames(
) -> None:
parts = [f"part{i}".encode() for i in range(10)]
payload = b"".join(zstandard.compress(p) for p in parts)
- out = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
p = HttpPayloadParser(
out,
length=len(payload),
@@ -2459,7 +2490,9 @@ async def test_http_payload_zstandard_many_small_frames(
class TestDeflateBuffer:
async def test_feed_data(self, protocol: BaseProtocol) -> None:
- buf = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ buf = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
dbuf = DeflateBuffer(buf, "deflate")
dbuf.decompressor = mock.Mock()
@@ -2544,7 +2577,9 @@ async def test_feed_eof_no_err_zstandard(self, protocol: BaseProtocol) -> None:
assert buf._eof
async def test_empty_body(self, protocol: BaseProtocol) -> None:
- buf = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ buf = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
dbuf = DeflateBuffer(buf, "deflate")
dbuf.feed_eof()
@@ -2569,7 +2604,9 @@ async def test_streaming_decompress_large_payload(
original = b"A" * (3 * 2**20)
compressed = zlib.compress(original)
- buf = aiohttp.StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ buf = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
dbuf = DeflateBuffer(buf, "deflate")
# Feed compressed data in chunks (simulating network streaming)
diff --git a/tests/test_http_writer.py b/tests/test_http_writer.py
index 251645f2398..3c667bb7344 100644
--- a/tests/test_http_writer.py
+++ b/tests/test_http_writer.py
@@ -11,6 +11,7 @@
from aiohttp import ClientConnectionResetError, hdrs, http
from aiohttp.base_protocol import BaseProtocol
from aiohttp.compression_utils import ZLibBackend
+from aiohttp.helpers import DEFAULT_CHUNK_SIZE
from aiohttp.http_writer import _serialize_headers
@@ -1423,7 +1424,7 @@ async def test_write_drain_condition_with_large_buffer(
protocol._drain_helper.reset_mock() # type: ignore[attr-defined]
# Write large amount of data with drain=True
- large_data = b"x" * (2**18 + 1) # Just over LIMIT
+ large_data = b"x" * (DEFAULT_CHUNK_SIZE + 1) # Just over LIMIT
await msg.write(large_data, drain=True)
# Drain should be called because drain=True AND buffer_size > LIMIT
@@ -1452,12 +1453,12 @@ async def test_write_no_drain_with_large_buffer(
protocol._drain_helper.reset_mock() # type: ignore[attr-defined]
# Write large amount of data with drain=False
- large_data = b"x" * (2**18 + 1) # Just over LIMIT
+ large_data = b"x" * (DEFAULT_CHUNK_SIZE + 1) # Just over LIMIT
await msg.write(large_data, drain=False)
# Drain should NOT be called because drain=False
assert not protocol._drain_helper.called # type: ignore[attr-defined]
- assert msg.buffer_size == (2**18 + 1) # Buffer not reset
+ assert msg.buffer_size == (DEFAULT_CHUNK_SIZE + 1) # Buffer not reset
assert large_data in buf
diff --git a/tests/test_multipart.py b/tests/test_multipart.py
index 0d01bf6a5ae..1f80f9c2a21 100644
--- a/tests/test_multipart.py
+++ b/tests/test_multipart.py
@@ -19,7 +19,7 @@
CONTENT_TRANSFER_ENCODING,
CONTENT_TYPE,
)
-from aiohttp.helpers import parse_mimetype
+from aiohttp.helpers import DEFAULT_CHUNK_SIZE, parse_mimetype
from aiohttp.multipart import (
BodyPartReader,
BodyPartReaderPayload,
@@ -739,9 +739,11 @@ async def test_filename(self) -> None:
assert "foo.html" == part.filename
async def test_reading_long_part(self) -> None:
- size = 2 * 2**18
+ size = 2 * DEFAULT_CHUNK_SIZE
protocol = mock.Mock(_reading_paused=False)
- stream = StreamReader(protocol, 2**18, loop=asyncio.get_event_loop())
+ stream = StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_event_loop()
+ )
stream.feed_data(b"0" * size + b"\r\n--:--")
stream.feed_eof()
obj = aiohttp.BodyPartReader(BOUNDARY, {}, stream)
diff --git a/tests/test_payload.py b/tests/test_payload.py
index f5454ec46f1..3284c213d55 100644
--- a/tests/test_payload.py
+++ b/tests/test_payload.py
@@ -13,7 +13,7 @@
from aiohttp import payload
from aiohttp.abc import AbstractStreamWriter
-from aiohttp.payload import READ_SIZE
+from aiohttp.helpers import DEFAULT_CHUNK_SIZE
class BufferWriter(AbstractStreamWriter):
@@ -331,14 +331,13 @@ def mock_read(size: int | None = None) -> bytes:
async def test_bytesio_payload_large_data_multiple_chunks() -> None:
"""Test BytesIOPayload with large data requiring multiple read chunks."""
- chunk_size = 2**18 # 256KiB (READ_SIZE)
- data = b"x" * (chunk_size + 1000) # Slightly larger than READ_SIZE
+ data = b"x" * (DEFAULT_CHUNK_SIZE + 1000)
payload_bytesio = payload.BytesIOPayload(io.BytesIO(data))
writer = MockStreamWriter()
await payload_bytesio.write_with_length(writer, None)
assert writer.get_written_bytes() == data
- assert len(writer.get_written_bytes()) == chunk_size + 1000
+ assert len(writer.get_written_bytes()) == DEFAULT_CHUNK_SIZE + 1000
async def test_bytesio_payload_remaining_bytes_exhausted() -> None:
@@ -355,21 +354,20 @@ async def test_bytesio_payload_remaining_bytes_exhausted() -> None:
async def test_iobase_payload_exact_chunk_size_limit() -> None:
"""Test IOBasePayload with content length matching exactly one read chunk."""
- chunk_size = 2**18 # 256KiB (READ_SIZE)
- data = b"x" * chunk_size + b"extra" # Slightly larger than one read chunk
+ data = b"x" * DEFAULT_CHUNK_SIZE + b"extra" # Slightly larger than one read chunk
p = payload.IOBasePayload(io.BytesIO(data))
writer = MockStreamWriter()
- await p.write_with_length(writer, chunk_size)
+ await p.write_with_length(writer, DEFAULT_CHUNK_SIZE)
written = writer.get_written_bytes()
- assert len(written) == chunk_size
- assert written == data[:chunk_size]
+ assert len(written) == DEFAULT_CHUNK_SIZE
+ assert written == data[:DEFAULT_CHUNK_SIZE]
async def test_iobase_payload_reads_in_chunks() -> None:
- """Test IOBasePayload reads data in chunks of READ_SIZE, not all at once."""
- # Create a large file that's multiple times larger than READ_SIZE
- large_data = b"x" * (READ_SIZE * 3 + 1000) # ~192KB + 1000 bytes
+ """Test IOBasePayload reads data in chunks of default size, not all at once."""
+ # Create a large file that's multiple times larger than DEFAULT_CHUNK_SIZE
+ large_data = b"x" * (DEFAULT_CHUNK_SIZE * 3 + 1000) # ~192KB + 1000 bytes
# Mock the file-like object to track read calls
mock_file = unittest.mock.Mock(spec=io.BytesIO)
@@ -386,11 +384,11 @@ def mock_read(size: int) -> bytes:
if call_count == 1:
return large_data[:size]
elif call_count == 2:
- return large_data[READ_SIZE : READ_SIZE + size]
+ return large_data[DEFAULT_CHUNK_SIZE : DEFAULT_CHUNK_SIZE + size]
elif call_count == 3:
- return large_data[READ_SIZE * 2 : READ_SIZE * 2 + size]
+ return large_data[DEFAULT_CHUNK_SIZE * 2 : DEFAULT_CHUNK_SIZE * 2 + size]
else:
- return large_data[READ_SIZE * 3 :]
+ return large_data[DEFAULT_CHUNK_SIZE * 3 :]
mock_file.read.side_effect = mock_read
@@ -400,17 +398,17 @@ def mock_read(size: int) -> bytes:
# Write with a large content_length
await payload_obj.write_with_length(writer, len(large_data))
- # Verify that reads were limited to READ_SIZE
+ # Verify that reads were limited to DEFAULT_CHUNK_SIZE
assert len(read_sizes) > 1 # Should have multiple reads
for read_size in read_sizes:
assert (
- read_size <= READ_SIZE
- ), f"Read size {read_size} exceeds READ_SIZE {READ_SIZE}"
+ read_size <= DEFAULT_CHUNK_SIZE
+ ), f"Read size {read_size} exceeds DEFAULT_CHUNK_SIZE {DEFAULT_CHUNK_SIZE}"
async def test_iobase_payload_large_content_length() -> None:
"""Test IOBasePayload with very large content_length doesn't read all at once."""
- data = b"x" * (READ_SIZE + 1000)
+ data = b"x" * (DEFAULT_CHUNK_SIZE + 1000)
# Create a custom file-like object that tracks read sizes
class TrackingBytesIO(io.BytesIO):
@@ -430,20 +428,20 @@ def read(self, size: int | None = -1) -> bytes:
large_content_length = 10 * 1024 * 1024 # 10MB
await payload_obj.write_with_length(writer, large_content_length)
- # Verify no single read exceeded READ_SIZE
+ # Verify no single read exceeded DEFAULT_CHUNK_SIZE
for read_size in tracking_file.read_sizes:
assert (
- read_size <= READ_SIZE
- ), f"Read size {read_size} exceeds READ_SIZE {READ_SIZE}"
+ read_size <= DEFAULT_CHUNK_SIZE
+ ), f"Read size {read_size} exceeds DEFAULT_CHUNK_SIZE {DEFAULT_CHUNK_SIZE}"
# Verify the correct amount of data was written
assert writer.get_written_bytes() == data
async def test_textio_payload_reads_in_chunks() -> None:
- """Test TextIOPayload reads data in chunks of READ_SIZE, not all at once."""
- # Create a large text file that's multiple times larger than READ_SIZE
- large_text = "x" * (READ_SIZE * 3 + 1000) # ~192KB + 1000 chars
+ """Test TextIOPayload reads data in chunks of default size, not all at once."""
+ # Create a large text file that's multiple times larger than DEFAULT_CHUNK_SIZE
+ large_text = "x" * (DEFAULT_CHUNK_SIZE * 3 + 1000) # ~192KB + 1000 chars
# Mock the file-like object to track read calls
mock_file = unittest.mock.Mock(spec=io.StringIO)
@@ -461,11 +459,11 @@ def mock_read(size: int) -> str:
if call_count == 1:
return large_text[:size]
elif call_count == 2:
- return large_text[READ_SIZE : READ_SIZE + size]
+ return large_text[DEFAULT_CHUNK_SIZE : DEFAULT_CHUNK_SIZE + size]
elif call_count == 3:
- return large_text[READ_SIZE * 2 : READ_SIZE * 2 + size]
+ return large_text[DEFAULT_CHUNK_SIZE * 2 : DEFAULT_CHUNK_SIZE * 2 + size]
else:
- return large_text[READ_SIZE * 3 :]
+ return large_text[DEFAULT_CHUNK_SIZE * 3 :]
mock_file.read.side_effect = mock_read
@@ -475,17 +473,17 @@ def mock_read(size: int) -> str:
# Write with a large content_length
await payload_obj.write_with_length(writer, len(large_text.encode("utf-8")))
- # Verify that reads were limited to READ_SIZE
+ # Verify that reads were limited to DEFAULT_CHUNK_SIZE
assert len(read_sizes) > 1 # Should have multiple reads
for read_size in read_sizes:
assert (
- read_size <= READ_SIZE
- ), f"Read size {read_size} exceeds READ_SIZE {READ_SIZE}"
+ read_size <= DEFAULT_CHUNK_SIZE
+ ), f"Read size {read_size} exceeds DEFAULT_CHUNK_SIZE {DEFAULT_CHUNK_SIZE}"
async def test_textio_payload_large_content_length() -> None:
"""Test TextIOPayload with very large content_length doesn't read all at once."""
- text_data = "x" * (READ_SIZE + 1000)
+ text_data = "x" * (DEFAULT_CHUNK_SIZE + 1000)
# Create a custom file-like object that tracks read sizes
class TrackingStringIO(io.StringIO):
@@ -505,11 +503,11 @@ def read(self, size: int | None = -1) -> str:
large_content_length = 10 * 1024 * 1024 # 10MB
await payload_obj.write_with_length(writer, large_content_length)
- # Verify no single read exceeded READ_SIZE
+ # Verify no single read exceeded DEFAULT_CHUNK_SIZE
for read_size in tracking_file.read_sizes:
assert (
- read_size <= READ_SIZE
- ), f"Read size {read_size} exceeds READ_SIZE {READ_SIZE}"
+ read_size <= DEFAULT_CHUNK_SIZE
+ ), f"Read size {read_size} exceeds DEFAULT_CHUNK_SIZE {DEFAULT_CHUNK_SIZE}"
# Verify the correct amount of data was written
assert writer.get_written_bytes() == text_data.encode("utf-8")
diff --git a/tests/test_streams.py b/tests/test_streams.py
index a16b991e0ce..aa50b5ca2d3 100644
--- a/tests/test_streams.py
+++ b/tests/test_streams.py
@@ -12,6 +12,7 @@
from re_assert import Matches
from aiohttp import streams
+from aiohttp.helpers import DEFAULT_CHUNK_SIZE
from aiohttp.http_exceptions import LineTooLong
DATA = b"line1\nline2\nline3\n"
@@ -25,7 +26,7 @@ def chunkify(seq, n):
async def create_stream():
loop = asyncio.get_event_loop()
protocol = mock.Mock(_reading_paused=False)
- stream = streams.StreamReader(protocol, 2**18, loop=loop)
+ stream = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop)
stream.feed_data(DATA)
stream.feed_eof()
return stream
@@ -1293,7 +1294,7 @@ async def set_err():
async def test_feed_data_waiters(protocol) -> None:
loop = asyncio.get_event_loop()
- reader = streams.StreamReader(protocol, 2**18, loop=loop)
+ reader = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop)
waiter = reader._waiter = loop.create_future()
eof_waiter = reader._eof_waiter = loop.create_future()
@@ -1321,7 +1322,7 @@ async def test_feed_data_completed_waiters(protocol) -> None:
async def test_feed_eof_waiters(protocol) -> None:
loop = asyncio.get_event_loop()
- reader = streams.StreamReader(protocol, 2**18, loop=loop)
+ reader = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop)
waiter = reader._waiter = loop.create_future()
eof_waiter = reader._eof_waiter = loop.create_future()
@@ -1353,7 +1354,7 @@ async def test_feed_eof_cancelled(protocol) -> None:
async def test_on_eof(protocol) -> None:
loop = asyncio.get_event_loop()
- reader = streams.StreamReader(protocol, 2**18, loop=loop)
+ reader = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop)
on_eof = mock.Mock()
reader.on_eof(on_eof)
@@ -1374,7 +1375,7 @@ async def test_on_eof_empty_reader() -> None:
async def test_on_eof_exc_in_callback(protocol) -> None:
loop = asyncio.get_event_loop()
- reader = streams.StreamReader(protocol, 2**18, loop=loop)
+ reader = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop)
on_eof = mock.Mock()
on_eof.side_effect = ValueError
@@ -1409,7 +1410,7 @@ async def test_on_eof_eof_is_set(protocol) -> None:
async def test_on_eof_eof_is_set_exception(protocol) -> None:
loop = asyncio.get_event_loop()
- reader = streams.StreamReader(protocol, 2**18, loop=loop)
+ reader = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop)
reader.feed_eof()
on_eof = mock.Mock()
@@ -1455,7 +1456,7 @@ async def test_set_exception_cancelled(protocol) -> None:
async def test_set_exception_eof_callbacks(protocol) -> None:
loop = asyncio.get_event_loop()
- reader = streams.StreamReader(protocol, 2**18, loop=loop)
+ reader = streams.StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=loop)
on_eof = mock.Mock()
reader.on_eof(on_eof)
diff --git a/tests/test_web_request.py b/tests/test_web_request.py
index fdbc3456d18..fa2f257c402 100644
--- a/tests/test_web_request.py
+++ b/tests/test_web_request.py
@@ -14,6 +14,7 @@
from aiohttp import HttpVersion
from aiohttp.base_protocol import BaseProtocol
+from aiohttp.helpers import DEFAULT_CHUNK_SIZE
from aiohttp.http_exceptions import BadHttpMessage, LineTooLong
from aiohttp.http_parser import RawRequestMessage
from aiohttp.streams import StreamReader
@@ -877,7 +878,7 @@ def test_clone_headers_dict() -> None:
async def test_cannot_clone_after_read(protocol: BaseProtocol) -> None:
- payload = StreamReader(protocol, 2**18, loop=asyncio.get_event_loop())
+ payload = StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_event_loop())
payload.feed_data(b"data")
payload.feed_eof()
req = make_mocked_request("GET", "/path", payload=payload)
@@ -948,7 +949,7 @@ async def test_multipart_formdata(protocol) -> None:
async def test_multipart_formdata_field_missing_name(protocol: BaseProtocol) -> None:
# Ensure ValueError is raised when Content-Disposition has no name
- payload = StreamReader(protocol, 2**18, loop=asyncio.get_event_loop())
+ payload = StreamReader(protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_event_loop())
payload.feed_data(
b"-----------------------------326931944431359\r\n"
b"Content-Disposition: form-data\r\n" # Missing name!
@@ -1000,7 +1001,9 @@ async def test_multipart_formdata_headers_too_many(protocol: BaseProtocol) -> No
b"--b--\r\n"
)
content_type = "multipart/form-data; boundary=b"
- payload = StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ payload = StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
payload.feed_data(body)
payload.feed_eof()
req = make_mocked_request(
@@ -1027,7 +1030,9 @@ async def test_multipart_formdata_header_too_long(protocol: BaseProtocol) -> Non
b"--b--\r\n"
)
content_type = "multipart/form-data; boundary=b"
- payload = StreamReader(protocol, 2**18, loop=asyncio.get_running_loop())
+ payload = StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
payload.feed_data(body)
payload.feed_eof()
req = make_mocked_request(
diff --git a/tests/test_websocket_writer.py b/tests/test_websocket_writer.py
index 272374c7187..646f199d720 100644
--- a/tests/test_websocket_writer.py
+++ b/tests/test_websocket_writer.py
@@ -12,6 +12,7 @@
from aiohttp._websocket.reader import WebSocketDataQueue
from aiohttp.base_protocol import BaseProtocol
from aiohttp.compression_utils import ZLibBackend
+from aiohttp.helpers import DEFAULT_CHUNK_SIZE
from aiohttp.http import WebSocketReader, WebSocketWriter
@@ -152,7 +153,9 @@ async def test_send_compress_cancelled(
monkeypatch.setattr("aiohttp._websocket.writer.WEBSOCKET_MAX_SYNC_CHUNK_SIZE", 1024)
writer = WebSocketWriter(protocol, transport, compress=15)
loop = asyncio.get_running_loop()
- queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**18, loop=loop)
+ queue = WebSocketDataQueue(
+ mock.Mock(_reading_paused=False), DEFAULT_CHUNK_SIZE, loop=loop
+ )
reader = WebSocketReader(queue, 50000)
# Replace executor with slow one to make race condition reproducible
@@ -299,7 +302,9 @@ async def test_concurrent_messages(
):
writer = WebSocketWriter(protocol, transport, compress=15)
loop = asyncio.get_running_loop()
- queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**18, loop=loop)
+ queue = WebSocketDataQueue(
+ mock.Mock(_reading_paused=False), DEFAULT_CHUNK_SIZE, loop=loop
+ )
reader = WebSocketReader(queue, 50000)
writers = []
payloads = []
From 78487856ce091229e33267d700d7d3ff5495a857 Mon Sep 17 00:00:00 2001
From: Rui Xi
Date: Tue, 14 Apr 2026 04:42:19 +0800
Subject: [PATCH 011/191] [PR #12264/af05010 backport][3.14] Reject HTTP/1.1
requests without Host header (#12367)
(cherry picked from commit af05010f61ca9be00a98a0885c8ae213adf4cf45)
---
CHANGES/10600.bugfix.rst | 2 +
aiohttp/_http_parser.pyx | 7 +-
aiohttp/http_parser.py | 5 +-
tests/test_http_parser.py | 220 +++++++++++++++++++-------------
tests/test_web_urldispatcher.py | 6 +-
5 files changed, 143 insertions(+), 97 deletions(-)
create mode 100644 CHANGES/10600.bugfix.rst
diff --git a/CHANGES/10600.bugfix.rst b/CHANGES/10600.bugfix.rst
new file mode 100644
index 00000000000..eba47bf56e6
--- /dev/null
+++ b/CHANGES/10600.bugfix.rst
@@ -0,0 +1,2 @@
+Fixed http parser not rejecting HTTP/1.1 requests that do not have valid Host header.
+-- by :user:`Cycloctane`.
diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx
index e5e3bec8689..d8bb4f1c39f 100644
--- a/aiohttp/_http_parser.pyx
+++ b/aiohttp/_http_parser.pyx
@@ -457,6 +457,7 @@ cdef class HttpParser:
cdef _on_headers_complete(self):
self._process_header()
+ http_version = self.http_version()
should_close = not cparser.llhttp_should_keep_alive(self._cparser)
upgrade = self._cparser.upgrade
chunked = self._cparser.flags & cparser.F_CHUNKED
@@ -465,6 +466,8 @@ cdef class HttpParser:
headers = CIMultiDictProxy(CIMultiDict(self._headers))
if self._cparser.type == cparser.HTTP_REQUEST:
+ if http_version == HttpVersion11 and hdrs.HOST not in headers:
+ raise BadHttpMessage("Missing 'Host' header in request.")
h_upg = headers.get("upgrade", "")
allowed = upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES
if allowed or self._cparser.method == cparser.HTTP_CONNECT:
@@ -488,11 +491,11 @@ cdef class HttpParser:
method = http_method_str(self._cparser.method)
msg = _new_request_message(
method, self._path,
- self.http_version(), headers, raw_headers,
+ http_version, headers, raw_headers,
should_close, encoding, upgrade, chunked, self._url)
else:
msg = _new_response_message(
- self.http_version(), self._cparser.status_code, self._reason,
+ http_version, self._cparser.status_code, self._reason,
headers, raw_headers, should_close, encoding,
upgrade, chunked)
diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py
index dce38f1c058..f3ea42f6e3f 100644
--- a/aiohttp/http_parser.py
+++ b/aiohttp/http_parser.py
@@ -49,7 +49,7 @@
LineTooLong,
TransferEncodingError,
)
-from .http_writer import HttpVersion, HttpVersion10
+from .http_writer import HttpVersion, HttpVersion10, HttpVersion11
from .streams import EMPTY_PAYLOAD, StreamReader
from .typedefs import RawHeaders
@@ -686,6 +686,9 @@ def parse_message(self, lines: list[bytes]) -> RawRequestMessage:
chunked,
) = self.parse_headers(lines[1:])
+ if version_o == HttpVersion11 and hdrs.HOST not in headers:
+ raise BadHttpMessage("Missing 'Host' header in request.")
+
if close is None: # then the headers weren't set in the request
if version_o <= HttpVersion10: # HTTP 1.0 must asks to not close
close = True
diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
index 9c65fb95257..c3a8669cb31 100644
--- a/tests/test_http_parser.py
+++ b/tests/test_http_parser.py
@@ -160,6 +160,7 @@ def test_c_parser_loaded():
def test_parse_headers(parser: Any) -> None:
text = b"""GET /test HTTP/1.1\r
+Host: a\r
test: a line\r
test2: data\r
\r
@@ -168,8 +169,16 @@ def test_parse_headers(parser: Any) -> None:
assert len(messages) == 1
msg = messages[0][0]
- assert list(msg.headers.items()) == [("test", "a line"), ("test2", "data")]
- assert msg.raw_headers == ((b"test", b"a line"), (b"test2", b"data"))
+ assert list(msg.headers.items()) == [
+ ("Host", "a"),
+ ("test", "a line"),
+ ("test2", "data"),
+ ]
+ assert msg.raw_headers == (
+ (b"Host", b"a"),
+ (b"test", b"a line"),
+ (b"test2", b"data"),
+ )
assert not msg.should_close
assert msg.compression is None
assert not msg.upgrade
@@ -177,6 +186,7 @@ def test_parse_headers(parser: Any) -> None:
def test_reject_obsolete_line_folding(parser: Any) -> None:
text = b"""GET /test HTTP/1.1\r
+Host: a\r
test: line\r
Content-Length: 48\r
test2: data\r
@@ -251,7 +261,7 @@ def test_cve_2023_37276(parser: Any) -> None:
r'"(),/:;<=>?@[\]{}',
)
def test_bad_header_name(parser: Any, rfc9110_5_6_2_token_delim: str) -> None:
- text = f"POST / HTTP/1.1\r\nhead{rfc9110_5_6_2_token_delim}er: val\r\n\r\n".encode()
+ text = f"POST / HTTP/1.1\r\nHost: a\r\nhead{rfc9110_5_6_2_token_delim}er: val\r\n\r\n".encode()
expectation = pytest.raises(http_exceptions.BadHttpMessage)
if rfc9110_5_6_2_token_delim == ":":
# Inserting colon into header just splits name/value earlier.
@@ -279,7 +289,7 @@ def test_bad_header_name(parser: Any, rfc9110_5_6_2_token_delim: str) -> None:
),
)
def test_bad_headers(parser: Any, hdr: str) -> None:
- text = f"POST / HTTP/1.1\r\n{hdr}\r\n\r\n".encode()
+ text = f"POST / HTTP/1.1\r\nHost: a\r\n{hdr}\r\n\r\n".encode()
with pytest.raises(http_exceptions.BadHttpMessage):
parser.feed_data(text)
@@ -304,7 +314,7 @@ def test_unpaired_surrogate_in_header_py(
max_field_size=8190,
)
protocol._parser = parser
- text = b"POST / HTTP/1.1\r\n\xff\r\n\r\n"
+ text = b"POST / HTTP/1.1\r\nHost: a\r\n\xff\r\n\r\n"
message = None
try:
parser.feed_data(text)
@@ -407,6 +417,12 @@ def test_duplicate_host_header_rejected(parser: HttpRequestParser) -> None:
parser.feed_data(text)
+def test_missing_host_header_rejected(parser: HttpRequestParser) -> None:
+ text = b"GET /admin HTTP/1.1\r\n\r\n"
+ with pytest.raises(http_exceptions.BadHttpMessage, match="Missing 'Host' header"):
+ parser.feed_data(text)
+
+
@pytest.mark.parametrize(
("hdr1", "hdr2"),
(
@@ -457,7 +473,7 @@ def test_bad_chunked(parser: HttpRequestParser) -> None:
def test_whitespace_before_header(parser: Any) -> None:
- text = b"GET / HTTP/1.1\r\n\tContent-Length: 1\r\n\r\nX"
+ text = b"GET / HTTP/1.1\r\nHost: a\r\n\tContent-Length: 1\r\n\r\nX"
with pytest.raises(http_exceptions.BadHttpMessage):
parser.feed_data(text)
@@ -488,7 +504,7 @@ def test_parse_unusual_request_line(parser) -> None:
def test_parse(parser) -> None:
- text = b"GET /test HTTP/1.1\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
assert len(messages) == 1
msg, _ = messages[0]
@@ -497,10 +513,11 @@ def test_parse(parser) -> None:
assert msg.method == "GET"
assert msg.path == "/test"
assert msg.version == (1, 1)
+ assert msg.headers["Host"] == "a"
async def test_parse_body(parser) -> None:
- text = b"GET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\nContent-Length: 4\r\n\r\nbody"
messages, upgrade, tail = parser.feed_data(text)
assert len(messages) == 1
_, payload = messages[0]
@@ -509,7 +526,7 @@ async def test_parse_body(parser) -> None:
async def test_parse_body_with_CRLF(parser) -> None:
- text = b"\r\nGET /test HTTP/1.1\r\nContent-Length: 4\r\n\r\nbody"
+ text = b"\r\nGET /test HTTP/1.1\r\nHost: a\r\nContent-Length: 4\r\n\r\nbody"
messages, upgrade, tail = parser.feed_data(text)
assert len(messages) == 1
_, payload = messages[0]
@@ -518,7 +535,7 @@ async def test_parse_body_with_CRLF(parser) -> None:
def test_parse_delayed(parser) -> None:
- text = b"GET /test HTTP/1.1\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\n"
messages, upgrade, tail = parser.feed_data(text)
assert len(messages) == 0
assert not upgrade
@@ -531,8 +548,9 @@ def test_parse_delayed(parser) -> None:
def test_headers_multi_feed(parser) -> None:
text1 = b"GET /test HTTP/1.1\r\n"
- text2 = b"test: line"
- text3 = b" continue\r\n\r\n"
+ text2 = b"Host: a\r\n"
+ text3 = b"test: line"
+ text4 = b" continue\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text1)
assert len(messages) == 0
@@ -541,18 +559,21 @@ def test_headers_multi_feed(parser) -> None:
assert len(messages) == 0
messages, upgrade, tail = parser.feed_data(text3)
+ assert len(messages) == 0
+
+ messages, upgrade, tail = parser.feed_data(text4)
assert len(messages) == 1
msg = messages[0][0]
- assert list(msg.headers.items()) == [("test", "line continue")]
- assert msg.raw_headers == ((b"test", b"line continue"),)
+ assert list(msg.headers.items()) == [("Host", "a"), ("test", "line continue")]
+ assert msg.raw_headers == ((b"Host", b"a"), (b"test", b"line continue"))
assert not msg.should_close
assert msg.compression is None
assert not msg.upgrade
def test_headers_split_field(parser) -> None:
- text1 = b"GET /test HTTP/1.1\r\n"
+ text1 = b"GET /test HTTP/1.1\r\nHost: a\r\n"
text2 = b"t"
text3 = b"es"
text4 = b"t: value\r\n\r\n"
@@ -565,8 +586,8 @@ def test_headers_split_field(parser) -> None:
assert len(messages) == 1
msg = messages[0][0]
- assert list(msg.headers.items()) == [("test", "value")]
- assert msg.raw_headers == ((b"test", b"value"),)
+ assert list(msg.headers.items()) == [("Host", "a"), ("test", "value")]
+ assert msg.raw_headers == ((b"Host", b"a"), (b"test", b"value"))
assert not msg.should_close
assert msg.compression is None
assert not msg.upgrade
@@ -574,7 +595,7 @@ def test_headers_split_field(parser) -> None:
def test_parse_headers_multi(parser) -> None:
text = (
- b"GET /test HTTP/1.1\r\n"
+ b"GET /test HTTP/1.1\r\nHost: a\r\n"
b"Set-Cookie: c1=cookie1\r\n"
b"Set-Cookie: c2=cookie2\r\n\r\n"
)
@@ -584,10 +605,12 @@ def test_parse_headers_multi(parser) -> None:
msg = messages[0][0]
assert list(msg.headers.items()) == [
+ ("Host", "a"),
("Set-Cookie", "c1=cookie1"),
("Set-Cookie", "c2=cookie2"),
]
assert msg.raw_headers == (
+ (b"Host", b"a"),
(b"Set-Cookie", b"c1=cookie1"),
(b"Set-Cookie", b"c2=cookie2"),
)
@@ -603,14 +626,14 @@ def test_conn_default_1_0(parser) -> None:
def test_conn_default_1_1(parser) -> None:
- text = b"GET /test HTTP/1.1\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert not msg.should_close
def test_conn_close(parser) -> None:
- text = b"GET /test HTTP/1.1\r\nconnection: close\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: close\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert msg.should_close
@@ -631,14 +654,14 @@ def test_conn_keep_alive_1_0(parser) -> None:
def test_conn_keep_alive_1_1(parser) -> None:
- text = b"GET /test HTTP/1.1\r\nconnection: keep-alive\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: keep-alive\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert not msg.should_close
def test_conn_close_comma_list(parser) -> None:
- text = b"GET /test HTTP/1.1\r\nconnection: close, keep-alive\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: close, keep-alive\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert msg.should_close
@@ -647,6 +670,7 @@ def test_conn_close_comma_list(parser) -> None:
def test_conn_close_multiple_headers(parser) -> None:
text = (
b"GET /test HTTP/1.1\r\n"
+ b"Host: a\r\n"
b"connection: keep-alive\r\n"
b"connection: close\r\n\r\n"
)
@@ -663,14 +687,14 @@ def test_conn_other_1_0(parser) -> None:
def test_conn_other_1_1(parser) -> None:
- text = b"GET /test HTTP/1.1\r\nconnection: test\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: test\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert not msg.should_close
def test_request_chunked(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg, payload = messages[0]
assert msg.chunked
@@ -680,14 +704,14 @@ def test_request_chunked(parser) -> None:
def test_te_header_non_ascii(parser: HttpRequestParser) -> None:
# K = Kelvin sign, not valid ascii.
- text = "GET /test HTTP/1.1\r\nTransfer-Encoding: chunKed\r\n\r\n"
+ text = "GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunKed\r\n\r\n"
with pytest.raises(http_exceptions.BadHttpMessage):
parser.feed_data(text.encode())
def test_upgrade_header_non_ascii(parser: HttpRequestParser) -> None:
# K = Kelvin sign, not valid ascii.
- text = "GET /test HTTP/1.1\r\nUpgrade: websocKet\r\n\r\n"
+ text = "GET /test HTTP/1.1\r\nHost: a\r\nUpgrade: websocKet\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text.encode())
assert not upgrade
@@ -695,6 +719,7 @@ def test_upgrade_header_non_ascii(parser: HttpRequestParser) -> None:
def test_request_te_chunked_with_content_length(parser: HttpRequestParser) -> None:
text = (
b"GET /test HTTP/1.1\r\n"
+ b"Host: a\r\n"
b"content-length: 1234\r\n"
b"transfer-encoding: chunked\r\n\r\n"
)
@@ -706,7 +731,7 @@ def test_request_te_chunked_with_content_length(parser: HttpRequestParser) -> No
def test_request_te_chunked123(parser: Any) -> None:
- text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked123\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked123\r\n\r\n"
with pytest.raises(
http_exceptions.BadHttpMessage,
match="Request has invalid `Transfer-Encoding`",
@@ -715,14 +740,14 @@ def test_request_te_chunked123(parser: Any) -> None:
async def test_request_te_last_chunked(parser: Any) -> None:
- text = b"GET /test HTTP/1.1\r\nTransfer-Encoding: not, chunked\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: not, chunked\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
# https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3
assert await messages[0][1].read() == b"Test"
def test_request_te_first_chunked(parser: Any) -> None:
- text = b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked, not\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked, not\r\n\r\n1\r\nT\r\n3\r\nest\r\n0\r\n\r\n"
# https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3
with pytest.raises(
http_exceptions.BadHttpMessage,
@@ -734,6 +759,7 @@ def test_request_te_first_chunked(parser: Any) -> None:
def test_conn_upgrade(parser: Any) -> None:
text = (
b"GET /test HTTP/1.1\r\n"
+ b"Host: a\r\n"
b"connection: upgrade\r\n"
b"upgrade: websocket\r\n\r\n"
)
@@ -747,6 +773,7 @@ def test_conn_upgrade(parser: Any) -> None:
def test_conn_upgrade_comma_list(parser) -> None:
text = (
b"GET /test HTTP/1.1\r\n"
+ b"host: a\r\n"
b"connection: keep-alive, upgrade\r\n"
b"upgrade: websocket\r\n\r\n"
)
@@ -760,6 +787,7 @@ def test_conn_upgrade_comma_list(parser) -> None:
def test_conn_upgrade_multiple_headers(parser) -> None:
text = (
b"GET /test HTTP/1.1\r\n"
+ b"host: a\r\n"
b"connection: keep-alive\r\n"
b"connection: upgrade\r\n"
b"upgrade: websocket\r\n\r\n"
@@ -773,7 +801,7 @@ def test_conn_upgrade_multiple_headers(parser) -> None:
def test_bad_upgrade(parser) -> None:
"""Test not upgraded if missing Upgrade header."""
- text = b"GET /test HTTP/1.1\r\nconnection: upgrade\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\nconnection: upgrade\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert not msg.upgrade
@@ -781,21 +809,21 @@ def test_bad_upgrade(parser) -> None:
def test_compression_empty(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ncontent-encoding: \r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: \r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert msg.compression is None
def test_compression_deflate(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ncontent-encoding: deflate\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: deflate\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert msg.compression == "deflate"
def test_compression_gzip(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ncontent-encoding: gzip\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: gzip\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert msg.compression == "gzip"
@@ -803,7 +831,7 @@ def test_compression_gzip(parser) -> None:
@pytest.mark.skipif(brotli is None, reason="brotli is not installed")
def test_compression_brotli(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ncontent-encoding: br\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: br\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert msg.compression == "br"
@@ -811,7 +839,7 @@ def test_compression_brotli(parser) -> None:
@pytest.mark.skipif(zstandard is None, reason="zstandard is not installed")
def test_compression_zstd(parser: HttpRequestParser) -> None:
- text = b"GET /test HTTP/1.1\r\ncontent-encoding: zstd\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: zstd\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert msg.compression == "zstd"
@@ -825,7 +853,7 @@ def test_compression_zstd(parser: HttpRequestParser) -> None:
),
)
def test_compression_non_ascii(parser: HttpRequestParser, enc: bytes) -> None:
- text = b"GET /test HTTP/1.1\r\ncontent-encoding: " + enc + b"\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: " + enc + b"\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
# Non-ascii input should not evaluate to a valid encoding scheme.
@@ -833,14 +861,14 @@ def test_compression_non_ascii(parser: HttpRequestParser, enc: bytes) -> None:
def test_compression_unknown(parser: HttpRequestParser) -> None:
- text = b"GET /test HTTP/1.1\r\ncontent-encoding: compress\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-encoding: compress\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert msg.compression is None
def test_url_connect(parser: Any) -> None:
- text = b"CONNECT www.google.com HTTP/1.1\r\ncontent-length: 0\r\n\r\n"
+ text = b"CONNECT www.google.com HTTP/1.1\r\nHost: a\r\ncontent-length: 0\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg, payload = messages[0]
assert upgrade
@@ -848,7 +876,7 @@ def test_url_connect(parser: Any) -> None:
def test_headers_connect(parser: Any) -> None:
- text = b"CONNECT www.google.com HTTP/1.1\r\ncontent-length: 0\r\n\r\n"
+ text = b"CONNECT www.google.com HTTP/1.1\r\nHost: a\r\ncontent-length: 0\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg, payload = messages[0]
assert upgrade
@@ -858,6 +886,7 @@ def test_headers_connect(parser: Any) -> None:
def test_url_absolute(parser: Any) -> None:
text = (
b"GET https://www.google.com/path/to.html HTTP/1.1\r\n"
+ b"Host: a\r\n"
b"content-length: 0\r\n\r\n"
)
messages, upgrade, tail = parser.feed_data(text)
@@ -868,21 +897,21 @@ def test_url_absolute(parser: Any) -> None:
def test_headers_old_websocket_key1(parser: Any) -> None:
- text = b"GET /test HTTP/1.1\r\nSEC-WEBSOCKET-KEY1: line\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\nSEC-WEBSOCKET-KEY1: line\r\n\r\n"
with pytest.raises(http_exceptions.BadHttpMessage):
parser.feed_data(text)
def test_headers_content_length_err_1(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ncontent-length: line\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-length: line\r\n\r\n"
with pytest.raises(http_exceptions.BadHttpMessage):
parser.feed_data(text)
def test_headers_content_length_err_2(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ncontent-length: -1\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ncontent-length: -1\r\n\r\n"
with pytest.raises(http_exceptions.BadHttpMessage):
parser.feed_data(text)
@@ -905,7 +934,7 @@ def test_headers_content_length_err_2(parser) -> None:
@pytest.mark.parametrize("pad2", _pad.keys(), ids=["post-" + n for n in _pad.values()])
@pytest.mark.parametrize("pad1", _pad.keys(), ids=["pre-" + n for n in _pad.values()])
def test_invalid_header_spacing(parser, pad1: bytes, pad2: bytes, hdr: bytes) -> None:
- text = b"GET /test HTTP/1.1\r\n%s%s%s: value\r\n\r\n" % (pad1, hdr, pad2)
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\n%s%s%s: value\r\n\r\n" % (pad1, hdr, pad2)
expectation = pytest.raises(http_exceptions.BadHttpMessage)
if pad1 == pad2 == b"" and hdr != b"":
# one entry in param matrix is correct: non-empty name, not padded
@@ -915,19 +944,19 @@ def test_invalid_header_spacing(parser, pad1: bytes, pad2: bytes, hdr: bytes) ->
def test_empty_header_name(parser) -> None:
- text = b"GET /test HTTP/1.1\r\n:test\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\n:test\r\n\r\n"
with pytest.raises(http_exceptions.BadHttpMessage):
parser.feed_data(text)
def test_invalid_header(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ntest line\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ntest line\r\n\r\n"
with pytest.raises(http_exceptions.BadHttpMessage):
parser.feed_data(text)
def test_invalid_name(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ntest[]: line\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ntest[]: line\r\n\r\n"
with pytest.raises(http_exceptions.BadHttpMessage):
parser.feed_data(text)
@@ -946,15 +975,15 @@ def test_max_header_field_size(parser, size) -> None:
def test_max_header_size_under_limit(parser: HttpRequestParser) -> None:
name = b"t" * 8185
- text = b"GET /test HTTP/1.1\r\n" + name + b":data\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\n" + name + b":data\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert msg.method == "GET"
assert msg.path == "/test"
assert msg.version == (1, 1)
- assert msg.headers == CIMultiDict({name.decode(): "data"})
- assert msg.raw_headers == ((name, b"data"),)
+ assert msg.headers == CIMultiDict([("Host", "a"), (name.decode(), "data")])
+ assert msg.raw_headers == ((b"Host", b"a"), (name, b"data"))
assert not msg.should_close
assert msg.compression is None
assert not msg.upgrade
@@ -986,7 +1015,7 @@ def test_max_header_combined_size(parser: HttpRequestParser) -> None:
async def test_max_trailer_size(parser: HttpRequestParser, size: int) -> None:
value = b"t" * size
text = (
- b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
+ b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n"
+ hex(4000)[2:].encode()
+ b"\r\n"
+ b"b" * 4000
@@ -1012,7 +1041,7 @@ async def test_max_headers(
parser: HttpRequestParser, headers: int, trailers: int
) -> None:
text = (
- b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked"
+ b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked"
+ b"".join(b"\r\nHeader-%d: Value" % i for i in range(headers))
+ b"\r\n\r\n4\r\ntest\r\n0"
+ b"".join(b"\r\nTrailer-%d: Value" % i for i in range(trailers))
@@ -1028,15 +1057,15 @@ async def test_max_headers(
def test_max_header_value_size_under_limit(parser: HttpRequestParser) -> None:
value = b"A" * 8185
- text = b"GET /test HTTP/1.1\r\ndata:" + value + b"\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ndata:" + value + b"\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert msg.method == "GET"
assert msg.path == "/test"
assert msg.version == (1, 1)
- assert msg.headers == CIMultiDict({"data": value.decode()})
- assert msg.raw_headers == ((b"data", value),)
+ assert msg.headers == CIMultiDict([("Host", "a"), ("data", value.decode())])
+ assert msg.raw_headers == ((b"Host", b"a"), (b"data", value))
assert not msg.should_close
assert msg.compression is None
assert not msg.upgrade
@@ -1046,7 +1075,7 @@ def test_max_header_value_size_under_limit(parser: HttpRequestParser) -> None:
async def test_chunk_splits_after_pause(parser: HttpRequestParser) -> None:
text = (
- b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
+ b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n"
+ b"1\r\nb\r\n" * 50000
+ b"0\r\n\r\n"
)
@@ -1272,15 +1301,15 @@ def test_max_header_value_size_continuation_under_limit(
def test_http_request_parser(parser) -> None:
- text = b"GET /path HTTP/1.1\r\n\r\n"
+ text = b"GET /path HTTP/1.1\r\nHost: a\r\n\r\n"
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert msg.method == "GET"
assert msg.path == "/path"
assert msg.version == (1, 1)
- assert msg.headers == CIMultiDict()
- assert msg.raw_headers == ()
+ assert msg.headers == CIMultiDict({"Host": "a"})
+ assert msg.raw_headers == ((b"Host", b"a"),)
assert not msg.should_close
assert msg.compression is None
assert not msg.upgrade
@@ -1311,7 +1340,7 @@ def test_http_request_bad_status_line(parser) -> None:
def test_http_request_bad_status_line_number(
parser: Any, nonascii_digit: bytes
) -> None:
- text = b"GET /digit HTTP/1." + nonascii_digit + b"\r\n\r\n"
+ text = b"GET /digit HTTP/1." + nonascii_digit + b"\r\nHost: a\r\n\r\n"
with pytest.raises(http_exceptions.BadStatusLine):
parser.feed_data(text)
@@ -1319,19 +1348,19 @@ def test_http_request_bad_status_line_number(
def test_http_request_bad_status_line_separator(parser: Any) -> None:
# single code point, old, multibyte NFKC, multibyte NFKD
utf8sep = "\N{arabic ligature sallallahou alayhe wasallam}".encode()
- text = b"GET /ligature HTTP/1" + utf8sep + b"1\r\n\r\n"
+ text = b"GET /ligature HTTP/1" + utf8sep + b"1\r\nHost: a\r\n\r\n"
with pytest.raises(http_exceptions.BadStatusLine):
parser.feed_data(text)
def test_http_request_bad_status_line_whitespace(parser: Any) -> None:
- text = b"GET\n/path\fHTTP/1.1\r\n\r\n"
+ text = b"GET\n/path\fHTTP/1.1\r\nHost: a\r\n\r\n"
with pytest.raises(http_exceptions.BadStatusLine):
parser.feed_data(text)
def test_http_request_message_after_close(parser: HttpRequestParser) -> None:
- text = b"GET / HTTP/1.1\r\nConnection: close\r\n\r\nInvalid\r\n\r\n"
+ text = b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close\r\n\r\nInvalid\r\n\r\n"
with pytest.raises(
http_exceptions.BadHttpMessage, match="Data after `Connection: close`"
):
@@ -1339,7 +1368,7 @@ def test_http_request_message_after_close(parser: HttpRequestParser) -> None:
def test_http_request_message_after_close_comma_list(parser: HttpRequestParser) -> None:
- text = b"GET / HTTP/1.1\r\nConnection: close, keep-alive\r\n\r\nInvalid\r\n\r\n"
+ text = b"GET / HTTP/1.1\r\nHost: a\r\nConnection: close, keep-alive\r\n\r\nInvalid\r\n\r\n"
with pytest.raises(
http_exceptions.BadHttpMessage, match="Data after `Connection: close`"
):
@@ -1349,6 +1378,7 @@ def test_http_request_message_after_close_comma_list(parser: HttpRequestParser)
def test_http_request_upgrade(parser: HttpRequestParser) -> None:
text = (
b"GET /test HTTP/1.1\r\n"
+ b"Host: a\r\n"
b"connection: upgrade\r\n"
b"upgrade: websocket\r\n\r\n"
b"some raw data"
@@ -1364,6 +1394,7 @@ def test_http_request_upgrade(parser: HttpRequestParser) -> None:
async def test_http_request_upgrade_unknown(parser: Any) -> None:
text = (
b"POST / HTTP/1.1\r\n"
+ b"Host: a\r\n"
b"Connection: Upgrade\r\n"
b"Content-Length: 2\r\n"
b"Upgrade: unknown\r\n"
@@ -1397,7 +1428,7 @@ def xfail_c_parser_url(request) -> None:
def test_http_request_parser_utf8_request_line(parser) -> None:
messages, upgrade, tail = parser.feed_data(
# note the truncated unicode sequence
- b"GET /P\xc3\xbcnktchen\xa0\xef\xb7 HTTP/1.1\r\n" +
+ b"GET /P\xc3\xbcnktchen\xa0\xef\xb7 HTTP/1.1\r\nHost: a\r\n" +
# for easier grep: ASCII 0xA0 more commonly known as non-breaking space
# note the leading and trailing spaces
"sTeP: \N{latin small letter sharp s}nek\t\N{no-break space} "
@@ -1408,8 +1439,8 @@ def test_http_request_parser_utf8_request_line(parser) -> None:
assert msg.method == "GET"
assert msg.path == "/Pünktchen\udca0\udcef\udcb7"
assert msg.version == (1, 1)
- assert msg.headers == CIMultiDict([("STEP", "ßnek\t\xa0")])
- assert msg.raw_headers == ((b"sTeP", "ßnek\t\xa0".encode()),)
+ assert msg.headers == CIMultiDict([("Host", "a"), ("STEP", "ßnek\t\xa0")])
+ assert msg.raw_headers == ((b"Host", b"a"), (b"sTeP", "ßnek\t\xa0".encode()))
assert not msg.should_close
assert msg.compression is None
assert not msg.upgrade
@@ -1420,15 +1451,15 @@ def test_http_request_parser_utf8_request_line(parser) -> None:
def test_http_request_parser_utf8(parser) -> None:
- text = "GET /path HTTP/1.1\r\nx-test:тест\r\n\r\n".encode()
+ text = "GET /path HTTP/1.1\r\nHost: a\r\nx-test:тест\r\n\r\n".encode()
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
assert msg.method == "GET"
assert msg.path == "/path"
assert msg.version == (1, 1)
- assert msg.headers == CIMultiDict([("X-TEST", "тест")])
- assert msg.raw_headers == ((b"x-test", "тест".encode()),)
+ assert msg.headers == CIMultiDict([("Host", "a"), ("X-TEST", "тест")])
+ assert msg.raw_headers == ((b"Host", b"a"), (b"x-test", "тест".encode()))
assert not msg.should_close
assert msg.compression is None
assert not msg.upgrade
@@ -1437,16 +1468,19 @@ def test_http_request_parser_utf8(parser) -> None:
def test_http_request_parser_non_utf8(parser) -> None:
- text = "GET /path HTTP/1.1\r\nx-test:тест\r\n\r\n".encode("cp1251")
+ text = "GET /path HTTP/1.1\r\nHost: a\r\nx-test:тест\r\n\r\n".encode("cp1251")
msg = parser.feed_data(text)[0][0][0]
assert msg.method == "GET"
assert msg.path == "/path"
assert msg.version == (1, 1)
assert msg.headers == CIMultiDict(
- [("X-TEST", "тест".encode("cp1251").decode("utf8", "surrogateescape"))]
+ [
+ ("Host", "a"),
+ ("X-TEST", "тест".encode("cp1251").decode("utf8", "surrogateescape")),
+ ]
)
- assert msg.raw_headers == ((b"x-test", "тест".encode("cp1251")),)
+ assert msg.raw_headers == ((b"Host", b"a"), (b"x-test", "тест".encode("cp1251")))
assert not msg.should_close
assert msg.compression is None
assert not msg.upgrade
@@ -1455,7 +1489,7 @@ def test_http_request_parser_non_utf8(parser) -> None:
def test_http_request_parser_two_slashes(parser) -> None:
- text = b"GET //path HTTP/1.1\r\n\r\n"
+ text = b"GET //path HTTP/1.1\r\nHost: a\r\n\r\n"
msg = parser.feed_data(text)[0][0][0]
assert msg.method == "GET"
@@ -1476,17 +1510,19 @@ def test_http_request_parser_bad_method(
parser, rfc9110_5_6_2_token_delim: bytes
) -> None:
with pytest.raises(http_exceptions.BadHttpMethod):
- parser.feed_data(rfc9110_5_6_2_token_delim + b'ET" /get HTTP/1.1\r\n\r\n')
+ parser.feed_data(
+ rfc9110_5_6_2_token_delim + b'ET" /get HTTP/1.1\r\nHost: a\r\n\r\n'
+ )
def test_http_request_parser_bad_version(parser) -> None:
with pytest.raises(http_exceptions.BadHttpMessage):
- parser.feed_data(b"GET //get HT/11\r\n\r\n")
+ parser.feed_data(b"GET //get HT/11\r\nHost: a\r\n\r\n")
def test_http_request_parser_bad_version_number(parser: Any) -> None:
with pytest.raises(http_exceptions.BadHttpMessage):
- parser.feed_data(b"GET /test HTTP/1.32\r\n\r\n")
+ parser.feed_data(b"GET /test HTTP/1.32\r\nHost: a\r\n\r\n")
def test_http_request_parser_bad_ascii_uri(parser: Any) -> None:
@@ -1510,15 +1546,15 @@ def test_http_request_max_status_line(parser, size) -> None:
def test_http_request_max_status_line_under_limit(parser: HttpRequestParser) -> None:
path = b"t" * 8172
messages, upgraded, tail = parser.feed_data(
- b"GET /path" + path + b" HTTP/1.1\r\n\r\n"
+ b"GET /path" + path + b" HTTP/1.1\r\nHost: a\r\n\r\n"
)
msg = messages[0][0]
assert msg.method == "GET"
assert msg.path == "/path" + path.decode()
assert msg.version == (1, 1)
- assert msg.headers == CIMultiDict()
- assert msg.raw_headers == ()
+ assert msg.headers == CIMultiDict({"Host": "a"})
+ assert msg.raw_headers == ((b"Host", b"a"),)
assert not msg.should_close
assert msg.compression is None
assert not msg.upgrade
@@ -1755,7 +1791,7 @@ def test_http_response_parser_code_not_ascii(response, nonascii_digit: bytes) ->
def test_http_request_chunked_payload(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n"
msg, payload = parser.feed_data(text)[0][0]
assert msg.chunked
@@ -1771,12 +1807,13 @@ def test_http_request_chunked_payload(parser) -> None:
def test_http_request_chunked_payload_and_next_message(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n"
msg, payload = parser.feed_data(text)[0][0]
messages, upgraded, tail = parser.feed_data(
b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"
b"POST /test2 HTTP/1.1\r\n"
+ b"Host: a\r\n"
b"transfer-encoding: chunked\r\n\r\n"
)
@@ -1794,7 +1831,7 @@ def test_http_request_chunked_payload_and_next_message(parser) -> None:
def test_http_request_chunked_payload_chunks(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n"
msg, payload = parser.feed_data(text)[0][0]
parser.feed_data(b"4\r\ndata\r")
@@ -1817,7 +1854,7 @@ def test_http_request_chunked_payload_chunks(parser) -> None:
def test_parse_chunked_payload_chunk_extension(parser) -> None:
- text = b"GET /test HTTP/1.1\r\ntransfer-encoding: chunked\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\ntransfer-encoding: chunked\r\n\r\n"
msg, payload = parser.feed_data(text)[0][0]
parser.feed_data(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\ntest: test\r\n\r\n")
@@ -1829,7 +1866,7 @@ def test_parse_chunked_payload_chunk_extension(parser) -> None:
async def test_request_chunked_with_trailer(parser: HttpRequestParser) -> None:
- text = b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n0\r\ntest: trailer\r\nsecond: test trailer\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n0\r\ntest: trailer\r\nsecond: test trailer\r\n\r\n"
messages, upgraded, tail = parser.feed_data(text)
assert not tail
msg, payload = messages[0]
@@ -1839,7 +1876,7 @@ async def test_request_chunked_with_trailer(parser: HttpRequestParser) -> None:
async def test_request_chunked_reject_bad_trailer(parser: HttpRequestParser) -> None:
- text = b"GET /test HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nbad\ntrailer\r\n\r\n"
+ text = b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nbad\ntrailer\r\n\r\n"
with pytest.raises(http_exceptions.BadHttpMessage, match=r"b'bad\\ntrailer'"):
parser.feed_data(text)
@@ -1852,7 +1889,7 @@ def test_parse_no_length_or_te_on_post(
protocol = RequestHandler(server, loop=loop)
parser = request_cls(protocol, loop, limit=DEFAULT_CHUNK_SIZE)
protocol._parser = parser
- text = b"POST /test HTTP/1.1\r\n\r\n"
+ text = b"POST /test HTTP/1.1\r\nHost: a\r\n\r\n"
msg, payload = parser.feed_data(text)[0][0]
assert payload.is_eof()
@@ -1885,7 +1922,7 @@ def test_parse_length_payload(response) -> None:
def test_parse_no_length_payload(parser) -> None:
- text = b"PUT / HTTP/1.1\r\n\r\n"
+ text = b"PUT / HTTP/1.1\r\nHost: a\r\n\r\n"
msg, payload = parser.feed_data(text)[0][0]
assert payload.is_eof()
@@ -2055,7 +2092,7 @@ async def test_parse_chunked_payload_with_lf_in_extensions(
def test_partial_url(parser: HttpRequestParser) -> None:
messages, upgrade, tail = parser.feed_data(b"GET /te")
assert len(messages) == 0
- messages, upgrade, tail = parser.feed_data(b"st HTTP/1.1\r\n\r\n")
+ messages, upgrade, tail = parser.feed_data(b"st HTTP/1.1\r\nHost: a\r\n\r\n")
assert len(messages) == 1
msg, payload = messages[0]
@@ -2078,7 +2115,7 @@ def test_partial_url(parser: HttpRequestParser) -> None:
],
)
def test_parse_uri_percent_encoded(parser, uri, path, query, fragment) -> None:
- text = (f"GET {uri} HTTP/1.1\r\n\r\n").encode()
+ text = (f"GET {uri} HTTP/1.1\r\nHost: a\r\n\r\n").encode()
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
@@ -2092,7 +2129,7 @@ def test_parse_uri_percent_encoded(parser, uri, path, query, fragment) -> None:
def test_parse_uri_utf8(parser) -> None:
if not isinstance(parser, HttpRequestParserPy):
pytest.xfail("Not valid HTTP. Maybe update py-parser to reject later.")
- text = ("GET /путь?ключ=знач#фраг HTTP/1.1\r\n\r\n").encode()
+ text = ("GET /путь?ключ=знач#фраг HTTP/1.1\r\nHost: a\r\n\r\n").encode()
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
@@ -2104,7 +2141,8 @@ def test_parse_uri_utf8(parser) -> None:
def test_parse_uri_utf8_percent_encoded(parser) -> None:
text = (
- "GET %s HTTP/1.1\r\n\r\n" % quote("/путь?ключ=знач#фраг", safe="/?=#")
+ "GET %s HTTP/1.1\r\nHost: a\r\n\r\n"
+ % quote("/путь?ключ=знач#фраг", safe="/?=#")
).encode()
messages, upgrade, tail = parser.feed_data(text)
msg = messages[0][0]
diff --git a/tests/test_web_urldispatcher.py b/tests/test_web_urldispatcher.py
index 85138a343b0..05f028a88e4 100644
--- a/tests/test_web_urldispatcher.py
+++ b/tests/test_web_urldispatcher.py
@@ -249,7 +249,7 @@ async def test_follow_symlink_directory_traversal(
# We need to use a raw socket to test this, as the client will normalize
# the path before sending it to the server.
reader, writer = await asyncio.open_connection(client.host, client.port)
- writer.write(b"GET /../private_file HTTP/1.1\r\n\r\n")
+ writer.write(b"GET /../private_file HTTP/1.1\r\nHost: a\r\n\r\n")
response = await reader.readuntil(b"\r\n\r\n")
assert b"404 Not Found" in response
writer.close()
@@ -301,14 +301,14 @@ async def test_follow_symlink_directory_traversal_after_normalization(
# We need to use a raw socket to test this, as the client will normalize
# the path before sending it to the server.
reader, writer = await asyncio.open_connection(client.host, client.port)
- writer.write(b"GET /my_symlink/../private_file HTTP/1.1\r\n\r\n")
+ writer.write(b"GET /my_symlink/../private_file HTTP/1.1\r\nHost: a\r\n\r\n")
response = await reader.readuntil(b"\r\n\r\n")
assert b"404 Not Found" in response
writer.close()
await writer.wait_closed()
reader, writer = await asyncio.open_connection(client.host, client.port)
- writer.write(b"GET /my_symlink/symlink_target_file HTTP/1.1\r\n\r\n")
+ writer.write(b"GET /my_symlink/symlink_target_file HTTP/1.1\r\nHost: a\r\n\r\n")
response = await reader.readuntil(b"\r\n\r\n")
assert b"200 OK" in response
response = await reader.readuntil(b"readable")
From 0544166ae26ed6e1f5bb63def9de6726205b125a Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Mon, 13 Apr 2026 22:31:39 +0100
Subject: [PATCH 012/191] [PR #12364/3f772cf7 backport][3.14] Disable cov/xdist
by default (#12368)
**This is a backport of PR #12364 as merged into master
(3f772cf70b552671c5ffca0a7e8b308fca323a25).**
Co-authored-by: Sam Bull
---
.github/workflows/ci-cd.yml | 13 +++++++------
CHANGES/12364.contrib.rst | 1 +
setup.cfg | 8 --------
3 files changed, 8 insertions(+), 14 deletions(-)
create mode 100644 CHANGES/12364.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 34488f65681..bb87dc5de62 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -222,7 +222,8 @@ jobs:
PIP_USER: 1
run: >-
PATH="${HOME}/Library/Python/3.11/bin:${HOME}/.local/bin:${PATH}"
- pytest --junitxml=junit.xml -m 'not dev_mode and not autobahn'
+ pytest --junitxml=junit.xml --numprocesses=auto --cov=aiohttp/ --cov=tests/
+ -m 'not dev_mode and not autobahn'
shell: bash
- name: Re-run the failing tests with maximum verbosity
if: failure()
@@ -230,7 +231,7 @@ jobs:
COLOR: yes
AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }}
run: >- # `exit 1` makes sure that the job remains red with flaky runs
- pytest --no-cov --numprocesses=0 -vvvvv --lf && exit 1
+ pytest --no-cov -vvvvv --lf && exit 1
shell: bash
- name: Run dev_mode tests
env:
@@ -238,7 +239,7 @@ jobs:
AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }}
PIP_USER: 1
PYTHONDEVMODE: 1
- run: pytest -m dev_mode --cov-append --numprocesses=0
+ run: pytest -m dev_mode --cov=aiohttp/ --cov=tests/ --cov-append
shell: bash
- name: Turn coverage into xml
env:
@@ -330,7 +331,7 @@ jobs:
PIP_USER: 1
run: >-
PATH="${HOME}/Library/Python/3.11/bin:${HOME}/.local/bin:${PATH}"
- pytest --junitxml=junit.xml --numprocesses=0 -m autobahn
+ pytest --junitxml=junit.xml --cov=aiohttp/ --cov=tests/ -m autobahn
shell: bash
- name: Turn coverage into xml
env:
@@ -398,7 +399,7 @@ jobs:
uses: CodSpeedHQ/action@v4
with:
mode: instrumentation
- run: python -Im pytest --no-cov --numprocesses=0 -vvvvv --codspeed
+ run: python -Im pytest --no-cov -vvvvv --codspeed
cython-coverage:
@@ -447,7 +448,7 @@ jobs:
PIP_USER: 1
run: >-
pytest tests/test_client_functional.py tests/test_http_parser.py tests/test_http_writer.py tests/test_web_functional.py tests/test_web_response.py tests/test_websocket_parser.py
- --cov-config=.coveragerc-cython.toml
+ --cov-config=.coveragerc-cython.toml --cov=aiohttp/ --cov=tests/ --numprocesses=auto
-m 'not dev_mode and not autobahn'
shell: bash
- name: Turn coverage into xml
diff --git a/CHANGES/12364.contrib.rst b/CHANGES/12364.contrib.rst
new file mode 100644
index 00000000000..21b9eb1b271
--- /dev/null
+++ b/CHANGES/12364.contrib.rst
@@ -0,0 +1 @@
+Disabled ``coverage`` and ``xdist`` by default to ease local development -- by :user:`Dreamsorcerer`.
diff --git a/setup.cfg b/setup.cfg
index 01e43450d95..62f68b11eab 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -41,9 +41,6 @@ exclude_lines =
[tool:pytest]
addopts =
- # `pytest-xdist`:
- --numprocesses=auto
-
# show 10 slowest invocations:
--durations=10
@@ -56,11 +53,6 @@ addopts =
# show values of the local vars in errors:
--showlocals
- # `pytest-cov`:
- -p pytest_cov
- --cov=aiohttp
- --cov=tests/
-
-m "not dev_mode and not autobahn and not internal"
filterwarnings =
error
From 3c04f9fc168c73c0363cb0cfe6b4e43f97e1cfa8 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 14 Apr 2026 00:07:05 +0100
Subject: [PATCH 013/191] [PR #12202/07bd8c1d backport][3.14] docs: clarify
params query canonicalization behavior (#12369)
**This is a backport of PR #12202 as merged into master
(07bd8c1d36d19e5ee38146d44607d758a81893b3).**
Co-authored-by: Dr Alex Mitre
---
docs/client_quickstart.rst | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/docs/client_quickstart.rst b/docs/client_quickstart.rst
index d00333c1e48..2c6b50941d2 100644
--- a/docs/client_quickstart.rst
+++ b/docs/client_quickstart.rst
@@ -119,8 +119,9 @@ that case you can specify multiple values for each key::
expect = 'http://httpbin.org/get?key=value2&key=value1'
assert str(r.url) == expect
-You can also pass :class:`str` content as param, but beware -- content
-is not encoded by library. Note that ``+`` is not encoded::
+You can also pass :class:`str` content as param. The value is used as a
+query string, but passing ``params`` does not disable URL
+canonicalization. Note that ``+`` is not encoded::
async with session.get('http://httpbin.org/get',
params='key=value+1') as r:
@@ -146,7 +147,9 @@ is not encoded by library. Note that ``+`` is not encoded::
.. warning::
- Passing *params* overrides ``encoded=True``, never use both options.
+ Passing *params* overrides ``encoded=True``. Never use both options
+ if you need to preserve exact query-string bytes.
+ Build the full URL (including query) instead.
Response Content and Status Code
================================
From 79f438dcbb1933e378d96d79ef65a1b32dab416b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 14 Apr 2026 11:07:06 +0000
Subject: [PATCH 014/191] Bump actions/cache from 5.0.4 to 5.0.5 (#12373)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [actions/cache](https://github.com/actions/cache) from 5.0.4 to
5.0.5.
Release notes
Sourced from actions/cache's
releases.
v5.0.5
What's Changed
Full Changelog: https://github.com/actions/cache/compare/v5...v5.0.5
Changelog
Sourced from actions/cache's
changelog.
Releases
How to prepare a release
[!NOTE]
Relevant for maintainers with write access only.
- Switch to a new branch from
main.
- Run
npm test to ensure all tests are passing.
- Update the version in
https://github.com/actions/cache/blob/main/package.json.
- Run
npm run build to update the compiled files.
- Update this
https://github.com/actions/cache/blob/main/RELEASES.md
with the new version and changes in the ## Changelog
section.
- Run
licensed cache to update the license report.
- Run
licensed status and resolve any warnings by
updating the https://github.com/actions/cache/blob/main/.licensed.yml
file with the exceptions.
- Commit your changes and push your branch upstream.
- Open a pull request against
main and get it reviewed
and merged.
- Draft a new release https://github.com/actions/cache/releases
use the same version number used in
package.json
- Create a new tag with the version number.
- Auto generate release notes and update them to match the changes you
made in
RELEASES.md.
- Toggle the set as the latest release option.
- Publish the release.
- Navigate to https://github.com/actions/cache/actions/workflows/release-new-action-version.yml
- There should be a workflow run queued with the same version
number.
- Approve the run to publish the new version and update the major tags
for this action.
Changelog
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/ci-cd.yml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index bb87dc5de62..f4c6b972cf7 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -71,7 +71,7 @@ jobs:
with:
python-version: 3.11
- name: Cache PyPI
- uses: actions/cache@v5.0.4
+ uses: actions/cache@v5.0.5
with:
key: pip-lint-${{ hashFiles('requirements/*.txt') }}
path: ~/.cache/pip
@@ -120,7 +120,7 @@ jobs:
with:
submodules: true
- name: Cache llhttp generated files
- uses: actions/cache@v5.0.4
+ uses: actions/cache@v5.0.5
id: cache
with:
key: llhttp-${{ hashFiles('vendor/llhttp/package*.json', 'vendor/llhttp/src/**/*') }}
@@ -184,7 +184,7 @@ jobs:
echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}"
shell: bash
- name: Cache PyPI
- uses: actions/cache@v5.0.4
+ uses: actions/cache@v5.0.5
with:
key: pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}-${{ hashFiles('requirements/*.txt') }}
path: ${{ steps.pip-cache.outputs.dir }}
@@ -296,7 +296,7 @@ jobs:
echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}"
shell: bash
- name: Cache PyPI
- uses: actions/cache@v5.0.4
+ uses: actions/cache@v5.0.5
with:
key: pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}-${{ hashFiles('requirements/*.txt') }}
path: ${{ steps.pip-cache.outputs.dir }}
From ceca3a194cbde8c5253cd28eb9460490b9d0165a Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 14 Apr 2026 13:36:00 +0100
Subject: [PATCH 015/191] [PR #12332/06e510b2 backport][3.14] Security: Silent
Exception Swallowing in Server Request Handler Factory (#12370)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
**This is a backport of PR #12332 as merged into master
(06e510b2be211f2a1e3a37ed7bea0a3b5cf2906e).**
Signed-off-by: Trần Bách <45133811+barttran2k@users.noreply.github.com>
Co-authored-by: Trần Bách <45133811+barttran2k@users.noreply.github.com>
Co-authored-by: Sam Bull
---
aiohttp/web_server.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/aiohttp/web_server.py b/aiohttp/web_server.py
index 778e6cf81c4..97cf2a60a91 100644
--- a/aiohttp/web_server.py
+++ b/aiohttp/web_server.py
@@ -81,4 +81,11 @@ def __call__(self) -> RequestHandler:
for k, v in self._kwargs.items()
if k in ["debug", "access_log_class"]
}
- return RequestHandler(self, loop=self._loop, **kwargs)
+ handler = RequestHandler(self, loop=self._loop, **kwargs)
+ handler.logger.warning(
+ "Failed to create request handler with custom kwargs %r, "
+ "falling back to filtered kwargs. This may indicate a "
+ "misconfiguration.",
+ self._kwargs,
+ )
+ return handler
From 5c17b642a0b918b99e8fc335ecc1e041eaeabab9 Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Wed, 15 Apr 2026 19:16:28 +0100
Subject: [PATCH 016/191] Benchmark tests for decompression optimisations
(#12358) (#12381)
(cherry picked from commit adf7799b26d400f0a1e9e28b9d5928792c86269f)
---
CHANGES/12358.misc.rst | 1 +
aiohttp/web_response.py | 12 ++------
tests/test_benchmarks_client.py | 30 ++++++++++++++++++++
tests/test_benchmarks_web_request.py | 42 ++++++++++++++++++++++++++++
tests/test_web_response.py | 20 -------------
5 files changed, 75 insertions(+), 30 deletions(-)
create mode 100644 CHANGES/12358.misc.rst
create mode 100644 tests/test_benchmarks_web_request.py
diff --git a/CHANGES/12358.misc.rst b/CHANGES/12358.misc.rst
new file mode 100644
index 00000000000..7b035023500
--- /dev/null
+++ b/CHANGES/12358.misc.rst
@@ -0,0 +1 @@
+Changed ``zlib_executor_size`` default so compressed payloads are async by default -- by :user:`Dreamsorcerer`.
diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py
index 91abbb4b61c..7a14ada4dbb 100644
--- a/aiohttp/web_response.py
+++ b/aiohttp/web_response.py
@@ -16,7 +16,7 @@
from . import hdrs, payload
from .abc import AbstractStreamWriter
-from .compression_utils import ZLibCompressor
+from .compression_utils import MAX_SYNC_CHUNK_SIZE, ZLibCompressor
from .helpers import (
ETAG_ANY,
QUOTED_ETAG_RE,
@@ -35,7 +35,6 @@
from .typedefs import JSONBytesEncoder, JSONEncoder, LooseHeaders
REASON_PHRASES = {http_status.value: http_status.phrase for http_status in HTTPStatus}
-LARGE_BODY_SIZE = 1024**2
__all__ = (
"ContentCoding",
@@ -665,7 +664,7 @@ def __init__(
headers: LooseHeaders | None = None,
content_type: str | None = None,
charset: str | None = None,
- zlib_executor_size: int | None = None,
+ zlib_executor_size: int = MAX_SYNC_CHUNK_SIZE,
zlib_executor: Executor | None = None,
) -> None:
if body is not None and text is not None:
@@ -846,13 +845,6 @@ async def _do_start_compression(self, coding: ContentCoding) -> None:
executor=self._zlib_executor,
)
assert self._body is not None
- if self._zlib_executor_size is None and len(self._body) > LARGE_BODY_SIZE:
- warnings.warn(
- "Synchronous compression of large response bodies "
- f"({len(self._body)} bytes) might block the async event loop. "
- "Consider providing a custom value to zlib_executor_size/"
- "zlib_executor response properties or disabling compression on it."
- )
self._compressed_body = (
await compressor.compress(self._body) + compressor.flush()
)
diff --git a/tests/test_benchmarks_client.py b/tests/test_benchmarks_client.py
index 8e75ef9c040..3c362a500a7 100644
--- a/tests/test_benchmarks_client.py
+++ b/tests/test_benchmarks_client.py
@@ -504,6 +504,36 @@ def _run() -> None:
loop.run_until_complete(run_client_benchmark())
+@pytest.mark.usefixtures("parametrize_zlib_backend")
+def test_ten_compressed_responses_iter_chunked_1mb(
+ loop: asyncio.AbstractEventLoop,
+ aiohttp_client: AiohttpClient,
+ benchmark: BenchmarkFixture,
+) -> None:
+ """Benchmark compressed GET request read via large iter_chunked."""
+ MB = 2**20
+ data = b"x" * 10 * MB
+
+ async def handler(request: web.Request) -> web.Response:
+ resp = web.Response(body=data)
+ resp.enable_compression()
+ return resp
+
+ app = web.Application()
+ app.router.add_route("GET", "/", handler)
+
+ async def run_client_benchmark() -> None:
+ client = await aiohttp_client(app)
+ resp = await client.get("/")
+ async for _ in resp.content.iter_chunked(MB):
+ pass
+ await client.close()
+
+ @benchmark
+ def _run() -> None:
+ loop.run_until_complete(run_client_benchmark())
+
+
def test_ten_streamed_responses_iter_chunks(
loop: asyncio.AbstractEventLoop,
aiohttp_client: AiohttpClient,
diff --git a/tests/test_benchmarks_web_request.py b/tests/test_benchmarks_web_request.py
new file mode 100644
index 00000000000..81afe7824e4
--- /dev/null
+++ b/tests/test_benchmarks_web_request.py
@@ -0,0 +1,42 @@
+"""codspeed benchmarks for web request reading."""
+
+import asyncio
+import zlib
+
+import pytest
+from pytest_codspeed import BenchmarkFixture
+
+from aiohttp import web
+from aiohttp.pytest_plugin import AiohttpClient
+
+
+@pytest.mark.usefixtures("parametrize_zlib_backend")
+def test_read_compressed_post_body(
+ loop: asyncio.AbstractEventLoop,
+ aiohttp_client: AiohttpClient,
+ benchmark: BenchmarkFixture,
+) -> None:
+ """Benchmark server Request.read() with a compressed POST body."""
+ original = b"B" * (5 * 2**20)
+ compressed = zlib.compress(original)
+
+ async def handler(request: web.Request) -> web.Response:
+ body = await request.read()
+ return web.Response(text=str(len(body)))
+
+ app = web.Application(client_max_size=10 * 2**20)
+ app.router.add_post("/", handler)
+
+ async def run_benchmark() -> None:
+ client = await aiohttp_client(app)
+ resp = await client.post(
+ "/",
+ data=compressed,
+ headers={"Content-Encoding": "deflate"},
+ )
+ assert int(await resp.read()) == len(original)
+ await client.close()
+
+ @benchmark
+ def _run() -> None:
+ loop.run_until_complete(run_benchmark())
diff --git a/tests/test_web_response.py b/tests/test_web_response.py
index 799d34ce063..5299f1c8a3d 100644
--- a/tests/test_web_response.py
+++ b/tests/test_web_response.py
@@ -584,26 +584,6 @@ async def test_force_compression_deflate() -> None:
assert "deflate" == resp.headers.get(hdrs.CONTENT_ENCODING)
-@pytest.mark.usefixtures("parametrize_zlib_backend")
-async def test_force_compression_deflate_large_payload() -> None:
- """Make sure a warning is thrown for large payloads compressed in the event loop."""
- req = make_request(
- "GET", "/", headers=CIMultiDict({hdrs.ACCEPT_ENCODING: "gzip, deflate"})
- )
- resp = Response(body=b"large")
-
- resp.enable_compression(ContentCoding.deflate)
- assert resp.compression
-
- with (
- pytest.warns(Warning, match="Synchronous compression of large response bodies"),
- mock.patch("aiohttp.web_response.LARGE_BODY_SIZE", 2),
- ):
- msg = await resp.prepare(req)
- assert msg is not None
- assert "deflate" == resp.headers.get(hdrs.CONTENT_ENCODING)
-
-
@pytest.mark.usefixtures("parametrize_zlib_backend")
async def test_force_compression_no_accept_deflate() -> None:
req = make_request("GET", "/")
From 3c56715def42e13f8783a585414e44337291b6f7 Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Sat, 18 Apr 2026 15:38:10 +0100
Subject: [PATCH 017/191] Optimise decompression size (#12357) (#12391)
(cherry picked from commit 53f6e919dbae7b65627018f015014e0fe70b4d92)
---
aiohttp/compression_utils.py | 10 ++++-
aiohttp/http_parser.py | 9 +++--
aiohttp/streams.py | 63 +++++++++++++++++--------------
aiohttp/web_request.py | 4 ++
tests/test_flowcontrol_streams.py | 2 +-
5 files changed, 53 insertions(+), 35 deletions(-)
diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py
index c836b6a3da4..a30894eb0b7 100644
--- a/aiohttp/compression_utils.py
+++ b/aiohttp/compression_utils.py
@@ -326,9 +326,15 @@ def decompress_sync(
) -> bytes:
"""Decompress the given data."""
if hasattr(self._obj, "decompress"):
- result = cast(bytes, self._obj.decompress(data, max_length))
+ if max_length == ZLIB_MAX_LENGTH_UNLIMITED:
+ result = cast(bytes, self._obj.decompress(data))
+ else:
+ result = cast(bytes, self._obj.decompress(data, max_length))
else:
- result = cast(bytes, self._obj.process(data, max_length))
+ if max_length == ZLIB_MAX_LENGTH_UNLIMITED:
+ result = cast(bytes, self._obj.process(data))
+ else:
+ result = cast(bytes, self._obj.process(data, max_length))
# Only way to know that brotli has no further data is checking we get no output
self._last_empty = result == b""
return result
diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py
index f3ea42f6e3f..9e229cc4459 100644
--- a/aiohttp/http_parser.py
+++ b/aiohttp/http_parser.py
@@ -2,6 +2,7 @@
import asyncio
import re
import string
+import sys
from contextlib import suppress
from enum import IntEnum
from re import Pattern
@@ -1130,10 +1131,12 @@ def feed_data(self, chunk: bytes, size: int) -> bool:
encoding=self.encoding, suppress_deflate_header=True
)
+ low_water = self.out._low_water
+ max_length = (
+ 0 if low_water >= sys.maxsize else max(self._max_decompress_size, low_water)
+ )
try:
- chunk = self.decompressor.decompress_sync(
- chunk, max_length=self._max_decompress_size
- )
+ chunk = self.decompressor.decompress_sync(chunk, max_length=max_length)
except Exception:
raise ContentEncodingError(
"Can not decode content-encoding: %s" % self.encoding
diff --git a/aiohttp/streams.py b/aiohttp/streams.py
index 42b427ee0f0..196469c005a 100644
--- a/aiohttp/streams.py
+++ b/aiohttp/streams.py
@@ -1,5 +1,6 @@
import asyncio
import collections
+import sys
import warnings
from collections.abc import Awaitable, Callable
from typing import Final, Generic, TypeVar
@@ -67,31 +68,7 @@ async def __anext__(self) -> tuple[bytes, bool]:
return rv
-class AsyncStreamReaderMixin:
-
- __slots__ = ()
-
- def __aiter__(self) -> AsyncStreamIterator[bytes]:
- return AsyncStreamIterator(self.readline) # type: ignore[attr-defined]
-
- def iter_chunked(self, n: int) -> AsyncStreamIterator[bytes]:
- """Returns an asynchronous iterator that yields chunks of size n."""
- return AsyncStreamIterator(lambda: self.read(n)) # type: ignore[attr-defined]
-
- def iter_any(self) -> AsyncStreamIterator[bytes]:
- """Yield all available data as soon as it is received."""
- return AsyncStreamIterator(self.readany) # type: ignore[attr-defined]
-
- def iter_chunks(self) -> ChunkTupleAsyncStreamIterator:
- """Yield chunks of data as they are received by the server.
-
- The yielded objects are tuples
- of (bytes, bool) as returned by the StreamReader.readchunk method.
- """
- return ChunkTupleAsyncStreamIterator(self) # type: ignore[arg-type]
-
-
-class StreamReader(AsyncStreamReaderMixin):
+class StreamReader:
"""An enhancement of asyncio.StreamReader.
Supports asynchronous iteration by line, chunk or as available::
@@ -176,9 +153,35 @@ def __repr__(self) -> str:
info.append("e=%r" % self._exception)
return "<%s>" % " ".join(info)
+ def __aiter__(self) -> AsyncStreamIterator[bytes]:
+ return AsyncStreamIterator(self.readline)
+
+ def iter_chunked(self, n: int) -> AsyncStreamIterator[bytes]:
+ """Returns an asynchronous iterator that yields chunks of size n."""
+ self.set_read_chunk_size(n)
+ return AsyncStreamIterator(lambda: self.read(n))
+
+ def iter_any(self) -> AsyncStreamIterator[bytes]:
+ """Yield all available data as soon as it is received."""
+ return AsyncStreamIterator(self.readany)
+
+ def iter_chunks(self) -> ChunkTupleAsyncStreamIterator:
+ """Yield chunks of data as they are received by the server.
+
+ The yielded objects are tuples
+ of (bytes, bool) as returned by the StreamReader.readchunk method.
+ """
+ return ChunkTupleAsyncStreamIterator(self)
+
def get_read_buffer_limits(self) -> tuple[int, int]:
return (self._low_water, self._high_water)
+ def set_read_chunk_size(self, n: int) -> None:
+ """Raise buffer limits to match the consumer's chunk size."""
+ if n > self._low_water:
+ self._low_water = n
+ self._high_water = n * 2
+
def exception(self) -> BaseException | None:
return self._exception
@@ -427,10 +430,8 @@ async def read(self, n: int = -1) -> bytes:
return b""
if n < 0:
- # This used to just loop creating a new waiter hoping to
- # collect everything in self._buffer, but that would
- # deadlock if the subprocess sends more than self.limit
- # bytes. So just call self.readany() until EOF.
+ # Reading everything — remove decompression chunk limit.
+ self.set_read_chunk_size(sys.maxsize)
blocks = []
while True:
block = await self.readany()
@@ -439,6 +440,7 @@ async def read(self, n: int = -1) -> bytes:
blocks.append(block)
return b"".join(blocks)
+ self.set_read_chunk_size(n)
# TODO: should be `if` instead of `while`
# because waiter maybe triggered on chunk end,
# without feeding any data
@@ -612,6 +614,9 @@ async def wait_eof(self) -> None:
def feed_data(self, data: bytes, n: int = 0) -> bool:
return False
+ def set_read_chunk_size(self, n: int) -> None:
+ return
+
async def readline(self, *, max_line_length: int | None = None) -> bytes:
return b""
diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py
index 47d50a12d5c..25d7ebc8e22 100644
--- a/aiohttp/web_request.py
+++ b/aiohttp/web_request.py
@@ -678,6 +678,10 @@ async def read(self) -> bytes:
Returns bytes object with full request content.
"""
if self._read_bytes is None:
+ # Raise the buffer limits so compressed payloads decompress in
+ # larger chunks instead of many small pause/resume cycles.
+ if self._client_max_size:
+ self._payload.set_read_chunk_size(self._client_max_size)
body = bytearray()
while True:
chunk = await self._payload.readany()
diff --git a/tests/test_flowcontrol_streams.py b/tests/test_flowcontrol_streams.py
index e71107b0e00..03b6eec44f5 100644
--- a/tests/test_flowcontrol_streams.py
+++ b/tests/test_flowcontrol_streams.py
@@ -82,7 +82,7 @@ async def test_readexactly(self, stream) -> None:
stream.feed_data(b"data", 4)
res = await stream.readexactly(3)
assert res == b"dat"
- assert not stream._protocol.resume_reading.called
+ assert stream._protocol.resume_reading.called
async def test_feed_data(self, stream) -> None:
stream._protocol._reading_paused = False
From 6d81b02276d4caf21608ab16e3f15ef2eb4dde32 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Mon, 20 Apr 2026 09:34:53 +0200
Subject: [PATCH 018/191] [PR #12394/24ed3b34 backport][3.14] docs: mention
fake server testing pattern (#12396)
**This is a backport of PR #12394 as merged into master
(24ed3b343f4673b54b7b5a95bb441e1f7f93120b).**
Fixes #980.
Co-authored-by: nightcityblade
---
docs/testing.rst | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/docs/testing.rst b/docs/testing.rst
index a7b93e714f6..15437f38134 100644
--- a/docs/testing.rst
+++ b/docs/testing.rst
@@ -490,6 +490,12 @@ basis, the TestClient object can be used directly::
A full list of the utilities provided can be found at the
:mod:`api reference `
+For end-to-end client code that talks to an external service, it is
+recommended to run a small fake server rather than patching private aiohttp
+internals. The ``examples/fake_server.py`` demo shows such an approach: start
+a local :class:`~aiohttp.web.Application`, point a custom resolver at it, and
+exercise the client against that controlled endpoint.
+
Testing API Reference
---------------------
From 5d74702751484f849c41354ce0f501be5978bed0 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Mon, 20 Apr 2026 15:27:40 +0100
Subject: [PATCH 019/191] [PR #12350/7eb05774 backport][3.13] Fix octal tests
(#12401)
**This is a backport of PR #12350 as merged into master
(7eb057747aa8ac6b0031fd032b8e2bda53d98b53).**
Co-authored-by: Sam Bull
---
tests/test_cookie_helpers.py | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/tests/test_cookie_helpers.py b/tests/test_cookie_helpers.py
index 767e1eaa34a..fead869d6f3 100644
--- a/tests/test_cookie_helpers.py
+++ b/tests/test_cookie_helpers.py
@@ -1095,13 +1095,13 @@ def test_parse_set_cookie_headers_date_formats_with_attributes() -> None:
@pytest.mark.parametrize(
("header", "expected_name", "expected_value", "expected_coded"),
[
- # Test cookie values with octal escape sequences
- (r'name="\012newline\012"', "name", "\nnewline\n", r'"\012newline\012"'),
+ # Test cookie values with octal escape sequences (printable chars only)
+ (r'name="\050parens\051"', "name", "(parens)", r'"\050parens\051"'),
(
- r'tab="\011separated\011values"',
- "tab",
- "\tseparated\tvalues",
- r'"\011separated\011values"',
+ r'punct="\053plus\053values"',
+ "punct",
+ "+plus+values",
+ r'"\053plus\053values"',
),
(
r'mixed="hello\040world\041"',
@@ -1110,10 +1110,10 @@ def test_parse_set_cookie_headers_date_formats_with_attributes() -> None:
r'"hello\040world\041"',
),
(
- r'complex="\042quoted\042 text with \012 newline"',
+ r'complex="\042quoted\042 text with \055 hyphen"',
"complex",
- '"quoted" text with \n newline',
- r'"\042quoted\042 text with \012 newline"',
+ '"quoted" text with - hyphen',
+ r'"\042quoted\042 text with \055 hyphen"',
),
],
)
From 4f5858566f30aa5b17c4d3eaed956d3455bfa5bb Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 21 Apr 2026 20:02:56 +0100
Subject: [PATCH 020/191] [PR #12406/b0601d64 backport][3.14] Avoid
installation of backports.zstd on Python 3.14 in linting deps (#12407)
**This is a backport of PR #12406 as merged into master
(b0601d64c65b1f7ac4823e3c6ef630bc4e0b8890).**
Co-authored-by: Michael Seifert
---
CHANGES/12406.contrib.rst | 2 ++
requirements/lint.in | 2 +-
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/12406.contrib.rst
diff --git a/CHANGES/12406.contrib.rst b/CHANGES/12406.contrib.rst
new file mode 100644
index 00000000000..9bcbee91e09
--- /dev/null
+++ b/CHANGES/12406.contrib.rst
@@ -0,0 +1,2 @@
+Avoid installation of backports.zstd on Python 3.14 in linting dependency set
+-- by :user:`seifertm`.
diff --git a/requirements/lint.in b/requirements/lint.in
index 5bfd3c31c65..97ee198b9c9 100644
--- a/requirements/lint.in
+++ b/requirements/lint.in
@@ -1,5 +1,5 @@
aiodns
-backports.zstd; implementation_name == "cpython"
+backports.zstd; implementation_name == "cpython" and python_version < "3.14"
blockbuster
freezegun
isal
From d7193863355710592420f89cfcc3342861f957a7 Mon Sep 17 00:00:00 2001
From: jmestwa-coder
Date: Mon, 4 May 2026 05:19:52 +0530
Subject: [PATCH 021/191] [3.14] Validate Content-Length format in
ClientRequest (backport of #12385) (#12445)
---
aiohttp/client_reqrep.py | 10 ++++------
tests/test_client_request.py | 19 ++++++++++++++++++-
2 files changed, 22 insertions(+), 7 deletions(-)
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
index d8dd4971e4e..aa0d48159c0 100644
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -87,6 +87,7 @@
_CONNECTION_CLOSED_EXCEPTION = ClientConnectionError("Connection closed")
_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
json_re = re.compile(r"^application/(?:[\w.+-]+?\+)?json")
+_DIGITS_RE = re.compile(r"\d+", re.ASCII)
def _gen_default_accept_encoding() -> str:
@@ -903,12 +904,9 @@ def _get_content_length(self) -> int | None:
return None
content_length_hdr = self.headers[hdrs.CONTENT_LENGTH]
- try:
- return int(content_length_hdr)
- except ValueError:
- raise ValueError(
- f"Invalid Content-Length header: {content_length_hdr}"
- ) from None
+ if not _DIGITS_RE.fullmatch(content_length_hdr):
+ raise ValueError(f"Invalid Content-Length header: {content_length_hdr!r}")
+ return int(content_length_hdr)
@property
def skip_auto_headers(self) -> CIMultiDict[None]:
diff --git a/tests/test_client_request.py b/tests/test_client_request.py
index 3d5898abc0d..5e23f085d45 100644
--- a/tests/test_client_request.py
+++ b/tests/test_client_request.py
@@ -1667,7 +1667,24 @@ def test_get_content_length(make_request: _RequestMaker) -> None:
# Invalid Content-Length header
req.headers["Content-Length"] = "invalid"
- with pytest.raises(ValueError, match="Invalid Content-Length header: invalid"):
+ with pytest.raises(ValueError, match="Invalid Content-Length header"):
+ req._get_content_length()
+
+
+async def test_get_content_length_invalid_formats(
+ make_request: _RequestMaker,
+) -> None:
+ req = make_request("GET", URL("http://python.org/"))
+
+ req.headers["Content-Length"] = "100"
+ assert req._get_content_length() == 100
+
+ req.headers["Content-Length"] = "-100"
+ with pytest.raises(ValueError, match="Invalid Content-Length header"):
+ req._get_content_length()
+
+ req.headers["Content-Length"] = "५" # Devengali number 5
+ with pytest.raises(ValueError, match="Invalid Content-Length header"):
req._get_content_length()
From 422566982274d0b5b8679c8f2c5d28630c2b803b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 00:57:51 +0000
Subject: [PATCH 022/191] Bump softprops/action-gh-release from 2 to 3 (#12362)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[softprops/action-gh-release](https://github.com/softprops/action-gh-release)
from 2 to 3.
Release notes
Sourced from softprops/action-gh-release's
releases.
v3.0.0
3.0.0 is a major release that moves the action runtime
from Node 20 to Node 24.
Use v3 on GitHub-hosted runners and self-hosted fleets that
already support the
Node 24 Actions runtime. If you still need the last Node 20-compatible
line, stay on
v2.6.2.
What's Changed
Other Changes 🔄
- Move the action runtime and bundle target to Node 24
- Update
@types/node to the Node 24 line and allow future
Dependabot updates
- Keep the floating major tag on
v3; v2
remains pinned to the latest 2.x release
v2.6.2
What's Changed
Other Changes 🔄
Full Changelog: https://github.com/softprops/action-gh-release/compare/v2...v2.6.2
v2.6.1
2.6.1 is a patch release focused on restoring linked
discussion thread creation when
discussion_category_name is set. It fixes
[#764](https://github.com/softprops/action-gh-release/issues/764),
where the draft-first publish flow
stopped carrying the discussion category through the final publish
step.
If you still hit an issue after upgrading, please open a report with
the bug template and include a minimal repro or sanitized workflow
snippet where possible.
What's Changed
Bug fixes 🐛
v2.6.0
2.6.0 is a minor release centered on
previous_tag support for
generate_release_notes,
which lets workflows pin GitHub's comparison base explicitly instead of
relying on the default range.
It also includes the recent concurrent asset upload recovery fix, a
working_directory docs sync,
a checked-bundle freshness guard for maintainers, and clearer
immutable-prerelease guidance where
GitHub platform behavior imposes constraints on how prerelease asset
uploads can be published.
If you still hit an issue after upgrading, please open a report with
the bug template and include a minimal repro or sanitized workflow
snippet where possible.
What's Changed
... (truncated)
Changelog
Sourced from softprops/action-gh-release's
changelog.
0.1.13
- fix issue with multiple runs concatenating release bodies #145
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sam Bull
---
.github/workflows/ci-cd.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index f4c6b972cf7..9573decd95e 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -692,7 +692,7 @@ jobs:
# Confusingly, this action also supports updating releases, not
# just creating them. This is what we want here, since we've manually
# created the release above.
- uses: softprops/action-gh-release@v2
+ uses: softprops/action-gh-release@v3
with:
# dist/ contains the built packages, which smoketest-artifacts/
# contains the signatures and certificates.
From 299693b485b98b9a14ee4ddf57a1b10b59fdc0f9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 02:37:14 +0100
Subject: [PATCH 023/191] Bump actions/upload-artifact from 6 to 7 (#12156)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[actions/upload-artifact](https://github.com/actions/upload-artifact)
from 6 to 7.
Release notes
Sourced from actions/upload-artifact's
releases.
v7.0.0
v7 What's new
Direct Uploads
Adds support for uploading single files directly (unzipped). Callers
can set the new archive parameter to false to
skip zipping the file during upload. Right now, we only support single
files. The action will fail if the glob passed resolves to multiple
files. The name parameter is also ignored with this
setting. Instead, the name of the artifact will be the name of the
uploaded file.
ESM
To support new versions of the @actions/* packages,
we've upgraded the package to ESM.
What's Changed
New Contributors
Full Changelog: https://github.com/actions/upload-artifact/compare/v6...v7.0.0
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
You can trigger a rebase of this PR by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/ci-cd.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 9573decd95e..52b4fac1e02 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -135,7 +135,7 @@ jobs:
run: |
make generate-llhttp
- name: Upload llhttp generated files
- uses: actions/upload-artifact@v6
+ uses: actions/upload-artifact@v7
with:
name: llhttp
path: vendor/llhttp/build
@@ -527,7 +527,7 @@ jobs:
run: |
python -m build --sdist
- name: Upload artifacts
- uses: actions/upload-artifact@v6
+ uses: actions/upload-artifact@v7
with:
name: dist-sdist
path: dist
@@ -619,7 +619,7 @@ jobs:
CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }}
CIBW_ARCHS_MACOS: x86_64 arm64 universal2
- name: Upload wheels
- uses: actions/upload-artifact@v6
+ uses: actions/upload-artifact@v7
with:
name: >-
dist-${{ matrix.os }}-${{ matrix.musl }}-${{
From c2fdd7395f37fd81161dff5936d26533a674808e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 02:45:29 +0000
Subject: [PATCH 024/191] Bump cryptography from 46.0.4 to 46.0.5 (#12056)
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.4
to 46.0.5.
Changelog
Sourced from cryptography's
changelog.
46.0.5 - 2026-02-10
* An attacker could create a malicious public key that reveals portions
of your
private key when using certain uncommon elliptic curves (binary curves).
This version now includes additional security checks to prevent this
attack.
This issue only affects binary elliptic curves, which are rarely used in
real-world applications. Credit to **XlabAI Team of Tencent Xuanwu Lab
and
Atuin Automated Vulnerability Discovery Engine** for reporting the
issue.
**CVE-2026-26007**
* Support for ``SECT*`` binary elliptic curves is deprecated and will be
removed in the next release.
.. v46-0-4:
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
You can trigger a rebase of this PR by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sam Bull
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 0bc7d27d78f..5cc3b3d7b58 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -59,7 +59,7 @@ coverage==7.13.4
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==46.0.4
+cryptography==46.0.5
# via trustme
cython==3.2.4
# via -r requirements/cython.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 7d67f91b6f2..14bbbc746dd 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -59,7 +59,7 @@ coverage==7.13.4
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==46.0.4
+cryptography==46.0.5
# via trustme
distlib==0.4.0
# via virtualenv
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 9cea924d820..1db16f4ccd1 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -23,7 +23,7 @@ cfgv==3.5.0
# via pre-commit
click==8.3.1
# via slotscheck
-cryptography==46.0.4
+cryptography==46.0.5
# via trustme
distlib==0.4.0
# via virtualenv
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 93ee412a37c..c34601a5ebd 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -18,7 +18,7 @@ coverage==7.13.4
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==46.0.4
+cryptography==46.0.5
# via trustme
exceptiongroup==1.3.1
# via pytest
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 115d8891785..10e66451c36 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -33,7 +33,7 @@ coverage==7.13.4
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==46.0.4
+cryptography==46.0.5
# via trustme
exceptiongroup==1.3.1
# via pytest
diff --git a/requirements/test.txt b/requirements/test.txt
index f9603ae4a09..6fa162e1ea7 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -33,7 +33,7 @@ coverage==7.13.4
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==46.0.4
+cryptography==46.0.5
# via trustme
exceptiongroup==1.3.1
# via pytest
From f1dfd44d8be450cba8e85e82cb4df80dd280ec32 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 02:46:17 +0000
Subject: [PATCH 025/191] Bump imagesize from 1.4.1 to 1.5.0 (#12191)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [imagesize](https://github.com/shibukawa/imagesize_py) from 1.4.1
to 1.5.0.
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
You can trigger a rebase of this PR by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sam Bull
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 5cc3b3d7b58..a807f9cae83 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -94,7 +94,7 @@ idna==3.11
# requests
# trustme
# yarl
-imagesize==1.4.1
+imagesize==1.5.0
# via sphinx
iniconfig==2.3.0
# via pytest
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 14bbbc746dd..1184c9144fa 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -92,7 +92,7 @@ idna==3.11
# requests
# trustme
# yarl
-imagesize==1.4.1
+imagesize==1.5.0
# via sphinx
iniconfig==2.3.0
# via pytest
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index de53158c046..aef9eb6fedc 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -20,7 +20,7 @@ docutils==0.21.2
# via sphinx
idna==3.11
# via requests
-imagesize==1.4.1
+imagesize==1.5.0
# via sphinx
jinja2==3.1.6
# via
diff --git a/requirements/doc.txt b/requirements/doc.txt
index b2b5697d659..c3214743726 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -20,7 +20,7 @@ docutils==0.21.2
# via sphinx
idna==3.11
# via requests
-imagesize==1.4.1
+imagesize==1.5.0
# via sphinx
jinja2==3.1.6
# via
From 09d2f03db1ed82033e3b1ca4c7686301da44d7c5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 02:51:57 +0000
Subject: [PATCH 026/191] Bump actions/github-script from 8 to 9 (#12348)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [actions/github-script](https://github.com/actions/github-script)
from 8 to 9.
Release notes
Sourced from actions/github-script's
releases.
v9.0.0
New features:
getOctokit factory function —
Available directly in the script context. Create additional
authenticated Octokit clients with different tokens for multi-token
workflows, GitHub App tokens, and cross-org access. See Creating
additional clients with getOctokit for details and
examples.
- Orchestration ID in user-agent — The
ACTIONS_ORCHESTRATION_ID environment variable is
automatically appended to the user-agent string for request
tracing.
Breaking changes:
require('@actions/github') no longer works in
scripts. The upgrade to @actions/github v9
(ESM-only) means require('@actions/github') will fail at
runtime. If you previously used patterns like const { getOctokit }
= require('@actions/github') to create secondary clients, use the
new injected getOctokit function instead — it's available
directly in the script context with no imports needed.
getOctokit is now an injected function parameter.
Scripts that declare const getOctokit = ... or let
getOctokit = ... will get a SyntaxError because
JavaScript does not allow const/let
redeclaration of function parameters. Use the injected
getOctokit directly, or use var getOctokit =
... if you need to redeclare it.
- If your script accesses other
@actions/github internals
beyond the standard github/octokit client, you
may need to update those references for v9 compatibility.
What's Changed
New Contributors
Full Changelog: https://github.com/actions/github-script/compare/v8.0.0...v9.0.0
Commits
3a2844b
Merge pull request #700
from actions/salmanmkc/expose-getoctokit + prepare re...
ca10bbd
fix: use @octokit/core/types import for v7
compatibility
86e48e2
merge: incorporate main branch changes
c108472
chore: rebuild dist for v9 upgrade and getOctokit factory
afff112
Merge pull request #712
from actions/salmanmkc/deployment-false + fix user-ag...
ff8117e
ci: fix user-agent test to handle orchestration ID
81c6b78
ci: use deployment: false to suppress deployment noise from integration
tests
3953caf
docs: update README examples from @v8 to @v9, add getOctokit docs
and v9 brea...
c17d55b
ci: add getOctokit integration test job
a047196
test: add getOctokit integration tests via callAsyncFunction
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sam Bull
---
.github/workflows/labels.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml
index e3f10214082..cb8e99f572d 100644
--- a/.github/workflows/labels.yml
+++ b/.github/workflows/labels.yml
@@ -11,7 +11,7 @@ jobs:
name: Backport label added
if: ${{ github.event.pull_request.user.type != 'Bot' }}
steps:
- - uses: actions/github-script@v8
+ - uses: actions/github-script@v9
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
From 65bd5bcbbee14c67fdd86a1714a841c8f186130e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 02:53:34 +0000
Subject: [PATCH 027/191] Bump multidict from 6.7.0 to 6.7.1 (#12001)
Bumps [multidict](https://github.com/aio-libs/multidict) from 6.7.0 to
6.7.1.
Release notes
Sourced from multidict's
releases.
6.7.1
Bug fixes
Changelog
Sourced from multidict's
changelog.
6.7.1
(2026-01-25)
Bug fixes
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
You can trigger a rebase of this PR by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sam Bull
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/cython.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/multidict.txt | 2 +-
requirements/runtime-deps.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
9 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 1048f618136..3598283c782 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -28,7 +28,7 @@ gunicorn==25.1.0
# via -r requirements/base-ft.in
idna==3.11
# via yarl
-multidict==6.7.0
+multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
diff --git a/requirements/base.txt b/requirements/base.txt
index 4c8df4dd20a..f27766b8194 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -28,7 +28,7 @@ gunicorn==25.1.0
# via -r requirements/base.in
idna==3.11
# via yarl
-multidict==6.7.0
+multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index a807f9cae83..c2d5184bb24 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -114,7 +114,7 @@ markupsafe==3.0.3
# via jinja2
mdurl==0.1.2
# via markdown-it-py
-multidict==6.7.0
+multidict==6.7.1
# via
# -r requirements/multidict.in
# -r requirements/runtime-deps.in
diff --git a/requirements/cython.txt b/requirements/cython.txt
index 03fa291992c..193a2299871 100644
--- a/requirements/cython.txt
+++ b/requirements/cython.txt
@@ -6,7 +6,7 @@
#
cython==3.2.4
# via -r requirements/cython.in
-multidict==6.7.0
+multidict==6.7.1
# via -r requirements/multidict.in
typing-extensions==4.15.0
# via multidict
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 1184c9144fa..76e048db44f 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -112,7 +112,7 @@ markupsafe==3.0.3
# via jinja2
mdurl==0.1.2
# via markdown-it-py
-multidict==6.7.0
+multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
diff --git a/requirements/multidict.txt b/requirements/multidict.txt
index 39d1c2a3c8e..144a7966d26 100644
--- a/requirements/multidict.txt
+++ b/requirements/multidict.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/multidict.txt --resolver=backtracking --strip-extras requirements/multidict.in
#
-multidict==6.7.0
+multidict==6.7.1
# via -r requirements/multidict.in
typing-extensions==4.15.0
# via multidict
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index 6d38a801a10..f3a3df711ab 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -26,7 +26,7 @@ frozenlist==1.8.0
# aiosignal
idna==3.11
# via yarl
-multidict==6.7.0
+multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 10e66451c36..b61eec5f04a 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -63,7 +63,7 @@ markdown-it-py==4.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-multidict==6.7.0
+multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
diff --git a/requirements/test.txt b/requirements/test.txt
index 6fa162e1ea7..62cc042c3ba 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -63,7 +63,7 @@ markdown-it-py==4.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-multidict==6.7.0
+multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
From ae0bdcc34b8b58792d54b6199ee2da9c91d6717b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 13:18:56 +0000
Subject: [PATCH 028/191] Bump dependabot/fetch-metadata from 2 to 3 (#12344)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[dependabot/fetch-metadata](https://github.com/dependabot/fetch-metadata)
from 2 to 3.
Release notes
Sourced from dependabot/fetch-metadata's
releases.
v3.0.0
The breaking change is requiring Node.js version v24 as the Actions
runtime.
What's Changed
New Contributors
Full Changelog: https://github.com/dependabot/fetch-metadata/compare/v2...v3.0.0
v2.5.0
What's Changed
... (truncated)
Commits
ffa630c
v3.0.0 (#686)
ec8fff2
Merge pull request #674
from dependabot/dependabot/npm_and_yarn/picomatch-2.3.2
caf48bd
build(deps-dev): bump picomatch from 2.3.1 to 2.3.2
13d8274
Upgrade @actions/github to ^9.0.0 and
@octokit/request-error to ^7.1.0 (#678)
b603099
Upgrade @actions/core from ^1.11.1 to ^3.0.0 (#677)
c5dc5b1
Enable noImplicitAny in tsconfig.json (#684)
a183f3c
Add typecheck step to CI (#685)
5e17564
Remove skipLibCheck from tsconfig.json (#683)
bb56eeb
Switch tsconfig module resolution to bundler (#682)
3632e3d
Remove vestigial outDir from tsconfig.json (#681)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sam Bull
---
.github/workflows/auto-merge.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml
index 56575750fe1..aa1b7c6890c 100644
--- a/.github/workflows/auto-merge.yml
+++ b/.github/workflows/auto-merge.yml
@@ -12,7 +12,7 @@ jobs:
steps:
- name: Dependabot metadata
id: metadata
- uses: dependabot/fetch-metadata@v2
+ uses: dependabot/fetch-metadata@v3
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs
From e23e25e1b05d5cc87da61c19056704a478d8b894 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 13:53:28 +0000
Subject: [PATCH 029/191] Bump librt from 0.8.1 to 0.9.0 (#12450)
Bumps [librt](https://github.com/mypyc/librt) from 0.8.1 to 0.9.0.
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 4 ++--
requirements/dev.txt | 2 +-
requirements/lint.txt | 4 ++--
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index c2d5184bb24..3d47fe07dde 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -26,7 +26,7 @@ attrs==25.4.0
# via -r requirements/runtime-deps.in
babel==2.18.0
# via sphinx
-backports-zstd==1.3.0 ; implementation_name == "cpython"
+backports-zstd==1.3.0 ; implementation_name == "cpython" and python_version < "3.14"
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
@@ -106,7 +106,7 @@ jinja2==3.1.6
# via
# sphinx
# towncrier
-librt==0.8.1
+librt==0.9.0
# via mypy
markdown-it-py==4.0.0
# via rich
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 76e048db44f..e021873638c 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -104,7 +104,7 @@ jinja2==3.1.6
# via
# sphinx
# towncrier
-librt==0.8.1
+librt==0.9.0
# via mypy
markdown-it-py==4.0.0
# via rich
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 1db16f4ccd1..77209a1d445 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -10,7 +10,7 @@ annotated-types==0.7.0
# via pydantic
async-timeout==5.0.1
# via valkey
-backports-zstd==1.3.0 ; implementation_name == "cpython"
+backports-zstd==1.3.0 ; implementation_name == "cpython" and python_version < "3.14"
# via -r requirements/lint.in
blockbuster==1.5.26
# via -r requirements/lint.in
@@ -45,7 +45,7 @@ iniconfig==2.3.0
# via pytest
isal==1.7.2
# via -r requirements/lint.in
-librt==0.8.1
+librt==0.9.0
# via mypy
markdown-it-py==4.0.0
# via rich
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index c34601a5ebd..35bb72c8c73 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -34,7 +34,7 @@ iniconfig==2.3.0
# via pytest
isal==1.8.0 ; python_version < "3.14"
# via -r requirements/test-common.in
-librt==0.8.1
+librt==0.9.0
# via mypy
markdown-it-py==4.0.0
# via rich
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index b61eec5f04a..f38961394dc 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -57,7 +57,7 @@ iniconfig==2.3.0
# via pytest
isal==1.8.0 ; python_version < "3.14"
# via -r requirements/test-common.in
-librt==0.8.1
+librt==0.9.0
# via mypy
markdown-it-py==4.0.0
# via rich
diff --git a/requirements/test.txt b/requirements/test.txt
index 62cc042c3ba..9a1a0470ef0 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -57,7 +57,7 @@ iniconfig==2.3.0
# via pytest
isal==1.7.2 ; python_version < "3.14"
# via -r requirements/test-common.in
-librt==0.8.1
+librt==0.9.0
# via mypy
markdown-it-py==4.0.0
# via rich
From e2d6ac0baeb8280092593d9e8e8b73b2538cd41c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 14:47:57 +0000
Subject: [PATCH 030/191] Bump regex from 2026.2.19 to 2026.2.28 (#12177)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [regex](https://github.com/mrabarnett/mrab-regex) from 2026.2.19
to 2026.2.28.
Changelog
Sourced from regex's
changelog.
Version: 2026.2.28
Replaced atomic operations with mutex on pattern object for
free-threaded Python.
Version: 2026.2.26
PR
[#598](https://github.com/mrabarnett/mrab-regex/issues/598): Fix race
condition in storage caching with atomic operations.
Replaced use of PyUnicode_GET_LENGTH with PyUnicode_GetLength.
Version: 2026.2.19
Added \z as alias of \Z, like in re module.
Added prefixmatch as alias of match, like in re module.
Version: 2026.1.15
Re-uploaded.
Version: 2026.1.14
Git issue 596: Specifying {e<=0} causes ca 210× slow-down.
Added RISC-V wheels.
Version: 2025.11.3
Git issue 594: Support relative PARNO in recursive
subpatterns.
Version: 2025.10.23
'setup.py' was missing from the source distribution.
Version: 2025.10.22
Fixed test in main.yml.
Version: 2025.10.21
Moved tests into subfolder.
Version: 2025.10.20
Re-organised files.
Updated to Unicode 17.0.0.
Version: 2025.9.20
... (truncated)
Commits
df2d5ac
Replaced atomic operations with mutex on pattern object for
free-threaded Pyt...
ed3d9ca
Replaced use of PyUnicode_GET_LENGTH with PyUnicode_GetLength.
28dd3e7
Merge pull request #598
from kevmo314/fix-storage-caching-race
cd631d8
Fix race condition in storage caching with atomic operations
- See full diff in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
You can trigger a rebase of this PR by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sam Bull
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 3d47fe07dde..b66ab9a7524 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -207,7 +207,7 @@ pyyaml==6.0.3
# via pre-commit
re-assert==1.1.0
# via -r requirements/test-common.in
-regex==2026.2.19
+regex==2026.2.28
# via re-assert
requests==2.32.5
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index e021873638c..3ea0c9d728c 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -202,7 +202,7 @@ pyyaml==6.0.3
# via pre-commit
re-assert==1.1.0
# via -r requirements/test-common.in
-regex==2026.2.19
+regex==2026.2.28
# via re-assert
requests==2.32.5
# via sphinx
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 35bb72c8c73..878bd428141 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -87,7 +87,7 @@ python-on-whales==0.80.0
# via -r requirements/test-common.in
re-assert==1.1.0
# via -r requirements/test-common.in
-regex==2026.2.19
+regex==2026.2.28
# via re-assert
rich==14.3.3
# via pytest-codspeed
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index f38961394dc..e83b5d1bb7f 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -122,7 +122,7 @@ python-on-whales==0.80.0
# via -r requirements/test-common.in
re-assert==1.1.0
# via -r requirements/test-common.in
-regex==2026.2.19
+regex==2026.2.28
# via re-assert
rich==14.3.3
# via pytest-codspeed
diff --git a/requirements/test.txt b/requirements/test.txt
index 9a1a0470ef0..8b8e81854f0 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -122,7 +122,7 @@ python-on-whales==0.80.0
# via -r requirements/test-common.in
re-assert==1.1.0
# via -r requirements/test-common.in
-regex==2026.2.19
+regex==2026.2.28
# via re-assert
rich==14.3.3
# via pytest-codspeed
From 54dc142f656d9c448ad0ebd835633a586a7ab3fd Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 14:59:57 +0000
Subject: [PATCH 031/191] Bump build from 1.3.0 to 1.4.0 (#11942)
Bumps [build](https://github.com/pypa/build) from 1.3.0 to 1.4.0.
Release notes
Sourced from build's
releases.
1.4.0
- Add
--quiet flag (PR #947)
- Add option to dump PEP 517 metadata with
--metadata (PR
#940, PR
#943)
- Support
UV environment variable (PR #971)
- Remove a workaround for 3.14b1 (PR #960)
- In 3.14 final release,
color defaults to
True already (PR #962)
- Pass sp-repo-review (PR #942)
- In pytest configuration,
log_level is better than
log_cli_level (PR #950)
- Split up typing and mypy (PR #944)
- Use
types-colorama (PR #945)
- In docs, first argument for
_has_dependency is a name
(PR #970)
- Fix test failure when
flit-core is installed (PR #921)
Changelog
Sourced from build's
changelog.
1.4.0 (2026-01-08)
- Add
--quiet flag
(:pr:947)
- Add option to dump PEP 517 metadata with
--metadata
(:pr:940, :pr:943)
- Support
UV environment variable
(:pr:971)
- Remove a workaround for 3.14b1
(:pr:
960)
- In 3.14 final release,
color defaults to
True already
(:pr:962)
- Pass sp-repo-review
(:pr:
942)
- In pytest configuration,
log_level is better than
log_cli_level
(:pr:950)
- Split up typing and mypy
(:pr:
944)
- Use
types-colorama
(:pr:945)
- In docs, first argument for
_has_dependency is a name
(PR :pr:970)
- Fix test failure when
flit-core is installed
(PR :pr:921)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
You can trigger a rebase of this PR by commenting `@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sam Bull
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index b66ab9a7524..f807f2aad11 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -36,7 +36,7 @@ blockbuster==1.5.26
# -r requirements/test-common.in
brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
-build==1.3.0
+build==1.4.0
# via pip-tools
certifi==2026.2.25
# via requests
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 3ea0c9d728c..78c5f3620f8 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -36,7 +36,7 @@ blockbuster==1.5.26
# -r requirements/test-common.in
brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
-build==1.3.0
+build==1.4.0
# via pip-tools
certifi==2026.2.25
# via requests
From 299ffe965315d3e5b03dd53a3dd4c9067e2e626f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 15:09:46 +0000
Subject: [PATCH 032/191] Bump packaging from 26.0 to 26.2 (#12449)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [packaging](https://github.com/pypa/packaging) from 26.0 to 26.2.
Release notes
Sourced from packaging's
releases.
26.2
What's Changed
Fixes:
Documentation:
Internal:
New Contributors
Full Changelog: https://github.com/pypa/packaging/compare/26.1...26.2
26.1
Features:
Behavior adaptations:
... (truncated)
Changelog
Sourced from packaging's
changelog.
26.2 - 2026-04-24
Fixes:
- Fix incorrect sysconfig var name for pyemscripten in
(:pull:
1160)
- Make
Version, Specifier,
SpecifierSet, Tag, Marker, and
Requirement pickle-safe
and backward-compatible with pickles created in 25.0-26.1 (including
references to the removed
packaging._structures module) (:pull:1163,
:pull:1168, :pull:1170,
:pull:1171)
- Re-export
ExceptionGroup in metadata for now in
(:pull:1164)
Documentation:
- Add errors section and fix missing details in
(:pull:
1159)
- Document our property-based test suite in
(:pull:
1167)
- Fix a
DirectUrl typo in (:pull:1167)
- Add example of
is_unsatisfiable in
(:pull:1166)
Internal:
- Enable the auditor persona on zizmor in
(:pull:
1158)
- Test new pickle guarantees in (:pull:
1174)
- Use new native ReadTheDocs uv integration in
(:pull:
1175)
26.1 - 2026-04-14
Features:
- PEP 783: add handling for Emscripten wheel tags in
(:pull:
804) (old name used in implementation, fixed in next
release)
- PEP 803: add handling for the
abi3.abi3t free-threading
tag in (:pull:1099)
- PEP 723: add
packaging.dependency_groups module, based
on the dependency-groups package in
(:pull:1065)
- Add the
packaging.direct_url module in
(:pull:944)
- Add the
packaging.errors module in
(:pull:1071)
- Add
SpecifierSet.is_unsatisfiable using ranges (new
internals that will be expanded in future versions) in
(:pull:1119)
- Add
create_compatible_tags_selector to select
compatible tags in (:pull:1110)
- Add a
key argument to
SpecifierSet.filter() in (:pull:1068)
- Support
& and | for
Marker's in (:pull:1146)
- Normalize
Version.__replace__ and add
Version.from_parts in (:pull:1078)
- Add an option to validate compressed tag set sort order in
parse_wheel_filename in (:pull:1150)
Behavior adaptations:
- Narrow exclusion of pre-releases for
<V.postN to
match spec in (:pull:1140)
- Narrow exclusion of post-releases for
>V to match
spec in (:pull:1141)
- Rename
format_full_version to
_format_full_version to make it visibly private in
(:pull:1125)
- Restrict local version to ASCII in (:pull:
1102)
Pylock (PEP 751) updates:
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
10 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 3598283c782..cc91c67fd67 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -32,7 +32,7 @@ multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
-packaging==26.0
+packaging==26.2
# via gunicorn
propcache==0.4.1
# via
diff --git a/requirements/base.txt b/requirements/base.txt
index f27766b8194..2eb77bc03f8 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -32,7 +32,7 @@ multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
-packaging==26.0
+packaging==26.2
# via gunicorn
propcache==0.4.1
# via
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index f807f2aad11..3c8e567785b 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -127,7 +127,7 @@ mypy-extensions==1.1.0
# via mypy
nodeenv==1.10.0
# via pre-commit
-packaging==26.0
+packaging==26.2
# via
# build
# gunicorn
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 78c5f3620f8..bb588cdcae6 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -124,7 +124,7 @@ mypy-extensions==1.1.0
# via mypy
nodeenv==1.10.0
# via pre-commit
-packaging==26.0
+packaging==26.2
# via
# build
# gunicorn
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index aef9eb6fedc..54885637357 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -28,7 +28,7 @@ jinja2==3.1.6
# towncrier
markupsafe==3.0.3
# via jinja2
-packaging==26.0
+packaging==26.2
# via sphinx
pyenchant==3.3.0
# via sphinxcontrib-spelling
diff --git a/requirements/doc.txt b/requirements/doc.txt
index c3214743726..2fb865888b4 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -28,7 +28,7 @@ jinja2==3.1.6
# towncrier
markupsafe==3.0.3
# via jinja2
-packaging==26.0
+packaging==26.2
# via sphinx
pygments==2.19.2
# via sphinx
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 77209a1d445..3026c475c36 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -57,7 +57,7 @@ mypy-extensions==1.1.0
# via mypy
nodeenv==1.10.0
# via pre-commit
-packaging==26.0
+packaging==26.2
# via pytest
pathspec==1.0.4
# via mypy
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 878bd428141..2068e9166c6 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -44,7 +44,7 @@ mypy==1.19.1 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
-packaging==26.0
+packaging==26.2
# via pytest
pathspec==1.0.4
# via mypy
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index e83b5d1bb7f..c8495179b3e 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -71,7 +71,7 @@ mypy==1.19.1 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
-packaging==26.0
+packaging==26.2
# via
# gunicorn
# pytest
diff --git a/requirements/test.txt b/requirements/test.txt
index 8b8e81854f0..f119eaa25b5 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -71,7 +71,7 @@ mypy==1.19.1 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
-packaging==26.0
+packaging==26.2
# via
# gunicorn
# pytest
From 9506e32f337048ceab5a50dd7fe1dcd858c41641 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 15:12:24 +0000
Subject: [PATCH 033/191] Bump idna from 3.11 to 3.13 (#12451)
Bumps [idna](https://github.com/kjd/idna) from 3.11 to 3.13.
Changelog
Sourced from idna's
changelog.
3.13 (2026-04-22)
+++++++++++++++++
- Correct classification error for codepoint U+A7F1
3.12 (2026-04-21)
+++++++++++++++++
- Update to Unicode 17.0.0.
- Issue a deprecation warning for the transitional argument.
- Added lazy-loading to provide some performance improvements.
- Removed vestiges of code related to Python 2 support, including
segmentation of data structures specific to Jython.
Thanks to Rodrigo Nogueira for contributions to this release.
Commits
89cdfd2
Release v3.13
1eb0686
Pre-release 3.13
5f20d1e
Merge pull request #220 from
kjd/unicode-next
4ea8425
Regenerate idnadata.py with correct NFKC_CF data
fd47341
Use NFKC_CF from Unicode data files instead of Python's unicodedata
module
a5304a4
Merge pull request #219 from
kjd/release-3.12
d80d6f9
Release v3.12
1bb44dd
Merge pull request #218 from
kjd/release-candidate-3.12rc0
909c49d
Release candidate for 3.12
c5459a1
Merge pull request #217 from
kjd/housekeeping-2
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/runtime-deps.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
11 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index cc91c67fd67..533c50b5f88 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -26,7 +26,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==25.1.0
# via -r requirements/base-ft.in
-idna==3.11
+idna==3.13
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/base.txt b/requirements/base.txt
index 2eb77bc03f8..abc83087f3d 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -26,7 +26,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==25.1.0
# via -r requirements/base.in
-idna==3.11
+idna==3.13
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 3c8e567785b..54b69c1677b 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -89,7 +89,7 @@ gunicorn==25.1.0
# via -r requirements/base.in
identify==2.6.17
# via pre-commit
-idna==3.11
+idna==3.13
# via
# requests
# trustme
diff --git a/requirements/dev.txt b/requirements/dev.txt
index bb588cdcae6..4d093bf6b9b 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -87,7 +87,7 @@ gunicorn==25.1.0
# via -r requirements/base.in
identify==2.6.17
# via pre-commit
-idna==3.11
+idna==3.13
# via
# requests
# trustme
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index 54885637357..1c34751a10b 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -18,7 +18,7 @@ click==8.3.1
# via towncrier
docutils==0.21.2
# via sphinx
-idna==3.11
+idna==3.13
# via requests
imagesize==1.5.0
# via sphinx
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 2fb865888b4..80626f3b482 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -18,7 +18,7 @@ click==8.3.1
# via towncrier
docutils==0.21.2
# via sphinx
-idna==3.11
+idna==3.13
# via requests
imagesize==1.5.0
# via sphinx
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 3026c475c36..aeb21bbe87c 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -39,7 +39,7 @@ freezegun==1.5.5
# via -r requirements/lint.in
identify==2.6.17
# via pre-commit
-idna==3.11
+idna==3.13
# via trustme
iniconfig==2.3.0
# via pytest
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index f3a3df711ab..dc6ff7fbdfe 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -24,7 +24,7 @@ frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
-idna==3.11
+idna==3.13
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 2068e9166c6..3397c08c305 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -28,7 +28,7 @@ forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.5
# via -r requirements/test-common.in
-idna==3.11
+idna==3.13
# via trustme
iniconfig==2.3.0
# via pytest
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index c8495179b3e..407a152ca28 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -49,7 +49,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==25.1.0
# via -r requirements/base-ft.in
-idna==3.11
+idna==3.13
# via
# trustme
# yarl
diff --git a/requirements/test.txt b/requirements/test.txt
index f119eaa25b5..14bb6dec7cf 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -49,7 +49,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==25.1.0
# via -r requirements/base.in
-idna==3.11
+idna==3.13
# via
# trustme
# yarl
From 985f27d032c1f271e351c4533dcb09689b1eaaf3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 4 May 2026 15:30:01 +0000
Subject: [PATCH 034/191] Bump mypy from 1.19.1 to 1.20.2 (#12448)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [mypy](https://github.com/python/mypy) from 1.19.1 to 1.20.2.
Changelog
Sourced from mypy's
changelog.
Mypy 1.20.2
- Use WAL with SQLite cache and fix close (Shantanu, PR 21154)
- Adjust SQLite journal mode (Ivan Levkivskyi, PR 21217)
- Correctly aggregate narrowing information on parent expressions
(Shantanu, PR 21206)
- Fix regression related to generic callables (Shantanu, PR 21208)
- Fix regression by avoiding widening types in some contexts
(Shantanu, PR 21242)
- Fix slicing in non-strict optional mode (Shantanu, PR 21282)
- mypyc: Fix match statement semantics for "or" pattern
(Shantanu, PR 21156)
- mypyc: Fix issue with module dunder attributes (Piotr Sawicki, PR 21275)
- Initial support for Python 3.15.0a8 (Marc Mueller, PR 21255)
Acknowledgements
Thanks to all mypy contributors who contributed to this release:
- A5rocks
- Aaron Wieczorek
- Adam Turner
- Ali Hamdan
- asce
- BobTheBuidler
- Brent Westbrook
- Brian Schubert
- bzoracler
- Chris Burroughs
- Christoph Tyralla
- Colin Watson
- Donghoon Nam
- E. M. Bray
- Emma Smith
- Ethan Sarp
- George Ogden
- getzze
- grayjk
- Gregor Riepl
- Ivan Levkivskyi
- James Hilliard
- James Le Cuirot
- Jeremy Nimmer
- Joren Hammudoglu
- Kai (Kazuya Ito)
- kaushal trivedi
- Kevin Kannammalil
- Lukas Geiger
- Łukasz Langa
- Marc Mueller
- Michael R. Crusoe
- michaelm-openai
- Neil Schemenauer
- Piotr Sawicki
... (truncated)
Commits
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sam Bull
---
aiohttp/client_reqrep.py | 2 +-
aiohttp/web_log.py | 22 ++++++++++++----------
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
8 files changed, 19 insertions(+), 17 deletions(-)
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
index aa0d48159c0..92b3529bf13 100644
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -1054,7 +1054,7 @@ def update_headers(self, headers: LooseHeaders | None) -> None:
if isinstance(headers, (dict, MultiDictProxy, MultiDict)):
headers = headers.items()
- for key, value in headers: # type: ignore[misc]
+ for key, value in headers: # type: ignore[str-unpack]
# A special case for Host header
if key in hdrs.HOST_ALL:
self.headers[key] = value
diff --git a/aiohttp/web_log.py b/aiohttp/web_log.py
index 27b50adf6d6..9283cc08b12 100644
--- a/aiohttp/web_log.py
+++ b/aiohttp/web_log.py
@@ -4,15 +4,17 @@
import os
import re
import time as time_mod
-from collections import namedtuple
from collections.abc import Iterable
-from typing import Callable, ClassVar
+from typing import Callable, ClassVar, NamedTuple
from .abc import AbstractAccessLogger
from .web_request import BaseRequest
from .web_response import StreamResponse
-KeyMethod = namedtuple("KeyMethod", "key method")
+
+class KeyMethod(NamedTuple):
+ key: str | tuple[str, str]
+ method: Callable[[BaseRequest, StreamResponse, float], str]
class AccessLogger(AbstractAccessLogger):
@@ -198,7 +200,7 @@ def _format_D(request: BaseRequest, response: StreamResponse, time: float) -> st
def _format_line(
self, request: BaseRequest, response: StreamResponse, time: float
- ) -> Iterable[tuple[str, Callable[[BaseRequest, StreamResponse, float], str]]]:
+ ) -> Iterable[tuple[str | tuple[str, str], str]]:
return [(key, method(request, response, time)) for key, method in self._methods]
@property
@@ -212,17 +214,17 @@ def log(self, request: BaseRequest, response: StreamResponse, time: float) -> No
fmt_info = self._format_line(request, response, time)
values = list()
- extra = dict()
+ extra: dict[str, str | dict[str, str]] = dict()
for key, value in fmt_info:
values.append(value)
- if key.__class__ is str:
+ if isinstance(key, str):
extra[key] = value
else:
- k1, k2 = key # type: ignore[misc]
- dct = extra.get(k1, {}) # type: ignore[var-annotated,has-type]
- dct[k2] = value # type: ignore[index,has-type]
- extra[k1] = dct # type: ignore[has-type,assignment]
+ k1, k2 = key
+ dct: dict[str, str] = extra.get(k1, {}) # type: ignore[assignment]
+ dct[k2] = value
+ extra[k1] = dct
self.logger.info(self._log_format % tuple(values), extra=extra)
except Exception:
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 54b69c1677b..4a0c909b64a 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -119,7 +119,7 @@ multidict==6.7.1
# -r requirements/multidict.in
# -r requirements/runtime-deps.in
# yarl
-mypy==1.19.1 ; implementation_name == "cpython"
+mypy==1.20.2 ; implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 4d093bf6b9b..fe600127a69 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -116,7 +116,7 @@ multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
-mypy==1.19.1 ; implementation_name == "cpython"
+mypy==1.20.2 ; implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index aeb21bbe87c..d3d38e3791b 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -51,7 +51,7 @@ markdown-it-py==4.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-mypy==1.19.1 ; implementation_name == "cpython"
+mypy==1.20.2 ; implementation_name == "cpython"
# via -r requirements/lint.in
mypy-extensions==1.1.0
# via mypy
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 3397c08c305..861d1d8b01e 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -40,7 +40,7 @@ markdown-it-py==4.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-mypy==1.19.1 ; implementation_name == "cpython"
+mypy==1.20.2 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 407a152ca28..9e12a371431 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -67,7 +67,7 @@ multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
-mypy==1.19.1 ; implementation_name == "cpython"
+mypy==1.20.2 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
diff --git a/requirements/test.txt b/requirements/test.txt
index 14bb6dec7cf..951b3eefdcb 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -67,7 +67,7 @@ multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
-mypy==1.19.1 ; implementation_name == "cpython"
+mypy==1.20.2 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
From f557ff00f46bbdab623f9c1cc8d30cf032e98f08 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 5 May 2026 11:23:32 +0000
Subject: [PATCH 035/191] Bump charset-normalizer from 3.4.4 to 3.4.7 (#12454)
Bumps [charset-normalizer](https://github.com/jawah/charset_normalizer)
from 3.4.4 to 3.4.7.
Release notes
Sourced from charset-normalizer's
releases.
Version 3.4.7
3.4.7
(2026-04-02)
Changed
- Pre-built optimized version using mypy[c] v1.20.
- Relax
setuptools constraint to
setuptools>=68,<82.1.
Fixed
- Correctly remove SIG remnant in utf-7 decoded string. (#718)
(#716)
Version 3.4.6
3.4.6
(2026-03-15)
Changed
- Flattened the logic in
charset_normalizer.md for higher
performance. Removed eligible(..) and
feed(...)
in favor of feed_info(...).
- Raised upper bound for mypy[c] to 1.20, for our optimized
version.
- Updated
UNICODE_RANGES_COMBINED using Unicode blocks
v17.
Fixed
- Edge case where noise difference between two candidates can be
almost insignificant. (#672)
- CLI
--normalize writing to wrong path when passing
multiple files in. (#702)
Misc
- Freethreaded pre-built wheels now shipped in PyPI starting with
3.14t. (#616)
Version 3.4.5
3.4.5
(2026-03-06)
Changed
- Update
setuptools constraint to
setuptools>=68,<=82.
- Raised upper bound of mypyc for the optional pre-built extension to
v1.19.1
Fixed
- Add explicit link to lib math in our optimized build. (#692)
- Logger level not restored correctly for empty byte sequences. (#701)
- TypeError when passing bytearray to from_bytes. (#703)
Misc
- Applied safe micro-optimizations in both our noise detector and
language detector.
- Rewrote the
query_yes_no function (inside CLI) to avoid
using ambiguous licensed code.
- Added
cd.py submodule into mypyc optional compilation
to reduce further the performance impact.
[!WARNING]
mypyc changed the usual binary output for the optimized wheel. Beware,
especially if using PyInstaller or alike. See jawah/charset_normalizer#714
Changelog
Sourced from charset-normalizer's
changelog.
3.4.7
(2026-04-02)
Changed
- Pre-built optimized version using mypy[c] v1.20.
- Relax
setuptools constraint to
setuptools>=68,<82.1.
Fixed
- Correctly remove SIG remnant in utf-7 decoded string. (#718)
(#716)
3.4.6
(2026-03-15)
Changed
- Flattened the logic in
charset_normalizer.md for higher
performance. Removed eligible(..) and
feed(...)
in favor of feed_info(...).
- Raised upper bound for mypy[c] to 1.20, for our optimized
version.
- Updated
UNICODE_RANGES_COMBINED using Unicode blocks
v17.
Fixed
- Edge case where noise difference between two candidates can be
almost insignificant. (#672)
- CLI
--normalize writing to wrong path when passing
multiple files in. (#702)
Misc
- Freethreaded pre-built wheels now shipped in PyPI starting with
3.14t. (#616)
3.4.5
(2026-03-06)
Changed
- Update
setuptools constraint to
setuptools>=68,<=82.
- Raised upper bound of mypyc for the optional pre-built extension to
v1.19.1
Fixed
- Add explicit link to lib math in our optimized build. (#692)
- Logger level not restored correctly for empty byte sequences. (#701)
- TypeError when passing bytearray to from_bytes. (#703)
Misc
- Applied safe micro-optimizations in both our noise detector and
language detector.
- Rewrote the
query_yes_no function (inside CLI) to avoid
using ambiguous licensed code.
- Added
cd.py submodule into mypyc optional compilation
to reduce further the performance impact.
Commits
0f07891
Merge pull request #729
from jawah/release-3.4.7
fdbeb29
chore: update dev, and ci requirements
b66f922
chore: add ft classifier
f94249d
chore: add test cases for utf_7 recent fix
95c866f
chore: bump version to 3.4.7
4f429bb
chore: bump mypy pre-commit to v1.20
b579cd6
fix: correctly remove SIG remnant in utf-7 decoded string
58bf944
:arrow_up: Bump github/codeql-action from 4.32.4 to 4.35.1 (#728)
44cf8a1
:arrow_up: Bump actions/download-artifact from 8.0.0 to 8.0.1 (#726)
362bc20
:arrow_up: Bump docker/setup-qemu-action from 3.7.0 to 4.0.0 (#725)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 4a0c909b64a..acea040b78d 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -47,7 +47,7 @@ cffi==2.0.0
# pytest-codspeed
cfgv==3.5.0
# via pre-commit
-charset-normalizer==3.4.4
+charset-normalizer==3.4.7
# via requests
click==8.3.1
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index fe600127a69..2faa726034c 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -47,7 +47,7 @@ cffi==2.0.0
# pytest-codspeed
cfgv==3.5.0
# via pre-commit
-charset-normalizer==3.4.4
+charset-normalizer==3.4.7
# via requests
click==8.3.1
# via
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index 1c34751a10b..e05049eea51 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -12,7 +12,7 @@ babel==2.18.0
# via sphinx
certifi==2026.2.25
# via requests
-charset-normalizer==3.4.4
+charset-normalizer==3.4.7
# via requests
click==8.3.1
# via towncrier
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 80626f3b482..680ae3c836c 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -12,7 +12,7 @@ babel==2.18.0
# via sphinx
certifi==2026.2.25
# via requests
-charset-normalizer==3.4.4
+charset-normalizer==3.4.7
# via requests
click==8.3.1
# via towncrier
From ddcc11c7e69c8cccbeea0adfa41646ea0359baf5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 5 May 2026 11:41:02 +0000
Subject: [PATCH 036/191] Bump gunicorn from 25.1.0 to 26.0.0 (#12456)
Bumps [gunicorn](https://github.com/benoitc/gunicorn) from 25.1.0 to
26.0.0.
Release notes
Sourced from gunicorn's
releases.
26.0.0
Breaking Changes
- Eventlet worker removed: The
eventlet
worker class has been dropped. Migrate to gevent,
gthread, or tornado.
New Features
- ASGI Framework Compatibility Suite: New end-to-end
compatibility test harness covering Starlette, FastAPI, Litestar, Quart,
Sanic, and BlackSheep. Current grid passes 438/444 tests (98%).
- ASGI Test Suite Expansion: 134 additional ASGI unit
tests covering protocol semantics, lifespan, websockets, and chunked
framing.
Security
- HTTP/1.1 Request-Target Validation (RFC 9112
sections 3.2.3, 3.2.4):
- Reject
authority-form request-target outside
CONNECT
- Reject
asterisk-form request-target outside
OPTIONS
- Reject
relative-reference request-targets
- Header Field Hardening (RFC 9110):
- Reject control characters in header field-value (section 5.5)
- Reject forbidden trailer field-names (section 6.5.1)
- Reject
Content-Length list form (RFC 9112 section
6.3)
- Request Smuggling Hardening:
- Tighten keepalive gate and scope
finish_body byte
cap
- Keep
_body_receiver alive across the keepalive
smuggling gate so pipelined requests cannot re-enter a closed body
- Address parser/protocol findings from a six-point WSGI/ASGI
audit
- PROXY Protocol (ASGI): Enforce
proxy_allow_ips and tighten v1/v2 parsing in the ASGI
callback parser.
- Connection Draining: Drain the connection on close
per RFC 9112 section 9.6 to prevent reset-on-close truncation.
Bug Fixes
- Body Framing on HEAD/204/304:
- Keep
Content-Length on HEAD and 304 responses (#3621)
- Drop body framing on HEAD/204/304 even when the framework set
it
- Warn once when an ASGI app emits a body for a no-body response
- HTTP/2 ASGI:
- Fix
_handle_stream_ended to set
_body_complete in the async HTTP/2 handler so request
bodies finalize correctly on stream end
- Add
InvalidChunkExtension mapping and fast-parser
support in ASGI tests (#3565)
- HTTP/1.1 100-Continue: Stop adding
Transfer-Encoding: chunked to 100-Continue interim
responses.
- WebSocket Close Handshake (RFC 6455):
- Comply with the close handshake state machine
- Close the transport after the close handshake completes
- Fix binary send when the
text key is
None
- Early Hints: Validate headers in the
early_hints callback to match process_headers;
pass only the header name to InvalidHeader (#3588).
- ASGI Framework Fixes:
- Fix ASGI disconnect handling for Django-style apps
- Fix Litestar request handling (use raw ASGI receive for
body/headers)
- Fix Litestar HTTP endpoints for compatibility tests
- Fix Quart headers endpoint to normalize keys to lowercase
- Fix Quart WebSocket close test app (missing
accept())
- Fix duplicate
Transfer-Encoding header for BlackSheep
streaming
... (truncated)
Commits
5d819cf
release: 26.0.0
b45c70d
Merge pull request #3611
from zc-mattcen/docs-typo
99c8d48
Merge pull request #3623
from benoitc/chore/drop-eventlet-add-h2-uvloop-test-...
5a655af
Merge pull request #3622
from benoitc/test/docker-port-and-ipv4-fixes
201df19
chore: remove eventlet worker; add h2 and uvloop to test deps
f4ac8e1
test: pass action name to dirty client and stabilize after TTOU
spam
54d38af
test: unblock docker fixtures on macOS hosts
68843c8
Merge pull request #3621
from benoitc/fix/asgi-preserve-content-length-on-hea...
31f2618
Merge pull request #3620
from benoitc/fix/asgi-proxy-protocol-trust-and-parsing
41ec752
fix: keep Content-Length on HEAD and 304 responses
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 533c50b5f88..b90f244de52 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -24,7 +24,7 @@ frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
-gunicorn==25.1.0
+gunicorn==26.0.0
# via -r requirements/base-ft.in
idna==3.13
# via yarl
diff --git a/requirements/base.txt b/requirements/base.txt
index abc83087f3d..a0cf0908ef5 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -24,7 +24,7 @@ frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
-gunicorn==25.1.0
+gunicorn==26.0.0
# via -r requirements/base.in
idna==3.13
# via yarl
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index acea040b78d..cd2199ae759 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -85,7 +85,7 @@ frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
-gunicorn==25.1.0
+gunicorn==26.0.0
# via -r requirements/base.in
identify==2.6.17
# via pre-commit
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 2faa726034c..e84976610f9 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -83,7 +83,7 @@ frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
-gunicorn==25.1.0
+gunicorn==26.0.0
# via -r requirements/base.in
identify==2.6.17
# via pre-commit
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 9e12a371431..efa93771272 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -47,7 +47,7 @@ frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
-gunicorn==25.1.0
+gunicorn==26.0.0
# via -r requirements/base-ft.in
idna==3.13
# via
diff --git a/requirements/test.txt b/requirements/test.txt
index 951b3eefdcb..967c7a519e3 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -47,7 +47,7 @@ frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
-gunicorn==25.1.0
+gunicorn==26.0.0
# via -r requirements/base.in
idna==3.13
# via
From 9c5f2334a6e424fed5f11af4ac28ce8cc3e893e5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 5 May 2026 11:49:39 +0000
Subject: [PATCH 037/191] Bump pathspec from 1.0.4 to 1.1.1 (#12458)
Bumps [pathspec](https://github.com/cpburnz/python-pathspec) from 1.0.4
to 1.1.1.
Release notes
Sourced from pathspec's
releases.
v1.1.1
Release v1.1.1. See CHANGES.rst.
v1.1.0
Release v1.1.0. See CHANGES.rst.
Changelog
Sourced from pathspec's
changelog.
1.1.1 (2026-04-26)
Improvements:
- Improved type checking with mypy and pyright.
Bug fixes:
- Fixed typing on
PathSpec[TPattern] to
PathSpec[TPattern_co].
- Added missing variant type-hint
type[Pattern] to
PathSpec.from_lines() parameter
pattern_factory.
- Fixed possible type error when using
+ and
+= operators on PathSpec.
1.1.0 (2026-04-22)
New features:
Issue
[#108](https://github.com/cpburnz/python-pathspec/issues/108)_:
Specialize pattern type for PathSpec as
PathSpec[TPattern] for better debugging of
PathSpec().patterns.
Bug fixes:
Issue
[#93](https://github.com/cpburnz/python-pathspec/issues/93)_: Git
discards invalid range notation. GitIgnoreSpecPattern now
discards patterns with invalid range notation like Git.
Pull
[#106](https://github.com/cpburnz/python-pathspec/issues/106)_:
Fix escape() not escaping backslash characters.
Improvements:
Pull
[#110](https://github.com/cpburnz/python-pathspec/issues/110)_:
Nicer debug print outs (and str for regex pattern).
.. _Pull
[#106](https://github.com/cpburnz/python-pathspec/issues/106): cpburnz/python-pathspec#106
.. _Issue
[#108](https://github.com/cpburnz/python-pathspec/issues/108): cpburnz/python-pathspec#108
.. _Pull
[#110](https://github.com/cpburnz/python-pathspec/issues/110): cpburnz/python-pathspec#110
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index cd2199ae759..0f4b1e6ad86 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -134,7 +134,7 @@ packaging==26.2
# pytest
# sphinx
# wheel
-pathspec==1.0.4
+pathspec==1.1.1
# via mypy
pip-tools==7.5.3
# via -r requirements/dev.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index e84976610f9..360fd8f7668 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -131,7 +131,7 @@ packaging==26.2
# pytest
# sphinx
# wheel
-pathspec==1.0.4
+pathspec==1.1.1
# via mypy
pip-tools==7.5.3
# via -r requirements/dev.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index d3d38e3791b..00eb08ea7d8 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -59,7 +59,7 @@ nodeenv==1.10.0
# via pre-commit
packaging==26.2
# via pytest
-pathspec==1.0.4
+pathspec==1.1.1
# via mypy
platformdirs==4.9.2
# via
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 861d1d8b01e..284d1f94af7 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -46,7 +46,7 @@ mypy-extensions==1.1.0
# via mypy
packaging==26.2
# via pytest
-pathspec==1.0.4
+pathspec==1.1.1
# via mypy
pkgconfig==1.5.5
# via -r requirements/test-common.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index efa93771272..5ce1ed51055 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -75,7 +75,7 @@ packaging==26.2
# via
# gunicorn
# pytest
-pathspec==1.0.4
+pathspec==1.1.1
# via mypy
pkgconfig==1.5.5
# via -r requirements/test-common.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 967c7a519e3..5af2b34871b 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -75,7 +75,7 @@ packaging==26.2
# via
# gunicorn
# pytest
-pathspec==1.0.4
+pathspec==1.1.1
# via mypy
pkgconfig==1.5.5
# via -r requirements/test-common.in
From 3a351ac6c384a1d55c18ed5d5faf75e3e96307e2 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 5 May 2026 11:54:34 +0000
Subject: [PATCH 038/191] Bump pydantic from 2.12.5 to 2.13.3 (#12461)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.12.5 to
2.13.3.
Release notes
Sourced from pydantic's
releases.
v2.13.3 2026-04-20
v2.13.3 (2026-04-20)
What's Changed
Fixes
- Handle
AttributeError subclasses with
from_attributes by @Viicos in #13096
Full Changelog: https://github.com/pydantic/pydantic/compare/v2.13.2...v2.13.3
v2.13.2 2026-04-17
v2.13.2 (2026-04-17)
What's Changed
Fixes
- Fix
ValidationInfo.field_name missing with
model_validate_json() by @Viicos in #13084
Full Changelog: https://github.com/pydantic/pydantic/compare/v2.13.1...v2.13.2
v2.13.1 2026-04-15
v2.13.1 (2026-04-15)
What's Changed
Fixes
Full Changelog: https://github.com/pydantic/pydantic/compare/v2.13.0...v2.13.1
v2.13.0 2026-04-13
v2.13.0 (2026-04-13)
The highlights of the v2.13 release are available in the blog
post.
Several minor changes (considered non-breaking changes according to our
versioning
policy) are also included in this release. Make sure to look into
them before upgrading.
This release contains the updated pydantic.v1 namespace,
matching version 1.10.26 which includes support for Python 3.14.
What's Changed
See the beta releases for all changes sinces 2.12.
Packaging
- Add zizmor for GitHub Actions workflow linting by
@Viicos in #13039
- Update jiter to v0.14.0 to fix a segmentation fault on musl Linux by
@Viicos in #13064
... (truncated)
Changelog
Sourced from pydantic's
changelog.
v2.13.3 (2026-04-20)
GitHub
release
What's Changed
Fixes
- Handle
AttributeError subclasses with
from_attributes by @Viicos in #13096
v2.13.2 (2026-04-17)
GitHub
release
What's Changed
Fixes
- Fix
ValidationInfo.field_name missing with
model_validate_json() by @Viicos in #13084
v2.13.1 (2026-04-15)
GitHub
release
What's Changed
Fixes
v2.13.0 (2026-04-13)
GitHub
release
The highlights of the v2.13 release are available in the blog
post.
Several minor changes (considered non-breaking changes according to our
versioning
policy)
are also included in this release. Make sure to look into them before
upgrading.
This release contains the updated pydantic.v1 namespace,
matching version 1.10.26 which includes support for Python 3.14.
What's Changed
See the beta releases for all changes sinces 2.12.
New Features
- Allow default factories of private attributes to take validated
model data by
@Viicos in #13013
Changes
... (truncated)
Commits
9e9a111
Fix backported test
1ec8c6a
Prepare release v2.13.3
fb4f204
Handle AttributeError subclasses with
from_attributes
ca3ddd1
Prepare release v2.13.2
000e823
Fix ValidationInfo.field_name missing with
model_validate_json()
d45d8be
Prepare release 2.13.1
54aca60
Fix ValidationInfo.data missing with
model_validate_json()
46bf4fa
Fix Pydantic release workflow (#13067)
1b359ed
Prepare release v2.13.0 (#13065)
b1bf194
Fix model equality when using runtime extra configuration
(#13062)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 4 ++--
requirements/dev.txt | 4 ++--
requirements/lint.txt | 4 ++--
requirements/test-common.txt | 4 ++--
requirements/test-ft.txt | 4 ++--
requirements/test.txt | 4 ++--
6 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 0f4b1e6ad86..86955541a23 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -160,9 +160,9 @@ pycares==4.11.0
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.12.5
+pydantic==2.13.3
# via python-on-whales
-pydantic-core==2.41.5
+pydantic-core==2.46.3
# via pydantic
pyenchant==3.3.0
# via sphinxcontrib-spelling
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 360fd8f7668..c8a99f52b5c 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -157,9 +157,9 @@ pycares==4.11.0
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.12.5
+pydantic==2.13.3
# via python-on-whales
-pydantic-core==2.41.5
+pydantic-core==2.46.3
# via pydantic
pygments==2.19.2
# via
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 00eb08ea7d8..53d0a12ee2f 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -73,9 +73,9 @@ pycares==4.11.0
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.12.5
+pydantic==2.13.3
# via python-on-whales
-pydantic-core==2.41.5
+pydantic-core==2.46.3
# via pydantic
pygments==2.19.2
# via
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 284d1f94af7..4f7b6917386 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -58,9 +58,9 @@ proxy-py==2.4.10
# via -r requirements/test-common.in
pycparser==3.0
# via cffi
-pydantic==2.12.5
+pydantic==2.13.3
# via python-on-whales
-pydantic-core==2.41.5
+pydantic-core==2.46.3
# via pydantic
pygments==2.19.2
# via
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 5ce1ed51055..dc924e08f12 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -93,9 +93,9 @@ pycares==4.11.0
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.12.5
+pydantic==2.13.3
# via python-on-whales
-pydantic-core==2.41.5
+pydantic-core==2.46.3
# via pydantic
pygments==2.19.2
# via
diff --git a/requirements/test.txt b/requirements/test.txt
index 5af2b34871b..13304c307e7 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -93,9 +93,9 @@ pycares==4.11.0
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.12.5
+pydantic==2.13.3
# via python-on-whales
-pydantic-core==2.41.5
+pydantic-core==2.46.3
# via pydantic
pygments==2.19.2
# via
From 867103f8507911c20ad22a95ffc3973966ecee40 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 5 May 2026 12:08:16 +0000
Subject: [PATCH 039/191] Bump requests from 2.32.5 to 2.33.1 (#12463)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [requests](https://github.com/psf/requests) from 2.32.5 to 2.33.1.
Release notes
Sourced from requests's
releases.
v2.33.1
2.33.1 (2026-03-30)
Bugfixes
- Fixed test cleanup for CVE-2026-25645 to avoid leaving unnecessary
files in the tmp directory. (#7305)
- Fixed Content-Type header parsing for malformed values. (#7309)
- Improved error consistency for malformed header values. (#7308)
New Contributors
Full Changelog: https://github.com/psf/requests/blob/main/HISTORY.md#2331-2026-03-30
v2.33.0
2.33.0 (2026-03-25)
Announcements
- 📣 Requests is adding inline types. If you have a typed code base
that uses Requests, please take a look at #7271.
Give it a try, and report any gaps or feedback you may have in the
issue. 📣
Security
- CVE-2026-25645
requests.utils.extract_zipped_paths now
extracts contents to a non-deterministic location to prevent malicious
file replacement. This does not affect default usage of Requests, only
applications calling the utility function directly.
Improvements
- Migrated to a PEP 517 build system using setuptools. (#7012)
Bugfixes
- Fixed an issue where an empty netrc entry could cause malformed
authentication to be applied to Requests on Python 3.11+. (#7205)
Deprecations
- Dropped support for Python 3.9 following its end of support. (#7196)
Documentation
- Various typo fixes and doc improvements.
New Contributors
Full Changelog: https://github.com/psf/requests/blob/main/HISTORY.md#2330-2026-03-25
Changelog
Sourced from requests's
changelog.
2.33.1 (2026-03-30)
Bugfixes
- Fixed test cleanup for CVE-2026-25645 to avoid leaving unnecessary
files in the tmp directory. (#7305)
- Fixed Content-Type header parsing for malformed values. (#7309)
- Improved error consistency for malformed header values. (#7308)
2.33.0 (2026-03-25)
Announcements
- 📣 Requests is adding inline types. If you have a typed code base
that
uses Requests, please take a look at #7271.
Give it a try, and report
any gaps or feedback you may have in the issue. 📣
Security
- CVE-2026-25645
requests.utils.extract_zipped_paths now
extracts
contents to a non-deterministic location to prevent malicious file
replacement. This does not affect default usage of Requests, only
applications calling the utility function directly.
Improvements
- Migrated to a PEP 517 build system using setuptools. (#7012)
Bugfixes
- Fixed an issue where an empty netrc entry could cause
malformed authentication to be applied to Requests on
Python 3.11+. (#7205)
Deprecations
- Dropped support for Python 3.9 following its end of support. (#7196)
Documentation
- Various typo fixes and doc improvements.
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 86955541a23..261a4d53bad 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -209,7 +209,7 @@ re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.2.28
# via re-assert
-requests==2.32.5
+requests==2.33.1
# via
# sphinx
# sphinxcontrib-spelling
diff --git a/requirements/dev.txt b/requirements/dev.txt
index c8a99f52b5c..0f24c9c2efd 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -204,7 +204,7 @@ re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.2.28
# via re-assert
-requests==2.32.5
+requests==2.33.1
# via sphinx
rich==14.3.3
# via pytest-codspeed
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index e05049eea51..080bbd7f499 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -34,7 +34,7 @@ pyenchant==3.3.0
# via sphinxcontrib-spelling
pygments==2.19.2
# via sphinx
-requests==2.32.5
+requests==2.33.1
# via
# sphinx
# sphinxcontrib-spelling
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 680ae3c836c..b52661badb7 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -32,7 +32,7 @@ packaging==26.2
# via sphinx
pygments==2.19.2
# via sphinx
-requests==2.32.5
+requests==2.33.1
# via sphinx
snowballstemmer==3.0.1
# via sphinx
From a7a8a485432a6f1f0fd2dd7ee8b41c996852b721 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 5 May 2026 12:12:47 +0000
Subject: [PATCH 040/191] Bump pytest-codspeed from 4.3.0 to 4.5.0 (#12462)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[//]: # (dependabot-start)
⚠️ **Dependabot is rebasing this PR** ⚠️
Rebasing might not happen immediately, so don't worry if this takes some
time.
Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.
---
[//]: # (dependabot-end)
Bumps [pytest-codspeed](https://github.com/CodSpeedHQ/pytest-codspeed)
from 4.3.0 to 4.5.0.
Release notes
Sourced from pytest-codspeed's
releases.
v4.5.0
What's Changed
This release adds first support for macOS walltime.
Please note that profiling and other instruments are not yet
available on macOS and will come in a later update.
Full Changelog: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v4.4.0...v4.5.0
v4.4.0
What's Changed
We now collect buildtime and runtime environment data to warn users
about differences in their runtime environment when comparing two runs
against one another.
This data includes toolchain metadata like version and build options,
as well as a list of dynamically loaded linked libraries.
New Contributors
Full Changelog: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v4.3.0...v4.4.0
Changelog
Sourced from pytest-codspeed's
changelog.
[4.5.0] - 2026-04-28
⚙️ Internals
[4.4.0] - 2026-04-14
🚀 Features
🐛 Bug Fixes
⚙️ Internals
Commits
2dc7398
Release v4.5.0 🚀
48f8ce8
chore: pre-build macos binary
facf050
chore: bump instrument-hooks submodule to use int32_t as pid
d42de44
ci: add macos integration test
34048c7
Release v4.4.0 🚀
bf7bd37
feat: collect Python toolchain information via instrument hooks
environment API
48a4822
fix: Exclude setup time from benchmark in walltime mode (#114)
aa267f3
ci: bump the python pinned python version
e92999a
fix: fix segfault caused by multiple activate_stack_trampoline
c02b664
chore: add CONTRIBUTING.md
- See full diff in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 261a4d53bad..73bfad054ee 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -183,7 +183,7 @@ pytest==9.0.2
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==4.3.0
+pytest-codspeed==4.5.0
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 0f24c9c2efd..564312f1e5c 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -178,7 +178,7 @@ pytest==9.0.2
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==4.3.0
+pytest-codspeed==4.5.0
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 53d0a12ee2f..09f3ac3d6fd 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -86,7 +86,7 @@ pytest==9.0.2
# -r requirements/lint.in
# pytest-codspeed
# pytest-mock
-pytest-codspeed==4.3.0
+pytest-codspeed==4.5.0
# via -r requirements/lint.in
pytest-mock==3.15.1
# via -r requirements/lint.in
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 4f7b6917386..b189234a462 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -73,7 +73,7 @@ pytest==9.0.2
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==4.3.0
+pytest-codspeed==4.5.0
# via -r requirements/test-common.in
pytest-cov==7.0.0
# via -r requirements/test-common.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index dc924e08f12..ba2892bc3e4 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -108,7 +108,7 @@ pytest==9.0.2
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==4.3.0
+pytest-codspeed==4.5.0
# via -r requirements/test-common.in
pytest-cov==7.0.0
# via -r requirements/test-common.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 13304c307e7..40c27fcb009 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -108,7 +108,7 @@ pytest==9.0.2
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==4.3.0
+pytest-codspeed==4.5.0
# via -r requirements/test-common.in
pytest-cov==7.0.0
# via -r requirements/test-common.in
From 5840d7b1930a93355f48da31b27882de7f011188 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 5 May 2026 12:17:42 +0000
Subject: [PATCH 041/191] Bump pre-commit from 4.5.1 to 4.6.0 (#12464)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [pre-commit](https://github.com/pre-commit/pre-commit) from 4.5.1
to 4.6.0.
Release notes
Sourced from pre-commit's
releases.
pre-commit v4.6.0
Features
pre-commit hook-impl: allow --hook-dir to
be missing to enable easier usage with git 2.54+ git hooks.
Fixes
pre-commit hook-impl: --hook-type is
required.
Changelog
Sourced from pre-commit's
changelog.
4.6.0 - 2026-04-21
Features
pre-commit hook-impl: allow --hook-dir to
be missing to enable easier
usage with git 2.54+ git hooks.
Fixes
pre-commit hook-impl: --hook-type is
required.
Commits
f35134b
v4.6.0
2a51ffc
Merge pull request #3662
from pre-commit/hook-impl-optional-hook-dir
d7dee32
make --hook-dir optional for hook-impl
965aeb1
Merge pull request #3661
from pre-commit/hook-impl-required
2eacc06
--hook-type is required for hook-impl
f5678bf
Merge pull request #3657
from pre-commit/pre-commit-ci-update-config
054cc5b
[pre-commit.ci] pre-commit autoupdate
5c0f302
Merge pull request #3652
from pre-commit/pre-commit-ci-update-config
a5d9114
[pre-commit.ci] pre-commit autoupdate
129a1f5
Merge pull request #3641
from pre-commit/mxr-patch-1
- Additional commits viewable in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 73bfad054ee..0d80c99aa17 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -148,7 +148,7 @@ pluggy==1.6.0
# via
# pytest
# pytest-cov
-pre-commit==4.5.1
+pre-commit==4.6.0
# via -r requirements/lint.in
propcache==0.4.1
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 564312f1e5c..cb93344d441 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -145,7 +145,7 @@ pluggy==1.6.0
# via
# pytest
# pytest-cov
-pre-commit==4.5.1
+pre-commit==4.6.0
# via -r requirements/lint.in
propcache==0.4.1
# via
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 09f3ac3d6fd..a59e7dcbb60 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -67,7 +67,7 @@ platformdirs==4.9.2
# virtualenv
pluggy==1.6.0
# via pytest
-pre-commit==4.5.1
+pre-commit==4.6.0
# via -r requirements/lint.in
pycares==4.11.0
# via aiodns
From 929a286eef3869505e15ccde0bb8750e950589fe Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 5 May 2026 12:37:18 +0000
Subject: [PATCH 042/191] Bump pytest-cov from 7.0.0 to 7.1.0 (#12453)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 7.0.0
to 7.1.0.
Changelog
Sourced from pytest-cov's
changelog.
7.1.0 (2026-03-21)
-
Fixed total coverage computation to always be consistent, regardless
of reporting settings.
Previously some reports could produce different total counts, and
consequently can make --cov-fail-under behave different depending on
reporting options.
See [#641](https://github.com/pytest-dev/pytest-cov/issues/641)
<https://github.com/pytest-dev/pytest-cov/issues/641>_.
-
Improve handling of ResourceWarning from sqlite3.
The plugin adds warning filter for sqlite3
ResourceWarning unclosed database (since 6.2.0).
It checks if there is already existing plugin for this message by
comparing filter regular expression.
When filter is specified on command line the message is escaped and does
not match an expected message.
A check for an escaped regular expression is added to handle this
case.
With this fix one can suppress ResourceWarning from
sqlite3 from command line::
pytest -W "ignore:unclosed database in <sqlite3.Connection
object at:ResourceWarning" ...
-
Various improvements to documentation.
Contributed by Art Pelling in
[#718](https://github.com/pytest-dev/pytest-cov/issues/718)
<https://github.com/pytest-dev/pytest-cov/pull/718>_ and
"vivodi" in
[#738](https://github.com/pytest-dev/pytest-cov/issues/738)
<https://github.com/pytest-dev/pytest-cov/pull/738>.
Also closed
[#736](https://github.com/pytest-dev/pytest-cov/issues/736)
<https://github.com/pytest-dev/pytest-cov/issues/736>.
-
Fixed some assertions in tests.
Contributed by in Markéta Machová in
[#722](https://github.com/pytest-dev/pytest-cov/issues/722)
<https://github.com/pytest-dev/pytest-cov/pull/722>_.
-
Removed unnecessary coverage configuration copying (meant as a backup
because reporting commands had configuration side-effects before
coverage 5.0).
Commits
66c8a52
Bump version: 7.0.0 → 7.1.0
f707662
Make the examples use pypy 3.11.
6049a78
Make context test use the old ctracer (seems the new sysmon tracer
behaves di...
8ebf20b
Update changelog.
861d30e
Remove the backup context manager - shouldn't be needed since coverage
5.0, ...
fd4c956
Pass the precision on the nulled total (seems that there's some caching
goion...
78c9c4e
Only run the 3.9 on older deps.
4849a92
Punctuation.
197c35e
Update changelog and hopefully I don't forget to publish release again
:))
14dc1c9
Update examples to use 3.11 and make the adhoc layout example look a bit
more...
- Additional commits viewable in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 0d80c99aa17..d696c55c624 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -187,7 +187,7 @@ pytest-codspeed==4.5.0
# via
# -r requirements/lint.in
# -r requirements/test-common.in
-pytest-cov==7.0.0
+pytest-cov==7.1.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index cb93344d441..43550edc62a 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -182,7 +182,7 @@ pytest-codspeed==4.5.0
# via
# -r requirements/lint.in
# -r requirements/test-common.in
-pytest-cov==7.0.0
+pytest-cov==7.1.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index b189234a462..28caba8dc46 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -75,7 +75,7 @@ pytest==9.0.2
# pytest-xdist
pytest-codspeed==4.5.0
# via -r requirements/test-common.in
-pytest-cov==7.0.0
+pytest-cov==7.1.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index ba2892bc3e4..4a4aaa6e44f 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -110,7 +110,7 @@ pytest==9.0.2
# pytest-xdist
pytest-codspeed==4.5.0
# via -r requirements/test-common.in
-pytest-cov==7.0.0
+pytest-cov==7.1.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 40c27fcb009..999599c98d8 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -110,7 +110,7 @@ pytest==9.0.2
# pytest-xdist
pytest-codspeed==4.5.0
# via -r requirements/test-common.in
-pytest-cov==7.0.0
+pytest-cov==7.1.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
From 437adfd8aba93ddda3d7b8e6c2b187526227c8cb Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 6 May 2026 10:59:53 +0000
Subject: [PATCH 043/191] Bump attrs from 25.4.0 to 26.1.0 (#12467)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [attrs](https://github.com/python-attrs/attrs) from 25.4.0 to
26.1.0.
Release notes
Sourced from attrs's
releases.
26.1.0
Highlights
The main outward change here only affects people using field
transformers, but it should be a nice quality of life improvement!
Full changelog below!
Special Thanks
This release would not be possible without my generous sponsors!
Thank you to all of you making sustainable maintenance possible! If
you would like to join them, go to https://github.com/sponsors/hynek
and check out the sweet perks!
Above and Beyond
Variomedia AG (@variomedia), Tidelift (@tidelift), Kraken Tech (@kraken-tech), Privacy Solutions GmbH (@privacy-solutions), FilePreviews (@filepreviews), Ecosystems (@ecosyste-ms), TestMu AI Open Source Office (Formerly
LambdaTest) (@LambdaTest-Inc), Doist (@Doist), Daniel Fortunov (@asqui), and Kevin P. Fleming (@kpfleming).
Maintenance Sustainers
Buttondown (@buttondown), Christopher Dignam (@chdsbd), Magnus Watn (@magnuswatn), David Cramer (@dcramer), Rivo Laks (@rivol), Polar (@polarsource), Mike Fiedler (@miketheman), Duncan Hill (@cricalix), Colin Marquardt (@cmarqu), Pieter Swinkels (@swinkels), Nick Libertini (@libertininick), Brian M. Dennis (@crossjam), Celebrity News AG (@celebritynewsag), The Westervelt Company (@westerveltco), Sławomir Ehlert (@slafs), Mostafa Khalil (@khadrawy), Filip Mularczyk (@mukiblejlok), Thomas Klinger
(@thmsklngr), Andreas Poehlmann (@ap--), August Trapper Bigelow (@atbigelow), Carlton Gibson (@carltongibson), and Roboflow (@roboflow).
Full Changelog
Backwards-incompatible Changes
- Field aliases are now resolved before calling
field_transformer, so transformers receive fully populated
Attribute objects with usable alias values
instead of None. The new
Attribute.alias_is_default flag indicates whether the alias
was auto-generated (True) or explicitly set by the user
(False). #1509
Changes
- Fix type annotations for
attrs.validators.optional(),
so it no longer rejects tuples with more than one validator. #1496
- The
attrs.validators.disabled() contextmanager can now
be nested. #1513
- Frozen classes can set
on_setattr=attrs.setters.NO_OP
in addition to None. #1515
- It's now possible to pass attrs instances
in addition to attrs classes to
attrs.fields(). #1529
This release contains contributions from @bysiber, @DavidCEllis, @finite-state-machine,
@hynek, @veeceey, and @vstinner.
Artifact Attestations
You can verify this release's artifact
attestions using GitHub's CLI
tool by downloading the sdist and wheel from PyPI and running:
$ gh attestation verify --owner python-attrs
attrs-26.1.0.tar.gz
... (truncated)
Changelog
Sourced from attrs's
changelog.
26.1.0 -
2026-03-19
Backwards-incompatible Changes
- Field aliases are now resolved before calling
field_transformer, so transformers receive fully populated
Attribute objects with usable alias values
instead of None.
The new Attribute.alias_is_default flag indicates whether
the alias was auto-generated (True) or explicitly set by
the user (False).
#1509
Changes
- Fix type annotations for
attrs.validators.optional(),
so it no longer rejects tuples with more than one validator.
#1496
- The
attrs.validators.disabled() contextmanager can now
be nested.
#1513
- Frozen classes can set
on_setattr=attrs.setters.NO_OP
in addition to None.
#1515
- It's now possible to pass attrs instances
in addition to attrs classes to
attrs.fields().
#1529
Commits
7bfc49e
Prepare 26.1.0
31e0286
Update test_validators.py for Python 3.15a7 (#1530)
48b8611
Add instance support to attrs.fields() (#1529)
3a68d49
dev: document missing git tags failure mode
a572c3a
Allow field(on_setattr=NO_OP) on frozen classes
af9c510
Fix validators.disabled() to save/restore state on nesting (#1513)
ab7f8b2
update dev
ce89f5d
Fix message passing in frozen errors
eccd966
Fix optional validator to accept tuples of len > 1 (#1496)
e92fe52
policies: tighten screws (#1528)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/runtime-deps.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
7 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index b90f244de52..1f6f712fee4 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -12,7 +12,7 @@ aiosignal==1.4.0
# via -r requirements/runtime-deps.in
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
-attrs==25.4.0
+attrs==26.1.0
# via -r requirements/runtime-deps.in
backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
# via -r requirements/runtime-deps.in
diff --git a/requirements/base.txt b/requirements/base.txt
index a0cf0908ef5..a8a4c998885 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -12,7 +12,7 @@ aiosignal==1.4.0
# via -r requirements/runtime-deps.in
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
-attrs==25.4.0
+attrs==26.1.0
# via -r requirements/runtime-deps.in
backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
# via -r requirements/runtime-deps.in
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index d696c55c624..e5ed250ccd9 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -22,7 +22,7 @@ async-timeout==5.0.1 ; python_version < "3.11"
# via
# -r requirements/runtime-deps.in
# valkey
-attrs==25.4.0
+attrs==26.1.0
# via -r requirements/runtime-deps.in
babel==2.18.0
# via sphinx
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 43550edc62a..7942783e26f 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -22,7 +22,7 @@ async-timeout==5.0.1 ; python_version < "3.11"
# via
# -r requirements/runtime-deps.in
# valkey
-attrs==25.4.0
+attrs==26.1.0
# via -r requirements/runtime-deps.in
babel==2.18.0
# via sphinx
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index dc6ff7fbdfe..9eb626271a1 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -12,7 +12,7 @@ aiosignal==1.4.0
# via -r requirements/runtime-deps.in
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
-attrs==25.4.0
+attrs==26.1.0
# via -r requirements/runtime-deps.in
backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
# via -r requirements/runtime-deps.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 4a4aaa6e44f..77bef1382ed 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -14,7 +14,7 @@ annotated-types==0.7.0
# via pydantic
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
-attrs==25.4.0
+attrs==26.1.0
# via -r requirements/runtime-deps.in
backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
# via -r requirements/runtime-deps.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 999599c98d8..a2533087c0e 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -14,7 +14,7 @@ annotated-types==0.7.0
# via pydantic
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
-attrs==25.4.0
+attrs==26.1.0
# via -r requirements/runtime-deps.in
backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
# via -r requirements/runtime-deps.in
From b740ddec27a92667e004811b972eb8896704f53b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 6 May 2026 11:41:19 +0000
Subject: [PATCH 044/191] Bump virtualenv from 21.1.0 to 21.3.1 (#12469)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.1.0 to
21.3.1.
Release notes
Sourced from virtualenv's
releases.
21.3.1
What's Changed
Full Changelog: https://github.com/pypa/virtualenv/compare/21.3.0...21.3.1
21.3.0
What's Changed
New Contributors
Full Changelog: https://github.com/pypa/virtualenv/compare/21.2.4...21.3.0
21.2.4
What's Changed
Full Changelog: https://github.com/pypa/virtualenv/compare/21.2.3...21.2.4
21.2.3
Full Changelog: https://github.com/pypa/virtualenv/compare/21.2.2...21.2.3
21.2.2
What's Changed
... (truncated)
Changelog
Sourced from virtualenv's
changelog.
Bugfixes - 21.3.1
-
Upgrade embedded wheels:
- pip to
26.1.1 from 26.1
(:issue:3138)
v21.3.0 (2026-04-27)
Features - 21.3.0
- Re-introduce
xonsh shell activator
(activate.xsh) previously removed in 20.7.0, and make the
plugin loader
prefer virtualenv's built-in entry points so a third-party package
cannot override them by registering a duplicate
name. (:issue:3003)
Bugfixes - 21.3.0
-
Upgrade embedded wheels:
- pip to
26.1 (:issue:3132)
v21.2.4 (2026-04-14)
Bugfixes - 21.2.4
- Security hardening: validate each entry of a seed wheel archive
before extracting it so a tampered wheel cannot escape
the app-data image directory via an absolute path or
..
traversal. (:issue:3118)
- Security hardening: verify the SHA-256 of every bundled seed wheel
when it is loaded so a corrupted or tampered file
on disk fails loud instead of being handed to pip. The hash table is
generated alongside
BUNDLE_SUPPORT by
tasks/upgrade_wheels.py. (:issue:3119)
- Security hardening: validate the distribution name and version
specifier passed to
pip download when acquiring a
seed wheel so extras, pip flags, or shell metacharacters cannot be
smuggled into the subprocess command line.
(:issue:3120)
- Security hardening: replace the string-prefix containment check in
virtualenv.util.zipapp with
Path.relative_to so the zipapp extraction helpers refuse
any path that does not resolve under the archive root.
(:issue:3121)
- Security hardening: do not silently fall back to an unverified HTTPS
context when the periodic update request to PyPI
fails TLS verification. The returned metadata drives which wheel version
virtualenv considers "up to date", so
accepting an unverified response lets a network-level attacker suppress
security updates. Set
VIRTUALENV_PERIODIC_UPDATE_INSECURE=1 to restore the
previous behavior on hosts with broken trust stores.
(:issue:3122)
... (truncated)
Commits
12ab495
release 21.3.1
22eadc4
[pre-commit.ci] pre-commit autoupdate (#3137)
6651daf
🐛 fix(seed): bump embedded pip to 26.1.1 (#3138)
936a36a
👷 ci: retry transient apt failures on Linux (#3139)
cb5a7d1
[pre-commit.ci] pre-commit autoupdate (#3133)
e917cc2
release 21.3.0
21152f1
Upgrade embedded pip/setuptools/wheel (#3132)
096bdcd
chore(deps): bump astral-sh/setup-uv from 8.0.0 to 8.1.0 (#3131)
01610dc
docs: Add usage instruction for Xonsh activation (#3130)
fb6ec7c
🐛 fix(test): prevent PowerShell activation test from crashing xdist
workers o...
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 4 ++--
requirements/dev.txt | 4 ++--
requirements/lint.txt | 4 ++--
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index e5ed250ccd9..f5575bf5e25 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -197,7 +197,7 @@ pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
-python-discovery==1.1.0
+python-discovery==1.3.0
# via virtualenv
python-on-whales==0.80.0
# via
@@ -285,7 +285,7 @@ uvloop==0.21.0 ; platform_system != "Windows"
# -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.1.0
+virtualenv==21.3.1
# via pre-commit
wait-for-it==2.3.0
# via -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 7942783e26f..794cfd3d493 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -192,7 +192,7 @@ pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
-python-discovery==1.1.0
+python-discovery==1.3.0
# via virtualenv
python-on-whales==0.80.0
# via
@@ -275,7 +275,7 @@ uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpytho
# -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.1.0
+virtualenv==21.3.1
# via pre-commit
wait-for-it==2.3.0
# via -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index a59e7dcbb60..410be497228 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -92,7 +92,7 @@ pytest-mock==3.15.1
# via -r requirements/lint.in
python-dateutil==2.9.0.post0
# via freezegun
-python-discovery==1.1.0
+python-discovery==1.3.0
# via virtualenv
python-on-whales==0.80.0
# via -r requirements/lint.in
@@ -127,7 +127,7 @@ uvloop==0.21.0 ; platform_system != "Windows"
# via -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.1.0
+virtualenv==21.3.1
# via pre-commit
zlib-ng==1.0.0
# via -r requirements/lint.in
From 2372a0be1e4ddbdd840846760e1aa1d62042b809 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 6 May 2026 11:51:26 +0000
Subject: [PATCH 045/191] Bump setuptools from 82.0.0 to 82.0.1 (#12470)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [setuptools](https://github.com/pypa/setuptools) from 82.0.0 to
82.0.1.
Changelog
Sourced from setuptools's
changelog.
v82.0.1
Bugfixes
- Fix the loading of
launcher manifest.xml file. (#5047)
- Replaced deprecated
json.__version__ with fixture in
tests. (#5186)
Improved Documentation
- Add advice about how to improve predictability when installing
sdists. (#5168)
Misc
Commits
5a13876
Bump version: 82.0.0 → 82.0.1
51ab8f1
Avoid using (deprecated) 'json.version' in tests (#5194)
f9c37b2
Docs/CI: Fix intersphinx references (#5195)
8173db2
Docs: Fix intersphinx references
09bafbc
Fix past tense on newsfragment
461ea56
Add news fragment
c4ffe53
Avoid using (deprecated) 'json.version' in tests
749258b
Cleanup pkg_resources dependencies and configuration (#5175)
2019c16
Parse ext-module.define-macros from
pyproject.toml as list of tuples (#5169)
b809c86
Sync setuptools schema with validate-pyproject (#5157)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index f5575bf5e25..535610671a7 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -301,5 +301,5 @@ zlib-ng==1.0.0
# The following packages are considered to be unsafe in a requirements file:
pip==26.0.1
# via pip-tools
-setuptools==82.0.0
+setuptools==82.0.1
# via pip-tools
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 794cfd3d493..c75165bee76 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -291,5 +291,5 @@ zlib-ng==1.0.0
# The following packages are considered to be unsafe in a requirements file:
pip==26.0.1
# via pip-tools
-setuptools==82.0.0
+setuptools==82.0.1
# via pip-tools
From 8b3a6085b1bcc531075da3ae7ad140ead8dbff00 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 6 May 2026 12:08:54 +0000
Subject: [PATCH 046/191] Bump pygments from 2.19.2 to 2.20.0 (#12475)
Bumps [pygments](https://github.com/pygments/pygments) from 2.19.2 to
2.20.0.
Release notes
Sourced from pygments's
releases.
2.20.0
-
New lexers:
-
Updated lexers:
- archetype: Fix catastrophic backtracking in GUID and ID patterns (#3064)
- ASN.1: Recognize minus sign and fix range operator (#3014,
#3060)
- C++: Add C++26 keywords (#2955),
add integer literal suffixes (#2966)
- ComponentPascal: Fix
analyse_text (#3028,
#3032)
- Coq renamed to Rocq (#2883,
#2908)
- Cython: Various improvements (#2932,
#2933)
- Debian control: Improve architecture parsing (#3052)
- Devicetree: Add support for overlay/fragments (#3021),
add bytestring support (#3022),
fix catastrophic backtracking (#3057)
- Fennel: Various improvements (#2911)
- Haskell: Handle escape sequences in character literals (#3069,
#1795)
- Java: Add module keywords (#2955)
- Lean4: Add operators
]', ]?,
]! (#2946)
- LESS: Support single-line comments (#3005)
- LilyPond: Update to 2.25.29 (#2974)
- LLVM: Support C-style comments (#3023,
#2978)
- Lua(u): Fix catastrophic backtracking (#3047)
- Macaulay2: Update to 1.25.05 (#2893),
1.25.11 (#2988)
- Mathematica: Various improvements (#2957)
- meson: Add additional operators (#2919)
- MySQL: Update keywords (#2970)
- org-Mode: Support both schedule and deadline (#2899)
- PHP: Add
__PROPERTY__ magic constant (#2924),
add reserved keywords (#3002)
- PostgreSQL: Add more keywords (#2985)
- protobuf: Fix namespace tokenization (#2929)
- Python: Add
t-string support (#2973,
#3009,
#3010)
- Tablegen: Fix infinite loop (#2972,
#2940)
- Tera Term macro: Add commands introduced in v5.3 through v5.6 (#2951)
- TOML: Support TOML 1.1.0 (#3026,
#3027)
- Turtle: Allow empty comment lines (#2980)
- XML: Added
.xbrl as file ending (#2890,
#2891)
-
Drop Python 3.8, and add Python 3.14 as a supported version (#2987,
#3012)
-
Various improvements to autopygmentize (#2894)
-
Update onedark style to support more token types (#2977)
-
Update rtt style to support more token types (#2895)
-
Cache entry points to improve performance (#2979)
-
Fix xterm-256 color table (#3043)
-
Fix kwargs dictionary getting mutated on each call (#3044)
Changelog
Sourced from pygments's
changelog.
Version 2.20.0
(released March 29th, 2026)
-
New lexers:
-
Updated lexers:
- archetype: Fix catastrophic backtracking in GUID and ID patterns (#3064)
- ASN.1: Recognize minus sign and fix range operator (#3014,
#3060)
- C++: Add C++26 keywords (#2955),
add integer literal suffixes (#2966)
- ComponentPascal: Fix
analyse_text (#3028,
#3032)
- Coq renamed to Rocq (#2883,
#2908)
- Cython: Various improvements (#2932,
#2933)
- Debian control: Improve architecture parsing (#3052)
- Devicetree: Add support for overlay/fragments (#3021),
add bytestring support (#3022),
fix catastrophic backtracking (#3057)
- Fennel: Various improvements (#2911)
- Haskell: Handle escape sequences in character literals (#3069,
#1795)
- Java: Add module keywords (#2955)
- Lean4: Add operators
]', ]?,
]! (#2946)
- LESS: Support single-line comments (#3005)
- LilyPond: Update to 2.25.29 (#2974)
- LLVM: Support C-style comments (#3023,
#2978)
- Lua(u): Fix catastrophic backtracking (#3047)
- Macaulay2: Update to 1.25.05 (#2893),
1.25.11 (#2988)
- Mathematica: Various improvements (#2957)
- meson: Add additional operators (#2919)
- MySQL: Update keywords (#2970)
- org-Mode: Support both schedule and deadline (#2899)
- PHP: Add
__PROPERTY__ magic constant (#2924),
add reserved keywords (#3002)
- PostgreSQL: Add more keywords (#2985)
- protobuf: Fix namespace tokenization (#2929)
- Python: Add
t-string support (#2973,
#3009,
#3010)
- Tablegen: Fix infinite loop (#2972,
#2940)
- Tera Term macro: Add commands introduced in v5.3 through v5.6 (#2951)
- TOML: Support TOML 1.1.0 (#3026,
#3027)
- Turtle: Allow empty comment lines (#2980)
- XML: Added
.xbrl as file ending (#2890,
#2891)
-
Drop Python 3.8, and add Python 3.14 as a supported version (#2987,
#3012)
-
Various improvements to autopygmentize (#2894)
-
Update onedark style to support more token types (#2977)
-
Update rtt style to support more token types (#2895)
-
Cache entry points to improve performance (#2979)
-
Fix xterm-256 color table (#3043)
-
Fix kwargs dictionary getting mutated on each call (#3044)
Commits
708197d
Fix underline length.
1d4538a
Prepare 2.20 release.
2ceaee4
Update CHANGES.
e3a3c54
Fix Haskell lexer: handle escape sequences in character literals (#3069)
d7c3453
Merge pull request #3071
from pygments/harden-html-formatter
0f97e7c
Harden the HTML formatter against CSS.
9f981b2
Update CHANGES.
1d88915
Update CHANGES.
c3d93ad
Fix ASN.1 lexer: recognize minus sign and fix range operator (#3060)
4f06bcf
fix bad behaving backtracking regex in CommonLispLexer
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
8 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 535610671a7..2c81c25bd66 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -166,7 +166,7 @@ pydantic-core==2.46.3
# via pydantic
pyenchant==3.3.0
# via sphinxcontrib-spelling
-pygments==2.19.2
+pygments==2.20.0
# via
# pytest
# rich
diff --git a/requirements/dev.txt b/requirements/dev.txt
index c75165bee76..61a4ae0962f 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -161,7 +161,7 @@ pydantic==2.13.3
# via python-on-whales
pydantic-core==2.46.3
# via pydantic
-pygments==2.19.2
+pygments==2.20.0
# via
# pytest
# rich
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index 080bbd7f499..a95f384fc40 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -32,7 +32,7 @@ packaging==26.2
# via sphinx
pyenchant==3.3.0
# via sphinxcontrib-spelling
-pygments==2.19.2
+pygments==2.20.0
# via sphinx
requests==2.33.1
# via
diff --git a/requirements/doc.txt b/requirements/doc.txt
index b52661badb7..aec4d0e8e80 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -30,7 +30,7 @@ markupsafe==3.0.3
# via jinja2
packaging==26.2
# via sphinx
-pygments==2.19.2
+pygments==2.20.0
# via sphinx
requests==2.33.1
# via sphinx
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 410be497228..7fb30b7affa 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -77,7 +77,7 @@ pydantic==2.13.3
# via python-on-whales
pydantic-core==2.46.3
# via pydantic
-pygments==2.19.2
+pygments==2.20.0
# via
# pytest
# rich
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 28caba8dc46..b9bf8359dc1 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -62,7 +62,7 @@ pydantic==2.13.3
# via python-on-whales
pydantic-core==2.46.3
# via pydantic
-pygments==2.19.2
+pygments==2.20.0
# via
# pytest
# rich
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 77bef1382ed..7be26f35a61 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -97,7 +97,7 @@ pydantic==2.13.3
# via python-on-whales
pydantic-core==2.46.3
# via pydantic
-pygments==2.19.2
+pygments==2.20.0
# via
# pytest
# rich
diff --git a/requirements/test.txt b/requirements/test.txt
index a2533087c0e..89f62f3d281 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -97,7 +97,7 @@ pydantic==2.13.3
# via python-on-whales
pydantic-core==2.46.3
# via pydantic
-pygments==2.19.2
+pygments==2.20.0
# via
# pytest
# rich
From be99ca1c406d9284b62da59769292aed9d913eeb Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 6 May 2026 12:10:42 +0000
Subject: [PATCH 047/191] Bump tomli from 2.4.0 to 2.4.1 (#12473)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [tomli](https://github.com/hukkin/tomli) from 2.4.0 to 2.4.1.
Changelog
Sourced from tomli's
changelog.
2.4.1
- Fixed
- Limit number of parts of a TOML key to address quadratic time
complexity
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
8 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 2c81c25bd66..74a43b6df33 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -244,7 +244,7 @@ sphinxcontrib-spelling==8.0.2 ; platform_system != "Windows"
# via -r requirements/doc-spelling.in
sphinxcontrib-towncrier==0.5.0a0
# via -r requirements/doc.in
-tomli==2.4.0
+tomli==2.4.1
# via
# build
# coverage
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 61a4ae0962f..31255ba538a 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -234,7 +234,7 @@ sphinxcontrib-serializinghtml==2.0.0
# via sphinx
sphinxcontrib-towncrier==0.5.0a0
# via -r requirements/doc.in
-tomli==2.4.0
+tomli==2.4.1
# via
# build
# coverage
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index a95f384fc40..f64de280c8c 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -61,7 +61,7 @@ sphinxcontrib-spelling==8.0.2 ; platform_system != "Windows"
# via -r requirements/doc-spelling.in
sphinxcontrib-towncrier==0.5.0a0
# via -r requirements/doc.in
-tomli==2.4.0
+tomli==2.4.1
# via
# sphinx
# towncrier
diff --git a/requirements/doc.txt b/requirements/doc.txt
index aec4d0e8e80..e09ceb721a3 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -54,7 +54,7 @@ sphinxcontrib-serializinghtml==2.0.0
# via sphinx
sphinxcontrib-towncrier==0.5.0a0
# via -r requirements/doc.in
-tomli==2.4.0
+tomli==2.4.1
# via
# sphinx
# towncrier
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 7fb30b7affa..75cfe9fe809 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -104,7 +104,7 @@ six==1.17.0
# via python-dateutil
slotscheck==0.19.1
# via -r requirements/lint.in
-tomli==2.4.0
+tomli==2.4.1
# via
# mypy
# pytest
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index b9bf8359dc1..9fb6a8a376c 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -95,7 +95,7 @@ setuptools-git==1.2
# via -r requirements/test-common.in
six==1.17.0
# via python-dateutil
-tomli==2.4.0
+tomli==2.4.1
# via
# coverage
# mypy
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 7be26f35a61..e09a0ec7a1e 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -130,7 +130,7 @@ setuptools-git==1.2
# via -r requirements/test-common.in
six==1.17.0
# via python-dateutil
-tomli==2.4.0
+tomli==2.4.1
# via
# coverage
# mypy
diff --git a/requirements/test.txt b/requirements/test.txt
index 89f62f3d281..7538efa214d 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -130,7 +130,7 @@ setuptools-git==1.2
# via -r requirements/test-common.in
six==1.17.0
# via python-dateutil
-tomli==2.4.0
+tomli==2.4.1
# via
# coverage
# mypy
From 6037db9984f5d398faba1e06fd2a389f0d31aa14 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 6 May 2026 12:40:24 +0000
Subject: [PATCH 048/191] Bump pytest from 9.0.2 to 9.0.3 (#12474)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.2 to
9.0.3.
Release notes
Sourced from pytest's
releases.
9.0.3
pytest 9.0.3 (2026-04-07)
Bug fixes
-
#12444:
Fixed pytest.approx which now correctly takes into account
~collections.abc.Mapping keys order to compare them.
-
#13634:
Blocking a conftest.py file using the -p no:
option is now explicitly disallowed.
Previously this resulted in an internal assertion failure during
plugin loading.
Pytest now raises a clear UsageError explaining that
conftest files are not plugins and cannot be disabled via
-p.
-
#13734:
Fixed crash when a test raises an exceptiongroup with
__tracebackhide__ = True.
-
#14195:
Fixed an issue where non-string messages passed to unittest.TestCase.subTest() were not
printed.
-
#14343:
Fixed use of insecure temporary directory (CVE-2025-71176).
Improved documentation
- #13388:
Clarified documentation for
-p vs
PYTEST_PLUGINS plugin loading and fixed an incorrect
-p example.
- #13731:
Clarified that capture fixtures (e.g.
capsys and
capfd) take precedence over the -s /
--capture=no command-line options in Accessing
captured output from a test function
<accessing-captured-output>.
- #14088:
Clarified that the default
pytest_collection hook sets
session.items before it calls
pytest_collection_finish, not after.
- #14255:
TOML integer log levels must be quoted: Updating reference
documentation.
Contributor-facing changes
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 74a43b6df33..edbc63e26fb 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -175,7 +175,7 @@ pyproject-hooks==1.2.0
# via
# build
# pip-tools
-pytest==9.0.2
+pytest==9.0.3
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 31255ba538a..89c2d73a16e 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -170,7 +170,7 @@ pyproject-hooks==1.2.0
# via
# build
# pip-tools
-pytest==9.0.2
+pytest==9.0.3
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 75cfe9fe809..39d3dfaec35 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -81,7 +81,7 @@ pygments==2.20.0
# via
# pytest
# rich
-pytest==9.0.2
+pytest==9.0.3
# via
# -r requirements/lint.in
# pytest-codspeed
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 9fb6a8a376c..88f7b23268a 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -66,7 +66,7 @@ pygments==2.20.0
# via
# pytest
# rich
-pytest==9.0.2
+pytest==9.0.3
# via
# -r requirements/test-common.in
# pytest-codspeed
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index e09a0ec7a1e..9ec4732babd 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -101,7 +101,7 @@ pygments==2.20.0
# via
# pytest
# rich
-pytest==9.0.2
+pytest==9.0.3
# via
# -r requirements/test-common.in
# pytest-codspeed
diff --git a/requirements/test.txt b/requirements/test.txt
index 7538efa214d..0cbfe9463f9 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -101,7 +101,7 @@ pygments==2.20.0
# via
# pytest
# rich
-pytest==9.0.2
+pytest==9.0.3
# via
# -r requirements/test-common.in
# pytest-codspeed
From 77872abd115b79a2be9ff97cebb504ebf43d5df0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 7 May 2026 11:43:32 +0000
Subject: [PATCH 049/191] Bump wheel from 0.46.3 to 0.47.0 (#12483)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [wheel](https://github.com/pypa/wheel) from 0.46.3 to 0.47.0.
Release notes
Sourced from wheel's
releases.
0.47.0
- Added the
wheel info subcommand to display metadata
about wheel files without unpacking them (#639)
- Fixed
WheelFile raising Missing RECORD
file when the wheel filename contains uppercase characters (e.g.
Django-3.2.5.whl) but the .dist-info directory
inside uses normalized lowercase naming (#411)
Changelog
Sourced from wheel's
changelog.
Release Notes
0.47.0 (2026-04-22)
- Added the
wheel info subcommand to display metadata
about wheel files without
unpacking them ([#639](https://github.com/pypa/wheel/issues/639)
<https://github.com/pypa/wheel/issues/639>_)
- Fixed
WheelFile raising Missing RECORD
file when the wheel filename contains
uppercase characters (e.g. Django-3.2.5.whl) but the
.dist-info directory
inside uses normalized lowercase naming
([#411](https://github.com/pypa/wheel/issues/411)
<https://github.com/pypa/wheel/issues/411>_)
0.46.3 (2026-01-22)
- Fixed
ImportError: cannot import name '_setuptools_logging'
from 'wheel' when
installed alongside an old version of setuptools and running the
bdist_wheel
command ([#676](https://github.com/pypa/wheel/issues/676)
<https://github.com/pypa/wheel/issues/676>_)
0.46.2 (2026-01-22)
- Restored the
bdist_wheel command for compatibility with
setuptools older than
v70.1
- Importing
wheel.bdist_wheel now emits a
FutureWarning instead of a
DeprecationWarning
- Fixed
wheel unpack potentially altering the permissions
of files outside of the
destination tree with maliciously crafted wheels (CVE-2026-24049)
0.46.1 (2025-04-08)
- Temporarily restored the
wheel.macosx_libfile module
([#659](https://github.com/pypa/wheel/issues/659)
<https://github.com/pypa/wheel/issues/659>_)
0.46.0 (2025-04-03)
- Dropped support for Python 3.8
- Removed the
bdist_wheel setuptools command
implementation and entry point.
The wheel.bdist_wheel module is now just an alias to
setuptools.command.bdist_wheel, emitting a deprecation
warning on import.
- Removed vendored
packaging in favor of a run-time
dependency on it
- Made the
wheel.metadata module private (with a
deprecation warning if it's
imported
- Made the
wheel.cli package private (no deprecation
warning)
- Fixed an exception when calling the
convert command
with an empty description
field
0.45.1 (2024-11-23)
- Fixed pure Python wheels converted from eggs and wininst files
having the ABI tag in
the file name
... (truncated)
Commits
efd83a7
Created a new release
bb69216
Reordered the changelog entries
d5a1763
fix(wheelfile): resolve .dist-info path case-insensitively when reading
wheel...
5718957
[pre-commit.ci] pre-commit autoupdate (#685)
6258068
chore: log_level is better than log_cli_level (#684)
2975deb
Require tox >= 4.22
47674ba
chore: add check-sdist to checks (#681)
56223f6
__package__ → __spec__.parent (#679)
0ce509e
Added the wheel info subcommand (#669)
39039c0
Improved the index page
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index edbc63e26fb..5413fdc353d 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -289,7 +289,7 @@ virtualenv==21.3.1
# via pre-commit
wait-for-it==2.3.0
# via -r requirements/test-common.in
-wheel==0.46.3
+wheel==0.47.0
# via pip-tools
yarl==1.22.0
# via -r requirements/runtime-deps.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 89c2d73a16e..c961fafe55c 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -279,7 +279,7 @@ virtualenv==21.3.1
# via pre-commit
wait-for-it==2.3.0
# via -r requirements/test-common.in
-wheel==0.46.3
+wheel==0.47.0
# via pip-tools
yarl==1.22.0
# via -r requirements/runtime-deps.in
From e1dc6655f25de08815515355d77aca629580e4de Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 7 May 2026 11:45:47 +0000
Subject: [PATCH 050/191] Bump librt from 0.9.0 to 0.10.0 (#12484)
Bumps [librt](https://github.com/mypyc/librt) from 0.9.0 to 0.10.0.
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 5413fdc353d..b2930c47173 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -106,7 +106,7 @@ jinja2==3.1.6
# via
# sphinx
# towncrier
-librt==0.9.0
+librt==0.10.0
# via mypy
markdown-it-py==4.0.0
# via rich
diff --git a/requirements/dev.txt b/requirements/dev.txt
index c961fafe55c..a787c04fdaa 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -104,7 +104,7 @@ jinja2==3.1.6
# via
# sphinx
# towncrier
-librt==0.9.0
+librt==0.10.0
# via mypy
markdown-it-py==4.0.0
# via rich
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 39d3dfaec35..56b407e0345 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -45,7 +45,7 @@ iniconfig==2.3.0
# via pytest
isal==1.7.2
# via -r requirements/lint.in
-librt==0.9.0
+librt==0.10.0
# via mypy
markdown-it-py==4.0.0
# via rich
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 88f7b23268a..e2519fd4a6b 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -34,7 +34,7 @@ iniconfig==2.3.0
# via pytest
isal==1.8.0 ; python_version < "3.14"
# via -r requirements/test-common.in
-librt==0.9.0
+librt==0.10.0
# via mypy
markdown-it-py==4.0.0
# via rich
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 9ec4732babd..b6299d50600 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -57,7 +57,7 @@ iniconfig==2.3.0
# via pytest
isal==1.8.0 ; python_version < "3.14"
# via -r requirements/test-common.in
-librt==0.9.0
+librt==0.10.0
# via mypy
markdown-it-py==4.0.0
# via rich
diff --git a/requirements/test.txt b/requirements/test.txt
index 0cbfe9463f9..623135eb77a 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -57,7 +57,7 @@ iniconfig==2.3.0
# via pytest
isal==1.7.2 ; python_version < "3.14"
# via -r requirements/test-common.in
-librt==0.9.0
+librt==0.10.0
# via mypy
markdown-it-py==4.0.0
# via rich
From 5ee867ad78a469ebe5922dc7811bb7cbbcf55f9c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 7 May 2026 12:04:40 +0000
Subject: [PATCH 051/191] Bump pip from 26.0.1 to 26.1.1 (#12481)
Bumps [pip](https://github.com/pypa/pip) from 26.0.1 to 26.1.1.
Changelog
Sourced from pip's
changelog.
26.1.1 (2026-05-04)
Bug Fixes
- Fix issue where uninstallation left behind empty directories. Revert
the
removal of the adjacent
__pycache__ directory when a .py
file is removed.
([#13973](https://github.com/pypa/pip/issues/13973)
<https://github.com/pypa/pip/issues/13973>_)
26.1 (2026-04-26)
Deprecations and Removals
- Drop support for Python 3.9.
(
[#13795](https://github.com/pypa/pip/issues/13795)
<https://github.com/pypa/pip/issues/13795>_)
Features
- Add experimental support to read requirements from standardized
pylock.toml files (
-r pylock.toml).
([#13876](https://github.com/pypa/pip/issues/13876)
<https://github.com/pypa/pip/issues/13876>_)
- Allow
--uploaded-prior-to to accept a duration in days
(e.g., P3D for 3 days ago).
([#13674](https://github.com/pypa/pip/issues/13674)
<https://github.com/pypa/pip/issues/13674>_)
Enhancements
- Speed up dependency resolution when there are complex conflicts.
(
[#13859](https://github.com/pypa/pip/issues/13859)
<https://github.com/pypa/pip/issues/13859>_)
- Reduce memory usage when resolving large dependency trees.
(
[#13843](https://github.com/pypa/pip/issues/13843)
<https://github.com/pypa/pip/issues/13843>_)
- Emit a deprecation warning when pip imports an unexpected module
after
installation of a distribution has started.
(
[#13912](https://github.com/pypa/pip/issues/13912)
<https://github.com/pypa/pip/issues/13912>_)
- Allow URL constraints to apply to requirements with extras.
(
[#12018](https://github.com/pypa/pip/issues/12018)
<https://github.com/pypa/pip/issues/12018>_)
- Allow unpinned requirements to use hashes from constraints.
Constraints
like
{name}=={version} --hash=... feeds into hash
verification for
a corresponding requirement.
([#9243](https://github.com/pypa/pip/issues/9243)
<https://github.com/pypa/pip/issues/9243>_)
- Improve conflict reports that involve direct URLs.
(
[#13932](https://github.com/pypa/pip/issues/13932)
<https://github.com/pypa/pip/issues/13932>_)
- Show all errors instead of first error for faulty
dependency_groups definitions.
([#13917](https://github.com/pypa/pip/issues/13917)
<https://github.com/pypa/pip/issues/13917>_)
Bug Fixes
- Fix recovery hint for missing RECORD file to use
--ignore-installed
instead of --force-reinstall.
([#12645](https://github.com/pypa/pip/issues/12645)
<https://github.com/pypa/pip/issues/12645>_)
- Fix misleading error message when a constraint file cannot be
opened. (
[#13226](https://github.com/pypa/pip/issues/13226)
<https://github.com/pypa/pip/issues/13226>_)
- Show the filename rather than the full URL when downloading files
from non-PyPI indexes in non-verbose mode.
(
[#13494](https://github.com/pypa/pip/issues/13494)
<https://github.com/pypa/pip/issues/13494>_)
- Remove the adjacent
__pycache__ directory when a .py
file is removed.
([#13725](https://github.com/pypa/pip/issues/13725)
<https://github.com/pypa/pip/issues/13725>_)
- Force UTF-8 encoding for :pep:
723 metadata.
([#13861](https://github.com/pypa/pip/issues/13861)
<https://github.com/pypa/pip/issues/13861>_)
- Minor performance improvement when filtering candidates during
resolution. (
[#13916](https://github.com/pypa/pip/issues/13916)
<https://github.com/pypa/pip/issues/13916>_)
- Fix a hang on Windows when stdout is closed during verbose output.
(
[#13927](https://github.com/pypa/pip/issues/13927)
<https://github.com/pypa/pip/issues/13927>_)
- Common path prefixes are determined by path segment, not character
by character. (
[#13847](https://github.com/pypa/pip/issues/13847)
<https://github.com/pypa/pip/issues/13847>_)
- Fix installing
.tar.gz source distributions that look
like a zip file.
([#13867](https://github.com/pypa/pip/issues/13867)
<https://github.com/pypa/pip/issues/13867>_)
... (truncated)
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index b2930c47173..7353c035354 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -299,7 +299,7 @@ zlib-ng==1.0.0
# -r requirements/test-common.in
# The following packages are considered to be unsafe in a requirements file:
-pip==26.0.1
+pip==26.1.1
# via pip-tools
setuptools==82.0.1
# via pip-tools
diff --git a/requirements/dev.txt b/requirements/dev.txt
index a787c04fdaa..8355ced4e2d 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -289,7 +289,7 @@ zlib-ng==1.0.0
# -r requirements/test-common.in
# The following packages are considered to be unsafe in a requirements file:
-pip==26.0.1
+pip==26.1.1
# via pip-tools
setuptools==82.0.1
# via pip-tools
From e6938c82850fde062bba092c0447e401cab729fa Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 8 May 2026 11:08:45 +0000
Subject: [PATCH 052/191] Bump urllib3 from 2.6.3 to 2.7.0 (#12488)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.6.3 to 2.7.0.
Release notes
Sourced from urllib3's
releases.
2.7.0
🚀 urllib3 is fundraising for HTTP/2 support
urllib3
is raising ~$40,000 USD to release HTTP/2 support and ensure
long-term sustainable maintenance of the project after a sharp decline
in financial support. If your company or organization uses Python and
would benefit from HTTP/2 support in Requests, pip, cloud SDKs, and
thousands of other projects please consider contributing
financially to ensure HTTP/2 support is developed sustainably and
maintained for the long-haul.
Thank you for your support.
Security
Addressed high-severity security issues. Impact was limited to
specific use cases detailed in the accompanying advisories; overall user
exposure was estimated to be marginal.
-
Decompression-bomb safeguards of the streaming API were bypassed:
- When
HTTPResponse.drain_conn() was called after the
response had been read and decompressed partially. (Reported by @Cycloctane)
- During the second
HTTPResponse.read(amt=N) or
HTTPResponse.stream(amt=N) call when the response was
decompressed using the official Brotli library. (Reported by
@kimkou2024)
See GHSA-mf9v-mfxr-j63j for details.
-
HTTP pools created using
ProxyManager.connection_from_url did not strip sensitive
headers specified in Retry.remove_headers_on_redirect when
redirecting to a different host. (GHSA-qccp-gfcp-xxvc reported by @christos-spearbit)
Deprecations and Removals
- Used
FutureWarning instead of
DeprecationWarning for better visibility of existing
deprecation notices. Rescheduled the removal of deprecated features to
version 3.0. (urllib3/urllib3#3764)
- Removed support for end-of-life Python 3.9. (urllib3/urllib3#3720)
- Removed support for end-of-life PyPy3.10. (urllib3/urllib3#4979)
- Bumped the minimum supported pyOpenSSL version to 19.0.0. (urllib3/urllib3#3777)
Bugfixes
- Fixed a bug where
HTTPResponse.read(amt=None) was
ignoring decompressed data buffered from previous partial reads. (urllib3/urllib3#3636)
- Fixed a bug where
HTTPResponse.read() could cache only
part of the response after a partial read when
cache_content=True. (urllib3/urllib3#4967)
- Fixed
HTTPResponse.stream() and
HTTPResponse.read_chunked() to handle amt=0.
(urllib3/urllib3#3793)
- Updated
_TYPE_BODY type alias to include missing
Iterable[str], matching the documented and runtime behavior
of chunked request bodies. (urllib3/urllib3#3798)
- Fixed
LocationParseError when paths resembling
schemeless URIs were passed to
HTTPConnectionPool.urlopen(). (urllib3/urllib3#3352)
- Fixed
BaseHTTPResponse.readinto() type annotation to
accept memoryview in addition to bytearray,
matching the io.RawIOBase.readinto contract and enabling
use with io.BufferedReader without type errors. (urllib3/urllib3#3764)
Changelog
Sourced from urllib3's
changelog.
2.7.0 (2026-05-07)
Security
Addressed high-severity security issues.
Impact was limited to specific use cases detailed in the accompanying
advisories; overall user exposure was estimated to be marginal.
-
Decompression-bomb safeguards of the streaming API were bypassed:
- When
HTTPResponse.drain_conn() was called after the
response had been
read and decompressed partially.
- During the second
HTTPResponse.read(amt=N) or
HTTPResponse.stream(amt=N) call when the response was
decompressed
using the official Brotli
<https://pypi.org/project/brotli/>__ library.
See GHSA-mf9v-mfxr-j63j
<https://github.com/urllib3/urllib3/security/advisories/GHSA-mf9v-mfxr-j63j>__
for details.
-
HTTP pools created using
ProxyManager.connection_from_url did not strip
sensitive headers specified in
Retry.remove_headers_on_redirect when
redirecting to a different host.
(GHSA-qccp-gfcp-xxvc
<https://github.com/urllib3/urllib3/security/advisories/GHSA-qccp-gfcp-xxvc>__)
Deprecations and Removals
- Used
FutureWarning instead of
DeprecationWarning for better
visibility of existing deprecation notices. Rescheduled the removal of
deprecated features to version 3.0.
([#3764](https://github.com/urllib3/urllib3/issues/3764)
<https://github.com/urllib3/urllib3/issues/3764>__)
- Removed support for end-of-life Python 3.9.
(
[#3720](https://github.com/urllib3/urllib3/issues/3720)
<https://github.com/urllib3/urllib3/issues/3720>__)
- Removed support for end-of-life PyPy3.10.
(
[#4979](https://github.com/urllib3/urllib3/issues/4979)
<https://github.com/urllib3/urllib3/issues/4979>__)
- Bumped the minimum supported pyOpenSSL version to 19.0.0.
(
[#3777](https://github.com/urllib3/urllib3/issues/3777)
<https://github.com/urllib3/urllib3/issues/3777>__)
Bugfixes
- Fixed a bug where
HTTPResponse.read(amt=None) was
ignoring decompressed
data buffered from previous partial reads.
([#3636](https://github.com/urllib3/urllib3/issues/3636)
<https://github.com/urllib3/urllib3/issues/3636>__)
- Fixed a bug where
HTTPResponse.read() could cache only
part of the
response after a partial read when cache_content=True.
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 7353c035354..3f591426905 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -277,7 +277,7 @@ typing-extensions==4.15.0 ; python_version < "3.13"
# virtualenv
typing-inspection==0.4.2
# via pydantic
-urllib3==2.6.3
+urllib3==2.7.0
# via requests
uvloop==0.21.0 ; platform_system != "Windows"
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 8355ced4e2d..39b38894931 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -267,7 +267,7 @@ typing-extensions==4.15.0 ; python_version < "3.13"
# virtualenv
typing-inspection==0.4.2
# via pydantic
-urllib3==2.6.3
+urllib3==2.7.0
# via requests
uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpython"
# via
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index f64de280c8c..88eea72170c 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -69,5 +69,5 @@ towncrier==25.8.0
# via
# -r requirements/doc.in
# sphinxcontrib-towncrier
-urllib3==2.6.3
+urllib3==2.7.0
# via requests
diff --git a/requirements/doc.txt b/requirements/doc.txt
index e09ceb721a3..4cff982765e 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -62,5 +62,5 @@ towncrier==25.8.0
# via
# -r requirements/doc.in
# sphinxcontrib-towncrier
-urllib3==2.6.3
+urllib3==2.7.0
# via requests
From 1cddba9efff057866872d017a6d1f7ef634bab65 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 8 May 2026 11:12:34 +0000
Subject: [PATCH 053/191] Bump filelock from 3.25.0 to 3.29.0 (#12487)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.25.0 to
3.29.0.
Release notes
Sourced from filelock's
releases.
3.29.0
What's Changed
Full Changelog: https://github.com/tox-dev/filelock/compare/3.28.0...3.29.0
3.28.0
What's Changed
Full Changelog: https://github.com/tox-dev/filelock/compare/3.27.0...3.28.0
3.27.0
What's Changed
Full Changelog: https://github.com/tox-dev/filelock/compare/3.26.1...3.27.0
3.26.1
What's Changed
New Contributors
Full Changelog: https://github.com/tox-dev/filelock/compare/3.26.0...3.26.1
3.26.0
What's Changed
Full Changelog: https://github.com/tox-dev/filelock/compare/3.25.2...3.26.0
... (truncated)
Changelog
Sourced from filelock's
changelog.
###########
Changelog
###########
3.29.0 (2026-04-19)
- ✨ feat(soft): enable stale lock detection on Windows
:pr:
534
- 🐛 fix(async): use single-thread executor for lock consistency
:pr:
533
- build(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1
:pr:
530 - by :user:dependabot[bot]
3.28.0 (2026-04-14)
- 🐛 fix(ci): unbreak release workflow, publish to PyPI again
:pr:
529
3.26.1 (2026-04-09)
- 🐛 fix(asyncio): add exit to BaseAsyncFileLock and
fix del loop handling :pr:
518 - by
:user:naarob
- build(deps): bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0
:pr:
525 - by :user:dependabot[bot]
3.26.0 (2026-04-06)
- ✨ feat(soft): add PID inspection and lock breaking
:pr:
524
- [pre-commit.ci] pre-commit autoupdate :pr:
523 - by
:user:pre-commit-ci[bot]
- build(deps): bump astral-sh/setup-uv from 7.6.0 to 8.0.0
:pr:
522 - by :user:dependabot[bot]
- Remove persist-credentials: false from release job
:pr:
520
- [pre-commit.ci] pre-commit autoupdate :pr:
519 - by
:user:pre-commit-ci[bot]
- 🔒 ci(workflows): add zizmor security auditing
:pr:
517
- [pre-commit.ci] pre-commit autoupdate :pr:
516 - by
:user:pre-commit-ci[bot]
- [pre-commit.ci] pre-commit autoupdate :pr:
514 - by
:user:pre-commit-ci[bot]
3.25.2 (2026-03-11)
- 🐛 fix(unix): suppress EIO on close in Docker bind mounts
:pr:
513
3.25.1 (2026-03-09)
- [pre-commit.ci] pre-commit autoupdate :pr:
510 - by
:user:pre-commit-ci[bot]
- 🐛 fix(win): restore best-effort lock file cleanup on release
:pr:
511
... (truncated)
Commits
469b47f
Release 3.29.0
e85d072
✨ feat(soft): enable stale lock detection on Windows (#534)
f5ee171
🐛 fix(async): use single-thread executor for lock consistency (#533)
2a95458
build(deps): bump actions/upload-artifact from 7.0.0 to 7.0.1 (#530)
55de20c
Release 3.28.0
476b0e4
🐛 fix(ci): unbreak release workflow, publish to PyPI again (#529)
824713e
✨ feat(rw): add SoftReadWriteLock for NFS and HPC clusters (#528)
9879de9
[pre-commit.ci] pre-commit autoupdate (#527)
4cfab49
Release 3.26.1
734c9f2
🐛 fix(asyncio): add exit to BaseAsyncFileLock and fix
del loop handli...
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 3f591426905..4c8b5c56687 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -71,7 +71,7 @@ exceptiongroup==1.3.1
# via pytest
execnet==2.1.2
# via pytest-xdist
-filelock==3.25.0
+filelock==3.29.0
# via
# python-discovery
# virtualenv
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 39b38894931..03ff327d70f 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -69,7 +69,7 @@ exceptiongroup==1.3.1
# via pytest
execnet==2.1.2
# via pytest-xdist
-filelock==3.25.0
+filelock==3.29.0
# via
# python-discovery
# virtualenv
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 56b407e0345..134975dd424 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -29,7 +29,7 @@ distlib==0.4.0
# via virtualenv
exceptiongroup==1.3.1
# via pytest
-filelock==3.25.0
+filelock==3.29.0
# via
# python-discovery
# virtualenv
From 5e87b254d6a62192ab9dfe91fd53c22f5e5c6c2c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 8 May 2026 11:34:02 +0000
Subject: [PATCH 054/191] Bump coverage from 7.13.4 to 7.13.5 (#12492)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.13.4
to 7.13.5.
Changelog
Sourced from coverage's
changelog.
Version 7.13.5 — 2026-03-17
-
Fix: issue 2138_ describes a memory leak that happened
when repeatedly
using the Coverage API with in-memory data. This is now fixed.
-
Fix: the markdown-formatted coverage report didn't fully escape
special
characters in file paths (issue 2141). This would be
very unlikely to
cause a problem, but now it's done properly, thanks to Ellie Ayla
<pull 2142_>.
-
Fix: the C extension wouldn't build on VS2019, but now it does
(issue 2145_).
.. _issue 2138: coveragepy/coveragepy#2138
.. _issue 2141: coveragepy/coveragepy#2141
.. _pull 2142: coveragepy/coveragepy#2142
.. _issue 2145: coveragepy/coveragepy#2145
.. _changes_7-13-4:
Commits
c88da14
docs: sample HTML for 7.13.5
e2ac3e1
build: sample HTML shouldn't include the status.json file
910f8f3
docs: prep for 7.13.5
3a4819c
style: make workflows more uniform
2a53705
chore: bump the action-dependencies group across 1 directory with 4
updates (...
e7c878d
chore: make upgrade
ab4db40
build: use --generate-hashes when pinning
a438753
chore: make upgrade
7b33457
refactor: some leftover pyupgrade 3.10 bits
2ff968d
refactor: this type wasn't used anywhere
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 4c8b5c56687..14b6bfa0d14 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -55,7 +55,7 @@ click==8.3.1
# slotscheck
# towncrier
# wait-for-it
-coverage==7.13.4
+coverage==7.13.5
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 03ff327d70f..c5f7ead1067 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -55,7 +55,7 @@ click==8.3.1
# slotscheck
# towncrier
# wait-for-it
-coverage==7.13.4
+coverage==7.13.5
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index e2519fd4a6b..68722097036 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -14,7 +14,7 @@ cffi==2.0.0
# pytest-codspeed
click==8.3.1
# via wait-for-it
-coverage==7.13.4
+coverage==7.13.5
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index b6299d50600..07bd4d8b3ac 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -29,7 +29,7 @@ cffi==2.0.0
# pytest-codspeed
click==8.3.1
# via wait-for-it
-coverage==7.13.4
+coverage==7.13.5
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/test.txt b/requirements/test.txt
index 623135eb77a..fa75fe59740 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -29,7 +29,7 @@ cffi==2.0.0
# pytest-codspeed
click==8.3.1
# via wait-for-it
-coverage==7.13.4
+coverage==7.13.5
# via
# -r requirements/test-common.in
# pytest-cov
From eb6f6c12af9fb377f35bc091e86fd27fdfd15301 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 9 May 2026 00:31:00 +0000
Subject: [PATCH 055/191] Bump python-on-whales from 0.80.0 to 0.81.0 (#12472)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[python-on-whales](https://github.com/gabrieldemarmiesse/python-on-whales)
from 0.80.0 to 0.81.0.
Release notes
Sourced from python-on-whales's
releases.
v0.81.0
What's Changed
New Contributors
Full Changelog: https://github.com/gabrieldemarmiesse/python-on-whales/compare/v0.80.0...v0.81.0
Commits
059d89a
Bump version to 0.81.0
f0b529e
feat(compose): Add support for docker compose up SERVICE
--no-deps (#708)
b615fc4
More accurate error handling when streaming stdout and stderr (#703)
- See full diff in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 14b6bfa0d14..f308820d5eb 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -199,7 +199,7 @@ python-dateutil==2.9.0.post0
# via freezegun
python-discovery==1.3.0
# via virtualenv
-python-on-whales==0.80.0
+python-on-whales==0.81.0
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index c5f7ead1067..c77fc891820 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -194,7 +194,7 @@ python-dateutil==2.9.0.post0
# via freezegun
python-discovery==1.3.0
# via virtualenv
-python-on-whales==0.80.0
+python-on-whales==0.81.0
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 134975dd424..5a04c5de2b0 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -94,7 +94,7 @@ python-dateutil==2.9.0.post0
# via freezegun
python-discovery==1.3.0
# via virtualenv
-python-on-whales==0.80.0
+python-on-whales==0.81.0
# via -r requirements/lint.in
pyyaml==6.0.3
# via pre-commit
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 68722097036..45b7f565a13 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -83,7 +83,7 @@ pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
-python-on-whales==0.80.0
+python-on-whales==0.81.0
# via -r requirements/test-common.in
re-assert==1.1.0
# via -r requirements/test-common.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 07bd4d8b3ac..4b07cb3e0be 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -118,7 +118,7 @@ pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
-python-on-whales==0.80.0
+python-on-whales==0.81.0
# via -r requirements/test-common.in
re-assert==1.1.0
# via -r requirements/test-common.in
diff --git a/requirements/test.txt b/requirements/test.txt
index fa75fe59740..83d348e32de 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -118,7 +118,7 @@ pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
-python-on-whales==0.80.0
+python-on-whales==0.81.0
# via -r requirements/test-common.in
re-assert==1.1.0
# via -r requirements/test-common.in
From e2d40c710e90d9d108a2671bab90097e35d32f2f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 9 May 2026 00:36:07 +0000
Subject: [PATCH 056/191] Bump platformdirs from 4.9.2 to 4.9.6 (#12476)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [platformdirs](https://github.com/tox-dev/platformdirs) from 4.9.2
to 4.9.6.
Release notes
Sourced from platformdirs's
releases.
4.9.6
What's Changed
Full Changelog: https://github.com/tox-dev/platformdirs/compare/4.9.5...4.9.6
4.9.4
What's Changed
Full Changelog: https://github.com/tox-dev/platformdirs/compare/4.9.3...4.9.4
4.9.3
What's Changed
New Contributors
Full Changelog: https://github.com/tox-dev/platformdirs/compare/4.9.2...4.9.3
Changelog
Sourced from platformdirs's
changelog.
###########
Changelog
###########
4.9.6 (2026-04-09)
- 🐛 fix(release): use double quotes for tag variable expansion
:pr:
477
4.9.5 (2026-04-06)
- 📝 docs(appauthor): clarify None vs False on Windows
:pr:
476
- Separates implementations of macOS dirs that share a default
:pr:
473 - by :user:Goddesen
- Remove persist-credentials: false from release job
:pr:
472
- fix: do not duplicate site dirs in Unix.iter_{config,site}_dirs()
when use_site_for_root is active :pr:
469 - by
:user:viccie30
- 🔧 fix(type): resolve ty 0.0.25 type errors :pr:
468
- 🔒 ci(workflows): add zizmor security auditing
:pr:
467
- 🐛 fix(release): generate docstrfmt-compatible changelog entries
:pr:
463
4.9.4 (2026-03-05)
- [pre-commit.ci] pre-commit autoupdate :pr:
461 - by
:user:pre-commit-ci[bot]
- Update README.md
- 📝 docs: add project logo to documentation :pr:
459
- Standardize .github files to .yaml suffix
- build(deps): bump the all group with 2 updates :pr:
457
- by :user:dependabot[bot]
- Move SECURITY.md to .github/SECURITY.md
- Add permissions to workflows :pr:
455
- Add security policy
- [pre-commit.ci] pre-commit autoupdate :pr:
454 - by
:user:pre-commit-ci[bot]
4.9.2 (2026-02-16)
- 📝 docs: restructure following Diataxis framework
:pr:
448
- 📝 docs(platforms): fix RST formatting and TOC hierarchy
:pr:
447
4.9.1 (2026-02-14)
- 📝 docs: enhance README, fix issues, and reorganize platforms.rst
:pr:
445
... (truncated)
Commits
56efd77
Release 4.9.6
d5d812a
🐛 fix(release): use double quotes for tag variable expansion (#477)
c2b0cee
build(deps): bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0 in
the al...
7688069
Release 4.9.5
104d28b
📝 docs(appauthor): clarify None vs False on Windows (#476)
0955048
[pre-commit.ci] pre-commit autoupdate (#475)
bd3c766
build(deps): bump astral-sh/setup-uv from 7.6.0 to 8.0.0 in the all
group (#474)
749ac3f
Separates implementations of macOS dirs that share a default (#473)
cb88156
Remove persist-credentials: false from release job (#472)
a501eab
[pre-commit.ci] pre-commit autoupdate (#470)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index f308820d5eb..de8e3a2f0d8 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -140,7 +140,7 @@ pip-tools==7.5.3
# via -r requirements/dev.in
pkgconfig==1.5.5
# via -r requirements/test-common.in
-platformdirs==4.9.2
+platformdirs==4.9.6
# via
# python-discovery
# virtualenv
diff --git a/requirements/dev.txt b/requirements/dev.txt
index c77fc891820..5e8d8d96fdb 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -137,7 +137,7 @@ pip-tools==7.5.3
# via -r requirements/dev.in
pkgconfig==1.5.5
# via -r requirements/test-common.in
-platformdirs==4.9.2
+platformdirs==4.9.6
# via
# python-discovery
# virtualenv
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 5a04c5de2b0..2ab08c76235 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -61,7 +61,7 @@ packaging==26.2
# via pytest
pathspec==1.1.1
# via mypy
-platformdirs==4.9.2
+platformdirs==4.9.6
# via
# python-discovery
# virtualenv
From e635dd06899ba7a17601e99afa43183b9b514710 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 9 May 2026 02:21:22 +0000
Subject: [PATCH 057/191] Bump mypy from 1.20.2 to 2.0.0 (#12478)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [mypy](https://github.com/python/mypy) from 1.20.2 to 2.0.0.
Changelog
Sourced from mypy's
changelog.
Mypy Release Notes
Next Release
Mypy 2.0
We’ve just uploaded mypy 2.0.0 to the Python Package Index (PyPI).
Mypy is a static type checker for Python. This release includes new
features, performance
improvements and bug fixes. There are also changes to options and
defaults.
You can install it as follows:
python3 -m pip install -U mypy
You can read the full documentation for this release on Read the Docs.
Enable --local-partial-types by Default
This flag affects the inference of types based on assignments in
other scopes.
For now, explicitly disabling this continues to be supported, but this
support will be removed
in the future as the legacy behaviour is hard to support with other
current and future features
in mypy, like the daemon or the new implementation of flexible
redefinitions.
Contributed by Ivan Levkivskyi, Jukka Lehtosalo, Shantanu in PR
21163.
Enable --strict-bytes by Default
Per PEP 688, mypy no
longer treats bytearray and memoryview
values as assignable to the bytes type.
Contributed by Shantanu in PR
18371.
New Behavior for --allow-redefinition
The --allow-redefinition flag now behaves like
--allow-redefinition-new in mypy 1.20
and earlier. The new behavior is generally more flexible. For example,
you can have different
types for a variable in different blocks:
# mypy: allow-redefinition
def foo(cond: bool) -> None:
if cond:
for x in ["a", "b"]:
# Type of "x" is "str" here
...
else:
for x in [1, 2]:
# Type of "x" is "int" here
...
... (truncated)
Commits
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Sam Bull
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Sam Bull
---
aiohttp/_websocket/writer.py | 6 +++---
aiohttp/compression_utils.py | 10 +++++-----
aiohttp/http_writer.py | 30 +++++++++++++----------------
aiohttp/multipart.py | 37 +++++++++++++++++++++---------------
aiohttp/web_request.py | 7 ++++---
aiohttp/web_response.py | 4 ++--
aiohttp/web_ws.py | 9 ++++++++-
requirements/constraints.txt | 4 +++-
requirements/dev.txt | 4 +++-
requirements/lint.txt | 4 +++-
requirements/test-common.txt | 4 +++-
requirements/test-ft.txt | 4 +++-
requirements/test.txt | 4 +++-
13 files changed, 75 insertions(+), 52 deletions(-)
diff --git a/aiohttp/_websocket/writer.py b/aiohttp/_websocket/writer.py
index d293171e38b..7daf4fc3394 100644
--- a/aiohttp/_websocket/writer.py
+++ b/aiohttp/_websocket/writer.py
@@ -158,9 +158,9 @@ def _write_websocket_frame(self, message: bytes, opcode: int, rsv: int) -> None:
# when aiohttp is acting as a client. Servers do not use a mask.
if use_mask:
mask = PACK_RANDBITS(self.get_random_bits())
- message = bytearray(message)
- websocket_mask(mask, message)
- self.transport.write(header + mask + message)
+ message_arr = bytearray(message)
+ websocket_mask(mask, message_arr)
+ self.transport.write(header + mask + message_arr)
self._output_size += MASK_LEN
elif msg_length > MSG_SIZE:
self.transport.write(header)
diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py
index a30894eb0b7..75b24d1cbbf 100644
--- a/aiohttp/compression_utils.py
+++ b/aiohttp/compression_utils.py
@@ -164,12 +164,12 @@ def __init__(
@abstractmethod
def decompress_sync(
- self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
+ self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
) -> bytes:
"""Decompress the given data."""
async def decompress(
- self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
+ self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
) -> bytes:
"""Decompress the given data."""
if (
@@ -215,10 +215,10 @@ def __init__(
kwargs["level"] = level
self._compressor = self._zlib_backend.compressobj(**kwargs)
- def compress_sync(self, data: bytes) -> bytes:
+ def compress_sync(self, data: Buffer) -> bytes:
return self._compressor.compress(data)
- async def compress(self, data: bytes) -> bytes:
+ async def compress(self, data: Buffer) -> bytes:
"""Compress the data and returned the compressed bytes.
Note that flush() must be called after the last call to compress()
@@ -366,7 +366,7 @@ def __init__(
super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size)
def decompress_sync(
- self, data: bytes, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
+ self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
) -> bytes:
# zstd uses -1 for unlimited, while zlib uses 0 for unlimited
# Convert the zlib convention (0=unlimited) to zstd convention (-1=unlimited)
diff --git a/aiohttp/http_writer.py b/aiohttp/http_writer.py
index 574bc9c263f..411a2aae882 100644
--- a/aiohttp/http_writer.py
+++ b/aiohttp/http_writer.py
@@ -11,7 +11,6 @@
List,
NamedTuple,
Optional,
- Union,
)
from multidict import CIMultiDict
@@ -24,6 +23,13 @@
__all__ = ("StreamWriter", "HttpVersion", "HttpVersion10", "HttpVersion11")
+if sys.version_info >= (3, 12):
+ from collections.abc import Buffer
+else:
+ from typing import Union
+
+ Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"]
+
MIN_PAYLOAD_FOR_WRITELINES = 2048
IS_PY313_BEFORE_313_2 = (3, 13, 0) <= sys.version_info < (3, 13, 2)
@@ -45,7 +51,7 @@ class HttpVersion(NamedTuple):
HttpVersion11 = HttpVersion(1, 1)
-_T_OnChunkSent = Optional[Callable[[bytes], Awaitable[None]]]
+_T_OnChunkSent = Optional[Callable[[Buffer], Awaitable[None]]]
_T_OnHeadersSent = Optional[Callable[["CIMultiDict[str]"], Awaitable[None]]]
@@ -86,7 +92,7 @@ def enable_compression(
) -> None:
self._compress = ZLibCompressor(encoding=encoding, strategy=strategy)
- def _write(self, chunk: bytes | bytearray | memoryview) -> None:
+ def _write(self, chunk: Buffer) -> None:
size = len(chunk)
self.buffer_size += size
self.output_size += size
@@ -95,7 +101,7 @@ def _write(self, chunk: bytes | bytearray | memoryview) -> None:
raise ClientConnectionResetError("Cannot write to closing transport")
transport.write(chunk)
- def _writelines(self, chunks: Iterable[bytes]) -> None:
+ def _writelines(self, chunks: Iterable[Buffer]) -> None:
size = 0
for chunk in chunks:
size += len(chunk)
@@ -109,18 +115,12 @@ def _writelines(self, chunks: Iterable[bytes]) -> None:
else:
transport.writelines(chunks)
- def _write_chunked_payload(
- self, chunk: Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"]
- ) -> None:
+ def _write_chunked_payload(self, chunk: Buffer) -> None:
"""Write a chunk with proper chunked encoding."""
chunk_len_pre = f"{len(chunk):x}\r\n".encode("ascii")
self._writelines((chunk_len_pre, chunk, b"\r\n"))
- def _send_headers_with_payload(
- self,
- chunk: Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"],
- is_eof: bool,
- ) -> None:
+ def _send_headers_with_payload(self, chunk: Buffer, is_eof: bool) -> None:
"""Send buffered headers with payload, coalescing into single write."""
# Mark headers as written
self._headers_written = True
@@ -153,11 +153,7 @@ def _send_headers_with_payload(
self._write(headers_buf)
async def write(
- self,
- chunk: bytes | bytearray | memoryview,
- *,
- drain: bool = True,
- LIMIT: int = 0x10000,
+ self, chunk: Buffer, *, drain: bool = True, LIMIT: int = 0x10000
) -> None:
"""
Writes chunk of data to a stream.
diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py
index 5cea890d343..7cc3d4db0e8 100644
--- a/aiohttp/multipart.py
+++ b/aiohttp/multipart.py
@@ -8,7 +8,7 @@
from collections import deque
from collections.abc import AsyncIterator, Iterator, Mapping, Sequence
from types import TracebackType
-from typing import TYPE_CHECKING, Any, Union, cast
+from typing import TYPE_CHECKING, Any, TypeVar, Union, cast
from urllib.parse import parse_qsl, unquote, urlencode
from multidict import CIMultiDict, CIMultiDictProxy
@@ -40,10 +40,15 @@
if sys.version_info >= (3, 11):
from typing import Self
else:
- from typing import TypeVar
-
Self = TypeVar("Self", bound="BodyPartReader")
+if sys.version_info >= (3, 12):
+ from collections.abc import Buffer
+else:
+ Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"]
+
+_Buffer = TypeVar("_Buffer", bound=Buffer)
+
__all__ = (
"MultipartReader",
"MultipartWriter",
@@ -499,7 +504,7 @@ def at_eof(self) -> bool:
"""Returns True if the boundary was reached or False otherwise."""
return self._at_eof
- def _apply_content_transfer_decoding(self, data: bytes) -> bytes:
+ def _apply_content_transfer_decoding(self, data: _Buffer) -> _Buffer | bytes:
"""Apply Content-Transfer-Encoding decoding if header is present."""
if CONTENT_TRANSFER_ENCODING in self.headers:
return self._decode_content_transfer(data)
@@ -510,7 +515,7 @@ def _needs_content_decoding(self) -> bool:
# https://datatracker.ietf.org/doc/html/rfc7578#section-4.8
return not self._is_form_data and CONTENT_ENCODING in self.headers
- def decode(self, data: bytes) -> bytes:
+ def decode(self, data: _Buffer) -> _Buffer | bytes:
"""Decodes data synchronously.
Decodes data according the specified Content-Encoding
@@ -519,12 +524,12 @@ def decode(self, data: bytes) -> bytes:
Note: For large payloads, consider using decode_iter() instead
to avoid blocking the event loop during decompression.
"""
- data = self._apply_content_transfer_decoding(data)
+ decoded = self._apply_content_transfer_decoding(data)
if self._needs_content_decoding():
- return self._decode_content(data)
- return data
+ return self._decode_content(decoded)
+ return decoded
- async def decode_iter(self, data: bytes) -> AsyncIterator[bytes]:
+ async def decode_iter(self, data: _Buffer) -> AsyncIterator[_Buffer | bytes]:
"""Async generator that yields decoded data chunks.
Decodes data according the specified Content-Encoding
@@ -533,14 +538,14 @@ async def decode_iter(self, data: bytes) -> AsyncIterator[bytes]:
This method offloads decompression to an executor for large payloads
to avoid blocking the event loop.
"""
- data = self._apply_content_transfer_decoding(data)
+ decoded = self._apply_content_transfer_decoding(data)
if self._needs_content_decoding():
- async for d in self._decode_content_async(data):
+ async for d in self._decode_content_async(decoded):
yield d
else:
- yield data
+ yield decoded
- def _decode_content(self, data: bytes) -> bytes:
+ def _decode_content(self, data: _Buffer) -> _Buffer | bytes:
encoding = self.headers.get(CONTENT_ENCODING, "").lower()
if encoding == "identity":
return data
@@ -552,7 +557,9 @@ def _decode_content(self, data: bytes) -> bytes:
raise RuntimeError(f"unknown content encoding: {encoding}")
- async def _decode_content_async(self, data: bytes) -> AsyncIterator[bytes]:
+ async def _decode_content_async(
+ self, data: _Buffer
+ ) -> AsyncIterator[_Buffer | bytes]:
encoding = self.headers.get(CONTENT_ENCODING, "").lower()
if encoding == "identity":
yield data
@@ -567,7 +574,7 @@ async def _decode_content_async(self, data: bytes) -> AsyncIterator[bytes]:
else:
raise RuntimeError(f"unknown content encoding: {encoding}")
- def _decode_content_transfer(self, data: bytes) -> bytes:
+ def _decode_content_transfer(self, data: _Buffer) -> _Buffer | bytes:
encoding = self.headers.get(CONTENT_TRANSFER_ENCODING, "").lower()
if encoding == "base64":
diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py
index 25d7ebc8e22..b5d5b1b6897 100644
--- a/aiohttp/web_request.py
+++ b/aiohttp/web_request.py
@@ -76,6 +76,7 @@ class FileField:
headers: CIMultiDictProxy[str]
+_Post = str | bytes | bytearray | FileField
_TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-"
# '-' at the end to prevent interpretation as range in a char class
@@ -134,7 +135,7 @@ class BaseRequest(MutableMapping[str | RequestKey[Any], Any], HeadersMixin):
"_transport_peername",
]
)
- _post: MultiDictProxy[str | bytes | FileField] | None = None
+ _post: MultiDictProxy[_Post] | None = None
_read_bytes: bytes | None = None
_seen_str_keys: set[str] = set()
@@ -717,7 +718,7 @@ async def multipart(self) -> MultipartReader:
max_size_error_cls=HTTPRequestEntityTooLarge,
)
- async def post(self) -> "MultiDictProxy[str | bytes | FileField]":
+ async def post(self) -> "MultiDictProxy[_Post]":
"""Return POST parameters."""
if self._post is not None:
return self._post
@@ -734,7 +735,7 @@ async def post(self) -> "MultiDictProxy[str | bytes | FileField]":
self._post = MultiDictProxy(MultiDict())
return self._post
- out: MultiDict[str | bytes | FileField] = MultiDict()
+ out: MultiDict[_Post] = MultiDict()
if content_type == "multipart/form-data":
multipart = await self.multipart()
diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py
index 7a14ada4dbb..daf29eccce4 100644
--- a/aiohttp/web_response.py
+++ b/aiohttp/web_response.py
@@ -720,7 +720,7 @@ def __init__(
self._zlib_executor = zlib_executor
@property
- def body(self) -> bytes | Payload | None:
+ def body(self) -> bytes | bytearray | Payload | None:
return self._body
@body.setter
@@ -799,7 +799,7 @@ async def write_eof(self, data: bytes = b"") -> None:
if self._eof_sent:
return
if self._compressed_body is None:
- body: bytes | Payload | None = self._body
+ body = self._body
else:
body = self._compressed_body
assert not data, f"data arg is not supported, got {data!r}"
diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py
index 5aa75b715a0..c01d95637cd 100644
--- a/aiohttp/web_ws.py
+++ b/aiohttp/web_ws.py
@@ -48,6 +48,13 @@
else:
from typing_extensions import TypeVar
+if sys.version_info >= (3, 12):
+ from collections.abc import Buffer
+else:
+ from typing import Union
+
+ Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"]
+
if sys.version_info >= (3, 11):
import asyncio as async_timeout
from typing import Self
@@ -740,7 +747,7 @@ async def receive_json(
data = await self.receive_str(timeout=timeout)
return loads(data) # type: ignore[arg-type]
- async def write(self, data: bytes) -> None:
+ async def write(self, data: Buffer) -> None:
raise RuntimeError("Cannot call .write() for websocket")
def __aiter__(self) -> Self:
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index de8e3a2f0d8..19c261bf849 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -18,6 +18,8 @@ alabaster==1.0.0
# via sphinx
annotated-types==0.7.0
# via pydantic
+ast-serialize==0.3.0
+ # via mypy
async-timeout==5.0.1 ; python_version < "3.11"
# via
# -r requirements/runtime-deps.in
@@ -119,7 +121,7 @@ multidict==6.7.1
# -r requirements/multidict.in
# -r requirements/runtime-deps.in
# yarl
-mypy==1.20.2 ; implementation_name == "cpython"
+mypy==2.0.0 ; implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 5e8d8d96fdb..3b93809d388 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -18,6 +18,8 @@ alabaster==1.0.0
# via sphinx
annotated-types==0.7.0
# via pydantic
+ast-serialize==0.3.0
+ # via mypy
async-timeout==5.0.1 ; python_version < "3.11"
# via
# -r requirements/runtime-deps.in
@@ -116,7 +118,7 @@ multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
-mypy==1.20.2 ; implementation_name == "cpython"
+mypy==2.0.0 ; implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 2ab08c76235..e34c078bb31 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -8,6 +8,8 @@ aiodns==3.6.1
# via -r requirements/lint.in
annotated-types==0.7.0
# via pydantic
+ast-serialize==0.3.0
+ # via mypy
async-timeout==5.0.1
# via valkey
backports-zstd==1.3.0 ; implementation_name == "cpython" and python_version < "3.14"
@@ -51,7 +53,7 @@ markdown-it-py==4.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-mypy==1.20.2 ; implementation_name == "cpython"
+mypy==2.0.0 ; implementation_name == "cpython"
# via -r requirements/lint.in
mypy-extensions==1.1.0
# via mypy
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 45b7f565a13..706ee9d76a6 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -6,6 +6,8 @@
#
annotated-types==0.7.0
# via pydantic
+ast-serialize==0.3.0
+ # via mypy
blockbuster==1.5.26
# via -r requirements/test-common.in
cffi==2.0.0
@@ -40,7 +42,7 @@ markdown-it-py==4.0.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-mypy==1.20.2 ; implementation_name == "cpython"
+mypy==2.0.0 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 4b07cb3e0be..6b886291c03 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -12,6 +12,8 @@ aiosignal==1.4.0
# via -r requirements/runtime-deps.in
annotated-types==0.7.0
# via pydantic
+ast-serialize==0.3.0
+ # via mypy
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
attrs==26.1.0
@@ -67,7 +69,7 @@ multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
-mypy==1.20.2 ; implementation_name == "cpython"
+mypy==2.0.0 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
diff --git a/requirements/test.txt b/requirements/test.txt
index 83d348e32de..50c05e00ff6 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -12,6 +12,8 @@ aiosignal==1.4.0
# via -r requirements/runtime-deps.in
annotated-types==0.7.0
# via pydantic
+ast-serialize==0.3.0
+ # via mypy
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
attrs==26.1.0
@@ -67,7 +69,7 @@ multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
-mypy==1.20.2 ; implementation_name == "cpython"
+mypy==2.0.0 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
From 9b89e732981e4949f8c4bbdc306f8e4efe76955e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 9 May 2026 02:39:43 +0000
Subject: [PATCH 058/191] Bump rich from 14.3.3 to 15.0.0 (#12480)
Bumps [rich](https://github.com/Textualize/rich) from 14.3.3 to 15.0.0.
Release notes
Sourced from rich's
releases.
The So Long 3.8 Release
A few fixes. The major version bump is to honor the passing of 3.8
support which reached its EOL in October 7, 2024
[15.0.0] - 2026-04-12
Changed
- Breaking change: Dropped support for Python3.8
Fixed
The Faster Startup Release
No new features in this release, but there should be improved startup
time for Rich apps, and potentially improved runtime if you have a lot
of links.
[14.3.4] - 2026-04-11
Changed
Changelog
Sourced from rich's
changelog.
[15.0.0] - 2026-04-12
Changed
- Breaking change: Dropped support for Python3.8
Fixed
[14.3.4] - 2026-04-11
Changed
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 19c261bf849..4c995ceb3a6 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -215,7 +215,7 @@ requests==2.33.1
# via
# sphinx
# sphinxcontrib-spelling
-rich==14.3.3
+rich==15.0.0
# via pytest-codspeed
setuptools-git==1.2
# via -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 3b93809d388..feede4999b0 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -208,7 +208,7 @@ regex==2026.2.28
# via re-assert
requests==2.33.1
# via sphinx
-rich==14.3.3
+rich==15.0.0
# via pytest-codspeed
setuptools-git==1.2
# via -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index e34c078bb31..fab751254b4 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -100,7 +100,7 @@ python-on-whales==0.81.0
# via -r requirements/lint.in
pyyaml==6.0.3
# via pre-commit
-rich==14.3.3
+rich==15.0.0
# via pytest-codspeed
six==1.17.0
# via python-dateutil
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 706ee9d76a6..a2036fca622 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -91,7 +91,7 @@ re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.2.28
# via re-assert
-rich==14.3.3
+rich==15.0.0
# via pytest-codspeed
setuptools-git==1.2
# via -r requirements/test-common.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 6b886291c03..ecc5acde368 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -126,7 +126,7 @@ re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.2.28
# via re-assert
-rich==14.3.3
+rich==15.0.0
# via pytest-codspeed
setuptools-git==1.2
# via -r requirements/test-common.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 50c05e00ff6..cbfde72c1e5 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -126,7 +126,7 @@ re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.2.28
# via re-assert
-rich==14.3.3
+rich==15.0.0
# via pytest-codspeed
setuptools-git==1.2
# via -r requirements/test-common.in
From 9e1d37fc55f41e221e66f5913098f4694f798ebc Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 10 May 2026 23:02:50 +0000
Subject: [PATCH 059/191] Bump cryptography from 46.0.5 to 48.0.0 (#12491)
Bumps [cryptography](https://github.com/pyca/cryptography) from 46.0.5
to 48.0.0.
Changelog
Sourced from cryptography's
changelog.
48.0.0 - 2026-05-04
* **BACKWARDS INCOMPATIBLE:** Support for Python 3.8 has been removed.
``cryptography`` now requires Python 3.9 or later.
* **BACKWARDS INCOMPATIBLE:** Loading an X.509 CRL whose inner
``TBSCertList.signature`` algorithm does not match the outer
``signatureAlgorithm`` now raises ``ValueError``. Previously, such CRLs
were parsed successfully and only rejected during signature validation.
* Added support for :doc:`/hazmat/primitives/asymmetric/mlkem` and
:doc:`/hazmat/primitives/asymmetric/mldsa` when using OpenSSL 3.5.0 or
later, in addition to the existing AWS-LC and BoringSSL support. This
means
post-quantum algorithms are now available to users of our wheels.
- Note: Going forward, we do not guarantee that all
functionality
in cryptography will be available when building against
OpenSSL. See :doc:/statements/state-of-openssl for more
information.
.. _v47-0-0:
47.0.0 - 2026-04-24
- Support for Python 3.8 is deprecated and will be removed in the next
cryptography release.
- BACKWARDS INCOMPATIBLE: Support for binary elliptic
curves
(
SECT* classes) has been removed. These curves are rarely
used and
have additional security considerations that make them undesirable.
- BACKWARDS INCOMPATIBLE: Support for OpenSSL 1.1.x
has been removed.
OpenSSL 3.0.0 or later is now required. LibreSSL, BoringSSL, and AWS-LC
continue to be supported.
- BACKWARDS INCOMPATIBLE: Dropped support for
LibreSSL < 4.1.
- BACKWARDS INCOMPATIBLE: Loading keys with
unsupported algorithms or
keys with unsupported explicit curve encodings now raises
:class:
~cryptography.exceptions.UnsupportedAlgorithm
instead of
ValueError. This change affects
:func:~cryptography.hazmat.primitives.serialization.load_pem_private_key,
:func:~cryptography.hazmat.primitives.serialization.load_der_private_key,
:func:~cryptography.hazmat.primitives.serialization.load_pem_public_key,
:func:~cryptography.hazmat.primitives.serialization.load_der_public_key,
and :meth:~cryptography.x509.Certificate.public_key when
called on
certificates with unsupported public key algorithms.
- BACKWARDS INCOMPATIBLE: When parsing elliptic curve
private keys, we now
reject keys that incorrectly encode a private key of the wrong length
because
such keys are impossible to process in a constant-time manner. We do not
believe keys with this problem are in wide use, however we may revert
this
change based on the feedback we receive.
- Deprecated passing 64-bit (8-byte) and 128-bit (16-byte) keys to
:class:
~cryptography.hazmat.decrepit.ciphers.algorithms.TripleDES.
In a
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 4c995ceb3a6..90d2b53815c 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -61,7 +61,7 @@ coverage==7.13.5
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==46.0.5
+cryptography==48.0.0
# via trustme
cython==3.2.4
# via -r requirements/cython.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index feede4999b0..259c997c64a 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -61,7 +61,7 @@ coverage==7.13.5
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==46.0.5
+cryptography==48.0.0
# via trustme
distlib==0.4.0
# via virtualenv
diff --git a/requirements/lint.txt b/requirements/lint.txt
index fab751254b4..36a222767e0 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -25,7 +25,7 @@ cfgv==3.5.0
# via pre-commit
click==8.3.1
# via slotscheck
-cryptography==46.0.5
+cryptography==48.0.0
# via trustme
distlib==0.4.0
# via virtualenv
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index a2036fca622..4f76e4f26f4 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -20,7 +20,7 @@ coverage==7.13.5
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==46.0.5
+cryptography==48.0.0
# via trustme
exceptiongroup==1.3.1
# via pytest
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index ecc5acde368..a2dbdb2db46 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -35,7 +35,7 @@ coverage==7.13.5
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==46.0.5
+cryptography==48.0.0
# via trustme
exceptiongroup==1.3.1
# via pytest
diff --git a/requirements/test.txt b/requirements/test.txt
index cbfde72c1e5..a4aa15f4698 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -35,7 +35,7 @@ coverage==7.13.5
# via
# -r requirements/test-common.in
# pytest-cov
-cryptography==46.0.5
+cryptography==48.0.0
# via trustme
exceptiongroup==1.3.1
# via pytest
From 3f7231c32570f1bb11163b57c156dba0deb8edd8 Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Mon, 11 May 2026 00:24:07 +0100
Subject: [PATCH 060/191] Deprecate aiohttp.pytest_plugin (#10785)
---
CHANGES/10785.misc.rst | 1 +
aiohttp/pytest_plugin.py | 4 ++++
setup.cfg | 2 ++
3 files changed, 7 insertions(+)
create mode 100644 CHANGES/10785.misc.rst
diff --git a/CHANGES/10785.misc.rst b/CHANGES/10785.misc.rst
new file mode 100644
index 00000000000..5b43f63a42d
--- /dev/null
+++ b/CHANGES/10785.misc.rst
@@ -0,0 +1 @@
+Added deprecation warning to ``aiohttp.pytest_plugin``, please switch to ``pytest-aiohttp`` -- by :user:`Dreamsorcerer`.
diff --git a/aiohttp/pytest_plugin.py b/aiohttp/pytest_plugin.py
index 7ea6be2f335..a2c7bdca03e 100644
--- a/aiohttp/pytest_plugin.py
+++ b/aiohttp/pytest_plugin.py
@@ -198,6 +198,10 @@ def pytest_pyfunc_call(pyfuncitem): # type: ignore[no-untyped-def]
"""Run coroutines in an event loop instead of a normal function call."""
fast = pyfuncitem.config.getoption("--aiohttp-fast")
if inspect.iscoroutinefunction(pyfuncitem.function):
+ warnings.warn(
+ "aiohttp.pytest_plugin will be removed in v4. Please install pytest-aiohttp.",
+ DeprecationWarning,
+ )
existing_loop = (
pyfuncitem.funcargs.get("proactor_loop")
or pyfuncitem.funcargs.get("selector_loop")
diff --git a/setup.cfg b/setup.cfg
index 62f68b11eab..5df4444e507 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -80,6 +80,8 @@ filterwarnings =
ignore:datetime.*utcnow\(\) is deprecated and scheduled for removal:DeprecationWarning:freezegun.api
# Weird issue in Python 3.13+ triggered in test_multipart.py
ignore:coroutine method 'aclose' of 'BodyPartReader._decode_content_async' was never awaited:RuntimeWarning
+ # Our own warning
+ ignore:aiohttp.pytest_plugin will be removed in v4:DeprecationWarning
junit_suite_name = aiohttp_test_suite
norecursedirs = dist docs build .tox .eggs
minversion = 3.8.2
From 50beb8de9b63739d1f0a2fb69a140526b1bbd42b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 May 2026 00:04:35 +0000
Subject: [PATCH 061/191] Bump click from 8.3.1 to 8.3.3 (#12489)
Bumps [click](https://github.com/pallets/click) from 8.3.1 to 8.3.3.
Release notes
Sourced from click's
releases.
8.3.3
This is the Click 8.3.3 fix release, which fixes bugs but does not
otherwise change behavior and should not result in breaking changes
compared to the latest feature release.
PyPI: https://pypi.org/project/click/8.3.3/
Changes: https://click.palletsprojects.com/page/changes/#version-8-3-3
Milestone: https://github.com/pallets/click/milestone/30
- Use :func:
shlex.split to split pager and editor
commands into argv
lists for :class:subprocess.Popen, removing
shell=True.
#1026
#1477
#2775
- Fix
TypeError when rendering help for an option whose
default value is
an object that doesn't support equality comparison with strings, such as
semver.Version. #3298
#3299
- Fix pager test pollution under parallel execution by using pytest's
tmp_path fixture instead of a shared temporary file path.
#3238
- Treat
Sentinel.UNSET values in a
default_map as absent, so they fall
through to the next default source instead of being used as the value.
#3224
#3240
- Patch
pdb.Pdb in CliRunner isolation so
pdb.set_trace(),
breakpoint(), and debuggers subclassing
pdb.Pdb (ipdb, pdbpp) can
interact with the real terminal instead of the captured I/O streams.
#654
#824
#843
#951
#3235
- Add optional randomized parallel test execution using
pytest-randomly and
pytest-xdist to detect test pollution and race conditions.
#3151
- Add contributor documentation for running stress tests, randomized
parallel tests, and Flask smoke tests. #3151
#3177
- Show custom
show_default string in prompts, matching
the existing
help text behavior. #2836
#2837
#3165
#3262
#3280
#3328
- Fix
default=True with boolean flag_value
always returning the
flag_value instead of True. The
default=True to flag_value
substitution now only applies to non-boolean flags, where
True acts as a
sentinel meaning "activate this flag by default". For boolean
flags,
default=True is returned as a literal value. #3111
#3239
- Mark
make_default_short_help as private API. #3189
#3250
CliRunner's redirected streams now expose the original
file descriptor
via fileno(), so that faulthandler,
subprocess, and other
C-level consumers no longer crash with
io.UnsupportedOperation.
#2865
- Change :class:
ParameterSource to an
:class:~enum.IntEnum and reorder
its members from most to least explicit, so values can be compared to
check whether a parameter was explicitly provided. #2879
#3248
8.3.2
This is the Click 8.3.2 fix release, which fixes bugs but does not
otherwise change behavior and should not result in breaking changes
compared to the latest feature release.
PyPI: https://pypi.org/project/click/8.3.2/
Changes: https://click.palletsprojects.com/page/changes/#version-8-3-2
Milestone: https://github.com/pallets/click/milestone/29
... (truncated)
Changelog
Sourced from click's
changelog.
Version 8.3.3
Released 2026-04-20
- Use :func:
shlex.split to split pager and editor
commands into argv
lists for :class:subprocess.Popen, removing
shell=True.
:issue:1026 :pr:1477
:pr:2775
- Fix
TypeError when rendering help for an option whose
default value is
an object that doesn't support equality comparison with strings, such as
semver.Version. :issue:3298
:pr:3299
- Fix pager test pollution under parallel execution by using pytest's
tmp_path fixture instead of a shared temporary file path.
:pr:3238
- Treat
Sentinel.UNSET values in a
default_map as absent, so they fall
through to the next default source instead of being used as the value.
:issue:3224 :pr:3240
- Patch
pdb.Pdb in CliRunner isolation so
pdb.set_trace(),
breakpoint(), and debuggers subclassing
pdb.Pdb (ipdb, pdbpp) can
interact with the real terminal instead of the captured I/O streams.
:issue:654 :issue:824 :issue:843
:pr:951 :pr:3235
- Add optional randomized parallel test execution using
pytest-randomly and
pytest-xdist to detect test pollution and race conditions.
:pr:3151
- Add contributor documentation for running stress tests, randomized
parallel tests, and Flask smoke tests. :pr:
3151
:pr:3177
- Show custom
show_default string in prompts, matching
the existing
help text behavior. :issue:2836 :pr:2837
:pr:3165 :pr:3262 :pr:3280
:pr:3328
- Fix
default=True with boolean flag_value
always returning the
flag_value instead of True. The
default=True to flag_value
substitution now only applies to non-boolean flags, where
True acts as a
sentinel meaning "activate this flag by default". For boolean
flags,
default=True is returned as a literal value.
:issue:3111 :pr:3239
- Mark
make_default_short_help as private API.
:issue:3189 :pr:3250
CliRunner's redirected streams now expose the original
file descriptor
via fileno(), so that faulthandler,
subprocess, and other
C-level consumers no longer crash with
io.UnsupportedOperation.
:issue:2865
- Change :class:
ParameterSource to an
:class:~enum.IntEnum and reorder
its members from most to least explicit, so values can be compared to
check whether a parameter was explicitly provided.
:issue:2879 :pr:3248
Version 8.3.2
Released 2026-04-02
- Fix handling of
flag_value when
is_flag=False to allow such options to be
used without an explicit value. :issue:3084
:pr:3152
- Hide
Sentinel.UNSET values as None when
using lookup_default().
:issue:3136 :pr:3199 :pr:3202
:pr:3209 :pr:3212 :pr:3224
... (truncated)
Commits
c06d2d0
Release 8.3.3
f1f191e
Apply format guidelines to commits since latest 8.3.2 release (#3343)
bb59ba0
Apply format guidelines to commits since latest 8.3.2 release
4a35225
Reduce blast-radius of UNSET in default_map
(#3240)
c07bb93
Merge branch 'stable' into unset-in-default-map
c7e1ba8
Reorder ParameterSource (#3248)
76552ff
Show default string in prompt (#3328)
ac5cec5
Reorder ParameterSource from most to least explicit
8c452e0
Merge branch 'stable' into show-default-string-in-prompt
8c95c73
Reconcile default value passing and default activation (#3239)
- Additional commits viewable in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
8 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 90d2b53815c..e02ab35bae9 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -51,7 +51,7 @@ cfgv==3.5.0
# via pre-commit
charset-normalizer==3.4.7
# via requests
-click==8.3.1
+click==8.3.3
# via
# pip-tools
# slotscheck
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 259c997c64a..de8e14beb83 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -51,7 +51,7 @@ cfgv==3.5.0
# via pre-commit
charset-normalizer==3.4.7
# via requests
-click==8.3.1
+click==8.3.3
# via
# pip-tools
# slotscheck
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index 88eea72170c..c486dab30a5 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -14,7 +14,7 @@ certifi==2026.2.25
# via requests
charset-normalizer==3.4.7
# via requests
-click==8.3.1
+click==8.3.3
# via towncrier
docutils==0.21.2
# via sphinx
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 4cff982765e..bfd975c6495 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -14,7 +14,7 @@ certifi==2026.2.25
# via requests
charset-normalizer==3.4.7
# via requests
-click==8.3.1
+click==8.3.3
# via towncrier
docutils==0.21.2
# via sphinx
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 36a222767e0..369afd866c5 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -23,7 +23,7 @@ cffi==2.0.0
# pytest-codspeed
cfgv==3.5.0
# via pre-commit
-click==8.3.1
+click==8.3.3
# via slotscheck
cryptography==48.0.0
# via trustme
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 4f76e4f26f4..55648c11515 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -14,7 +14,7 @@ cffi==2.0.0
# via
# cryptography
# pytest-codspeed
-click==8.3.1
+click==8.3.3
# via wait-for-it
coverage==7.13.5
# via
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index a2dbdb2db46..b0ec679c219 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -29,7 +29,7 @@ cffi==2.0.0
# cryptography
# pycares
# pytest-codspeed
-click==8.3.1
+click==8.3.3
# via wait-for-it
coverage==7.13.5
# via
diff --git a/requirements/test.txt b/requirements/test.txt
index a4aa15f4698..95054660a7a 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -29,7 +29,7 @@ cffi==2.0.0
# cryptography
# pycares
# pytest-codspeed
-click==8.3.1
+click==8.3.3
# via wait-for-it
coverage==7.13.5
# via
From 75e08fc9b1082af63d60a8a4fbe717ac0acb5ba6 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Mon, 11 May 2026 01:05:27 +0100
Subject: [PATCH 062/191] [PR #12493/76d3a7f1 backport][3.14] Don't re-await
main_task in run_app finally when it's already done (#12496)
**This is a backport of PR #12493 as merged into master
(76d3a7f12aa088346fafb1cfd82f9c591010d336).**
Co-authored-by: Andrew Karelin <36686667+AndrewKarelin@users.noreply.github.com>
---
CHANGES/12493.bugfix | 3 +++
aiohttp/web.py | 15 ++++++++++++---
tests/test_run_app.py | 23 +++++++++++++++++++++++
3 files changed, 38 insertions(+), 3 deletions(-)
create mode 100644 CHANGES/12493.bugfix
diff --git a/CHANGES/12493.bugfix b/CHANGES/12493.bugfix
new file mode 100644
index 00000000000..7a68daec0ba
--- /dev/null
+++ b/CHANGES/12493.bugfix
@@ -0,0 +1,3 @@
+Fixed :func:`aiohttp.web.run_app` losing inner traceback frames when an
+exception is raised during application startup (e.g. inside
+``cleanup_ctx`` or ``on_startup``). Regression since 3.10.6.
diff --git a/aiohttp/web.py b/aiohttp/web.py
index 071f51d9fe2..f717d94bfba 100644
--- a/aiohttp/web.py
+++ b/aiohttp/web.py
@@ -504,9 +504,18 @@ def run_app(
pass
finally:
try:
- main_task.cancel()
- with suppress(asyncio.CancelledError):
- loop.run_until_complete(main_task)
+ # Skip when ``main_task`` is already done (e.g. raised during startup).
+ # Re-running ``loop.run_until_complete`` on a finished task calls
+ # ``Future.result`` again, which does
+ # ``raise self._exception.with_traceback(self._exception_tb)`` and
+ # resets ``exc.__traceback__`` to the originally saved tb — by then
+ # shallow — clobbering the deep traceback the caller would otherwise
+ # see (frames from ``cleanup_ctx`` / ``on_startup`` and the user code
+ # that actually raised).
+ if not main_task.done():
+ main_task.cancel()
+ with suppress(asyncio.CancelledError):
+ loop.run_until_complete(main_task)
finally:
_cancel_tasks(asyncio.all_tasks(loop), loop)
loop.run_until_complete(loop.shutdown_asyncgens())
diff --git a/tests/test_run_app.py b/tests/test_run_app.py
index 899064aa165..f855fbe0a06 100644
--- a/tests/test_run_app.py
+++ b/tests/test_run_app.py
@@ -9,6 +9,7 @@
import subprocess
import sys
import time
+import traceback
from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine, Iterator
from typing import NoReturn
from unittest import mock
@@ -115,6 +116,28 @@ def test_run_app_close_loop(patched_loop) -> None:
assert patched_loop.is_closed()
+def test_run_app_preserves_startup_traceback(
+ patched_loop: asyncio.AbstractEventLoop,
+) -> None:
+ # Regression: when an exception is raised during startup (here in a
+ # cleanup_ctx async generator), the user code frame must remain in the
+ # traceback that propagates out of run_app. Previously the second
+ # loop.run_until_complete(main_task) in run_app's finally clobbered it.
+
+ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]:
+ raise RuntimeError("boom from failing_ctx")
+ yield # type: ignore[unreachable] # required to make this an async generator
+
+ app = web.Application()
+ app.cleanup_ctx.append(failing_ctx)
+
+ with pytest.raises(RuntimeError, match="boom from failing_ctx") as exc_info:
+ web.run_app(app, print=None, loop=patched_loop)
+
+ frames = [f.name for f in traceback.extract_tb(exc_info.tb)]
+ assert "failing_ctx" in frames, frames
+
+
mock_unix_server_single = [
mock.call(mock.ANY, "/tmp/testsock1.sock", ssl=None, backlog=128),
]
From 6b98c2b5848a53214a84fb5594d9194b756750ac Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 May 2026 17:15:17 +0000
Subject: [PATCH 063/191] Bump idna from 3.13 to 3.14 (#12505)
Bumps [idna](https://github.com/kjd/idna) from 3.13 to 3.14.
Changelog
Sourced from idna's
changelog.
3.14 (2026-05-10)
+++++++++++++++++
- Removed opportunity to process long inputs into quadratic
time by rejecting oversize inputs up-front. Closes a bypass
of the CVE-2024-3651 mitigation. [GHSA-65pc-fj4g-8rjx]
Thanks to Stan Ulbrych for reporting the issue.
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/runtime-deps.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
11 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 1f6f712fee4..0374faf854b 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -26,7 +26,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base-ft.in
-idna==3.13
+idna==3.14
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/base.txt b/requirements/base.txt
index a8a4c998885..bd1e85fa517 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -26,7 +26,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base.in
-idna==3.13
+idna==3.14
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index e02ab35bae9..41006350232 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -91,7 +91,7 @@ gunicorn==26.0.0
# via -r requirements/base.in
identify==2.6.17
# via pre-commit
-idna==3.13
+idna==3.14
# via
# requests
# trustme
diff --git a/requirements/dev.txt b/requirements/dev.txt
index de8e14beb83..b3403b8d3e0 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -89,7 +89,7 @@ gunicorn==26.0.0
# via -r requirements/base.in
identify==2.6.17
# via pre-commit
-idna==3.13
+idna==3.14
# via
# requests
# trustme
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index c486dab30a5..ad7435ac2a9 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -18,7 +18,7 @@ click==8.3.3
# via towncrier
docutils==0.21.2
# via sphinx
-idna==3.13
+idna==3.14
# via requests
imagesize==1.5.0
# via sphinx
diff --git a/requirements/doc.txt b/requirements/doc.txt
index bfd975c6495..e1843cbbe1d 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -18,7 +18,7 @@ click==8.3.3
# via towncrier
docutils==0.21.2
# via sphinx
-idna==3.13
+idna==3.14
# via requests
imagesize==1.5.0
# via sphinx
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 369afd866c5..e3c548c117f 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -41,7 +41,7 @@ freezegun==1.5.5
# via -r requirements/lint.in
identify==2.6.17
# via pre-commit
-idna==3.13
+idna==3.14
# via trustme
iniconfig==2.3.0
# via pytest
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index 9eb626271a1..54928256f50 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -24,7 +24,7 @@ frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
-idna==3.13
+idna==3.14
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 55648c11515..86d28a70240 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -30,7 +30,7 @@ forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.5
# via -r requirements/test-common.in
-idna==3.13
+idna==3.14
# via trustme
iniconfig==2.3.0
# via pytest
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index b0ec679c219..53060fdece3 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -51,7 +51,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base-ft.in
-idna==3.13
+idna==3.14
# via
# trustme
# yarl
diff --git a/requirements/test.txt b/requirements/test.txt
index 95054660a7a..9e4f5485bd2 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -51,7 +51,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base.in
-idna==3.13
+idna==3.14
# via
# trustme
# yarl
From f71545480af3b131daab76cee378d8f9e39b8eb7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 May 2026 17:31:03 +0000
Subject: [PATCH 064/191] Bump certifi from 2026.2.25 to 2026.4.22 (#12506)
Bumps [certifi](https://github.com/certifi/python-certifi) from
2026.2.25 to 2026.4.22.
Commits
5dddfb0
2026.04.22 (#410)
f99eccd
Bump peter-evans/create-pull-request from 8.1.0 to 8.1.1 (#404)
918bed0
Bump actions/upload-artifact from 7.0.0 to 7.0.1 (#405)
0a49067
Bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0 (#403)
acf6ce8
Bump actions/download-artifact from 8.0.0 to 8.0.1 (#398)
feb0ed2
Bump actions/download-artifact from 7.0.0 to 8.0.0 (#397)
d9c11a5
Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#396)
- See full diff in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 41006350232..6000c37e529 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -40,7 +40,7 @@ brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
build==1.4.0
# via pip-tools
-certifi==2026.2.25
+certifi==2026.4.22
# via requests
cffi==2.0.0
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index b3403b8d3e0..327c68b32a3 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -40,7 +40,7 @@ brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
build==1.4.0
# via pip-tools
-certifi==2026.2.25
+certifi==2026.4.22
# via requests
cffi==2.0.0
# via
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index ad7435ac2a9..5948a790e98 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -10,7 +10,7 @@ alabaster==1.0.0
# via sphinx
babel==2.18.0
# via sphinx
-certifi==2026.2.25
+certifi==2026.4.22
# via requests
charset-normalizer==3.4.7
# via requests
diff --git a/requirements/doc.txt b/requirements/doc.txt
index e1843cbbe1d..b3a8e3812b9 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -10,7 +10,7 @@ alabaster==1.0.0
# via sphinx
babel==2.18.0
# via sphinx
-certifi==2026.2.25
+certifi==2026.4.22
# via requests
charset-normalizer==3.4.7
# via requests
From a0d188e4e77bbc7b7f3ef4b950edef7bebae7423 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 May 2026 17:38:20 +0000
Subject: [PATCH 065/191] Bump pydantic from 2.13.3 to 2.13.4 (#12509)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.13.3 to
2.13.4.
Release notes
Sourced from pydantic's
releases.
v2.13.4 2026-05-06
v2.13.4 (2026-05-06)
What's Changed
Packaging
Fixes
Full Changelog: https://github.com/pydantic/pydantic/compare/v2.13.3...v2.13.4
Changelog
Sourced from pydantic's
changelog.
v2.13.4 (2026-05-06)
GitHub
release
What's Changed
Packaging
Fixes
Commits
cf67d4b
Fix linting
f0d8a21
Prepare release v2.13.4
5e3fe1d
Check for pydantic tag pattern in CI
7f9edcc
Document tagging conventions
b46a0c9
Adapt pydantic-core linker flags on macOS
50629c8
Update to PyPy 7.3.22
8522ebb
Preserve RootModel core metadata
a37f3af
Adapt MISSING sentinel test to work with unreleased
typing_extensions ver...
909259a
Remove Logfire example in documentation
2c4174c
Bump libc from 0.2.155 to 0.2.185
- See full diff in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 4 ++--
requirements/dev.txt | 4 ++--
requirements/lint.txt | 4 ++--
requirements/test-common.txt | 4 ++--
requirements/test-ft.txt | 4 ++--
requirements/test.txt | 4 ++--
6 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 6000c37e529..7d8f69621c5 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -162,9 +162,9 @@ pycares==4.11.0
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.13.3
+pydantic==2.13.4
# via python-on-whales
-pydantic-core==2.46.3
+pydantic-core==2.46.4
# via pydantic
pyenchant==3.3.0
# via sphinxcontrib-spelling
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 327c68b32a3..417d711801f 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -159,9 +159,9 @@ pycares==4.11.0
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.13.3
+pydantic==2.13.4
# via python-on-whales
-pydantic-core==2.46.3
+pydantic-core==2.46.4
# via pydantic
pygments==2.20.0
# via
diff --git a/requirements/lint.txt b/requirements/lint.txt
index e3c548c117f..e717e642f86 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -75,9 +75,9 @@ pycares==4.11.0
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.13.3
+pydantic==2.13.4
# via python-on-whales
-pydantic-core==2.46.3
+pydantic-core==2.46.4
# via pydantic
pygments==2.20.0
# via
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 86d28a70240..7b7b72325b5 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -60,9 +60,9 @@ proxy-py==2.4.10
# via -r requirements/test-common.in
pycparser==3.0
# via cffi
-pydantic==2.13.3
+pydantic==2.13.4
# via python-on-whales
-pydantic-core==2.46.3
+pydantic-core==2.46.4
# via pydantic
pygments==2.20.0
# via
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 53060fdece3..b5994bf5f20 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -95,9 +95,9 @@ pycares==4.11.0
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.13.3
+pydantic==2.13.4
# via python-on-whales
-pydantic-core==2.46.3
+pydantic-core==2.46.4
# via pydantic
pygments==2.20.0
# via
diff --git a/requirements/test.txt b/requirements/test.txt
index 9e4f5485bd2..fbcee9e02fe 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -95,9 +95,9 @@ pycares==4.11.0
# via aiodns
pycparser==3.0
# via cffi
-pydantic==2.13.3
+pydantic==2.13.4
# via python-on-whales
-pydantic-core==2.46.3
+pydantic-core==2.46.4
# via pydantic
pygments==2.20.0
# via
From d4474a81517b3c9f6a1aa3b8373820d5cf97403c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 May 2026 17:41:27 +0000
Subject: [PATCH 066/191] Bump pkgconfig from 1.5.5 to 1.6.0 (#12511)
Bumps [pkgconfig](https://github.com/matze/pkgconfig) from 1.5.5 to
1.6.0.
Release notes
Sourced from pkgconfig's
releases.
v1.6.0
Changes see there: https://github.com/matze/pkgconfig?tab=readme-ov-file#changelog
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 7d8f69621c5..5c3127c00e3 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -140,7 +140,7 @@ pathspec==1.1.1
# via mypy
pip-tools==7.5.3
# via -r requirements/dev.in
-pkgconfig==1.5.5
+pkgconfig==1.6.0
# via -r requirements/test-common.in
platformdirs==4.9.6
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 417d711801f..48db9ec0cc4 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -137,7 +137,7 @@ pathspec==1.1.1
# via mypy
pip-tools==7.5.3
# via -r requirements/dev.in
-pkgconfig==1.5.5
+pkgconfig==1.6.0
# via -r requirements/test-common.in
platformdirs==4.9.6
# via
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 7b7b72325b5..c354df011a1 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -50,7 +50,7 @@ packaging==26.2
# via pytest
pathspec==1.1.1
# via mypy
-pkgconfig==1.5.5
+pkgconfig==1.6.0
# via -r requirements/test-common.in
pluggy==1.6.0
# via
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index b5994bf5f20..2cdd63c9fa4 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -79,7 +79,7 @@ packaging==26.2
# pytest
pathspec==1.1.1
# via mypy
-pkgconfig==1.5.5
+pkgconfig==1.6.0
# via -r requirements/test-common.in
pluggy==1.6.0
# via
diff --git a/requirements/test.txt b/requirements/test.txt
index fbcee9e02fe..95818452a3b 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -79,7 +79,7 @@ packaging==26.2
# pytest
pathspec==1.1.1
# via mypy
-pkgconfig==1.5.5
+pkgconfig==1.6.0
# via -r requirements/test-common.in
pluggy==1.6.0
# via
From 9108cef4700467ce8fa6d98a1e2195a3a420affe Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 May 2026 17:44:25 +0000
Subject: [PATCH 067/191] Bump propcache from 0.4.1 to 0.5.2 (#12508)
Bumps [propcache](https://github.com/aio-libs/propcache) from 0.4.1 to
0.5.2.
Release notes
Sourced from propcache's
releases.
0.5.2
0.5.0 and 0.5.1 were tagged earlier today but never reached PyPI:
0.5.0's deploy failed at cibuildwheel's post-build pytest on
free-threaded armv7l musllinux (SIGBUS under QEMU emulation while
importing the C extension), and 0.5.1's deploy hit a transient sigstore
Rekor 502 during the attestation step. 0.5.2 is the first of the three
to actually publish.
Features
Removals and backward incompatible breaking changes
Packaging updates and notes for downstreams
-
Changed the Cython build dependency from ~= 3.1.0 to
>= 3.2.0,
removing the upper version bound to avoid conflicts for downstream
packagers
-- by :user:jameshilliard and
:user:gundalow.
The upstream Cython version is pinned to 3.2.4 in the CI/CD
environment.
Related issues and pull requests on GitHub:
#184,
#188,
#214.
-
Start building and shipping riscv64 wheels
-- by :user:justeph.
Related issues and pull requests on GitHub:
#194.
-
The :pep:517 build backend now supports a new
build-inplace
config setting (and PROPCACHE_BUILD_INPLACE environment
variable)
for controlling whether to build the project in-tree or in a
temporary directory. It only affects wheels and is set up to build
in a temporary directory by default. It does not affect editable
wheel builds; they will keep being built in-tree regardless.
Here's an example of using this setting:
.. code-block:: console
... (truncated)
Changelog
Sourced from propcache's
changelog.
0.5.2
(2026-05-08)
No significant changes.
0.5.1
(2026-05-08)
No significant changes.
0.5.0
(2026-05-08)
Features
Removals and backward incompatible breaking changes
Packaging updates and notes for downstreams
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/runtime-deps.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
7 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 0374faf854b..5a3f5931cff 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -34,7 +34,7 @@ multidict==6.7.1
# yarl
packaging==26.2
# via gunicorn
-propcache==0.4.1
+propcache==0.5.2
# via
# -r requirements/runtime-deps.in
# yarl
diff --git a/requirements/base.txt b/requirements/base.txt
index bd1e85fa517..901d436e736 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -34,7 +34,7 @@ multidict==6.7.1
# yarl
packaging==26.2
# via gunicorn
-propcache==0.4.1
+propcache==0.5.2
# via
# -r requirements/runtime-deps.in
# yarl
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 5c3127c00e3..74cdbb93f93 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -152,7 +152,7 @@ pluggy==1.6.0
# pytest-cov
pre-commit==4.6.0
# via -r requirements/lint.in
-propcache==0.4.1
+propcache==0.5.2
# via
# -r requirements/runtime-deps.in
# yarl
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 48db9ec0cc4..c1fd0c7499d 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -149,7 +149,7 @@ pluggy==1.6.0
# pytest-cov
pre-commit==4.6.0
# via -r requirements/lint.in
-propcache==0.4.1
+propcache==0.5.2
# via
# -r requirements/runtime-deps.in
# yarl
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index 54928256f50..5ec563bdf8a 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -30,7 +30,7 @@ multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
-propcache==0.4.1
+propcache==0.5.2
# via
# -r requirements/runtime-deps.in
# yarl
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 2cdd63c9fa4..0e9e527fba3 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -85,7 +85,7 @@ pluggy==1.6.0
# via
# pytest
# pytest-cov
-propcache==0.4.1
+propcache==0.5.2
# via
# -r requirements/runtime-deps.in
# yarl
diff --git a/requirements/test.txt b/requirements/test.txt
index 95818452a3b..95948604613 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -85,7 +85,7 @@ pluggy==1.6.0
# via
# pytest
# pytest-cov
-propcache==0.4.1
+propcache==0.5.2
# via
# -r requirements/runtime-deps.in
# yarl
From c2d7192c94aca90aea53e976f254146527ce8b05 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 17:07:45 +0000
Subject: [PATCH 068/191] Bump requests from 2.33.1 to 2.34.0 (#12515)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [requests](https://github.com/psf/requests) from 2.33.1 to 2.34.0.
Release notes
Sourced from requests's
releases.
v2.34.0
2.34.0 (2026-05-11)
Announcements
-
Requests 2.34.0 introduces inline types, replacing those provided by
typeshed. Public API types should be fully compatible with mypy,
pyright,
and ty. We believe types are comprehensive but if you find
issues, please
report them to the pinned
tracking issue.
Special thanks to @bastimeyer, @cthoyt, @edgarrmondragon,
and @srittau for
helping review and test the types ahead of the release. (#7272)
Improvements
- Digest Auth hashing algorithms have added
usedforsecurity=False to clarify
security considerations. (#7310)
- Requests added support for Python 3.15 based on beta1. Downstream
projects
should be able to start testing prior to its release in October. (#7422)
- Requests added support for Python 3.14t. (#7419)
Bugfixes
Response.history no longer contains a reference to
itself, preventing
accidental looping when traversing the history list. (#7328)
- Requests no longer performs greedy matching on no_proxy domains. The
proxy_bypass implementation has been updated with CPython's fix from
bpo-39057. (#7427)
- Requests no longer incorrectly strips duplicate leading slashes in
URI paths. This should address user issues with specific presigned
URLs. Note the full fix requires urllib3 2.7.0+. (#7315)
New Contributors
Full Changelog: https://github.com/psf/requests/blob/main/HISTORY.md#2340-2026-05-11
Changelog
Sourced from requests's
changelog.
2.34.0 (2026-05-11)
Announcements
-
Requests 2.34.0 introduces inline types, replacing those provided by
typeshed. Public API types should be fully compatible with mypy,
pyright,
and ty. We believe types are comprehensive but if you find issues,
please
report them to the pinned tracking issue.
Special thanks to @bastimeyer, @cthoyt, @edgarrmondragon,
and @srittau for
helping review and test the types ahead of the release. (#7272)
Improvements
- Digest Auth hashing algorithms have added
usedforsecurity=False to clarify
security considerations. (#7310)
- Requests added support for Python 3.15 based on beta1. Downstream
projects
should be able to start testing prior to its release in October. (#7422)
- Requests added support for Python 3.14t. (#7419)
Bugfixes
Response.history no longer contains a reference to
itself, preventing
accidental looping when traversing the history list. (#7328)
- Requests no longer performs greedy matching on no_proxy domains. The
proxy_bypass implementation has been updated with CPython's fix from
bpo-39057. (#7427)
- Requests no longer incorrectly strips duplicate leading slashes in
URI paths. This should address user issues with specific presigned
URLs. Note the full fix requires urllib3 2.7.0+. (#7315)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 74cdbb93f93..629a4f6e569 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -211,7 +211,7 @@ re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.2.28
# via re-assert
-requests==2.33.1
+requests==2.34.0
# via
# sphinx
# sphinxcontrib-spelling
diff --git a/requirements/dev.txt b/requirements/dev.txt
index c1fd0c7499d..2abd49ce456 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -206,7 +206,7 @@ re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.2.28
# via re-assert
-requests==2.33.1
+requests==2.34.0
# via sphinx
rich==15.0.0
# via pytest-codspeed
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index 5948a790e98..3817798839b 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -34,7 +34,7 @@ pyenchant==3.3.0
# via sphinxcontrib-spelling
pygments==2.20.0
# via sphinx
-requests==2.33.1
+requests==2.34.0
# via
# sphinx
# sphinxcontrib-spelling
diff --git a/requirements/doc.txt b/requirements/doc.txt
index b3a8e3812b9..ed90f60b969 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -32,7 +32,7 @@ packaging==26.2
# via sphinx
pygments==2.20.0
# via sphinx
-requests==2.33.1
+requests==2.34.0
# via sphinx
snowballstemmer==3.0.1
# via sphinx
From d766caab7927ed39798dfc7804d8d72df9111aba Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 17:30:36 +0000
Subject: [PATCH 069/191] Bump coverage from 7.13.5 to 7.14.0 (#12518)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.13.5
to 7.14.0.
Changelog
Sourced from coverage's
changelog.
Version 7.14.0 — 2026-05-10
-
Feature: now when running one of the reporting commands, if there are
parallel data files that need combining, they will be implicitly
combined
before creating the report. There is no option to avoid the combination;
let
us know if you have a use case that requires it. Thanks, Tim Hatch
<pull 2162_>. Closes issue 1781.
-
Fix: the output from combine was too verbose, listing
each file
considered. Now it shows a single line with the counts of files
combined,
files skipped, and files with errors. The -q flag
suppresses this line.
The old detailed lines are available with the new
--debug=combine option.
-
Fix: running a Python file through a symlink now sets the sys.path
correctly,
matching regular Python behavior. Fixes issue 2157_.
-
Fix: Collector.flush_data could fail with
"RuntimeError: Set changed
size during iteration" when a tracer in another thread added a line
to the
per-file set that add_lines (or add_arcs) was
iterating. The values
passed to CoverageData are now snapshotted via
dict.copy() and
set.copy(), which are atomic under the GIL. Thanks,
Alex Vandiver <pull 2165_>_.
-
Fix: the soft keyword lazy is now bolded in HTML
reports.
-
We are no longer testing eventlet support. Eventlet started issuing
stern
deprecation warnings that break our tests. Our support code is still
there.
.. _issue 1781: coveragepy/coveragepy#1781
.. _issue 2157: coveragepy/coveragepy#2157
.. _pull 2162: coveragepy/coveragepy#2162
.. _pull 2165: coveragepy/coveragepy#2165
.. _changes_7-13-5:
Commits
646351b
docs: sample HTML for 7.14.0
39cd015
docs: prep for 7.14.0
649e8aa
docs: thanks Alex Vandiver for #2165
8cd392e
fix: snapshot data in Collector.flush_data to avoid threading race (#2165)
c48e0ed
fix: less output for combining
c2a3a28
docs: explain the change from #2162
1cd47aa
fix: implicit combine-during-report now removes the combined data
files
2d99fd7
feat: automatically combine coverage in report, thanks Tim Hatch (#2162)
9fbdcdf
fix: lazy soft keywords are bolded
5de7d02
build: oops, misplaced quote
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 629a4f6e569..84fa5f30ad8 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -57,7 +57,7 @@ click==8.3.3
# slotscheck
# towncrier
# wait-for-it
-coverage==7.13.5
+coverage==7.14.0
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 2abd49ce456..08f5841ea61 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -57,7 +57,7 @@ click==8.3.3
# slotscheck
# towncrier
# wait-for-it
-coverage==7.13.5
+coverage==7.14.0
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index c354df011a1..de3560d0672 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -16,7 +16,7 @@ cffi==2.0.0
# pytest-codspeed
click==8.3.3
# via wait-for-it
-coverage==7.13.5
+coverage==7.14.0
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 0e9e527fba3..fb8c5f4df55 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -31,7 +31,7 @@ cffi==2.0.0
# pytest-codspeed
click==8.3.3
# via wait-for-it
-coverage==7.13.5
+coverage==7.14.0
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/test.txt b/requirements/test.txt
index 95948604613..c9c9091f827 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -31,7 +31,7 @@ cffi==2.0.0
# pytest-codspeed
click==8.3.3
# via wait-for-it
-coverage==7.13.5
+coverage==7.14.0
# via
# -r requirements/test-common.in
# pytest-cov
From aad2c91e20529e06123f03104a4530f3e1db79a7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 17:44:58 +0000
Subject: [PATCH 070/191] Bump markdown-it-py from 4.0.0 to 4.2.0 (#12520)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps
[markdown-it-py](https://github.com/executablebooks/markdown-it-py) from
4.0.0 to 4.2.0.
Release notes
Sourced from markdown-it-py's
releases.
v4.2.0
What's Changed
Full Changelog: https://github.com/executablebooks/markdown-it-py/compare/v4.1.0...v4.2.0
v4.1.0
What's Changed
New Contributors
Full Changelog: https://github.com/executablebooks/markdown-it-py/compare/v4.0.0...v4.1.0
Changelog
Sourced from markdown-it-py's
changelog.
4.2.0 - 2026-05-07
- ✨ Add
make_fence_rule() factory for configurable fence
markers in #394
4.1.0 - 2025-05-06
- ✨ Add
gfm-like2 preset with task lists, alerts, and
single-tilde strikethrough core plugins in #388
- ✨ Allow plugins to register inline terminator characters in #391
- 👌 Fix quadratic complexity in
fragments_join /
text_join in #389,
thanks to @petricevich
- 👌 Add
--stdin option to CLI for reading Markdown from
standard input in #379,
thanks to @mcepl
- 🔧 Add typing to Scanner in #382,
thanks to
@Alunderin
Full Changelog: https://github.com/executablebooks/markdown-it-py/compare/v4.0.0...v4.1.0
Commits
36c5f54
🚀 RELEASE v4.2.0 (#395)
96cf077
✨ Add make_fence_rule() factory for configurable fence
markers (#394)
3b4ff6d
🚀 RELEASE v4.1.0 (#393)
8951f26
🔧 Update pre-commit hooks (#392)
693bb24
✨ Add gfm-like2 preset with task lists, alerts, and
single-tilde strikethro...
df6fd36
✨Allow plugins to register inline terminator characters (#391)
d4ea0ca
👌 Fix quadratic complexity in fragments_join /
text_join (#389)
8933147
🔧 Add typing to Scanner (#382)
2f6ae10
🔧 Add AGENTS.md and copilot-setup-steps workflow (#380)
49043e4
Add --stdin option to CLI for reading Markdown from standard input (#379)
- See full diff in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 84fa5f30ad8..ee94cdb4afb 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -110,7 +110,7 @@ jinja2==3.1.6
# towncrier
librt==0.10.0
# via mypy
-markdown-it-py==4.0.0
+markdown-it-py==4.2.0
# via rich
markupsafe==3.0.3
# via jinja2
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 08f5841ea61..80a685ae1d8 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -108,7 +108,7 @@ jinja2==3.1.6
# towncrier
librt==0.10.0
# via mypy
-markdown-it-py==4.0.0
+markdown-it-py==4.2.0
# via rich
markupsafe==3.0.3
# via jinja2
diff --git a/requirements/lint.txt b/requirements/lint.txt
index e717e642f86..bdba9591100 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -49,7 +49,7 @@ isal==1.7.2
# via -r requirements/lint.in
librt==0.10.0
# via mypy
-markdown-it-py==4.0.0
+markdown-it-py==4.2.0
# via rich
mdurl==0.1.2
# via markdown-it-py
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index de3560d0672..7ab95c58174 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -38,7 +38,7 @@ isal==1.8.0 ; python_version < "3.14"
# via -r requirements/test-common.in
librt==0.10.0
# via mypy
-markdown-it-py==4.0.0
+markdown-it-py==4.2.0
# via rich
mdurl==0.1.2
# via markdown-it-py
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index fb8c5f4df55..6e5609d00b3 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -61,7 +61,7 @@ isal==1.8.0 ; python_version < "3.14"
# via -r requirements/test-common.in
librt==0.10.0
# via mypy
-markdown-it-py==4.0.0
+markdown-it-py==4.2.0
# via rich
mdurl==0.1.2
# via markdown-it-py
diff --git a/requirements/test.txt b/requirements/test.txt
index c9c9091f827..810b7716cbf 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -61,7 +61,7 @@ isal==1.7.2 ; python_version < "3.14"
# via -r requirements/test-common.in
librt==0.10.0
# via mypy
-markdown-it-py==4.0.0
+markdown-it-py==4.2.0
# via rich
mdurl==0.1.2
# via markdown-it-py
From 2aedb8e7bfeeca977a46c0de3299013e90f6fdd6 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 17:58:36 +0000
Subject: [PATCH 071/191] Bump build from 1.4.0 to 1.5.0 (#12522)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [build](https://github.com/pypa/build) from 1.4.0 to 1.5.0.
Release notes
Sourced from build's
releases.
1.5.0
What's Changed
Full Changelog: https://github.com/pypa/build/compare/1.4.4...1.5.0
1.4.4
What's Changed
Full Changelog: https://github.com/pypa/build/compare/1.4.3...1.4.4
1.4.3
What's Changed
... (truncated)
Changelog
Sourced from build's
changelog.
####################
1.5.0 (2026-04-30)
####################
Features
- Drop Python 3.9 support - by :user:
henryiii
(:issue:1036)
Bugfixes
- Make
--ignore-installed opt-in from the API via
fresh=True - by :user:henryiii
(:issue:1056)
Miscellaneous
####################
1.4.4 (2026-04-22)
####################
Bugfixes
- Fix release pipeline generating
CHANGELOG.rst entries
with inconsistent heading levels, which broke sphinx -W
and pinned Read the Docs stable at 1.4.0 - by
:user:gaborbernat. (:issue:1031)
- Revert :pr:
1039 from build 1.4.3, no longer check
direct_url (for now) - by :user:henryiii
(:issue:1039)
- Add
--ignore-installed to pip install command to
prevent issues with packages already present in the isolated
build environment - by :user:henryiii
(:issue:1037) (:issue:1040)
- Partial revert of :pr:
973, keeping log messages in one
entry, multiple lines. (:issue:1044)
Miscellaneous
####################
1.4.3 (2026-04-10)
####################
Features
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index ee94cdb4afb..dc34de8137b 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -38,7 +38,7 @@ blockbuster==1.5.26
# -r requirements/test-common.in
brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
-build==1.4.0
+build==1.5.0
# via pip-tools
certifi==2026.4.22
# via requests
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 80a685ae1d8..eeb894fa8fa 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -38,7 +38,7 @@ blockbuster==1.5.26
# -r requirements/test-common.in
brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
-build==1.4.0
+build==1.5.0
# via pip-tools
certifi==2026.4.22
# via requests
From 00e20b1487cdf3d1a45df2b4f07d03942345b848 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 17:58:54 +0000
Subject: [PATCH 072/191] Bump virtualenv from 21.3.1 to 21.3.2 (#12521)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.3.1 to
21.3.2.
Release notes
Sourced from virtualenv's
releases.
21.3.2
What's Changed
Full Changelog: https://github.com/pypa/virtualenv/compare/21.3.1...21.3.2
Changelog
Sourced from virtualenv's
changelog.
#################
Release History
#################
.. towncrier-draft-entries:: [UNRELEASED DRAFT]
.. towncrier release notes start
v21.3.2 (2026-05-12)
No significant changes.
v21.3.1 (2026-05-05)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index dc34de8137b..8ba96fc89d4 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -287,7 +287,7 @@ uvloop==0.21.0 ; platform_system != "Windows"
# -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.3.1
+virtualenv==21.3.2
# via pre-commit
wait-for-it==2.3.0
# via -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index eeb894fa8fa..28029d1b62b 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -277,7 +277,7 @@ uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpytho
# -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.3.1
+virtualenv==21.3.2
# via pre-commit
wait-for-it==2.3.0
# via -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index bdba9591100..1d73174b5d4 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -129,7 +129,7 @@ uvloop==0.21.0 ; platform_system != "Windows"
# via -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.3.1
+virtualenv==21.3.2
# via pre-commit
zlib-ng==1.0.0
# via -r requirements/lint.in
From b0b0e097d21985d864f7b736f8002a9edb69a45c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 18:06:47 +0000
Subject: [PATCH 073/191] Bump regex from 2026.2.28 to 2026.5.9 (#12507)
Bumps [regex](https://github.com/mrabarnett/mrab-regex) from 2026.2.28
to 2026.5.9.
Changelog
Sourced from regex's
changelog.
Version: 2026.5.9
Reverse matching with full unicode casefolding could lead to
out-of-range string indexes.
Version: 2026.4.4
A fix for older Python versions before free-threading was
supported.
Version: 2026.4.3
More fixes for free-threading.
Version: 2026.3.32
Fixed segfault.
Version: 2026.3.31
Fixed bug again.
Version: 2026.3.30
Fixed bug.
Version: 2026.3.28
Fixed version.
Version: 2026.3.27
Various fixes, including ones to improve free-threading
support.
Version: 2026.2.28
Replaced atomic operations with mutex on pattern object for
free-threaded Python.
Version: 2026.2.26
PR
[#598](https://github.com/mrabarnett/mrab-regex/issues/598): Fix race
condition in storage caching with atomic operations.
Replaced use of PyUnicode_GET_LENGTH with PyUnicode_GetLength.
Version: 2026.2.19
Added \z as alias of \Z, like in re module.
Added prefixmatch as alias of match, like in re module.
Version: 2026.1.15
... (truncated)
Commits
e57d185
Reverse matching with full unicode casefolding lead to out-of-range
string in...
bc57b04
A fix for older Python versions before free-threading was
supported.
773e213
More fixes for free-threading.
5d51c75
Fixed segfault.
2aff2db
Fixed bug again.
16af8ae
Fixed bug.
2356563
Fixed bug.
f579e8f
Fixed version.
55315a0
Fixed version.
923d78e
Various fixes, including ones to improve free-threading support.
- See full diff in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 8ba96fc89d4..2c055a9927e 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -209,7 +209,7 @@ pyyaml==6.0.3
# via pre-commit
re-assert==1.1.0
# via -r requirements/test-common.in
-regex==2026.2.28
+regex==2026.5.9
# via re-assert
requests==2.34.0
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 28029d1b62b..bcd6d3e2c3a 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -204,7 +204,7 @@ pyyaml==6.0.3
# via pre-commit
re-assert==1.1.0
# via -r requirements/test-common.in
-regex==2026.2.28
+regex==2026.5.9
# via re-assert
requests==2.34.0
# via sphinx
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 7ab95c58174..f157accaadd 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -89,7 +89,7 @@ python-on-whales==0.81.0
# via -r requirements/test-common.in
re-assert==1.1.0
# via -r requirements/test-common.in
-regex==2026.2.28
+regex==2026.5.9
# via re-assert
rich==15.0.0
# via pytest-codspeed
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 6e5609d00b3..03b8ab3498c 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -124,7 +124,7 @@ python-on-whales==0.81.0
# via -r requirements/test-common.in
re-assert==1.1.0
# via -r requirements/test-common.in
-regex==2026.2.28
+regex==2026.5.9
# via re-assert
rich==15.0.0
# via pytest-codspeed
diff --git a/requirements/test.txt b/requirements/test.txt
index 810b7716cbf..69c79767a29 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -124,7 +124,7 @@ python-on-whales==0.81.0
# via -r requirements/test-common.in
re-assert==1.1.0
# via -r requirements/test-common.in
-regex==2026.2.28
+regex==2026.5.9
# via re-assert
rich==15.0.0
# via pytest-codspeed
From a551dbccb2e242024d01b13c5f42015a98612097 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 12 May 2026 18:12:27 +0000
Subject: [PATCH 074/191] Bump mypy from 2.0.0 to 2.1.0 (#12519)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [mypy](https://github.com/python/mypy) from 2.0.0 to 2.1.0.
Changelog
Sourced from mypy's
changelog.
Mypy Release Notes
Next Release
Mypy 2.1
We’ve just uploaded mypy 2.1.0 to the Python Package Index (PyPI).
Mypy is a static type checker for Python. This release includes new
features, performance
improvements and bug fixes. You can install it as follows:
python3 -m pip install -U mypy
You can read the full documentation for this release on Read the Docs.
librt.vecs: Fast Growable Array Type for Mypyc
The new librt.vecs module provides an efficient growable
array type vec that is
optimized for mypyc use. It provides fast, packed arrays with integer
and floating point
value types, which can be several times faster than
list, and tens of times faster
than array.array in code compiled using mypyc. It also
supports nested vec objects and
non-value-type items, such as vec[vec[str]].
Refer to the documentation
for
the details.
Contributed by Jukka Lehtosalo.
librt.random: Fast Pseudo-Random Number Generation
The new librt.random module provides fast pseudo-random
number generation that is
optimized for code compiled using mypyc. It can be 3x to 10x faster than
the stdlib
random module in compiled code.
Refer to the documentation
for
the details.
Contributed by Jukka Lehtosalo (PR 21433).
Mypyc Improvements
- Make compilation order with multiple files consistent (Piotr
Sawicki, PR 21419)
- Fix crash on accessing
StopAsyncIteration (Piotr
Sawicki, PR 21406)
- Fix incremental compilation with
separate flag
(Vaggelis Danias, PR 21299)
Fixes to Crashes
- Fix crash on partial type with
--allow-redefinition and
global declaration (Jukka Lehtosalo, PR 21428)
- Fix broken awaitable generator patching (Ivan Levkivskyi, PR 21435)
Changes to Messages
... (truncated)
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 4 ++--
requirements/dev.txt | 4 ++--
requirements/lint.txt | 4 ++--
requirements/test-common.txt | 4 ++--
requirements/test-ft.txt | 4 ++--
requirements/test.txt | 4 ++--
6 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 2c055a9927e..a9c87bdf559 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -108,7 +108,7 @@ jinja2==3.1.6
# via
# sphinx
# towncrier
-librt==0.10.0
+librt==0.11.0
# via mypy
markdown-it-py==4.2.0
# via rich
@@ -121,7 +121,7 @@ multidict==6.7.1
# -r requirements/multidict.in
# -r requirements/runtime-deps.in
# yarl
-mypy==2.0.0 ; implementation_name == "cpython"
+mypy==2.1.0 ; implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index bcd6d3e2c3a..2bb442d04d5 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -106,7 +106,7 @@ jinja2==3.1.6
# via
# sphinx
# towncrier
-librt==0.10.0
+librt==0.11.0
# via mypy
markdown-it-py==4.2.0
# via rich
@@ -118,7 +118,7 @@ multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
-mypy==2.0.0 ; implementation_name == "cpython"
+mypy==2.1.0 ; implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 1d73174b5d4..7bc393389a4 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -47,13 +47,13 @@ iniconfig==2.3.0
# via pytest
isal==1.7.2
# via -r requirements/lint.in
-librt==0.10.0
+librt==0.11.0
# via mypy
markdown-it-py==4.2.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-mypy==2.0.0 ; implementation_name == "cpython"
+mypy==2.1.0 ; implementation_name == "cpython"
# via -r requirements/lint.in
mypy-extensions==1.1.0
# via mypy
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index f157accaadd..e3c5d6b7c41 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -36,13 +36,13 @@ iniconfig==2.3.0
# via pytest
isal==1.8.0 ; python_version < "3.14"
# via -r requirements/test-common.in
-librt==0.10.0
+librt==0.11.0
# via mypy
markdown-it-py==4.2.0
# via rich
mdurl==0.1.2
# via markdown-it-py
-mypy==2.0.0 ; implementation_name == "cpython"
+mypy==2.1.0 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 03b8ab3498c..d38d630a88d 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -59,7 +59,7 @@ iniconfig==2.3.0
# via pytest
isal==1.8.0 ; python_version < "3.14"
# via -r requirements/test-common.in
-librt==0.10.0
+librt==0.11.0
# via mypy
markdown-it-py==4.2.0
# via rich
@@ -69,7 +69,7 @@ multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
-mypy==2.0.0 ; implementation_name == "cpython"
+mypy==2.1.0 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
diff --git a/requirements/test.txt b/requirements/test.txt
index 69c79767a29..3c4b0917287 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -59,7 +59,7 @@ iniconfig==2.3.0
# via pytest
isal==1.7.2 ; python_version < "3.14"
# via -r requirements/test-common.in
-librt==0.10.0
+librt==0.11.0
# via mypy
markdown-it-py==4.2.0
# via rich
@@ -69,7 +69,7 @@ multidict==6.7.1
# via
# -r requirements/runtime-deps.in
# yarl
-mypy==2.0.0 ; implementation_name == "cpython"
+mypy==2.1.0 ; implementation_name == "cpython"
# via -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
From 9f1b9883f9dfac953431fca61e2870d886670346 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 13 May 2026 01:05:01 +0000
Subject: [PATCH 075/191] Bump identify from 2.6.17 to 2.6.19 (#12510)
Bumps [identify](https://github.com/pre-commit/identify) from 2.6.17 to
2.6.19.
Commits
b39f637
v2.6.19
c976888
Merge pull request #588
from hofbi/patch-1
6110d73
Add support for 'tif' file extension
ccbd337
Merge pull request #587
from pre-commit/pre-commit-ci-update-config
f5af264
[pre-commit.ci] pre-commit autoupdate
a0be598
Merge pull request #586
from pre-commit/pre-commit-ci-update-config
b184043
[pre-commit.ci] pre-commit autoupdate
07a8017
v2.6.18
2609c0a
Merge pull request #581
from pre-commit/mxr-patch-1
74d7931
Configure pyproject.toml to have custom 'pyproject' file type
- See full diff in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index a9c87bdf559..e07b9b3a4a5 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -89,7 +89,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base.in
-identify==2.6.17
+identify==2.6.19
# via pre-commit
idna==3.14
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 2bb442d04d5..bb57abdd2de 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -87,7 +87,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base.in
-identify==2.6.17
+identify==2.6.19
# via pre-commit
idna==3.14
# via
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 7bc393389a4..63dd740c911 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -39,7 +39,7 @@ forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.5
# via -r requirements/lint.in
-identify==2.6.17
+identify==2.6.19
# via pre-commit
idna==3.14
# via trustme
From 8acd7c4876f0c3fcff7ccc4158f7b9f976637c1c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 13 May 2026 11:40:11 +0000
Subject: [PATCH 076/191] Bump pytest-codspeed from 4.5.0 to 5.0.1 (#12529)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [pytest-codspeed](https://github.com/CodSpeedHQ/pytest-codspeed)
from 4.5.0 to 5.0.1.
Release notes
Sourced from pytest-codspeed's
releases.
v5.0.1
What's Changed
Full Changelog: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v5.0.0...v5.0.1
v5.0.0
Highlights
MacOS walltime profiling is now available with the codspeed
cli v4.16.1 and above.
pytest-codspeed can now be used in free threaded mode.
This has been tested with 3.14t and 3.15t.
For this, we have dropped usage of cffi in favor of the
native extension support.
What's Changed
Full Changelog: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v4.5.0...v5.0.0
Changelog
Sourced from pytest-codspeed's
changelog.
[5.0.1] - 2026-05-13
💼 Other
[5.0.0] - 2026-05-13
🚀 Features
⚡ Performance
- Bind callgrind start/stop directly to avoid extra frame by
@art049 in #96
⚙️ Internals
Commits
d4d9dc6
Release v5.0.1 🚀
ad709a5
build: enable free-threaded wheels in cibuildwheel (#121)
080d620
Release v5.0.0 🚀
befdebf
chore: ignore common compilation warnings for instrument-hooks
ee98055
chore: use unsigned bash in the macos test
5a205c8
feat: use instrument_hooks markers in walltime
fda1fbc
feat(hooks): declare native extension free-thread safe (#120)
f3ed388
perf(hooks): bind callgrind start/stop directly to avoid extra
frame
e4a419e
chore: bump pinned uv version to 0.11.14
ee07afb
ci: add python 3.15 and 3.15t to test matrix
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 3 +--
requirements/dev.txt | 3 +--
requirements/lint.txt | 3 +--
requirements/test-common.txt | 6 ++----
requirements/test-ft.txt | 3 +--
requirements/test.txt | 3 +--
6 files changed, 7 insertions(+), 14 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index e07b9b3a4a5..30251b5d3df 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -46,7 +46,6 @@ cffi==2.0.0
# via
# cryptography
# pycares
- # pytest-codspeed
cfgv==3.5.0
# via pre-commit
charset-normalizer==3.4.7
@@ -185,7 +184,7 @@ pytest==9.0.3
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==4.5.0
+pytest-codspeed==5.0.1
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index bb57abdd2de..23d0c3618b5 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -46,7 +46,6 @@ cffi==2.0.0
# via
# cryptography
# pycares
- # pytest-codspeed
cfgv==3.5.0
# via pre-commit
charset-normalizer==3.4.7
@@ -180,7 +179,7 @@ pytest==9.0.3
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==4.5.0
+pytest-codspeed==5.0.1
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 63dd740c911..77bc593f283 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -20,7 +20,6 @@ cffi==2.0.0
# via
# cryptography
# pycares
- # pytest-codspeed
cfgv==3.5.0
# via pre-commit
click==8.3.3
@@ -88,7 +87,7 @@ pytest==9.0.3
# -r requirements/lint.in
# pytest-codspeed
# pytest-mock
-pytest-codspeed==4.5.0
+pytest-codspeed==5.0.1
# via -r requirements/lint.in
pytest-mock==3.15.1
# via -r requirements/lint.in
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index e3c5d6b7c41..8d1ed8401ba 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -11,9 +11,7 @@ ast-serialize==0.3.0
blockbuster==1.5.26
# via -r requirements/test-common.in
cffi==2.0.0
- # via
- # cryptography
- # pytest-codspeed
+ # via cryptography
click==8.3.3
# via wait-for-it
coverage==7.14.0
@@ -75,7 +73,7 @@ pytest==9.0.3
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==4.5.0
+pytest-codspeed==5.0.1
# via -r requirements/test-common.in
pytest-cov==7.1.0
# via -r requirements/test-common.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index d38d630a88d..43e29aeee45 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -28,7 +28,6 @@ cffi==2.0.0
# via
# cryptography
# pycares
- # pytest-codspeed
click==8.3.3
# via wait-for-it
coverage==7.14.0
@@ -110,7 +109,7 @@ pytest==9.0.3
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==4.5.0
+pytest-codspeed==5.0.1
# via -r requirements/test-common.in
pytest-cov==7.1.0
# via -r requirements/test-common.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 3c4b0917287..96fc50138c1 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -28,7 +28,6 @@ cffi==2.0.0
# via
# cryptography
# pycares
- # pytest-codspeed
click==8.3.3
# via wait-for-it
coverage==7.14.0
@@ -110,7 +109,7 @@ pytest==9.0.3
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==4.5.0
+pytest-codspeed==5.0.1
# via -r requirements/test-common.in
pytest-cov==7.1.0
# via -r requirements/test-common.in
From 7b8cbab4d5f3e788c205abae2596ce18f5e347f5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 13 May 2026 18:13:18 +0000
Subject: [PATCH 077/191] Bump idna from 3.14 to 3.15 (#12530)
Bumps [idna](https://github.com/kjd/idna) from 3.14 to 3.15.
Changelog
Sourced from idna's
changelog.
3.15 (2026-05-12)
- Enforce DNS-length cap on individual labels early in
check_label,
short-circuiting contextual-rule processing for oversized input
while staying compatible with UTS 46 usage.
- Tidy core helpers: hoist bidi category sets to module-level
frozensets (avoiding per-codepoint list construction), simplify
length checks, and reuse the shared
_unicode_dots_re from
idna.core in the codec module.
- Use
raise ... from err for proper exception chaining
and
switch internal string formatting to f-strings.
- Allow
flit_core 4.x in the build backend.
- Expand the ruff lint set (flake8-bugbear, flake8-simplify,
pyupgrade, perflint) and apply the surfaced fixes; pin lint CI
to Python 3.14.
- Add Dependabot configuration for GitHub Actions.
- Convert README and HISTORY from reStructuredText to Markdown.
- Reference CVE-2026-45409 for the 3.14 advisory in place of the
initial GHSA identifier.
Thanks to Felix Yan, Stan Ulbrych, and metsw24-max for
contributions to this release.
Commits
af30a09
Release 3.15
30314d4
Pre-release 3.15rc0
05d4b21
Merge pull request #237 from
kjd/convert-docs-to-markdown
2987fdb
Convert README and HISTORY from reStructuredText to Markdown
59fa800
Merge pull request #236 from
kjd/dependabot/github_actions/actions-f3e34333ea
def6983
Merge branch 'master' into
dependabot/github_actions/actions-f3e34333ea
bbd8004
Merge pull request #234 from
StanFromIreland/patch-1
edd07c0
Bump github/codeql-action from 3.35.2 to 4.35.2 in the actions
group
5557db0
Merge branch 'master' into patch-1
f11746c
Merge pull request #235 from
StanFromIreland/patch-2
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/runtime-deps.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
11 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 5a3f5931cff..4997512fad4 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -26,7 +26,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base-ft.in
-idna==3.14
+idna==3.15
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/base.txt b/requirements/base.txt
index 901d436e736..c62f33b577e 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -26,7 +26,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base.in
-idna==3.14
+idna==3.15
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 30251b5d3df..2afed24f06d 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -90,7 +90,7 @@ gunicorn==26.0.0
# via -r requirements/base.in
identify==2.6.19
# via pre-commit
-idna==3.14
+idna==3.15
# via
# requests
# trustme
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 23d0c3618b5..6e7943731a9 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -88,7 +88,7 @@ gunicorn==26.0.0
# via -r requirements/base.in
identify==2.6.19
# via pre-commit
-idna==3.14
+idna==3.15
# via
# requests
# trustme
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index 3817798839b..e83817f8bc2 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -18,7 +18,7 @@ click==8.3.3
# via towncrier
docutils==0.21.2
# via sphinx
-idna==3.14
+idna==3.15
# via requests
imagesize==1.5.0
# via sphinx
diff --git a/requirements/doc.txt b/requirements/doc.txt
index ed90f60b969..d66b2d07989 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -18,7 +18,7 @@ click==8.3.3
# via towncrier
docutils==0.21.2
# via sphinx
-idna==3.14
+idna==3.15
# via requests
imagesize==1.5.0
# via sphinx
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 77bc593f283..c02e3891d1d 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -40,7 +40,7 @@ freezegun==1.5.5
# via -r requirements/lint.in
identify==2.6.19
# via pre-commit
-idna==3.14
+idna==3.15
# via trustme
iniconfig==2.3.0
# via pytest
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index 5ec563bdf8a..05fb65c337a 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -24,7 +24,7 @@ frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
-idna==3.14
+idna==3.15
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 8d1ed8401ba..9cb3e3314e3 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -28,7 +28,7 @@ forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.5
# via -r requirements/test-common.in
-idna==3.14
+idna==3.15
# via trustme
iniconfig==2.3.0
# via pytest
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 43e29aeee45..3b707adccbb 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -50,7 +50,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base-ft.in
-idna==3.14
+idna==3.15
# via
# trustme
# yarl
diff --git a/requirements/test.txt b/requirements/test.txt
index 96fc50138c1..e8f13fd1986 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -50,7 +50,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base.in
-idna==3.14
+idna==3.15
# via
# trustme
# yarl
From e4d7411c2e5c5f7af466de090f07eaf2921c03ad Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 13 May 2026 18:13:52 +0000
Subject: [PATCH 078/191] Bump python-discovery from 1.3.0 to 1.3.1 (#12528)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [python-discovery](https://github.com/tox-dev/python-discovery)
from 1.3.0 to 1.3.1.
Release notes
Sourced from python-discovery's
releases.
v1.3.1
What's Changed
New Contributors
Full Changelog: https://github.com/tox-dev/python-discovery/compare/1.3.0...1.3.1
Changelog
Sourced from python-discovery's
changelog.
Bug fixes - 1.3.1
- export normalize_isa and deprecate KNOWN_ARCHITECTURES - by
:user:
rahuldevikar. (:issue:59)
- discover uv-managed Pythons on Windows. Previously the glob assumed
Unix layout (
<root>/<key>/bin/python) and
silently found nothing on Windows, where uv places
python.exe directly under the install root - by
:user:gaborbernat. (:issue:65)
- Canonicalize GraalVM to match GraalPy Python interpreter in
PythonSpec and PythonInfo. - by :user:
timfel.
(:issue:73)
v1.3.0 (2026-05-05)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 2afed24f06d..3d09edb1f5e 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -198,7 +198,7 @@ pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
-python-discovery==1.3.0
+python-discovery==1.3.1
# via virtualenv
python-on-whales==0.81.0
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 6e7943731a9..7fa0003ee9d 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -193,7 +193,7 @@ pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
-python-discovery==1.3.0
+python-discovery==1.3.1
# via virtualenv
python-on-whales==0.81.0
# via
diff --git a/requirements/lint.txt b/requirements/lint.txt
index c02e3891d1d..7a74bdffe84 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -93,7 +93,7 @@ pytest-mock==3.15.1
# via -r requirements/lint.in
python-dateutil==2.9.0.post0
# via freezegun
-python-discovery==1.3.0
+python-discovery==1.3.1
# via virtualenv
python-on-whales==0.81.0
# via -r requirements/lint.in
From 8c40e4ea8e6ef4dbb472564baf1cc1d6b6f056b9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 13 May 2026 18:14:25 +0000
Subject: [PATCH 079/191] Bump imagesize from 1.5.0 to 2.0.0 (#12525)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[//]: # (dependabot-start)
⚠️ **Dependabot is rebasing this PR** ⚠️
Rebasing might not happen immediately, so don't worry if this takes some
time.
Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.
---
[//]: # (dependabot-end)
Bumps [imagesize](https://github.com/shibukawa/imagesize_py) from 1.5.0
to 2.0.0.
Commits
5ab28d4
bump module version to 2.0
63d6afb
Merge pull request #82
from shibukawa/codex/update-readme-and-setup-instructi...
2946066
docs: clarify EXIF orientation formats in v2.0 notes
53eff2e
Merge pull request #81
from shibukawa/codex/refactor-code-to-reduce-duplication
ac14f2a
Refactor duplicated JPEG segment parsing logic
48ab954
Merge pull request #80
from shibukawa/codex/add-avif-exif-rotation-support
5cada10
Add AVIF EXIF rotation support
232c6d5
Merge pull request #79
from shibukawa/codex/add-heic/heif-support-and-rotation
324c970
Add HEIC/HEIF size and rotation support
7b7bb5f
Merge pull request #78
from shibukawa/codex/add-pypi-link-and-python-version-...
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 3d09edb1f5e..3af35076bde 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -95,7 +95,7 @@ idna==3.15
# requests
# trustme
# yarl
-imagesize==1.5.0
+imagesize==2.0.0
# via sphinx
iniconfig==2.3.0
# via pytest
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 7fa0003ee9d..9c7796fa8a3 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -93,7 +93,7 @@ idna==3.15
# requests
# trustme
# yarl
-imagesize==1.5.0
+imagesize==2.0.0
# via sphinx
iniconfig==2.3.0
# via pytest
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index e83817f8bc2..20aa6b758be 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -20,7 +20,7 @@ docutils==0.21.2
# via sphinx
idna==3.15
# via requests
-imagesize==1.5.0
+imagesize==2.0.0
# via sphinx
jinja2==3.1.6
# via
diff --git a/requirements/doc.txt b/requirements/doc.txt
index d66b2d07989..07019543c0b 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -20,7 +20,7 @@ docutils==0.21.2
# via sphinx
idna==3.15
# via requests
-imagesize==1.5.0
+imagesize==2.0.0
# via sphinx
jinja2==3.1.6
# via
From 6d09be73c36bfe553db7b60740e09360b54055d1 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Wed, 13 May 2026 19:24:00 +0000
Subject: [PATCH 080/191] [PR #12395/7eb0e802 backport][3.14] Fix CookieError
crash with control characters on CPython builds with CVE-2026-3644 patch
(#12533)
**This is a backport of PR #12395 as merged into master
(7eb0e8029cdb270a98a203bc5d4bad32cdc2c19c).**
Co-authored-by: Rodrigo Nogueira
---
CHANGES/12395.bugfix.rst | 4 ++
aiohttp/_cookie_helpers.py | 62 +++++++++++++------
tests/test_cookie_helpers.py | 117 ++++++++++++++++++++++++++++++++++-
3 files changed, 161 insertions(+), 22 deletions(-)
create mode 100644 CHANGES/12395.bugfix.rst
diff --git a/CHANGES/12395.bugfix.rst b/CHANGES/12395.bugfix.rst
new file mode 100644
index 00000000000..e3c67bfa7aa
--- /dev/null
+++ b/CHANGES/12395.bugfix.rst
@@ -0,0 +1,4 @@
+Fixed a crash (:external+python:exc:`~http.cookies.CookieError`) in the cookie parser when receiving cookies
+containing ASCII control characters on CPython builds with the :cve:`2026-3644`
+patch. The parser now gracefully skips cookies whose value contains control
+characters instead of letting the exception propagate -- by :user:`rodrigobnogueira`.
diff --git a/aiohttp/_cookie_helpers.py b/aiohttp/_cookie_helpers.py
index aca86b7f771..00ca820768e 100644
--- a/aiohttp/_cookie_helpers.py
+++ b/aiohttp/_cookie_helpers.py
@@ -7,7 +7,7 @@
import re
from collections.abc import Sequence
-from http.cookies import Morsel
+from http.cookies import CookieError, Morsel
from typing import cast
from .log import internal_logger
@@ -106,9 +106,16 @@ def preserve_morsel_with_coded_value(cookie: Morsel[str]) -> Morsel[str]:
# bypass validation and set already validated state. This is more stable than
# setting protected attributes directly and unlikely to change since it would
# break pickling.
- mrsl_val.__setstate__( # type: ignore[attr-defined]
- {"key": cookie.key, "value": cookie.value, "coded_value": cookie.coded_value}
- )
+ try:
+ mrsl_val.__setstate__( # type: ignore[attr-defined]
+ {
+ "key": cookie.key,
+ "value": cookie.value,
+ "coded_value": cookie.coded_value,
+ }
+ )
+ except CookieError:
+ return cookie
return mrsl_val
@@ -206,10 +213,18 @@ def parse_cookie_header(header: str) -> list[tuple[str, Morsel[str]]]:
invalid_names.append(key)
else:
morsel = Morsel()
- morsel.__setstate__( # type: ignore[attr-defined]
- {"key": key, "value": _unquote(value), "coded_value": value}
- )
- cookies.append((key, morsel))
+ try:
+ morsel.__setstate__( # type: ignore[attr-defined]
+ {
+ "key": key,
+ "value": _unquote(value),
+ "coded_value": value,
+ }
+ )
+ except CookieError:
+ pass
+ else:
+ cookies.append((key, morsel))
# Move to next cookie or end
i = next_semi + 1 if next_semi != -1 else n
@@ -231,9 +246,12 @@ def parse_cookie_header(header: str) -> list[tuple[str, Morsel[str]]]:
# bypass validation and set already validated state. This is more stable than
# setting protected attributes directly and unlikely to change since it would
# break pickling.
- morsel.__setstate__( # type: ignore[attr-defined]
- {"key": key, "value": _unquote(value), "coded_value": value}
- )
+ try:
+ morsel.__setstate__( # type: ignore[attr-defined]
+ {"key": key, "value": _unquote(value), "coded_value": value}
+ )
+ except CookieError:
+ continue
cookies.append((key, morsel))
@@ -323,15 +341,19 @@ def parse_set_cookie_headers(headers: Sequence[str]) -> list[tuple[str, Morsel[s
# Create new morsel
current_morsel = Morsel()
# Preserve the original value as coded_value (with quotes if present)
- # We use __setstate__ instead of the public set() API because it allows us to
- # bypass validation and set already validated state. This is more stable than
- # setting protected attributes directly and unlikely to change since it would
- # break pickling.
- current_morsel.__setstate__( # type: ignore[attr-defined]
- {"key": key, "value": _unquote(value), "coded_value": value}
- )
- parsed_cookies.append((key, current_morsel))
- morsel_seen = True
+ try:
+ current_morsel.__setstate__( # type: ignore[attr-defined]
+ {
+ "key": key,
+ "value": _unquote(value),
+ "coded_value": value,
+ }
+ )
+ except CookieError:
+ current_morsel = None
+ else:
+ parsed_cookies.append((key, current_morsel))
+ morsel_seen = True
else:
# Invalid cookie string - no value for non-attribute
break
diff --git a/tests/test_cookie_helpers.py b/tests/test_cookie_helpers.py
index fead869d6f3..be6228d698f 100644
--- a/tests/test_cookie_helpers.py
+++ b/tests/test_cookie_helpers.py
@@ -1134,6 +1134,65 @@ def test_parse_set_cookie_headers_uses_unquote_with_octal(
assert morsel.coded_value == expected_coded
+@pytest.mark.parametrize(
+ ("header", "expected_name", "expected_coded"),
+ [
+ pytest.param(
+ r'name="\012newline\012"',
+ "name",
+ r'"\012newline\012"',
+ id="newline-octal-012",
+ ),
+ pytest.param(
+ r'tab="\011separated\011values"',
+ "tab",
+ r'"\011separated\011values"',
+ id="tab-octal-011",
+ ),
+ ],
+)
+def test_parse_set_cookie_headers_ctl_chars_from_octal(
+ header: str, expected_name: str, expected_coded: str
+) -> None:
+ """Ensure octal escapes that decode to control characters don't crash the parser.
+
+ CPython builds with the CVE-2026-3644 patch reject control characters in
+ cookies. When octal unquoting produces a control character, the parser
+ skips the cookie entirely instead of raising CookieError.
+ """
+ result = parse_set_cookie_headers([header])
+
+ # On CPython with CVE-2026-3644 patch the cookie is rejected (result is empty);
+ # on older builds it may be accepted with the decoded value.
+ # Either way, no crash.
+ if result:
+ name, morsel = result[0]
+ assert name == expected_name
+ assert morsel.coded_value == expected_coded
+
+
+def test_parse_set_cookie_headers_literal_ctl_chars() -> None:
+ r"""Ensure literal control characters in a cookie value don't crash the parser.
+
+ If the raw header itself contains a control character (e.g. BEL \\x07),
+ both the decoded value and coded_value are unsalvageable. The parser
+ should gracefully skip the cookie instead of raising CookieError.
+ """
+ result = parse_set_cookie_headers(['name="a\x07b"'])
+ # On CPython with CVE-2026-3644 patch the cookie is skipped;
+ # on older builds it may be accepted. Either way, no crash.
+ if result:
+ assert result[0][0] == "name"
+
+
+def test_parse_set_cookie_headers_literal_ctl_chars_preserves_others() -> None:
+ """Ensure a cookie with literal control chars doesn't break subsequent cookies."""
+ result = parse_set_cookie_headers(['bad="a\x07b"; good=value', "another=cookie"])
+ # "good" is an attribute of "bad" (same header), so it's not a separate cookie.
+ # "another" is in a separate header and must always be preserved.
+ assert any(name == "another" for name, _ in result)
+
+
# Tests for parse_cookie_header (RFC 6265 compliant Cookie header parser)
@@ -1597,8 +1656,17 @@ def test_parse_cookie_header_empty_key_in_fallback(
assert name2 == "another"
assert morsel2.value == "test"
- assert "Cannot load cookie. Illegal cookie name" in caplog.text
- assert "''" in caplog.text
+
+def test_parse_cookie_header_literal_ctl_chars() -> None:
+ r"""Ensure literal control characters in a cookie value don't crash the parser.
+
+ If the raw header itself contains a control character (e.g. BEL \\x07),
+ the cookie is unsalvageable. The parser should gracefully skip it.
+ """
+ result = parse_cookie_header('name="a\x07b"; good=cookie')
+ # On CPython with CVE-2026-3644 patch the bad cookie is skipped;
+ # on older builds it may be accepted. Either way, no crash.
+ assert any(name == "good" for name, _ in result)
@pytest.mark.parametrize(
@@ -1789,3 +1857,48 @@ def test_unquote_compatibility_with_simplecookie(test_value: str) -> None:
f"our={_unquote(test_value)!r}, "
f"SimpleCookie={simplecookie_unquote(test_value)!r}"
)
+
+
+@pytest.fixture
+def mock_strict_morsel(
+ monkeypatch: pytest.MonkeyPatch,
+) -> None:
+ original_setstate = Morsel.__setstate__ # type: ignore[attr-defined]
+
+ def _mock_setstate(self: Morsel[str], state: dict[str, str]) -> None:
+ if any(ord(c) < 32 for c in state.get("value", "")):
+ raise CookieError()
+ original_setstate(self, state)
+
+ monkeypatch.setattr(
+ "aiohttp._cookie_helpers.Morsel.__setstate__",
+ _mock_setstate,
+ )
+
+
+@pytest.mark.usefixtures("mock_strict_morsel")
+def test_cookie_helpers_cve_fallback() -> None:
+ # Clean value: mock delegates to original_setstate → succeeds
+ m: Morsel[str] = Morsel()
+ m.__setstate__({"key": "k", "value": "clean", "coded_value": "clean"}) # type: ignore[attr-defined]
+ assert m.key == "k"
+
+ # With strict morsel: any CTL char in value → CookieError → rejected
+ with pytest.raises(CookieError):
+ Morsel().__setstate__( # type: ignore[attr-defined]
+ {"key": "k", "value": "v\n", "coded_value": "v\\012"}
+ )
+ with pytest.raises(CookieError):
+ Morsel().__setstate__( # type: ignore[attr-defined]
+ {"key": "k", "value": "v\n", "coded_value": "v\n"}
+ )
+
+ cookie: Morsel[str] = Morsel()
+ cookie._key, cookie._value, cookie._coded_value = "k", "v\n", "v\n" # type: ignore[attr-defined]
+ assert preserve_morsel_with_coded_value(cookie) is cookie
+
+ assert parse_cookie_header("f=b\x07r;") == []
+ assert parse_cookie_header("f=b\x07r") == []
+ assert parse_cookie_header('f="b\x07r";') == []
+ assert parse_set_cookie_headers(['f="b\x07r";']) == []
+ assert parse_set_cookie_headers([r'name="\012newline\012"']) == []
From ad9baf0b3abf97190e18ffff6dcc0034372bf955 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 14 May 2026 13:14:44 +0000
Subject: [PATCH 081/191] Bump requests from 2.34.0 to 2.34.1 (#12539)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [requests](https://github.com/psf/requests) from 2.34.0 to 2.34.1.
Release notes
Sourced from requests's
releases.
v2.34.1
2.34.1 (2026-05-13)
Bugfixes
- Widened
json input type from dict and
list to Mapping
and Sequence. (#7436)
- Changed
headers input type to MutableMapping and
removed None from
Request.headers typing to improve handling for users. (#7431)
Response.reason moved from str | None to
str to improve handling
for users. (#7437)
- Fixed a bug where some bodies with custom
__getattr__
implementations
weren't being properly detected as Iterables. (#7433)
New Contributors
Full Changelog: https://github.com/psf/requests/blob/main/HISTORY.md#2341-2026-05-13
Changelog
Sourced from requests's
changelog.
2.34.1 (2026-05-13)
Bugfixes
- Widened
json input type from dict and
list to Mapping
and Sequence. (#7436)
- Changed
headers input type to MutableMapping and
removed None from
Request.headers typing to improve handling for users. (#7431)
Response.reason moved from str | None to
str to improve handling
for users. (#7437)
- Fixed a bug where some bodies with custom
__getattr__
implementations
weren't being properly detected as Iterables. (#7433)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 3af35076bde..2ce0fa22d9c 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -210,7 +210,7 @@ re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.5.9
# via re-assert
-requests==2.34.0
+requests==2.34.1
# via
# sphinx
# sphinxcontrib-spelling
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 9c7796fa8a3..00b84161e87 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -205,7 +205,7 @@ re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.5.9
# via re-assert
-requests==2.34.0
+requests==2.34.1
# via sphinx
rich==15.0.0
# via pytest-codspeed
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index 20aa6b758be..415d2024cf8 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -34,7 +34,7 @@ pyenchant==3.3.0
# via sphinxcontrib-spelling
pygments==2.20.0
# via sphinx
-requests==2.34.0
+requests==2.34.1
# via
# sphinx
# sphinxcontrib-spelling
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 07019543c0b..347ddac1e95 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -32,7 +32,7 @@ packaging==26.2
# via sphinx
pygments==2.20.0
# via sphinx
-requests==2.34.0
+requests==2.34.1
# via sphinx
snowballstemmer==3.0.1
# via sphinx
From 45ac3c9dbc8129d9fb55ac160b7c8a478c6a5c5d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 14 May 2026 14:28:23 +0000
Subject: [PATCH 082/191] Bump virtualenv from 21.3.2 to 21.3.3 (#12538)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.3.2 to
21.3.3.
Release notes
Sourced from virtualenv's
releases.
21.3.3
What's Changed
Full Changelog: https://github.com/pypa/virtualenv/compare/21.3.2...21.3.3
Changelog
Sourced from virtualenv's
changelog.
Bugfixes - 21.3.3
- recognize GraalPy interpreters using the normalized
GraalPy name - by :user:timfel.
(:issue:3144)
v21.3.2 (2026-05-12)
No significant changes.
v21.3.1 (2026-05-05)
Bugfixes - 21.3.1
-
Upgrade embedded wheels:
- pip to
26.1.1 from 26.1
(:issue:3138)
v21.3.0 (2026-04-27)
Features - 21.3.0
- Re-introduce
xonsh shell activator
(activate.xsh) previously removed in 20.7.0, and make the
plugin loader
prefer virtualenv's built-in entry points so a third-party package
cannot override them by registering a duplicate
name. (:issue:3003)
Bugfixes - 21.3.0
-
Upgrade embedded wheels:
- pip to
26.1 (:issue:3132)
v21.2.4 (2026-04-14)
Bugfixes - 21.2.4
- Security hardening: validate each entry of a seed wheel archive
before extracting it so a tampered wheel cannot escape
the app-data image directory via an absolute path or
..
traversal. (:issue:3118)
- Security hardening: verify the SHA-256 of every bundled seed wheel
when it is loaded so a corrupted or tampered file
... (truncated)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 2ce0fa22d9c..cda8e6e1b17 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -286,7 +286,7 @@ uvloop==0.21.0 ; platform_system != "Windows"
# -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.3.2
+virtualenv==21.3.3
# via pre-commit
wait-for-it==2.3.0
# via -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 00b84161e87..8c0621cd569 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -276,7 +276,7 @@ uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpytho
# -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.3.2
+virtualenv==21.3.3
# via pre-commit
wait-for-it==2.3.0
# via -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 7a74bdffe84..3a68cb3101b 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -128,7 +128,7 @@ uvloop==0.21.0 ; platform_system != "Windows"
# via -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.3.2
+virtualenv==21.3.3
# via pre-commit
zlib-ng==1.0.0
# via -r requirements/lint.in
From ccbcb64dcbcdf3dcbf533219c3a724f014413247 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 02:33:58 +0000
Subject: [PATCH 083/191] Bump aiodns from 3.6.1 to 4.0.0 (#11947)
Bumps [aiodns](https://github.com/saghul/aiodns) from 3.6.1 to 4.0.0.
Changelog
Sourced from aiodns's
changelog.
4.0.0
- Breaking change: Requires pycares >= 5.0.0
- Added new
query_dns() method returning native pycares
5.x DNSResult types
- Deprecated
query() method - still works with
backward-compatible result types
- Deprecated
gethostbyname() method - use
getaddrinfo() instead
- Added compatibility layer for pycares 4.x result types to ease
migration
- Updated dependencies
- Bumped pycares from 4.11.0 to 5.0.1 (#220)
- Bumped pytest from 8.4.2 to 9.0.2 (#224)
- Bumped pytest-asyncio from 1.2.0 to 1.3.0 (#223)
- Bumped mypy from 1.19.0 to 1.19.1 (#219)
- Bumped winloop from 0.3.1 to 0.4.0 (#210)
- Bumped actions/upload-artifact from 5 to 6 (#222)
- Bumped actions/download-artifact from 6.0.0 to 7.0.0 (#221)
Commits
> **Note**
> Automatic rebases have been disabled on this pull request as it has
been open for over 30 days.
---------
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston
---
aiohttp/resolver.py | 7 +++----
requirements/base-ft.txt | 4 ++--
requirements/base.txt | 4 ++--
requirements/constraints.txt | 4 ++--
requirements/dev.txt | 4 ++--
requirements/lint.txt | 4 ++--
requirements/runtime-deps.txt | 4 ++--
requirements/test-ft.txt | 4 ++--
requirements/test.txt | 4 ++--
9 files changed, 19 insertions(+), 20 deletions(-)
diff --git a/aiohttp/resolver.py b/aiohttp/resolver.py
index 85c36da0c31..84b5ffb667e 100644
--- a/aiohttp/resolver.py
+++ b/aiohttp/resolver.py
@@ -127,8 +127,7 @@ async def resolve(
hosts: list[ResolveResult] = []
for node in resp.nodes:
address: tuple[bytes, int] | tuple[bytes, int, int, int] = node.addr
- family = node.family
- if family == socket.AF_INET6:
+ if node.family == socket.AF_INET6:
if len(address) > 3 and address[3]:
# This is essential for link-local IPv6 addresses.
# LL IPv6 is a VERY rare case. Strictly speaking, we should use
@@ -142,7 +141,7 @@ async def resolve(
resolved_host = address[0].decode("ascii")
port = address[1]
else: # IPv4
- assert family == socket.AF_INET
+ assert node.family == socket.AF_INET
resolved_host = address[0].decode("ascii")
port = address[1]
hosts.append(
@@ -150,7 +149,7 @@ async def resolve(
hostname=host,
host=resolved_host,
port=port,
- family=family,
+ family=node.family,
proto=0,
flags=_NUMERIC_SOCKET_FLAGS,
)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 4997512fad4..10fa8f02117 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/base-ft.txt --strip-extras requirements/base-ft.in
#
-aiodns==3.6.1
+aiodns==4.0.0
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
@@ -38,7 +38,7 @@ propcache==0.5.2
# via
# -r requirements/runtime-deps.in
# yarl
-pycares==4.11.0
+pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
diff --git a/requirements/base.txt b/requirements/base.txt
index c62f33b577e..20730032d38 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/base.txt --strip-extras requirements/base.in
#
-aiodns==3.6.1
+aiodns==4.0.0
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
@@ -38,7 +38,7 @@ propcache==0.5.2
# via
# -r requirements/runtime-deps.in
# yarl
-pycares==4.11.0
+pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index cda8e6e1b17..2553d26bc91 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/constraints.txt --strip-extras requirements/constraints.in
#
-aiodns==3.6.1
+aiodns==4.0.0
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
@@ -157,7 +157,7 @@ propcache==0.5.2
# yarl
proxy-py==2.4.10
# via -r requirements/test-common.in
-pycares==4.11.0
+pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 8c0621cd569..d90bd2671fe 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/dev.txt --strip-extras requirements/dev.in
#
-aiodns==3.6.1
+aiodns==4.0.0
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
@@ -154,7 +154,7 @@ propcache==0.5.2
# yarl
proxy-py==2.4.10
# via -r requirements/test-common.in
-pycares==4.11.0
+pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 3a68cb3101b..15db6efe0b0 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/lint.txt --resolver=backtracking --strip-extras requirements/lint.in
#
-aiodns==3.6.1
+aiodns==4.0.0
# via -r requirements/lint.in
annotated-types==0.7.0
# via pydantic
@@ -70,7 +70,7 @@ pluggy==1.6.0
# via pytest
pre-commit==4.6.0
# via -r requirements/lint.in
-pycares==4.11.0
+pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index 05fb65c337a..df3530154c3 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/runtime-deps.txt --strip-extras requirements/runtime-deps.in
#
-aiodns==3.6.1
+aiodns==4.0.0
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
@@ -34,7 +34,7 @@ propcache==0.5.2
# via
# -r requirements/runtime-deps.in
# yarl
-pycares==4.11.0
+pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 3b707adccbb..4b6022c47cf 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/test-ft.txt --strip-extras requirements/test-ft.in
#
-aiodns==3.6.1
+aiodns==4.0.0
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
@@ -90,7 +90,7 @@ propcache==0.5.2
# yarl
proxy-py==2.4.10
# via -r requirements/test-common.in
-pycares==4.11.0
+pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
diff --git a/requirements/test.txt b/requirements/test.txt
index e8f13fd1986..d05424534b9 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/test.txt --strip-extras requirements/test.in
#
-aiodns==3.6.1
+aiodns==4.0.0
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
@@ -90,7 +90,7 @@ propcache==0.5.2
# yarl
proxy-py==2.4.10
# via -r requirements/test-common.in
-pycares==4.11.0
+pycares==5.0.1
# via aiodns
pycparser==3.0
# via cffi
From 554d8af54ab4e34ac4c8e1e5107ed747e1580d0d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 11:27:53 +0000
Subject: [PATCH 084/191] Bump requests from 2.34.1 to 2.34.2 (#12546)
Bumps [requests](https://github.com/psf/requests) from 2.34.1 to 2.34.2.
Release notes
Sourced from requests's
releases.
v2.34.2
2.34.2 (2026-05-14)
- Moved
headers input type back to Mapping
to avoid invariance issues with MutableMapping and inferred
dict types. Users calling Request.headers.update() may need
to narrow typing in their code. (#7441)
Full Changelog: https://github.com/psf/requests/blob/main/HISTORY.md#2342-2026-05-14
Changelog
Sourced from requests's
changelog.
2.34.2 (2026-05-14)
- Moved
headers input type back to Mapping
to avoid invariance issues
with MutableMapping and inferred dict types. Users calling
Request.headers.update() may need to narrow typing in their
code. (#7441)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 2553d26bc91..50a0807def8 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -210,7 +210,7 @@ re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.5.9
# via re-assert
-requests==2.34.1
+requests==2.34.2
# via
# sphinx
# sphinxcontrib-spelling
diff --git a/requirements/dev.txt b/requirements/dev.txt
index d90bd2671fe..9487db895b6 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -205,7 +205,7 @@ re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.5.9
# via re-assert
-requests==2.34.1
+requests==2.34.2
# via sphinx
rich==15.0.0
# via pytest-codspeed
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index 415d2024cf8..ed58e6c876f 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -34,7 +34,7 @@ pyenchant==3.3.0
# via sphinxcontrib-spelling
pygments==2.20.0
# via sphinx
-requests==2.34.1
+requests==2.34.2
# via
# sphinx
# sphinxcontrib-spelling
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 347ddac1e95..660c70163e8 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -32,7 +32,7 @@ packaging==26.2
# via sphinx
pygments==2.20.0
# via sphinx
-requests==2.34.1
+requests==2.34.2
# via sphinx
snowballstemmer==3.0.1
# via sphinx
From 680cac4c8cffecd343f1ac9f081da5f5c20350ba Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 15 May 2026 11:30:53 +0000
Subject: [PATCH 085/191] Bump pytest-codspeed from 5.0.1 to 5.0.2 (#12545)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[//]: # (dependabot-start)
⚠️ **Dependabot is rebasing this PR** ⚠️
Rebasing might not happen immediately, so don't worry if this takes some
time.
Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.
---
[//]: # (dependabot-end)
Bumps [pytest-codspeed](https://github.com/CodSpeedHQ/pytest-codspeed)
from 5.0.1 to 5.0.2.
Release notes
Sourced from pytest-codspeed's
releases.
v5.0.2
What's Changed
Full Changelog: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v5.0.1...v5.0.2
Changelog
Sourced from pytest-codspeed's
changelog.
[5.0.2] - 2026-05-14
🚀 Features
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 50a0807def8..e2e39becce8 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -184,7 +184,7 @@ pytest==9.0.3
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==5.0.1
+pytest-codspeed==5.0.2
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 9487db895b6..236052a9f8d 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -179,7 +179,7 @@ pytest==9.0.3
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==5.0.1
+pytest-codspeed==5.0.2
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 15db6efe0b0..cc38079bb98 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -87,7 +87,7 @@ pytest==9.0.3
# -r requirements/lint.in
# pytest-codspeed
# pytest-mock
-pytest-codspeed==5.0.1
+pytest-codspeed==5.0.2
# via -r requirements/lint.in
pytest-mock==3.15.1
# via -r requirements/lint.in
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 9cb3e3314e3..48a9114ab27 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -73,7 +73,7 @@ pytest==9.0.3
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==5.0.1
+pytest-codspeed==5.0.2
# via -r requirements/test-common.in
pytest-cov==7.1.0
# via -r requirements/test-common.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 4b6022c47cf..2013c7512c7 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -109,7 +109,7 @@ pytest==9.0.3
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==5.0.1
+pytest-codspeed==5.0.2
# via -r requirements/test-common.in
pytest-cov==7.1.0
# via -r requirements/test-common.in
diff --git a/requirements/test.txt b/requirements/test.txt
index d05424534b9..c3dcb79a2b1 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -109,7 +109,7 @@ pytest==9.0.3
# pytest-cov
# pytest-mock
# pytest-xdist
-pytest-codspeed==5.0.1
+pytest-codspeed==5.0.2
# via -r requirements/test-common.in
pytest-cov==7.1.0
# via -r requirements/test-common.in
From 8808809e5c5d489cfe70abe52b06d5afb088fecd Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Sat, 16 May 2026 17:41:59 +0100
Subject: [PATCH 086/191] Fix unpickler (#12557)
---
aiohttp/cookiejar.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py
index a0330ec901a..cd19c9e79cc 100644
--- a/aiohttp/cookiejar.py
+++ b/aiohttp/cookiejar.py
@@ -39,13 +39,18 @@
_SIMPLE_COOKIE = SimpleCookie()
-class _RestrictedCookieUnpickler(pickle.Unpickler):
+class _RestrictedCookieUnpickler(pickle._Unpickler):
"""A restricted unpickler that only allows cookie-related types.
This prevents arbitrary code execution when loading pickled cookie data
from untrusted sources. Only types that are expected in a serialized
CookieJar are permitted.
+ Subclasses :class:`pickle._Unpickler` (the pure-Python implementation)
+ rather than :class:`pickle.Unpickler` because the accelerated unpickler
+ on some implementations (notably PyPy) does not dispatch through
+ :meth:`find_class` overrides.
+
See: https://docs.python.org/3/library/pickle.html#restricting-globals
"""
From 2a76d8e0bddd875139c5163fe48f985281b5ed6d Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 16 May 2026 17:30:49 +0000
Subject: [PATCH 087/191] [PR #12436/a868ca9d backport][3.14] Fix digest
authentication for URLs with reserved characters (#12558)
Co-authored-by: J. Nick Koston
---
CHANGES/12436.bugfix.rst | 1 +
aiohttp/client_middleware_digest_auth.py | 6 ++-
tests/test_client_middleware_digest_auth.py | 41 +++++++++++++++++++++
3 files changed, 47 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/12436.bugfix.rst
diff --git a/CHANGES/12436.bugfix.rst b/CHANGES/12436.bugfix.rst
new file mode 100644
index 00000000000..d6f7e160697
--- /dev/null
+++ b/CHANGES/12436.bugfix.rst
@@ -0,0 +1 @@
+Fixed digest authentication failing for requests whose path or query string contains percent-encoded reserved characters; the digest signature now uses the encoded request-target that is sent on the wire instead of the decoded form -- by :user:`bdraco`.
diff --git a/aiohttp/client_middleware_digest_auth.py b/aiohttp/client_middleware_digest_auth.py
index 64257a65c18..8151dea5154 100644
--- a/aiohttp/client_middleware_digest_auth.py
+++ b/aiohttp/client_middleware_digest_auth.py
@@ -246,7 +246,11 @@ async def _encode(self, method: str, url: URL, body: Payload | Literal[b""]) ->
# Convert string values to bytes once
nonce_bytes = nonce.encode("utf-8")
realm_bytes = realm.encode("utf-8")
- path = URL(url).path_qs
+ # Use the encoded request-target (raw_path_qs) since that is what is
+ # transmitted on the wire and what the server signs against. Using the
+ # decoded form would cause digest verification to fail when the path
+ # or query string contains percent-encoded reserved characters.
+ path = URL(url).raw_path_qs
# Process QoP
qop = ""
diff --git a/tests/test_client_middleware_digest_auth.py b/tests/test_client_middleware_digest_auth.py
index c490fb70d78..0d2d6ad3325 100644
--- a/tests/test_client_middleware_digest_auth.py
+++ b/tests/test_client_middleware_digest_auth.py
@@ -189,6 +189,47 @@ async def test_encode_digest_with_md5(
assert "algorithm=MD5" in header
+@pytest.mark.parametrize(
+ ("url", "expected_uri"),
+ [
+ (
+ URL("http://example.com/axis-cgi/io/port.cgi?action=9:\\"),
+ "/axis-cgi/io/port.cgi?action=9:%5C",
+ ),
+ (
+ URL("http://example.com/path with space/file"),
+ "/path%20with%20space/file",
+ ),
+ (
+ URL("http://example.com/p?q=a&b=1+2"),
+ "/p?q=a&b=1+2",
+ ),
+ (
+ URL.build(
+ scheme="http",
+ host="example.com",
+ path="/p",
+ query={"x": "[]"},
+ ),
+ "/p?x=%5B%5D",
+ ),
+ ],
+ ids=["backslash-and-colon", "space-in-path", "ampersand-and-plus", "brackets"],
+)
+async def test_encode_uri_uses_wire_encoded_request_target(
+ auth_mw_with_challenge: DigestAuthMiddleware,
+ url: URL,
+ expected_uri: str,
+) -> None:
+ """The digest uri/A2 must use the encoded request-target sent on the wire.
+
+ Servers compute the digest signature against the encoded request-target
+ they actually receive, so the client must sign the same encoded form.
+ """
+ header = await auth_mw_with_challenge._encode("GET", url, b"")
+ assert f'uri="{expected_uri}"' in header
+
+
@pytest.mark.parametrize(
"algorithm", ["MD5-SESS", "SHA-SESS", "SHA-256-SESS", "SHA-512-SESS"]
)
From c4d7bf24a4c40131784a46db1e0575550506f43f Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Sat, 16 May 2026 11:26:58 -0700
Subject: [PATCH 088/191] [3.14] Fix AppKey repr tests for collections.abc
typing module change (#12563)
Fixes #12560
---
tests/test_web_app.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/test_web_app.py b/tests/test_web_app.py
index d83c292b6d0..21e33ffb277 100644
--- a/tests/test_web_app.py
+++ b/tests/test_web_app.py
@@ -293,6 +293,7 @@ def test_appkey_repr_nonconcrete() -> None:
# pytest-xdist:
"",
"",
+ "",
)
@@ -309,6 +310,7 @@ def test_appkey_repr_annotated() -> None:
# pytest-xdist:
"",
"",
+ "",
)
From 2c459116859d19bf4ef79f15fea71766291ccb1c Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 16 May 2026 13:02:25 -0700
Subject: [PATCH 089/191] [PR #12565/edc1cc2b backport][3.13] Bump benchmark CI
job timeout to 15 minutes (#12566)
Co-authored-by: aio-libs bot
Co-authored-by: Claude Opus 4.7 (1M context)
Fixes #12561
---
.github/workflows/ci-cd.yml | 2 +-
CHANGES/12561.misc.rst | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/12561.misc.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 5d1a2487c2f..7349fb3930a 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -273,7 +273,7 @@ jobs:
if: >-
needs.pre-setup.outputs.upstream-repository-id == github.repository_id
runs-on: ubuntu-latest
- timeout-minutes: 12
+ timeout-minutes: 15
steps:
- name: Checkout project
uses: actions/checkout@v5
diff --git a/CHANGES/12561.misc.rst b/CHANGES/12561.misc.rst
new file mode 100644
index 00000000000..4201bb90b0d
--- /dev/null
+++ b/CHANGES/12561.misc.rst
@@ -0,0 +1,2 @@
+Bumped the benchmark CI job timeout from 12 to 15 minutes to prevent
+spurious failures on slower runners -- by :user:`aiolibsbot`.
From 1474536b8026d34d27feae9c30ee9883a002c4fa Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 16 May 2026 13:02:36 -0700
Subject: [PATCH 090/191] [PR #12565/edc1cc2b backport][3.14] Bump benchmark CI
job timeout to 15 minutes (#12567)
Co-authored-by: aio-libs bot
Co-authored-by: Claude Opus 4.7 (1M context)
Fixes #12561
---
.github/workflows/ci-cd.yml | 2 +-
CHANGES/12561.misc.rst | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/12561.misc.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 52b4fac1e02..cf48ccd6aea 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -366,7 +366,7 @@ jobs:
if: >-
needs.pre-setup.outputs.upstream-repository-id == github.repository_id
runs-on: ubuntu-latest
- timeout-minutes: 12
+ timeout-minutes: 15
steps:
- name: Checkout project
uses: actions/checkout@v6
diff --git a/CHANGES/12561.misc.rst b/CHANGES/12561.misc.rst
new file mode 100644
index 00000000000..4201bb90b0d
--- /dev/null
+++ b/CHANGES/12561.misc.rst
@@ -0,0 +1,2 @@
+Bumped the benchmark CI job timeout from 12 to 15 minutes to prevent
+spurious failures on slower runners -- by :user:`aiolibsbot`.
From 2c3daecc6f1dcc74baa1b1720ccdc5c18198f29d Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 16 May 2026 13:31:54 -0700
Subject: [PATCH 091/191] [PR #12564/80599593 backport][3.14] ci: report
slowest benchmarks via --durations=30 (#12568)
Co-authored-by: aio-libs bot
Co-authored-by: J. Nick Koston
Fixes https://github.com/aio-libs/aiohttp/issues/12562
---
.github/workflows/ci-cd.yml | 2 +-
CHANGES/12562.contrib.rst | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/12562.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index cf48ccd6aea..78953a31855 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -399,7 +399,7 @@ jobs:
uses: CodSpeedHQ/action@v4
with:
mode: instrumentation
- run: python -Im pytest --no-cov -vvvvv --codspeed
+ run: python -Im pytest --no-cov -vvvvv --codspeed --durations=30
cython-coverage:
diff --git a/CHANGES/12562.contrib.rst b/CHANGES/12562.contrib.rst
new file mode 100644
index 00000000000..0d89212d365
--- /dev/null
+++ b/CHANGES/12562.contrib.rst
@@ -0,0 +1 @@
+Added ``--durations=30`` to the benchmark CI run so the slowest tests are reported when the job hits its timeout -- by :user:`aiolibsbot`.
From bcea105dbb42d996acef5e35f065f4ba7ceb1531 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 16 May 2026 13:40:23 -0700
Subject: [PATCH 092/191] [PR #12321/1aaf43b3 backport][3.14] Add cdef type
declarations and inline upgrade check in HTTP parser (#12559)
Co-authored-by: J. Nick Koston
---
CHANGES/12321.misc.rst | 2 ++
aiohttp/_http_parser.pyx | 6 ++++--
docs/spelling_wordlist.txt | 1 +
3 files changed, 7 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12321.misc.rst
diff --git a/CHANGES/12321.misc.rst b/CHANGES/12321.misc.rst
new file mode 100644
index 00000000000..71f9db77675
--- /dev/null
+++ b/CHANGES/12321.misc.rst
@@ -0,0 +1,2 @@
+Added ``cdef`` type declarations and inlined the upgrade check in the HTTP parser
+-- by :user:`bdraco`.
diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx
index d8bb4f1c39f..146822b4887 100644
--- a/aiohttp/_http_parser.pyx
+++ b/aiohttp/_http_parser.pyx
@@ -455,6 +455,9 @@ cdef class HttpParser:
self._has_value = True
cdef _on_headers_complete(self):
+ cdef str h_upg
+ cdef str enc
+
self._process_header()
http_version = self.http_version()
@@ -469,8 +472,7 @@ cdef class HttpParser:
if http_version == HttpVersion11 and hdrs.HOST not in headers:
raise BadHttpMessage("Missing 'Host' header in request.")
h_upg = headers.get("upgrade", "")
- allowed = upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES
- if allowed or self._cparser.method == cparser.HTTP_CONNECT:
+ if (upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES) or self._cparser.method == cparser.HTTP_CONNECT:
self._upgraded = True
else:
if upgrade and self._cparser.status_code == 101:
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 2d39c4a1713..a422b523639 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -155,6 +155,7 @@ Indices
infos
initializer
inline
+inlined
intaking
io
IoT
From ab68167435c80919efd2e185debed12b7cac0cc8 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 16 May 2026 23:54:26 +0000
Subject: [PATCH 093/191] [PR #12571/2a35975f backport][3.14] Fix flakey
middleware recursion tests by using server URL (#12575)
Co-authored-by: J. Nick Koston
---
CHANGES/12571.contrib.rst | 4 ++++
tests/test_client_middleware.py | 4 ++--
2 files changed, 6 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12571.contrib.rst
diff --git a/CHANGES/12571.contrib.rst b/CHANGES/12571.contrib.rst
new file mode 100644
index 00000000000..8fb1b309375
--- /dev/null
+++ b/CHANGES/12571.contrib.rst
@@ -0,0 +1,4 @@
+Fixed two flakey ``test_middleware_uses_session_avoids_recursion_with_*`` tests
+that hard coded ``localhost`` in the inner middleware request; they now target
+the bound server URL so happy eyeballs cannot pick an unbound address on
+Windows runners -- by :user:`bdraco`.
diff --git a/tests/test_client_middleware.py b/tests/test_client_middleware.py
index da5bcece6e8..384dc354d76 100644
--- a/tests/test_client_middleware.py
+++ b/tests/test_client_middleware.py
@@ -957,7 +957,7 @@ async def log_middleware(
if request.url.path != "/log":
# Use the session from the request to make the logging call
async with request.session.post(
- f"http://localhost:{log_server.port}/log",
+ log_server.make_url("/log"),
json={"method": str(request.method), "url": str(request.url)},
) as resp:
assert resp.status == 200
@@ -1025,7 +1025,7 @@ async def log_middleware(
# Use the session from the request to make the logging call
# Disable middleware to avoid infinite recursion
async with request.session.post(
- f"http://localhost:{log_server.port}/log",
+ log_server.make_url("/log"),
json={"method": str(request.method), "url": str(request.url)},
middlewares=(), # This prevents infinite recursion
) as resp:
From c59d9a16583c6c8882b76c25ff36538177dbd847 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 16 May 2026 19:36:10 -0700
Subject: [PATCH 094/191] [PR #12576/5edd76f0 backport][3.14] Parallelize
Cython extension compilation (#12578)
Co-authored-by: J. Nick Koston
---
CHANGES/12576.packaging.rst | 4 ++++
setup.py | 14 +++++++++++++-
2 files changed, 17 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/12576.packaging.rst
diff --git a/CHANGES/12576.packaging.rst b/CHANGES/12576.packaging.rst
new file mode 100644
index 00000000000..dc748bfe726
--- /dev/null
+++ b/CHANGES/12576.packaging.rst
@@ -0,0 +1,4 @@
+Parallelized the Cython extension compilation by defaulting
+``build_ext.parallel`` to ``os.cpu_count()``, so each module's
+``gcc`` invocation now runs concurrently instead of one at a time
+-- by :user:`bdraco`.
diff --git a/setup.py b/setup.py
index 9f910fa823a..d44c4a595ce 100644
--- a/setup.py
+++ b/setup.py
@@ -3,6 +3,7 @@
import sys
from setuptools import Extension, setup
+from setuptools.command.build_ext import build_ext
if sys.version_info < (3, 10):
raise RuntimeError("aiohttp 3.x requires Python 3.10+")
@@ -87,8 +88,19 @@
]
+class ParallelBuildExt(build_ext):
+ def build_extensions(self) -> None:
+ if self.parallel is None:
+ self.parallel = os.cpu_count() or 1
+ super().build_extensions()
+
+
build_type = "Pure" if NO_EXTENSIONS else "Accelerated"
-setup_kwargs = {} if NO_EXTENSIONS else {"ext_modules": extensions}
+setup_kwargs = (
+ {}
+ if NO_EXTENSIONS
+ else {"ext_modules": extensions, "cmdclass": {"build_ext": ParallelBuildExt}}
+)
print("*********************", file=sys.stderr)
print("* {build_type} build *".format_map(locals()), file=sys.stderr)
From 8884d62b7fcf8e926412f63b7fec9de365c4c86a Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 16 May 2026 19:52:07 -0700
Subject: [PATCH 095/191] [PR #12576/5edd76f0 backport][3.13] Parallelize
Cython extension compilation (#12577)
Co-authored-by: J. Nick Koston
---
CHANGES/12576.packaging.rst | 4 ++++
setup.py | 14 +++++++++++++-
2 files changed, 17 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/12576.packaging.rst
diff --git a/CHANGES/12576.packaging.rst b/CHANGES/12576.packaging.rst
new file mode 100644
index 00000000000..dc748bfe726
--- /dev/null
+++ b/CHANGES/12576.packaging.rst
@@ -0,0 +1,4 @@
+Parallelized the Cython extension compilation by defaulting
+``build_ext.parallel`` to ``os.cpu_count()``, so each module's
+``gcc`` invocation now runs concurrently instead of one at a time
+-- by :user:`bdraco`.
diff --git a/setup.py b/setup.py
index fafb7dc7941..9006a19a4f6 100644
--- a/setup.py
+++ b/setup.py
@@ -3,6 +3,7 @@
import sys
from setuptools import Extension, setup
+from setuptools.command.build_ext import build_ext
if sys.version_info < (3, 9):
raise RuntimeError("aiohttp 3.x requires Python 3.9+")
@@ -70,8 +71,19 @@
]
+class ParallelBuildExt(build_ext):
+ def build_extensions(self) -> None:
+ if self.parallel is None:
+ self.parallel = os.cpu_count() or 1
+ super().build_extensions()
+
+
build_type = "Pure" if NO_EXTENSIONS else "Accelerated"
-setup_kwargs = {} if NO_EXTENSIONS else {"ext_modules": extensions}
+setup_kwargs = (
+ {}
+ if NO_EXTENSIONS
+ else {"ext_modules": extensions, "cmdclass": {"build_ext": ParallelBuildExt}}
+)
print("*********************", file=sys.stderr)
print("* {build_type} build *".format_map(locals()), file=sys.stderr)
From 0303bed8904baae1e6a1a059cf6ffbdbcf229adc Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 16 May 2026 19:52:18 -0700
Subject: [PATCH 096/191] [PR #12571/2a35975f backport][3.13] Fix flakey
middleware recursion tests by using server URL (#12574)
Co-authored-by: J. Nick Koston
---
CHANGES/12571.contrib.rst | 4 ++++
tests/test_client_middleware.py | 4 ++--
2 files changed, 6 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12571.contrib.rst
diff --git a/CHANGES/12571.contrib.rst b/CHANGES/12571.contrib.rst
new file mode 100644
index 00000000000..8fb1b309375
--- /dev/null
+++ b/CHANGES/12571.contrib.rst
@@ -0,0 +1,4 @@
+Fixed two flakey ``test_middleware_uses_session_avoids_recursion_with_*`` tests
+that hard coded ``localhost`` in the inner middleware request; they now target
+the bound server URL so happy eyeballs cannot pick an unbound address on
+Windows runners -- by :user:`bdraco`.
diff --git a/tests/test_client_middleware.py b/tests/test_client_middleware.py
index 217877759c0..6db56a744ca 100644
--- a/tests/test_client_middleware.py
+++ b/tests/test_client_middleware.py
@@ -957,7 +957,7 @@ async def log_middleware(
if request.url.path != "/log":
# Use the session from the request to make the logging call
async with request.session.post(
- f"http://localhost:{log_server.port}/log",
+ log_server.make_url("/log"),
json={"method": str(request.method), "url": str(request.url)},
) as resp:
assert resp.status == 200
@@ -1025,7 +1025,7 @@ async def log_middleware(
# Use the session from the request to make the logging call
# Disable middleware to avoid infinite recursion
async with request.session.post(
- f"http://localhost:{log_server.port}/log",
+ log_server.make_url("/log"),
json={"method": str(request.method), "url": str(request.url)},
middlewares=(), # This prevents infinite recursion
) as resp:
From 48cab10ea76eb7f91cbccfcd8d067340175a5fbe Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sun, 17 May 2026 02:56:48 +0000
Subject: [PATCH 097/191] [PR #12512/637e1265 backport][3.14] Threat model
chapter 1 (#12548)
Co-authored-by: Sam Bull
Co-authored-by: J. Nick Koston
---
CHANGES/12512.misc.rst | 1 +
THREAT_MODEL.md | 314 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 315 insertions(+)
create mode 100644 CHANGES/12512.misc.rst
create mode 100644 THREAT_MODEL.md
diff --git a/CHANGES/12512.misc.rst b/CHANGES/12512.misc.rst
new file mode 100644
index 00000000000..c3acbcce786
--- /dev/null
+++ b/CHANGES/12512.misc.rst
@@ -0,0 +1 @@
+Added ``THREAT_MODEL.md`` detailing our security stance -- by :user:`Dreamsorcerer`.
diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
new file mode 100644
index 00000000000..1c10ce1e219
--- /dev/null
+++ b/THREAT_MODEL.md
@@ -0,0 +1,314 @@
+# aiohttp Threat Model
+
+This document is a STRIDE-based threat model for the
+[aiohttp](https://github.com/aio-libs/aiohttp) library. It is a living document
+intended to (a) make explicit the implicit security assumptions baked into the
+codebase, (b) catalogue known classes of threat against each subsystem, and
+(c) record the existing and recommended mitigations.
+
+---
+
+## 1. Library Overview
+
+**aiohttp** is an `asyncio`-based HTTP client/server framework for Python. It
+provides:
+
+- An **HTTP/1.1 server** (`aiohttp.web`) including routing, middleware,
+ WebSocket support, static-file serving, and a Gunicorn worker.
+- An **HTTP/1.1 client** (`aiohttp.ClientSession`) including connection
+ pooling, TLS, proxy support, redirects, cookie handling, and WebSockets.
+- Shared **wire-protocol code**: HTTP/1 parser (vendored
+ [llhttp](https://github.com/nodejs/llhttp) wrapped in Cython, with a pure
+ Python fallback), HTTP writer, WebSocket framing, multipart, and compression.
+
+Key public APIs (non-exhaustive):
+
+| Surface | Entry points |
+| --- | --- |
+| Server | `aiohttp.web.Application`, `web.RouteTableDef`, `web.run_app`, `web.AppRunner`, `web.WebSocketResponse`, `web.FileResponse` |
+| Client | `aiohttp.ClientSession`, `aiohttp.TCPConnector`, `aiohttp.ClientResponse`, `aiohttp.WSMessage`, `aiohttp.BasicAuth` |
+| Shared | `aiohttp.MultipartReader`/`MultipartWriter`, `aiohttp.CookieJar`, `aiohttp.TraceConfig`, `aiohttp.resolver.AsyncResolver` |
+
+---
+
+## 2. Methodology
+
+We use [STRIDE](https://en.wikipedia.org/wiki/STRIDE_model):
+
+- **S**poofing — impersonating identity (host, user, peer, dependency).
+- **T**ampering — modifying data or code in flight or at rest.
+- **R**epudiation — denying that an action occurred.
+- **I**nformation Disclosure — leaking confidential data.
+- **D**enial of Service — exhausting CPU, memory, sockets, file descriptors.
+- **E**levation of Privilege — gaining unintended access.
+
+Risk is ranked **High / Medium / Low** based on a rough product of likelihood
+and impact, as judged by maintainers. Mitigations are split into
+**existing** (already implemented in the codebase) and **recommended** (not
+yet implemented or only partially implemented).
+
+---
+
+## 3. Overall Assets
+
+These cross-cutting assets apply across most sections; individual sections only
+list assets unique to that section.
+
+1. **Integrity of public-API behavior** — functions return what callers expect
+ and don't introduce protocol corruption (request smuggling, response
+ splitting, framing desync).
+2. **Confidentiality of data in transit** — TLS handling, header values,
+ cookies, request/response bodies are not leaked between connections,
+ sessions, or to log sinks.
+3. **Availability of host application** — aiohttp does not crash, deadlock, or
+ exhaust CPU/memory/FDs in the host process under hostile or malformed input.
+4. **Security of host application** — aiohttp does not become a vector for
+ attacks on the embedding application (SSRF, file disclosure, code execution,
+ privilege escalation through deserialisation, etc.).
+5. **Reputation & supply-chain integrity** — the released artifacts on PyPI are
+ what maintainers built and signed; the source on GitHub matches the artifacts;
+ the vendored llhttp matches upstream; CI/CD secrets are not exposed.
+
+---
+
+## 4. High-Level System Diagram
+
+```mermaid
+flowchart LR
+ Untrusted([Untrusted Internet])
+ Caller([Caller / host application])
+ Upstream([External HTTP servers])
+
+ subgraph Server[Server side]
+ direction TB
+ SP[web_protocol
connection lifecycle]
+ PARS[HTTP parser
_http_parser.pyx + llhttp]
+ REQ[web_request.Request]
+ DISP[web_urldispatcher + middleware]
+ HND{user handler}
+ RESP[web_response.Response
FileResponse / WebSocketResponse]
+ WR[http_writer]
+ SP --> PARS --> REQ --> DISP --> HND --> RESP --> WR
+ end
+
+ subgraph Client[Client side]
+ direction TB
+ CS[ClientSession]
+ CONN[TCPConnector
+ TLS, proxy, pooling]
+ RES[resolver]
+ CP[client_proto]
+ CR[client_reqrep.ClientResponse]
+ CS --> CONN --> RES
+ CONN --> CP --> CR --> CS
+ end
+
+ subgraph Shared[Shared wire-protocol code]
+ direction TB
+ PARS
+ WR
+ WS[http_websocket + _websocket/]
+ MP[multipart]
+ COMP[compression_utils]
+ PARS -.-> WS
+ WR -.-> WS
+ end
+
+ Untrusted -- HTTP/1, WS --> SP
+ WR -- HTTP/1, WS --> Untrusted
+
+ Caller --> CS
+ CS --> Caller
+ CONN -- HTTP/1, WS --> Upstream
+ Upstream --> CP
+
+ CJ[(CookieJar)] -. client only .-> CS
+ TR[TraceConfig] -. signals .-> CS
+```
+
+---
+
+## 5. Scope
+
+The threat surface is broken down into 19 sections. Each is modeled in its own
+subsection below.
+
+1. [HTTP/1 parser](#51-http1-parser)
+2. [HTTP/1 writer](#52-http1-writer)
+3. [WebSocket framing & per-message deflate](#53-websocket-framing--per-message-deflate)
+4. [Multipart parsing & encoding](#54-multipart-parsing--encoding)
+5. [Compression codecs](#55-compression-codecs)
+6. [Streams & payloads](#56-streams--payloads)
+7. [Server connection lifecycle](#57-server-connection-lifecycle)
+8. [Server routing & middleware](#58-server-routing--middleware)
+9. [Server request/response objects](#59-server-requestresponse-objects)
+10. [Server static file serving](#510-server-static-file-serving)
+11. [Server-side WebSocket handler](#511-server-side-websocket-handler)
+12. [Client API & request lifecycle](#512-client-api--request-lifecycle)
+13. [Connector / TLS / proxy / pooling](#513-connector--tls--proxy--pooling)
+14. [Client-side WebSocket](#514-client-side-websocket)
+15. [Client auth middlewares](#515-client-auth-middlewares)
+16. [Cookie handling](#516-cookie-handling)
+17. [DNS resolution](#517-dns-resolution)
+18. [Tracing & URL/header helpers](#518-tracing--urlheader-helpers)
+19. [Build & release supply chain](#519-build--release-supply-chain)
+
+---
+
+### 5.1. HTTP/1 parser
+
+**Scope.** Parsing of HTTP/1.0 and HTTP/1.1 request and response messages —
+request/status line, header block, chunked transfer-encoding, content-length
+framing, trailers — and the surface where parsed values flow into the rest of
+the library. Out of scope here: WebSocket framing ([§5.3](#53-websocket-framing--per-message-deflate)), multipart bodies
+([§5.4](#54-multipart-parsing--encoding)), compression ([§5.5](#55-compression-codecs)), HTTP-writer-side framing ([§5.2](#52-http1-writer)).
+
+**Components covered.**
+
+- `aiohttp/_http_parser.pyx` — Cython wrapper over vendored llhttp, default in
+ CPython builds.
+- `aiohttp/_cparser.pxd` — Cython declarations for llhttp.
+- `aiohttp/http_parser.py` — pure-Python `HttpRequestParser` / `HttpResponseParser`
+ used as a fallback (and as the canonical implementation when
+ `AIOHTTP_NO_EXTENSIONS=1`).
+- `aiohttp/_find_header.pxd` / `aiohttp/_find_header.h` — header-name interning.
+- `aiohttp/http_exceptions.py` — `BadHttpMessage`, `BadHttpMethod`,
+ `BadStatusLine`, `LineTooLong`, `InvalidHeader`, `TransferEncodingError`,
+ `ContentLengthError`.
+- `vendor/llhttp/` — vendored upstream parser, version `9.3.1` (see
+ `vendor/llhttp/package.json`). Generated via `make generate-llhttp`.
+
+**Selection.** A conditional re-import at the bottom of
+`aiohttp/http_parser.py` re-binds the public names to the Cython parser when
+`_http_parser` imports successfully and `AIOHTTP_NO_EXTENSIONS` is unset. There is no hybrid mode — both request and
+response parsers come from the same backend, so an inconsistent
+request-Cython/response-pure-Python configuration cannot occur in supported
+builds.
+
+**Trust boundaries & data flow.**
+
+```mermaid
+flowchart LR
+ Wire([Untrusted bytes]) --> Feed[parser.feed_data]
+ Feed --> Llhttp[llhttp / Python state machine]
+ Llhttp -->|RawRequestMessage
RawResponseMessage| Caller[web_protocol / client_proto]
+ Llhttp -->|StreamReader feed| Body[(Request/response body)]
+ Caller --> ReqResp[Request / ClientResponse]
+ ReqResp --> User([User handler / caller])
+```
+
+The parser is invoked on every byte that arrives from a socket, before any
+authentication. **Everything fed into `feed_data` is attacker-controlled** on
+the server side and **upstream-controlled** on the client side (proxies,
+upstream services, malicious origins reached via client). The output
+(`RawRequestMessage` / `RawResponseMessage`, raw header tuples, body chunks
+into `StreamReader`) is then handed to `web_protocol.RequestHandler` and
+`client_proto.ResponseHandler` respectively.
+
+**Trust assumptions about parser output:**
+
+- Header names are validated against a token regex; values are not normalised
+ beyond `lstrip`/`rstrip` and CR/LF/NUL rejection.
+- Header values are decoded `utf-8` with `surrogateescape`, so non-UTF-8 bytes
+ are *preserved* and *can round-trip back to the wire* if downstream code
+ re-emits them. Any sanitisation downstream of the parser is the
+ responsibility of consumers (logging, header reflection, proxying).
+- Methods are accepted as any RFC 7230 token; the parser does not canonicalise
+ case.
+- Versions are accepted by the regex `HTTP/(\d)\.(\d)` — i.e. `HTTP/0.9`,
+ `HTTP/2.0`, etc. all parse without rejection, even though they cannot be
+ served correctly.
+
+**Assets at risk.**
+
+- **Framing integrity** — that one wire message corresponds to one parsed
+ message; nothing the parser accepts can cause a desync between aiohttp and
+ an upstream/downstream peer (request smuggling).
+- **Allocator safety** — that a malicious peer cannot drive memory or CPU
+ usage to denial of service through parser-controlled allocations.
+- **Bytewise transparency** — that bytes accepted by the parser cannot inject
+ new framing or new header semantics downstream (CRLF injection, NUL
+ smuggling).
+
+**Threats (STRIDE).**
+
+| # | Component / Vector | STRIDE | Threat | Risk |
+| :--- | :--- | :--- | :--- | :--- |
+| 1.1 | Request line / status line | T | Smuggling via duplicate / conflicting framing headers (`Content-Length` × N, `Content-Length` + `Transfer-Encoding`, obfuscated `Transfer-Encoding`). | High |
+| 1.2 | Header block, line endings | T | Smuggling via bare-LF, obs-fold, optional CR-before-LF on the *request* parser. Request parser is strict; lenient flags apply only to the response parser. | Medium |
+| 1.3 | Header values, CR/LF/NUL | T / I | CRLF injection enabling response splitting / header injection if downstream re-emits values verbatim. Historically [CVE-2023-37276](https://github.com/aio-libs/aiohttp/security/advisories/GHSA-45c4-8wx5-qw6w). | High |
+| 1.4 | Header values, surrogateescape decode | I / T | Non-UTF-8 bytes round-trip through `Headers` and may be reflected by user code / proxies / logs into untrusted contexts. | Medium |
+| 1.5 | HTTP version regex | T | `HTTP/0.9` and `HTTP/2.0` accepted on the wire, opening a small surface for protocol-confusion against intermediaries that handle these specially. | Low |
+| 1.6 | Method token | I / T | Methods are not case-canonicalised; arbitrary tokens up to `max_line_size` accepted. May confuse downstream method-based authorisation if user code compares case-sensitively. | Low |
+| 1.7 | `Content-Length` parsing | T | Negative or non-decimal CL handling, multiple comma-separated CLs, CL with leading `+`/whitespace. | Medium |
+| 1.8 | `Transfer-Encoding: chunked` parsing | T | Lenient acceptance (`xchunked`, `chunked, identity`, doubled `chunked`) leading to smuggling against a non-aiohttp peer that interprets differently. | Medium |
+| 1.9 | Chunk size parsing | D | No upper bound on chunk-size value (Python unbounded int); huge chunk size could drive allocator before `client_max_size` rejects body. Mitigated by [§5.7](#57-server-connection-lifecycle) / `client_max_size`. | Low–Med |
+| 1.10 | Chunk extensions | D / T | Unbounded chunk-extension consumption per chunk; weak validation of extension syntax. | Low |
+| 1.11 | Parser error reporting | I | Exception messages may include up to ~100 bytes of malformed input, which can be surfaced in 4xx error bodies, logs, or `DEBUG=True` traces. | Low |
+| 1.12 | Cython ⇄ pure-Python divergence | T / S | Behaviour differences between llhttp and the Python fallback may produce parser-confusion if a deployment unintentionally switches backends (e.g. a user installs without compiled extensions). | Med |
+| 1.13 | Vendored llhttp version drift | S / T | An upstream llhttp CVE not picked up by aiohttp's vendoring cadence remains exploitable until `make generate-llhttp` is re-run and released. | Medium |
+| 1.14 | Build/regen of llhttp (`make generate-llhttp`) | S / T | Local tampering or supply-chain compromise of the npm `llhttp` package gets baked into the vendored C. Covered in [§5.19](#519-build--release-supply-chain) but originates here. | Medium |
+
+**Mitigations.**
+
+| # | Threat | Existing | Recommended |
+| :--- | :--- | :--- | :--- |
+| 1.1 | Smuggling via duplicate framing headers | llhttp rejects conflicting `Content-Length`. `http_parser.py:HttpRequestParserPy.parse_headers` rejects coexistence of CL + `Transfer-Encoding: chunked`. The full `SINGLETON_HEADERS` set (CL, CT, Host, TE, ETag, etc.) is duplicate-rejected by the request parser (strict mode); `#12302` disabled this check on the response parser (lax mode), since real-world servers commonly send duplicate `Content-Type` / `Server`. | If new singleton-sensitive headers emerge in HTTP/1.1 RFC errata, add to `SINGLETON_HEADERS`. |
+| 1.2 | Lenient response parsing | Lenient flags (`llhttp_set_lenient_headers`, `llhttp_set_lenient_optional_cr_before_lf`, `llhttp_set_lenient_spaces_after_chunk_size`) are only enabled on the **response** parser and only when `DEBUG` is False (set in `HttpResponseParser.__init__`). The **request** parser is strict. | Documented design decision: keep lenient response parsing for real-world server interop |
+| 1.3 | CRLF / NUL in header values | Bytes `\r`, `\n`, `\x00` rejected in header values (`_http_parser.pyx` callbacks; `http_parser.py:HeadersParser.parse_headers`). | Keep regression tests in `tests/test_http_parser.py` covering each forbidden byte both in name and value, and across both Cython and pure-Python parsers. |
+| 1.4 | Non-UTF-8 round-trip | None at parser layer (intentional — preserving original bytes is required for some use cases). | **Document in user-facing docs that header values are bytes-preserving; warn against reflecting headers verbatim into responses, logs, or sub-requests without re-validation.** |
+| 1.5 | HTTP version regex accepts 0.9 / 2.0 | None (regex is permissive). | **Tighten `VERSRE` (and llhttp configuration if possible) to reject anything outside `HTTP/1.0` and `HTTP/1.1`.** |
+| 1.6 | Method-case round-trip | Method token validated by regex; not canonicalised. | **Document that user route handlers / authorization checks should compare methods case-sensitively to the canonical RFC tokens, or use the framework's `web.RouteTableDef` decorators which already match canonical methods.** |
+| 1.7 | `Content-Length` parsing | llhttp validates CL is decimal and non-negative; pure-Python parser validates via `DIGITS.fullmatch(r"\d+")` before `int(...)`, rejecting `+`/`-`/non-ASCII-digit forms (`test_bad_headers`, `test_headers_content_length_err_*` cover these). | None. Cross-backend parity is covered by the shared parser tests. |
+| 1.8 | `Transfer-Encoding` lenience | `_is_chunked_te` requires `chunked` to be the last value; duplicate `chunked` rejected (`#10611`). Request parser strict. | None. |
+| 1.9 | Chunk-size DoS | The parser doesn't cap chunk size, but **server-side body length is bounded by `client_max_size` (default `1 MiB`)** in `web_request.py:BaseRequest.read`. Client-side responses are bounded by user-supplied `max_body_size` / streaming reads. | None. If a cap is ever needed at the parser level, plumb it through `HttpPayloadParser`. |
+| 1.10 | Chunk-extension DoS | Chunk-extension content is bounded by the same wire-level size constraints (it shares the chunk-size line with `max_line_size`). | **Add an explicit test that chunk-extension flooding cannot blow past `max_line_size`.** |
+| 1.11 | Parser error reflection | `http_parser.py` truncates to `[:100]` for line errors. Server-side error path renders 4xx with the exception message; tracebacks only when `DEBUG=True`. | **Audit any path where `BadHttpMessage` content is reflected to the client unsanitised (especially in custom `web_log` configurations).** |
+| 1.12 | Cython ⇄ pure-Python divergence | `tests/test_http_parser.py` parameterises tests over `REQUEST_PARSERS` / `RESPONSE_PARSERS` (pure-Python always; Cython when the extension imports). The high-leverage attack vectors are already covered under both backends: CL+TE (`test_content_length_transfer_encoding`), CL×N (`test_duplicate_singleton_header_rejected`), obs-fold (`test_reject_obsolete_line_folding`, `test_http_response_parser_obs_line_folding*`), CR/LF/NUL (`test_bad_headers`, `test_http_response_parser_null_byte_in_header_value`, `test_http_response_parser_bad_crlf`), version regex (`test_http_request_parser_bad_version*`, `test_http_response_parser_bad_version*`). | None. When new attack vectors emerge, add them to the parameterised tests. |
+| 1.13 | llhttp version drift | Manual upgrade via `make generate-llhttp`; vendor pinned in `vendor/llhttp/package.json`. | Track upstream releases (e.g. via Dependabot rule for `vendor/llhttp/package.json`), bump on every llhttp release, regenerate in CI. |
+| 1.14 | npm-side compromise of `llhttp` | The vendored output is checked into git, so a compromise during a future regen would be detectable in PR review. See [§5.19](#519-build--release-supply-chain). | **Make the llhttp build reproducible: pin Node.js version, commit the npm lockfile, and on every bump verify the regenerated C against upstream's release tarballs before committing.** |
+
+**Past advisories / hardening (recap).**
+
+- **GHSA-xx9p-xxvh-7g8j (CVE-2023-47641)** (3.8.0) — CL-vs-TE divergence
+ between the Cython and pure-Python parsers, allowing request smuggling
+ against deployments that switched backends.
+- **CVE-2023-37276 / GHSA-45c4-8wx5-qw6w** (3.8.5) — HTTP request smuggling
+ via CR/LF/NUL in header values. Both parsers reject these bytes at the
+ byte level.
+- **GHSA-pjjw-qhg8-p2p9** (3.8.6) — smuggling pair in vendored llhttp 8.1.1;
+ fixed by bumping llhttp to 9.
+- **GHSA-gfw2-4jvh-wgfg / GHSA-8qpw-xqxj-h4r2** (3.8.6 / 3.9.2) — pure-Python
+ parser accepted lenient separators / weak RFC validation that llhttp
+ rejected.
+- **GHSA-8495-4g3g-x7pr (CVE-2024-52304)** (3.10.11) — chunk-extension
+ newline smuggling in the pure-Python parser.
+- **GHSA-9548-qrrj-x5pj (CVE-2025-53643)** (3.12.14) — request smuggling
+ via the chunked-trailer section in the pure-Python parser.
+- **GHSA-69f9-5gxw-wvc2 (CVE-2025-69224)** (3.13.3) — Unicode codepoints
+ matched by `\d` in the pure-Python parser's regexes were treated as
+ digits.
+- **GHSA-g84x-mcqj-x9qq** (3.13.3) — CPU-DoS on `request.read()` when
+ the body arrives as a very large number of small chunks.
+- **PR #12137** (3.13.4) — precautionary hardening: pure-Python parser
+ now explicitly rejects duplicate `Transfer-Encoding: chunked` on
+ the request parser.
+- **GHSA-c427-h43c-vf67** (3.13.4) — duplicate `Host` header accepted
+ in request parser, bypassing `Application.add_domain()` host-based
+ routing / authorisation. Fixed by adding `Host` to the strict
+ request-parser singleton rejection set.
+- **GHSA-63hf-3vf5-4wqf (CVE-2026-34520)** (3.13.4) — llhttp accepted
+ NUL / control bytes in *response* header values, leaving the response
+ parser weaker than the request parser. Fixed by tightening the
+ response-side byte check.
+- **GHSA-w2fm-2cpv-w7v5 (CVE-2026-22815)** (3.13.4) — uncapped memory
+ growth on long header / trailer blocks. Fixed by enforcing
+ `max_field_size` / `max_headers` on the trailer block too.
+- **PR #12302** (3.13.5) — duplicate-singleton-header rejection
+ was breaking real-world response parsing (servers like Google APIs /
+ Werkzeug emit duplicate `Content-Type` / `Server`); fix disables the
+ check on the response parser (lax mode) while keeping it on the
+ request parser (strict).
+
+These are all currently in place; this section assumes no regression.
+
+---
From 75dd112eab47f70baf232b33cc19c4addb24f994 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Sun, 17 May 2026 09:09:43 -0700
Subject: [PATCH 098/191] [PR #12581/77e2e2d7 backport][3.13] Shrink slow
client and URL dispatcher benchmarks (#12586)
closed #12570.
Fixes #12569
---
CHANGES/12569.misc.rst | 3 +++
tests/test_benchmarks_web_urldispatcher.py | 6 +++---
2 files changed, 6 insertions(+), 3 deletions(-)
create mode 100644 CHANGES/12569.misc.rst
diff --git a/CHANGES/12569.misc.rst b/CHANGES/12569.misc.rst
new file mode 100644
index 00000000000..32af3f259b4
--- /dev/null
+++ b/CHANGES/12569.misc.rst
@@ -0,0 +1,3 @@
+Reduced payload sizes and request counts in the slowest client and URL
+dispatcher benchmarks so they no longer dominate CI runtime
+-- by :user:`bdraco`.
diff --git a/tests/test_benchmarks_web_urldispatcher.py b/tests/test_benchmarks_web_urldispatcher.py
index 936ed6320ed..c350af871b7 100644
--- a/tests/test_benchmarks_web_urldispatcher.py
+++ b/tests/test_benchmarks_web_urldispatcher.py
@@ -359,7 +359,7 @@ async def handler(request: web.Request) -> NoReturn:
requests = [
_mock_request(method="GET", path=f"/api/{customer}/update")
- for customer in range(250)
+ for customer in range(150)
]
async def run_url_dispatcher_benchmark() -> Optional[web.UrlMappingMatchInfo]:
@@ -401,7 +401,7 @@ async def handler(request: web.Request) -> NoReturn:
alnums = string.ascii_letters + string.digits
requests = []
- for i in range(250):
+ for i in range(150):
owner = "".join(random.sample(alnums, 10))
repo = "".join(random.sample(alnums, 10))
pull_number = random.randint(0, 250)
@@ -472,7 +472,7 @@ async def handler(request: web.Request) -> NoReturn:
alnums = string.ascii_letters + string.digits
requests = []
- for i in range(250):
+ for i in range(150):
owner = "".join(random.sample(alnums, 10))
repo = "".join(random.sample(alnums, 10))
pull_number = random.randint(0, 250)
From e936eee9e1a4082853414d8a77c0c0e8893e9896 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Sun, 17 May 2026 09:17:39 -0700
Subject: [PATCH 099/191] [PR #12581/77e2e2d7 backport][3.14] Shrink slow
client and URL dispatcher benchmarks (#12587)
closed #12570.
Fixes #12569
---
CHANGES/12569.misc.rst | 3 +++
tests/test_benchmarks_client.py | 20 ++++++++++----------
tests/test_benchmarks_web_urldispatcher.py | 6 +++---
3 files changed, 16 insertions(+), 13 deletions(-)
create mode 100644 CHANGES/12569.misc.rst
diff --git a/CHANGES/12569.misc.rst b/CHANGES/12569.misc.rst
new file mode 100644
index 00000000000..32af3f259b4
--- /dev/null
+++ b/CHANGES/12569.misc.rst
@@ -0,0 +1,3 @@
+Reduced payload sizes and request counts in the slowest client and URL
+dispatcher benchmarks so they no longer dominate CI runtime
+-- by :user:`bdraco`.
diff --git a/tests/test_benchmarks_client.py b/tests/test_benchmarks_client.py
index 3c362a500a7..8ebedcc4776 100644
--- a/tests/test_benchmarks_client.py
+++ b/tests/test_benchmarks_client.py
@@ -177,14 +177,14 @@ def _run() -> None:
loop.run_until_complete(run_client_benchmark())
-def test_one_hundred_get_requests_with_10mb_chunked_payload(
+def test_one_hundred_get_requests_with_1mb_chunked_payload(
loop: asyncio.AbstractEventLoop,
aiohttp_client: AiohttpClient,
benchmark: BenchmarkFixture,
) -> None:
- """Benchmark 100 GET requests with a payload of 10 MiB using read."""
+ """Benchmark 100 GET requests with a 1 MiB chunked payload using read."""
message_count = 100
- payload = b"a" * (10 * 2**20)
+ payload = b"a" * 2**20
async def handler(request: web.Request) -> web.Response:
resp = web.Response(body=payload)
@@ -206,14 +206,14 @@ def _run() -> None:
loop.run_until_complete(run_client_benchmark())
-def test_one_hundred_get_requests_iter_chunks_on_10mb_chunked_payload(
+def test_one_hundred_get_requests_iter_chunks_on_1mb_chunked_payload(
loop: asyncio.AbstractEventLoop,
aiohttp_client: AiohttpClient,
benchmark: BenchmarkFixture,
) -> None:
- """Benchmark 100 GET requests with a payload of 10 MiB using iter_chunks."""
+ """Benchmark 100 GET requests with a 1 MiB chunked payload using iter_chunks."""
message_count = 100
- payload = b"a" * (10 * 2**20)
+ payload = b"a" * 2**20
async def handler(request: web.Request) -> web.Response:
resp = web.Response(body=payload)
@@ -327,14 +327,14 @@ def _run() -> None:
loop.run_until_complete(run_client_benchmark())
-def test_one_hundred_get_requests_with_10mb_content_length_payload(
+def test_one_hundred_get_requests_with_1mb_content_length_payload(
loop: asyncio.AbstractEventLoop,
aiohttp_client: AiohttpClient,
benchmark: BenchmarkFixture,
) -> None:
- """Benchmark 100 GET requests with a payload of 10 MiB."""
+ """Benchmark 100 GET requests with a content-length payload of 1 MiB."""
message_count = 100
- payload = b"a" * (10 * 2**20)
+ payload = b"a" * 2**20
headers = {hdrs.CONTENT_LENGTH: str(len(payload))}
async def handler(request: web.Request) -> web.Response:
@@ -479,7 +479,7 @@ def test_ten_streamed_responses_iter_chunked_1mb(
"""Benchmark 10 streamed responses using iter_chunked 1 MiB."""
message_count = 10
MB = 2**20
- data = b"x" * 10 * MB
+ data = b"x" * 6 * MB
async def handler(request: web.Request) -> web.StreamResponse:
resp = web.StreamResponse()
diff --git a/tests/test_benchmarks_web_urldispatcher.py b/tests/test_benchmarks_web_urldispatcher.py
index 339eaef8a0e..f01adb6da5c 100644
--- a/tests/test_benchmarks_web_urldispatcher.py
+++ b/tests/test_benchmarks_web_urldispatcher.py
@@ -359,7 +359,7 @@ async def handler(request: web.Request) -> NoReturn:
requests = [
_mock_request(method="GET", path=f"/api/{customer}/update")
- for customer in range(250)
+ for customer in range(150)
]
async def run_url_dispatcher_benchmark() -> web.UrlMappingMatchInfo | None:
@@ -401,7 +401,7 @@ async def handler(request: web.Request) -> NoReturn:
alnums = string.ascii_letters + string.digits
requests = []
- for i in range(250):
+ for i in range(150):
owner = "".join(random.sample(alnums, 10))
repo = "".join(random.sample(alnums, 10))
pull_number = random.randint(0, 250)
@@ -472,7 +472,7 @@ async def handler(request: web.Request) -> NoReturn:
alnums = string.ascii_letters + string.digits
requests = []
- for i in range(250):
+ for i in range(150):
owner = "".join(random.sample(alnums, 10))
repo = "".join(random.sample(alnums, 10))
pull_number = random.randint(0, 250)
From f36abe1138733ff5a818eff56dffdfaff51179aa Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 17 May 2026 16:57:52 +0000
Subject: [PATCH 100/191] Bump ast-serialize from 0.3.0 to 0.4.0 (#12543)
Bumps [ast-serialize](https://github.com/mypyc/ast_serialize) from 0.3.0
to 0.4.0.
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index e2e39becce8..74128b5c99d 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -18,7 +18,7 @@ alabaster==1.0.0
# via sphinx
annotated-types==0.7.0
# via pydantic
-ast-serialize==0.3.0
+ast-serialize==0.4.0
# via mypy
async-timeout==5.0.1 ; python_version < "3.11"
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 236052a9f8d..0732c1da1e3 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -18,7 +18,7 @@ alabaster==1.0.0
# via sphinx
annotated-types==0.7.0
# via pydantic
-ast-serialize==0.3.0
+ast-serialize==0.4.0
# via mypy
async-timeout==5.0.1 ; python_version < "3.11"
# via
diff --git a/requirements/lint.txt b/requirements/lint.txt
index cc38079bb98..138d631a33e 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -8,7 +8,7 @@ aiodns==4.0.0
# via -r requirements/lint.in
annotated-types==0.7.0
# via pydantic
-ast-serialize==0.3.0
+ast-serialize==0.4.0
# via mypy
async-timeout==5.0.1
# via valkey
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 48a9114ab27..bea1cee2d18 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -6,7 +6,7 @@
#
annotated-types==0.7.0
# via pydantic
-ast-serialize==0.3.0
+ast-serialize==0.4.0
# via mypy
blockbuster==1.5.26
# via -r requirements/test-common.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 2013c7512c7..20d1e4288f3 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -12,7 +12,7 @@ aiosignal==1.4.0
# via -r requirements/runtime-deps.in
annotated-types==0.7.0
# via pydantic
-ast-serialize==0.3.0
+ast-serialize==0.4.0
# via mypy
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
diff --git a/requirements/test.txt b/requirements/test.txt
index c3dcb79a2b1..11d061de5b8 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -12,7 +12,7 @@ aiosignal==1.4.0
# via -r requirements/runtime-deps.in
annotated-types==0.7.0
# via pydantic
-ast-serialize==0.3.0
+ast-serialize==0.4.0
# via mypy
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
From c7562a6195e403e0c01ba100a4fbff04f1bb35c2 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sun, 17 May 2026 19:31:12 +0000
Subject: [PATCH 101/191] [PR #12589/b7e19ccd backport][3.13] Gate isal test
dependency to CPython (#12590)
Co-authored-by: J. Nick Koston
---
CHANGES/12589.contrib.rst | 7 +++++++
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/test-common.in | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
7 files changed, 13 insertions(+), 6 deletions(-)
create mode 100644 CHANGES/12589.contrib.rst
diff --git a/CHANGES/12589.contrib.rst b/CHANGES/12589.contrib.rst
new file mode 100644
index 00000000000..dc7f6400d57
--- /dev/null
+++ b/CHANGES/12589.contrib.rst
@@ -0,0 +1,7 @@
+Restricted the ``isal`` test dependency to CPython, since
+``isal`` 1.8.0 stopped publishing PyPy wheels and the source
+build requires ``nasm``, which is not available on the CI
+runners. The ``parametrize_zlib_backend`` fixture already
+calls ``pytest.importorskip``, so PyPy continues to exercise
+the ``zlib`` and ``zlib_ng`` backends with no further
+changes -- by :user:`bdraco`.
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 495ab431cc8..131b0c4f0fe 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -99,7 +99,7 @@ imagesize==1.4.1
# via sphinx
iniconfig==2.1.0
# via pytest
-isal==1.7.2 ; python_version < "3.14"
+isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index b3f9fc2981b..2b10c553956 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -97,7 +97,7 @@ imagesize==1.4.1
# via sphinx
iniconfig==2.1.0
# via pytest
-isal==1.7.2 ; python_version < "3.14"
+isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/test-common.in b/requirements/test-common.in
index 84193d7bc11..b2f6dd869e1 100644
--- a/requirements/test-common.in
+++ b/requirements/test-common.in
@@ -1,7 +1,7 @@
blockbuster
coverage
freezegun
-isal; python_version < "3.14" # no wheel for 3.14
+isal; python_version < "3.14" and implementation_name == "cpython" # no wheel for 3.14, no PyPy wheel for 1.8.0+
mypy; implementation_name == "cpython"
pkgconfig
proxy.py >= 2.4.4rc5
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index be1987311af..16c3a976898 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -32,7 +32,7 @@ idna==3.11
# via trustme
iniconfig==2.1.0
# via pytest
-isal==1.8.0 ; python_version < "3.14"
+isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython"
# via -r requirements/test-common.in
markdown-it-py==4.0.0
# via rich
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 0d9df55b375..18abb27d9cf 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -53,7 +53,7 @@ idna==3.11
# yarl
iniconfig==2.1.0
# via pytest
-isal==1.8.0 ; python_version < "3.14"
+isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython"
# via -r requirements/test-common.in
markdown-it-py==4.0.0
# via rich
diff --git a/requirements/test.txt b/requirements/test.txt
index 95b278f12fb..52b74930a05 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -53,7 +53,7 @@ idna==3.11
# yarl
iniconfig==2.1.0
# via pytest
-isal==1.7.2 ; python_version < "3.14"
+isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython"
# via -r requirements/test-common.in
markdown-it-py==3.0.0
# via rich
From f6f903219ab9634362afcaae029393a2b6c29335 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sun, 17 May 2026 20:48:09 +0000
Subject: [PATCH 102/191] [PR #12592/1a7de6fa backport][3.13] Fix flakey
test_tcp_connector_fingerprint_ok by aborting SSL shutdown (#12593)
Co-authored-by: J. Nick Koston
---
CHANGES/12592.contrib.rst | 6 ++++++
tests/test_client_functional.py | 6 ++++++
2 files changed, 12 insertions(+)
create mode 100644 CHANGES/12592.contrib.rst
diff --git a/CHANGES/12592.contrib.rst b/CHANGES/12592.contrib.rst
new file mode 100644
index 00000000000..76d2f8b7035
--- /dev/null
+++ b/CHANGES/12592.contrib.rst
@@ -0,0 +1,6 @@
+Fixed a flakey ``test_tcp_connector_fingerprint_ok`` by aborting
+the SSL shutdown on the test's TCP connector before returning.
+The graceful TLS close was occasionally outliving the test event
+loop on one of the CI jobs, and the teardown ``gc.collect()``
+then surfaced the still-open transport as a
+``PytestUnraisableExceptionWarning`` -- by :user:`bdraco`.
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
index 118ce19ca64..dde06fdc6dd 100644
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -831,6 +831,12 @@ async def handler(request):
async with client.get("/") as resp:
assert resp.status == 200
+ # Abort the SSL shutdown explicitly so the pooled TLS transport is
+ # released before the test loop tears down. Otherwise a slow graceful
+ # close can outlive the loop, and the teardown gc.collect() finalises
+ # the still-open socket as an unraisable ResourceWarning.
+ await connector.close(abort_ssl=True)
+
async def test_tcp_connector_fingerprint_fail(
aiohttp_server,
From cced874ce4a554716f16672cfc66cf1cf89f989b Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sun, 17 May 2026 20:52:43 +0000
Subject: [PATCH 103/191] [PR #12592/1a7de6fa backport][3.14] Fix flakey
test_tcp_connector_fingerprint_ok by aborting SSL shutdown (#12594)
Co-authored-by: J. Nick Koston
---
CHANGES/12592.contrib.rst | 6 ++++++
tests/test_client_functional.py | 6 ++++++
2 files changed, 12 insertions(+)
create mode 100644 CHANGES/12592.contrib.rst
diff --git a/CHANGES/12592.contrib.rst b/CHANGES/12592.contrib.rst
new file mode 100644
index 00000000000..76d2f8b7035
--- /dev/null
+++ b/CHANGES/12592.contrib.rst
@@ -0,0 +1,6 @@
+Fixed a flakey ``test_tcp_connector_fingerprint_ok`` by aborting
+the SSL shutdown on the test's TCP connector before returning.
+The graceful TLS close was occasionally outliving the test event
+loop on one of the CI jobs, and the teardown ``gc.collect()``
+then surfaced the still-open transport as a
+``PytestUnraisableExceptionWarning`` -- by :user:`bdraco`.
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
index 6d17172457b..4b4d12443b6 100644
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -822,6 +822,12 @@ async def handler(request):
async with client.get("/") as resp:
assert resp.status == 200
+ # Abort the SSL shutdown explicitly so the pooled TLS transport is
+ # released before the test loop tears down. Otherwise a slow graceful
+ # close can outlive the loop, and the teardown gc.collect() finalises
+ # the still-open socket as an unraisable ResourceWarning.
+ await connector.close(abort_ssl=True)
+
async def test_tcp_connector_fingerprint_fail(
aiohttp_server,
From 696761d80cbaf104dc0d7a49b6950e0a88ef6e6c Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sun, 17 May 2026 21:38:51 +0000
Subject: [PATCH 104/191] [PR #12589/b7e19ccd backport][3.14] Gate isal test
dependency to CPython (#12591)
Co-authored-by: J. Nick Koston
---
CHANGES/12589.contrib.rst | 7 +++++++
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/test-common.in | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
7 files changed, 13 insertions(+), 6 deletions(-)
create mode 100644 CHANGES/12589.contrib.rst
diff --git a/CHANGES/12589.contrib.rst b/CHANGES/12589.contrib.rst
new file mode 100644
index 00000000000..dc7f6400d57
--- /dev/null
+++ b/CHANGES/12589.contrib.rst
@@ -0,0 +1,7 @@
+Restricted the ``isal`` test dependency to CPython, since
+``isal`` 1.8.0 stopped publishing PyPy wheels and the source
+build requires ``nasm``, which is not available on the CI
+runners. The ``parametrize_zlib_backend`` fixture already
+calls ``pytest.importorskip``, so PyPy continues to exercise
+the ``zlib`` and ``zlib_ng`` backends with no further
+changes -- by :user:`bdraco`.
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 74128b5c99d..d9a72a96a16 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -99,7 +99,7 @@ imagesize==2.0.0
# via sphinx
iniconfig==2.3.0
# via pytest
-isal==1.7.2 ; python_version < "3.14"
+isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 0732c1da1e3..75d3eaaa4ab 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -97,7 +97,7 @@ imagesize==2.0.0
# via sphinx
iniconfig==2.3.0
# via pytest
-isal==1.7.2 ; python_version < "3.14"
+isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/test-common.in b/requirements/test-common.in
index 84193d7bc11..b2f6dd869e1 100644
--- a/requirements/test-common.in
+++ b/requirements/test-common.in
@@ -1,7 +1,7 @@
blockbuster
coverage
freezegun
-isal; python_version < "3.14" # no wheel for 3.14
+isal; python_version < "3.14" and implementation_name == "cpython" # no wheel for 3.14, no PyPy wheel for 1.8.0+
mypy; implementation_name == "cpython"
pkgconfig
proxy.py >= 2.4.4rc5
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index bea1cee2d18..f0234b57ba7 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -32,7 +32,7 @@ idna==3.15
# via trustme
iniconfig==2.3.0
# via pytest
-isal==1.8.0 ; python_version < "3.14"
+isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython"
# via -r requirements/test-common.in
librt==0.11.0
# via mypy
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 20d1e4288f3..baf5c381d3f 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -56,7 +56,7 @@ idna==3.15
# yarl
iniconfig==2.3.0
# via pytest
-isal==1.8.0 ; python_version < "3.14"
+isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython"
# via -r requirements/test-common.in
librt==0.11.0
# via mypy
diff --git a/requirements/test.txt b/requirements/test.txt
index 11d061de5b8..851bc7ab40b 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -56,7 +56,7 @@ idna==3.15
# yarl
iniconfig==2.3.0
# via pytest
-isal==1.7.2 ; python_version < "3.14"
+isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython"
# via -r requirements/test-common.in
librt==0.11.0
# via mypy
From 5a026a28e51aa2ff236131f2eb0e0161f67c49ba Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 17 May 2026 22:40:39 +0000
Subject: [PATCH 105/191] Bump isal from 1.7.2 to 1.8.0 (#11589)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [isal](https://github.com/pycompression/python-isal) from 1.7.2 to
1.8.0.
Release notes
Sourced from isal's
releases.
version 1.8.0
- Python 3.14 is supported.
- Python 3.8 and 3.9 are no longer supported.
- Fix an issue where flushing using igzip_threaded caused a gzip end
of stream
and started a new gzip stream. In essence creating a concatenated gzip
stream. Now it is in concordance with how single threaded gzip streams
are flushed using Z_SYNC_FLUSH.
- Change build backend to setuptools-scm which is more commonly used
and
supported.
- Include test packages in the source distribution, so source
distribution
installations can be verified.
- Fix an issue where some tests failed because they ignored
PYTHONPATH.
- Enable support for free-threading and build free-threaded wheels for
CPython 3.14. Thanks to
@lysnikolaou and
@ngoldbaum.
Changelog
Sourced from isal's
changelog.
version 1.8.0
- Python 3.14 is supported.
- Python 3.8 and 3.9 are no longer supported.
- Fix an issue where flushing using igzip_threaded caused a gzip end
of stream
and started a new gzip stream. In essence creating a concatenated gzip
stream. Now it is in concordance with how single threaded gzip streams
are flushed using Z_SYNC_FLUSH.
- Change build backend to setuptools-scm which is more commonly used
and
supported.
- Include test packages in the source distribution, so source
distribution
installations can be verified.
- Fix an issue where some tests failed because they ignored
PYTHONPATH.
- Enable support for free-threading and build free-threaded wheels for
CPython 3.14. Thanks to
@lysnikolaou and
@ngoldbaum.
Commits
bcaaa9b
Use a valid documentation URL
1eeaf31
Merge pull request #244
from pycompression/release_1.8.0
ddcc966
Prepare release 1.8.0
bb40ffa
Merge pull request #233
from lysnikolaou/free-threading
8e9681d
Credit where it's due
9492937
Update test_freethreading.py
0b9574a
Fix linting issues
474783e
Only support 3.14 for threaded builds.
edf5371
Merge branch 'develop' into free-threading
7d204aa
Merge pull request #242
from pycompression/314
- Additional commits viewable in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test.txt | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index d9a72a96a16..95ea5248658 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -99,7 +99,7 @@ imagesize==2.0.0
# via sphinx
iniconfig==2.3.0
# via pytest
-isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython"
+isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 75d3eaaa4ab..eabac796cb9 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -97,7 +97,7 @@ imagesize==2.0.0
# via sphinx
iniconfig==2.3.0
# via pytest
-isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython"
+isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython"
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 138d631a33e..ef45375b634 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -44,7 +44,7 @@ idna==3.15
# via trustme
iniconfig==2.3.0
# via pytest
-isal==1.7.2
+isal==1.8.0
# via -r requirements/lint.in
librt==0.11.0
# via mypy
diff --git a/requirements/test.txt b/requirements/test.txt
index 851bc7ab40b..4ea290d8e2c 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -56,7 +56,7 @@ idna==3.15
# yarl
iniconfig==2.3.0
# via pytest
-isal==1.7.2 ; python_version < "3.14" and implementation_name == "cpython"
+isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython"
# via -r requirements/test-common.in
librt==0.11.0
# via mypy
From 5cf0c708f04f194273479c2d9f77be3a9abba3a6 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Sun, 17 May 2026 15:45:36 -0700
Subject: [PATCH 106/191] [PR #12595/fbf44b7e backport][3.13] Switch
cibuildwheel to the uv build frontend (#12601)
---
.github/workflows/ci-cd.yml | 8 +++++++-
CHANGES/12595.contrib.rst | 5 +++++
docs/spelling_wordlist.txt | 1 +
pyproject.toml | 7 +++++++
4 files changed, 20 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/12595.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 7349fb3930a..0ae7a7eb1ed 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -459,7 +459,13 @@ jobs:
run: |
make cythonize
- name: Build wheels
- uses: pypa/cibuildwheel@v3.2.0
+ uses: pypa/cibuildwheel@v3.4.1
+ with:
+ # `build-frontend = "build[uv]"` (pyproject.toml) requires uv to be
+ # available on the runner for Windows and macOS. Installing
+ # cibuildwheel with the `uv` extra bundles uv with it; Linux
+ # already has uv inside the manylinux/musllinux container.
+ extras: uv
env:
CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }}
CIBW_ARCHS_MACOS: x86_64 arm64 universal2
diff --git a/CHANGES/12595.contrib.rst b/CHANGES/12595.contrib.rst
new file mode 100644
index 00000000000..54af2262fb1
--- /dev/null
+++ b/CHANGES/12595.contrib.rst
@@ -0,0 +1,5 @@
+Switched the ``cibuildwheel`` build frontend to ``build[uv]`` so
+that ``uv`` provisions every build-isolation virtual environment
+in the wheel matrix, replacing the per-ABI ``pip`` resolve with a
+roughly sub-second ``uv`` resolve
+-- by :user:`bdraco`.
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 7dc09edb127..77c421e3e79 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -1,4 +1,5 @@
abc
+ABI
addons
aiodns
aioes
diff --git a/pyproject.toml b/pyproject.toml
index d1550fee500..fabdf652580 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -160,6 +160,13 @@ include = [
[tool.cibuildwheel]
+# `build[uv]` makes cibuildwheel use `uv` for every venv it provisions,
+# both on the build side (passed to `python -m build` as `--installer=uv`)
+# and when materializing the build isolation environment. uv resolves and
+# installs the build requirements in roughly a second; the previous
+# pip-based path took noticeably longer per ABI and ran once per matrix
+# cell.
+build-frontend = "build[uv]"
test-command = ""
# don't build PyPy wheels, install from source instead
skip = "pp*"
From 2382ac9449ad981b3ae7eb107c267ab8ac45bc72 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sun, 17 May 2026 15:45:53 -0700
Subject: [PATCH 107/191] [PR #12595/fbf44b7e backport][3.14] Switch
cibuildwheel to the uv build frontend (#12598)
Co-authored-by: J. Nick Koston
---
.github/workflows/ci-cd.yml | 6 ++++++
CHANGES/12595.contrib.rst | 5 +++++
docs/spelling_wordlist.txt | 1 +
pyproject.toml | 7 +++++++
4 files changed, 19 insertions(+)
create mode 100644 CHANGES/12595.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 78953a31855..06303863c3c 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -615,6 +615,12 @@ jobs:
make cythonize
- name: Build wheels
uses: pypa/cibuildwheel@v3.4.1
+ with:
+ # `build-frontend = "build[uv]"` (pyproject.toml) requires uv to be
+ # available on the runner for Windows and macOS. Installing
+ # cibuildwheel with the `uv` extra bundles uv with it; Linux
+ # already has uv inside the manylinux/musllinux container.
+ extras: uv
env:
CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }}
CIBW_ARCHS_MACOS: x86_64 arm64 universal2
diff --git a/CHANGES/12595.contrib.rst b/CHANGES/12595.contrib.rst
new file mode 100644
index 00000000000..54af2262fb1
--- /dev/null
+++ b/CHANGES/12595.contrib.rst
@@ -0,0 +1,5 @@
+Switched the ``cibuildwheel`` build frontend to ``build[uv]`` so
+that ``uv`` provisions every build-isolation virtual environment
+in the wheel matrix, replacing the per-ABI ``pip`` resolve with a
+roughly sub-second ``uv`` resolve
+-- by :user:`bdraco`.
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index a422b523639..16f7da5ebe4 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -1,4 +1,5 @@
abc
+ABI
addons
aiodns
aioes
diff --git a/pyproject.toml b/pyproject.toml
index 72eaf0e04f2..c74565e4902 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -160,6 +160,13 @@ include = [
[tool.cibuildwheel]
+# `build[uv]` makes cibuildwheel use `uv` for every venv it provisions,
+# both on the build side (passed to `python -m build` as `--installer=uv`)
+# and when materializing the build isolation environment. uv resolves and
+# installs the build requirements in roughly a second; the previous
+# pip-based path took noticeably longer per ABI and ran once per matrix
+# cell.
+build-frontend = "build[uv]"
test-command = ""
# don't build PyPy wheels, install from source instead
skip = "pp*"
From e81a7d93287a708e8759d319e84e4b3d3bf64d7b Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Sun, 17 May 2026 17:33:00 -0700
Subject: [PATCH 108/191] [PR #12597/28d1214f backport][3.14] Stop calling
socket.getfqdn() in BaseRequest.host fallback (#12605)
Closes #9308.
---
CHANGES/12597.breaking.rst | 1 +
CHANGES/9308.breaking.rst | 12 +++++++
aiohttp/test_utils.py | 8 +++--
aiohttp/web_protocol.py | 11 ++++++
aiohttp/web_request.py | 18 ++++++++--
docs/web_reference.rst | 11 +++++-
tests/test_web_request.py | 69 ++++++++++++++++++++++++++++++++------
7 files changed, 113 insertions(+), 17 deletions(-)
create mode 120000 CHANGES/12597.breaking.rst
create mode 100644 CHANGES/9308.breaking.rst
diff --git a/CHANGES/12597.breaking.rst b/CHANGES/12597.breaking.rst
new file mode 120000
index 00000000000..651891d5065
--- /dev/null
+++ b/CHANGES/12597.breaking.rst
@@ -0,0 +1 @@
+9308.breaking.rst
\ No newline at end of file
diff --git a/CHANGES/9308.breaking.rst b/CHANGES/9308.breaking.rst
new file mode 100644
index 00000000000..afb3965ab8f
--- /dev/null
+++ b/CHANGES/9308.breaking.rst
@@ -0,0 +1,12 @@
+Stopped calling :func:`socket.getfqdn` as the fallback for
+:attr:`aiohttp.web.BaseRequest.host`. :func:`socket.getfqdn`
+performs blocking reverse DNS resolution on the event loop
+thread and can stall a worker for many seconds when the system
+resolver is slow, and could be triggered remotely by an HTTP/1.0
+request that omits the ``Host`` header. The fallback when no
+``Host`` header is present is now the local socket address the
+request arrived on (transport ``sockname``), or an empty string
+if no transport information is available. Code that relied on
+the FQDN being returned must now read it from
+:func:`socket.getfqdn` directly, off the event loop
+-- by :user:`bdraco`.
diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py
index ccc05e1ef5d..92a49b975ec 100644
--- a/aiohttp/test_utils.py
+++ b/aiohttp/test_utils.py
@@ -677,11 +677,10 @@ def set_dict(app: Any, key: str, value: Any) -> None:
def _create_transport(sslcontext: SSLContext | None = None) -> mock.Mock:
transport = mock.Mock()
- def get_extra_info(key: str) -> SSLContext | None:
+ def get_extra_info(key: str) -> SSLContext | tuple[str, int] | None:
if key == "sslcontext":
return sslcontext
- else:
- return None
+ return ("127.0.0.1", 80) if key == "sockname" else None
transport.get_extra_info.side_effect = get_extra_info
return transport
@@ -762,6 +761,9 @@ def make_mocked_request(
type(protocol).peername = mock.PropertyMock(
return_value=transport.get_extra_info("peername")
)
+ type(protocol).sockname = mock.PropertyMock(
+ return_value=transport.get_extra_info("sockname")
+ )
type(protocol).ssl_context = mock.PropertyMock(return_value=sslcontext)
if writer is sentinel:
diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py
index f170c8e5de6..69fe7b9bb29 100644
--- a/aiohttp/web_protocol.py
+++ b/aiohttp/web_protocol.py
@@ -275,6 +275,17 @@ def peername(
else self.transport.get_extra_info("peername")
)
+ @under_cached_property
+ def sockname(
+ self,
+ ) -> str | tuple[str, int, int, int] | tuple[str, int] | None:
+ """Return sockname if available."""
+ return (
+ None
+ if self.transport is None
+ else self.transport.get_extra_info("sockname")
+ )
+
@property
def keepalive_timeout(self) -> float:
return self._keepalive_timeout
diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py
index b5d5b1b6897..d3d9cc2989f 100644
--- a/aiohttp/web_request.py
+++ b/aiohttp/web_request.py
@@ -2,7 +2,6 @@
import datetime
import io
import re
-import socket
import string
import tempfile
import types
@@ -190,6 +189,7 @@ def __init__(
self._transport_sslcontext = protocol.ssl_context
self._transport_peername = protocol.peername
+ self._transport_sockname = protocol.sockname
if remote is not None:
self._cache["remote"] = remote
@@ -437,7 +437,9 @@ def host(self) -> str:
- overridden value by .clone(host=new_host) call.
- HOST HTTP header
- - socket.getfqdn() value
+ - local socket address the request arrived on
+ (transport ``sockname``)
+ - empty string if no transport information is available
For example, 'example.com' or 'localhost:8080'.
@@ -446,7 +448,17 @@ def host(self) -> str:
host = self._message.headers.get(hdrs.HOST)
if host is not None:
return host
- return socket.getfqdn()
+ sockname = self._transport_sockname
+ if sockname is None:
+ return ""
+ if isinstance(sockname, tuple):
+ # AF_INET6 returns a 4-tuple (host, port, flowinfo, scopeid);
+ # bracket the bare address so it matches the Host-header shape
+ # and is a valid URL authority component.
+ if len(sockname) == 4:
+ return f"[{sockname[0]}]"
+ return str(sockname[0])
+ return str(sockname)
@reify
def remote(self) -> str | None:
diff --git a/docs/web_reference.rst b/docs/web_reference.rst
index a3d9e1b284d..6fcb1a8f42f 100644
--- a/docs/web_reference.rst
+++ b/docs/web_reference.rst
@@ -118,7 +118,9 @@ and :ref:`aiohttp-web-signals` handlers.
- Overridden value by :meth:`~BaseRequest.clone` call.
- *Host* HTTP header
- - :func:`socket.getfqdn`
+ - local socket address the request arrived on
+ (transport ``sockname``)
+ - empty string if no transport information is available
Read-only :class:`str` property.
@@ -129,6 +131,13 @@ and :ref:`aiohttp-web-signals` handlers.
Call ``.clone(host=new_host)`` for setting up the value
explicitly.
+ .. versionchanged:: 3.13
+
+ The fallback when no ``Host`` header is present no longer
+ calls :func:`socket.getfqdn`, which performed blocking
+ reverse-DNS resolution on the event loop. The local socket
+ address (transport ``sockname``) is used instead.
+
.. seealso:: :ref:`aiohttp-web-forwarded-support`
.. attribute:: remote
diff --git a/tests/test_web_request.py b/tests/test_web_request.py
index fa2f257c402..d025c8c4812 100644
--- a/tests/test_web_request.py
+++ b/tests/test_web_request.py
@@ -1,7 +1,6 @@
import asyncio
import datetime
import logging
-import socket
import sys
import time
from collections.abc import Iterator, MutableMapping
@@ -42,16 +41,19 @@ def test_base_ctor() -> None:
URL("/path/to?a=1&b=2"),
)
+ protocol = mock.Mock()
+ protocol.ssl_context = None
+ protocol.peername = None
+ protocol.sockname = ("127.0.0.1", 80)
req = BaseRequest(
- message, mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock()
+ message, mock.Mock(), protocol, mock.Mock(), mock.Mock(), mock.Mock()
)
assert "GET" == req.method
assert HttpVersion(1, 1) == req.version
- # MacOS may return CamelCased host name, need .lower()
- # FQDN can be wider than host, e.g.
- # 'fv-az397-495' in 'fv-az397-495.internal.cloudapp.net'
- assert req.host.lower() in socket.getfqdn().lower()
+ # No Host header in this request, so host falls back to the
+ # local socket address the request arrived on.
+ assert req.host == "127.0.0.1"
assert "/path/to?a=1&b=2" == req.path_qs
assert "/path/to" == req.path
assert "a=1&b=2" == req.query_string
@@ -73,10 +75,10 @@ def test_ctor() -> None:
assert "GET" == req.method
assert HttpVersion(1, 1) == req.version
- # MacOS may return CamelCased host name, need .lower()
- # FQDN can be wider than host, e.g.
- # 'fv-az397-495' in 'fv-az397-495.internal.cloudapp.net'
- assert req.host.lower() in socket.getfqdn().lower()
+ # No Host header in this request, so host falls back to the
+ # local socket address the request arrived on (the default
+ # sockname configured by make_mocked_request).
+ assert req.host == "127.0.0.1"
assert "/path/to?a=1&b=2" == req.path_qs
assert "/path/to" == req.path
assert "a=1&b=2" == req.query_string
@@ -118,6 +120,53 @@ def test_deprecated_message() -> None:
assert req.message == req._message
+def test_host_falls_back_to_sockname_not_dns() -> None:
+ """Regression: request.host must not call socket.getfqdn().
+
+ socket.getfqdn() does blocking reverse DNS resolution on the
+ event loop thread and can stall a worker for many seconds when
+ the system resolver is slow. The fallback for a request with no
+ Host header is the local socket address the request arrived on,
+ not the system FQDN.
+ """
+ req = make_mocked_request("GET", "/")
+ assert req.host == "127.0.0.1"
+ assert str(req.url).startswith("http://127.0.0.1")
+
+
+def test_host_with_ipv6_sockname() -> None:
+ """AF_INET6 sockname is bracketed to form a valid URL authority.
+
+ A bare IPv6 string would cause ``URL.build(authority=...)`` to
+ raise ``ValueError``.
+ """
+ transport = mock.Mock()
+ transport.get_extra_info.side_effect = lambda key: (
+ ("::1", 80, 0, 0) if key == "sockname" else None
+ )
+ req = make_mocked_request("GET", "/", transport=transport)
+ assert req.host == "[::1]"
+ assert str(req.url) == "http://[::1]/"
+
+
+def test_host_with_unix_socket_sockname() -> None:
+ """Unix-socket transports expose sockname as a str path."""
+ transport = mock.Mock()
+ transport.get_extra_info.side_effect = lambda key: (
+ "/tmp/aiohttp.sock" if key == "sockname" else None
+ )
+ req = make_mocked_request("GET", "/", transport=transport)
+ assert req.host == "/tmp/aiohttp.sock"
+
+
+def test_host_with_no_transport_sockname() -> None:
+ """An empty string is returned when no sockname is available."""
+ transport = mock.Mock()
+ transport.get_extra_info.return_value = None
+ req = make_mocked_request("GET", "/", transport=transport)
+ assert req.host == ""
+
+
def test_doubleslashes() -> None:
# NB: //foo/bar is an absolute URL with foo netloc and /bar path
req = make_mocked_request("GET", "/bar//foo/")
From c284e2170cde9539da7796608cbbbe41f45aaa3f Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Sun, 17 May 2026 18:11:27 -0700
Subject: [PATCH 109/191] [PR #12603/dbe83fd0 backport][3.14] Fix flaky
test_handler_returns_not_response on PyPy via debug-mode fixture (#12608)
---
CHANGES/12603.contrib.rst | 8 ++++++++
tests/conftest.py | 18 ++++++++++++++++++
tests/test_web_functional.py | 10 ++++++----
3 files changed, 32 insertions(+), 4 deletions(-)
create mode 100644 CHANGES/12603.contrib.rst
diff --git a/CHANGES/12603.contrib.rst b/CHANGES/12603.contrib.rst
new file mode 100644
index 00000000000..51574d02868
--- /dev/null
+++ b/CHANGES/12603.contrib.rst
@@ -0,0 +1,8 @@
+Fixed flaky ``test_handler_returns_not_response`` and
+``test_handler_returns_none`` by routing ``loop.set_debug(True)``
+through a new ``loop_debug_mode`` fixture that disables debug
+mode before the ``aiohttp_client`` fixture finalizes. Leaving
+debug on through teardown let PyPy 3.11's asyncio slow-callback
+logger walk into ``Task.__repr__`` during connector close,
+surfacing a spurious ``RuntimeWarning: coroutine was never
+awaited`` -- by :user:`bdraco`.
diff --git a/tests/conftest.py b/tests/conftest.py
index 71e773ddca8..a1cd6a17a56 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -165,6 +165,24 @@ def tls_certificate_fingerprint_sha256(tls_certificate_pem_bytes):
return sha256(tls_cert_der).digest()
+@pytest.fixture
+async def loop_debug_mode() -> AsyncIterator[None]:
+ """Enable asyncio debug mode for the duration of the test.
+
+ Disables debug before teardown so PyPy 3.11's recursive
+ ``Task.__repr__``, triggered by the asyncio slow-callback
+ logger during connector close, does not surface a spurious
+ ``RuntimeWarning: coroutine ... was never awaited`` while
+ the ``aiohttp_client`` fixture finalizes.
+ """
+ loop = asyncio.get_running_loop()
+ loop.set_debug(True)
+ try:
+ yield
+ finally:
+ loop.set_debug(False)
+
+
@pytest.fixture
def unix_sockname(tmp_path, tmp_path_factory):
"""Generate an fs path to the UNIX domain socket for testing.
diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py
index d429e7ceaa0..c50ceaad4c9 100644
--- a/tests/test_web_functional.py
+++ b/tests/test_web_functional.py
@@ -92,8 +92,9 @@ async def handler(request):
await resp.release()
-async def test_handler_returns_not_response(aiohttp_server, aiohttp_client) -> None:
- asyncio.get_event_loop().set_debug(True)
+async def test_handler_returns_not_response(
+ aiohttp_server, aiohttp_client, loop_debug_mode: None
+) -> None:
logger = mock.Mock()
async def handler(request):
@@ -108,8 +109,9 @@ async def handler(request):
assert resp.status == 500
-async def test_handler_returns_none(aiohttp_server, aiohttp_client) -> None:
- asyncio.get_event_loop().set_debug(True)
+async def test_handler_returns_none(
+ aiohttp_server, aiohttp_client, loop_debug_mode: None
+) -> None:
logger = mock.Mock()
async def handler(request):
From ca7d8730687c9c7ec9e304fee7eb2207f517af69 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Sun, 17 May 2026 18:12:37 -0700
Subject: [PATCH 110/191] [PR #12597/28d1214f backport][3.13] Stop calling
socket.getfqdn() in BaseRequest.host fallback (#12604)
Closes #9308.
---
CHANGES/12597.breaking.rst | 1 +
CHANGES/9308.breaking.rst | 12 +++++++
aiohttp/test_utils.py | 10 ++++--
aiohttp/web_protocol.py | 11 ++++++
aiohttp/web_request.py | 18 ++++++++--
docs/web_reference.rst | 11 +++++-
tests/test_web_request.py | 69 ++++++++++++++++++++++++++++++++------
7 files changed, 115 insertions(+), 17 deletions(-)
create mode 120000 CHANGES/12597.breaking.rst
create mode 100644 CHANGES/9308.breaking.rst
diff --git a/CHANGES/12597.breaking.rst b/CHANGES/12597.breaking.rst
new file mode 120000
index 00000000000..651891d5065
--- /dev/null
+++ b/CHANGES/12597.breaking.rst
@@ -0,0 +1 @@
+9308.breaking.rst
\ No newline at end of file
diff --git a/CHANGES/9308.breaking.rst b/CHANGES/9308.breaking.rst
new file mode 100644
index 00000000000..afb3965ab8f
--- /dev/null
+++ b/CHANGES/9308.breaking.rst
@@ -0,0 +1,12 @@
+Stopped calling :func:`socket.getfqdn` as the fallback for
+:attr:`aiohttp.web.BaseRequest.host`. :func:`socket.getfqdn`
+performs blocking reverse DNS resolution on the event loop
+thread and can stall a worker for many seconds when the system
+resolver is slow, and could be triggered remotely by an HTTP/1.0
+request that omits the ``Host`` header. The fallback when no
+``Host`` header is present is now the local socket address the
+request arrived on (transport ``sockname``), or an empty string
+if no transport information is available. Code that relied on
+the FQDN being returned must now read it from
+:func:`socket.getfqdn` directly, off the event loop
+-- by :user:`bdraco`.
diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py
index 34b77d47cd5..4f38e965e16 100644
--- a/aiohttp/test_utils.py
+++ b/aiohttp/test_utils.py
@@ -19,8 +19,10 @@
Iterator,
List,
Optional,
+ Tuple,
Type,
TypeVar,
+ Union,
cast,
overload,
)
@@ -651,11 +653,10 @@ def set_dict(app: Any, key: str, value: Any) -> None:
def _create_transport(sslcontext: Optional[SSLContext] = None) -> mock.Mock:
transport = mock.Mock()
- def get_extra_info(key: str) -> Optional[SSLContext]:
+ def get_extra_info(key: str) -> Optional[Union[SSLContext, Tuple[str, int]]]:
if key == "sslcontext":
return sslcontext
- else:
- return None
+ return ("127.0.0.1", 80) if key == "sockname" else None
transport.get_extra_info.side_effect = get_extra_info
return transport
@@ -736,6 +737,9 @@ def make_mocked_request(
type(protocol).peername = mock.PropertyMock(
return_value=transport.get_extra_info("peername")
)
+ type(protocol).sockname = mock.PropertyMock(
+ return_value=transport.get_extra_info("sockname")
+ )
type(protocol).ssl_context = mock.PropertyMock(return_value=sslcontext)
if writer is sentinel:
diff --git a/aiohttp/web_protocol.py b/aiohttp/web_protocol.py
index 84e70e59088..b4056a639bc 100644
--- a/aiohttp/web_protocol.py
+++ b/aiohttp/web_protocol.py
@@ -288,6 +288,17 @@ def peername(
else self.transport.get_extra_info("peername")
)
+ @under_cached_property
+ def sockname(
+ self,
+ ) -> Optional[Union[str, Tuple[str, int, int, int], Tuple[str, int]]]:
+ """Return sockname if available."""
+ return (
+ None
+ if self.transport is None
+ else self.transport.get_extra_info("sockname")
+ )
+
@property
def keepalive_timeout(self) -> float:
return self._keepalive_timeout
diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py
index f2d4c511e44..e02b99f2793 100644
--- a/aiohttp/web_request.py
+++ b/aiohttp/web_request.py
@@ -2,7 +2,6 @@
import datetime
import io
import re
-import socket
import string
import tempfile
import types
@@ -200,6 +199,7 @@ def __init__(
self._transport_sslcontext = protocol.ssl_context
self._transport_peername = protocol.peername
+ self._transport_sockname = protocol.sockname
if remote is not None:
self._cache["remote"] = remote
@@ -426,7 +426,9 @@ def host(self) -> str:
- overridden value by .clone(host=new_host) call.
- HOST HTTP header
- - socket.getfqdn() value
+ - local socket address the request arrived on
+ (transport ``sockname``)
+ - empty string if no transport information is available
For example, 'example.com' or 'localhost:8080'.
@@ -435,7 +437,17 @@ def host(self) -> str:
host = self._message.headers.get(hdrs.HOST)
if host is not None:
return host
- return socket.getfqdn()
+ sockname = self._transport_sockname
+ if sockname is None:
+ return ""
+ if isinstance(sockname, tuple):
+ # AF_INET6 returns a 4-tuple (host, port, flowinfo, scopeid);
+ # bracket the bare address so it matches the Host-header shape
+ # and is a valid URL authority component.
+ if len(sockname) == 4:
+ return f"[{sockname[0]}]"
+ return str(sockname[0])
+ return str(sockname)
@reify
def remote(self) -> Optional[str]:
diff --git a/docs/web_reference.rst b/docs/web_reference.rst
index 83a9c7f0ecd..c0e8b983538 100644
--- a/docs/web_reference.rst
+++ b/docs/web_reference.rst
@@ -118,7 +118,9 @@ and :ref:`aiohttp-web-signals` handlers.
- Overridden value by :meth:`~BaseRequest.clone` call.
- *Host* HTTP header
- - :func:`socket.getfqdn`
+ - local socket address the request arrived on
+ (transport ``sockname``)
+ - empty string if no transport information is available
Read-only :class:`str` property.
@@ -129,6 +131,13 @@ and :ref:`aiohttp-web-signals` handlers.
Call ``.clone(host=new_host)`` for setting up the value
explicitly.
+ .. versionchanged:: 3.13
+
+ The fallback when no ``Host`` header is present no longer
+ calls :func:`socket.getfqdn`, which performed blocking
+ reverse-DNS resolution on the event loop. The local socket
+ address (transport ``sockname``) is used instead.
+
.. seealso:: :ref:`aiohttp-web-forwarded-support`
.. attribute:: remote
diff --git a/tests/test_web_request.py b/tests/test_web_request.py
index 7778128c19e..dfbfb369732 100644
--- a/tests/test_web_request.py
+++ b/tests/test_web_request.py
@@ -1,7 +1,6 @@
import asyncio
import datetime
import logging
-import socket
import time
from collections.abc import MutableMapping
from typing import Any
@@ -40,16 +39,19 @@ def test_base_ctor() -> None:
URL("/path/to?a=1&b=2"),
)
+ protocol = mock.Mock()
+ protocol.ssl_context = None
+ protocol.peername = None
+ protocol.sockname = ("127.0.0.1", 80)
req = BaseRequest(
- message, mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock(), mock.Mock()
+ message, mock.Mock(), protocol, mock.Mock(), mock.Mock(), mock.Mock()
)
assert "GET" == req.method
assert HttpVersion(1, 1) == req.version
- # MacOS may return CamelCased host name, need .lower()
- # FQDN can be wider than host, e.g.
- # 'fv-az397-495' in 'fv-az397-495.internal.cloudapp.net'
- assert req.host.lower() in socket.getfqdn().lower()
+ # No Host header in this request, so host falls back to the
+ # local socket address the request arrived on.
+ assert req.host == "127.0.0.1"
assert "/path/to?a=1&b=2" == req.path_qs
assert "/path/to" == req.path
assert "a=1&b=2" == req.query_string
@@ -71,10 +73,10 @@ def test_ctor() -> None:
assert "GET" == req.method
assert HttpVersion(1, 1) == req.version
- # MacOS may return CamelCased host name, need .lower()
- # FQDN can be wider than host, e.g.
- # 'fv-az397-495' in 'fv-az397-495.internal.cloudapp.net'
- assert req.host.lower() in socket.getfqdn().lower()
+ # No Host header in this request, so host falls back to the
+ # local socket address the request arrived on (the default
+ # sockname configured by make_mocked_request).
+ assert req.host == "127.0.0.1"
assert "/path/to?a=1&b=2" == req.path_qs
assert "/path/to" == req.path
assert "a=1&b=2" == req.query_string
@@ -116,6 +118,53 @@ def test_deprecated_message() -> None:
assert req.message == req._message
+def test_host_falls_back_to_sockname_not_dns() -> None:
+ """Regression: request.host must not call socket.getfqdn().
+
+ socket.getfqdn() does blocking reverse DNS resolution on the
+ event loop thread and can stall a worker for many seconds when
+ the system resolver is slow. The fallback for a request with no
+ Host header is the local socket address the request arrived on,
+ not the system FQDN.
+ """
+ req = make_mocked_request("GET", "/")
+ assert req.host == "127.0.0.1"
+ assert str(req.url).startswith("http://127.0.0.1")
+
+
+def test_host_with_ipv6_sockname() -> None:
+ """AF_INET6 sockname is bracketed to form a valid URL authority.
+
+ A bare IPv6 string would cause ``URL.build(authority=...)`` to
+ raise ``ValueError``.
+ """
+ transport = mock.Mock()
+ transport.get_extra_info.side_effect = lambda key: (
+ ("::1", 80, 0, 0) if key == "sockname" else None
+ )
+ req = make_mocked_request("GET", "/", transport=transport)
+ assert req.host == "[::1]"
+ assert str(req.url) == "http://[::1]/"
+
+
+def test_host_with_unix_socket_sockname() -> None:
+ """Unix-socket transports expose sockname as a str path."""
+ transport = mock.Mock()
+ transport.get_extra_info.side_effect = lambda key: (
+ "/tmp/aiohttp.sock" if key == "sockname" else None
+ )
+ req = make_mocked_request("GET", "/", transport=transport)
+ assert req.host == "/tmp/aiohttp.sock"
+
+
+def test_host_with_no_transport_sockname() -> None:
+ """An empty string is returned when no sockname is available."""
+ transport = mock.Mock()
+ transport.get_extra_info.return_value = None
+ req = make_mocked_request("GET", "/", transport=transport)
+ assert req.host == ""
+
+
def test_doubleslashes() -> None:
# NB: //foo/bar is an absolute URL with foo netloc and /bar path
req = make_mocked_request("GET", "/bar//foo/")
From f57a6ab518c8967027d1db17253b003f6f209f80 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Sun, 17 May 2026 18:29:31 -0700
Subject: [PATCH 111/191] [PR #12603/dbe83fd0 backport][3.13] Fix flaky
test_handler_returns_not_response on PyPy via debug-mode fixture (#12607)
---
CHANGES/12603.contrib.rst | 8 ++++++++
tests/conftest.py | 18 ++++++++++++++++++
tests/test_web_functional.py | 10 ++++++----
3 files changed, 32 insertions(+), 4 deletions(-)
create mode 100644 CHANGES/12603.contrib.rst
diff --git a/CHANGES/12603.contrib.rst b/CHANGES/12603.contrib.rst
new file mode 100644
index 00000000000..51574d02868
--- /dev/null
+++ b/CHANGES/12603.contrib.rst
@@ -0,0 +1,8 @@
+Fixed flaky ``test_handler_returns_not_response`` and
+``test_handler_returns_none`` by routing ``loop.set_debug(True)``
+through a new ``loop_debug_mode`` fixture that disables debug
+mode before the ``aiohttp_client`` fixture finalizes. Leaving
+debug on through teardown let PyPy 3.11's asyncio slow-callback
+logger walk into ``Task.__repr__`` during connector close,
+surfacing a spurious ``RuntimeWarning: coroutine was never
+awaited`` -- by :user:`bdraco`.
diff --git a/tests/conftest.py b/tests/conftest.py
index c299df5aa3f..cccf267941d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -160,6 +160,24 @@ def tls_certificate_fingerprint_sha256(tls_certificate_pem_bytes):
return sha256(tls_cert_der).digest()
+@pytest.fixture
+async def loop_debug_mode() -> AsyncIterator[None]:
+ """Enable asyncio debug mode for the duration of the test.
+
+ Disables debug before teardown so PyPy 3.11's recursive
+ ``Task.__repr__``, triggered by the asyncio slow-callback
+ logger during connector close, does not surface a spurious
+ ``RuntimeWarning: coroutine ... was never awaited`` while
+ the ``aiohttp_client`` fixture finalizes.
+ """
+ loop = asyncio.get_running_loop()
+ loop.set_debug(True)
+ try:
+ yield
+ finally:
+ loop.set_debug(False)
+
+
@pytest.fixture
def unix_sockname(tmp_path, tmp_path_factory):
"""Generate an fs path to the UNIX domain socket for testing.
diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py
index fe9cce27b8e..089b183858a 100644
--- a/tests/test_web_functional.py
+++ b/tests/test_web_functional.py
@@ -91,8 +91,9 @@ async def handler(request):
await resp.release()
-async def test_handler_returns_not_response(aiohttp_server, aiohttp_client) -> None:
- asyncio.get_event_loop().set_debug(True)
+async def test_handler_returns_not_response(
+ aiohttp_server, aiohttp_client, loop_debug_mode: None
+) -> None:
logger = mock.Mock()
async def handler(request):
@@ -107,8 +108,9 @@ async def handler(request):
assert resp.status == 500
-async def test_handler_returns_none(aiohttp_server, aiohttp_client) -> None:
- asyncio.get_event_loop().set_debug(True)
+async def test_handler_returns_none(
+ aiohttp_server, aiohttp_client, loop_debug_mode: None
+) -> None:
logger = mock.Mock()
async def handler(request):
From 67bedfd7c93d905a9f10b47aef495939242694ad Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 01:53:47 +0000
Subject: [PATCH 112/191] [PR #12596/91b061b0 backport][3.14] Switch
test-results upload to codecov-action (#12599)
Co-authored-by: J. Nick Koston
---
.github/workflows/ci-cd.yml | 8 ++++++--
CHANGES/12596.contrib.rst | 4 ++++
2 files changed, 10 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12596.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 06303863c3c..167bd6813a6 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -262,8 +262,10 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
- uses: codecov/test-results-action@v1
+ uses: codecov/codecov-action@v6
with:
+ files: ./junit.xml
+ report_type: test_results
token: ${{ secrets.CODECOV_TOKEN }}
autobahn:
@@ -354,8 +356,10 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
- uses: codecov/test-results-action@v1
+ uses: codecov/codecov-action@v6
with:
+ files: ./junit.xml
+ report_type: test_results
token: ${{ secrets.CODECOV_TOKEN }}
benchmark:
diff --git a/CHANGES/12596.contrib.rst b/CHANGES/12596.contrib.rst
new file mode 100644
index 00000000000..ff96abe5bfc
--- /dev/null
+++ b/CHANGES/12596.contrib.rst
@@ -0,0 +1,4 @@
+Switched the test-results upload step to ``codecov/codecov-action@v6``
+with ``report_type: test_results``, since
+``codecov/test-results-action`` is being deprecated in favor of
+``codecov-action`` -- by :user:`bdraco`.
From ee93c07bc0ed83683af22bd1534638332532d0a5 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Sun, 17 May 2026 19:50:02 -0700
Subject: [PATCH 113/191] [PR #12609/b26cfbff backport][3.14] Fix flaky
test_shutdown_handler_cancellation_suppressed (#12612)
---
tests/test_run_app.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/tests/test_run_app.py b/tests/test_run_app.py
index f855fbe0a06..d9e21c696c0 100644
--- a/tests/test_run_app.py
+++ b/tests/test_run_app.py
@@ -1284,6 +1284,7 @@ def test_shutdown_handler_cancellation_suppressed(
sock = unused_port_socket
port = sock.getsockname()[1]
actions = []
+ suppressed = asyncio.Event()
async def test() -> None:
async def test_resp(sess):
@@ -1295,7 +1296,8 @@ async def test_resp(sess):
async with ClientSession() as sess:
t = asyncio.create_task(test_resp(sess))
- await asyncio.sleep(0.5)
+ # Wait until the handler has observed its cancellation.
+ await asyncio.wait_for(suppressed.wait(), timeout=3)
# Handler is in-progress while we trigger server shutdown.
actions.append("PRESTOP")
async with sess.get(f"http://127.0.0.1:{port}/stop"):
@@ -1316,6 +1318,7 @@ async def handler(request: web.Request) -> web.Response:
await asyncio.sleep(5)
except asyncio.CancelledError:
actions.append("SUPPRESSED")
+ suppressed.set()
await asyncio.sleep(2)
actions.append("DONE")
return web.Response(text="FOO")
From e03870e9291eb201d8411d8beb68ffdd48072cb4 Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Mon, 18 May 2026 07:55:59 +0100
Subject: [PATCH 114/191] Create AGENTS.md (#12613)
---
AGENTS.md | 71 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 71 insertions(+)
create mode 100644 AGENTS.md
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 00000000000..ec687201121
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,71 @@
+# AGENTS.md
+
+This file provides guidance to AI coding agents working with this repository.
+
+## Branching
+
+- All new features and bug fixes target `master`. A bot creates backports to the `x.y` release branches automatically.
+
+## Build
+
+- Full dev setup: `make install-dev` (installs deps, cythonizes, builds extensions)
+- Vendored llhttp: `git submodule update --init` + `make generate-llhttp` (requires Node.js)
+- Cython extensions: `make cythonize` (.pyx → .c), then `pip install -e .` to compile
+- Pure Python mode: `AIOHTTP_NO_EXTENSIONS=1 pip install -e .`
+- `AIOHTTP_CYTHON_TRACE=1` enables Cython trace macros (only useful with linetrace-enabled .c files)
+
+## Test
+
+- Run all: `PYTHONPATH='.' pytest --numprocesses=auto`
+- Single test: `PYTHONPATH='.' pytest tests/test_foo.py::test_name`
+- Pure Python only: `PYTHONPATH='.' AIOHTTP_NO_EXTENSIONS=1 pytest`
+
+## Lint & Format
+
+- `pre-commit run --all-files` runs all checks (black, isort, flake8, pyupgrade, codespell)
+- `black` for formatting only, `mypy` for type checking
+- black with 88-col line length, isort with trailing commas
+
+## Documentation & code style
+
+- User-visible API changes need a docs update under `docs/` (`docs/client_reference.rst` / `docs/web_reference.rst` plus any narrative pages).
+- Docstrings in code, prose in Sphinx; `make doc` builds locally.
+- No docstrings or comments that just restate the code.
+
+## Changelog
+
+- Fragments in `CHANGES/{pr_or_issue}.{type}.rst` (valid types are defined in `pyproject.toml` under `[tool.towncrier]`).
+- Sign with `` -- by :user:`github-handle` ``.
+- Both issue and PR number wanted: keep the issue-numbered file and symlink: `ln -s 1234.bugfix.rst CHANGES/1240.bugfix.rst`.
+- Multiple fragments same category: `1234.feature.rst`, `1234.feature.1.rst`.
+
+## PRs
+
+- **Prove it works before opening the PR**. This means:
+ - Relevant tests pass locally.
+ - New behaviour is covered by a test.
+ - Any parser/websocket related changes have been tested with Cython extensions installed.
+- Use the shipped template at [`.github/PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md).
+ - A couple of sentences per section is plenty.
+ - Tick checklist boxes that apply; write `N/A` next to ones that do not.
+ - First-time contributors add themselves to `CONTRIBUTORS.txt` (alphabetical by first name).
+- **Draft.** Use `gh pr create --draft`.
+ - Every submission must be reviewed by a human before going out of draft; that review is the operator's job, not the maintainers'.
+ - Do not mark ready or request reviewers yourself.
+- **Disclosure.** One plain line at the bottom: `Drafted with ; reviewed by .`
+- **No `Co-Authored-By:`** LLM trailers in commits or PR body.
+- Agent run output (test logs) goes in a collapsed `` block **below** the template summary.
+
+**Commits**: Don't use conventional commits; match recent imperative subjects.
+
+## Threat model
+
+`THREAT_MODEL.md` is a living document and should be revised when:
+
+- A CVE / GHSA is filed against aiohttp.
+- The parser configuration changes (llhttp lenient flags, size limits, version regex).
+- Any default referenced in the document changes (`client_max_size`, `keepalive_timeout`, `max_redirects`, `limit`, `limit_per_host`, etc.).
+- The vendored llhttp version is bumped.
+- A public API surface is added or removed in `client.py` / `web_*.py` / `multipart.py`.
+
+When a chunk's content is materially affected, update both the chunk and the relevant entries in §6.1–§6.4. The "Past advisories / hardening (recap)" subsection of each chunk is the audit trail for what has been verified-in-place.
From 3427840086f831564920652845fcce7461c0ccc3 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Mon, 18 May 2026 00:23:06 -0700
Subject: [PATCH 115/191] [PR #12600/f402c3d8 backport][3.14] Silence uvloop
0.22+ asyncio.AbstractEventLoopPolicy deprecation (#12616)
---
CHANGES/12600.contrib.rst | 4 ++++
setup.cfg | 4 ++++
2 files changed, 8 insertions(+)
create mode 100644 CHANGES/12600.contrib.rst
diff --git a/CHANGES/12600.contrib.rst b/CHANGES/12600.contrib.rst
new file mode 100644
index 00000000000..4d361389191
--- /dev/null
+++ b/CHANGES/12600.contrib.rst
@@ -0,0 +1,4 @@
+Silenced the ``DeprecationWarning`` raised by ``uvloop`` 0.22+ when it
+accesses the ``asyncio.AbstractEventLoopPolicy`` alias that Python 3.14
+marks for removal in 3.16, so the test suite passes against the newer
+``uvloop`` release -- by :user:`bdraco`.
diff --git a/setup.cfg b/setup.cfg
index 5df4444e507..6e82d8c00bd 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -82,6 +82,10 @@ filterwarnings =
ignore:coroutine method 'aclose' of 'BodyPartReader._decode_content_async' was never awaited:RuntimeWarning
# Our own warning
ignore:aiohttp.pytest_plugin will be removed in v4:DeprecationWarning
+ # uvloop 0.22+ accesses the deprecated asyncio.AbstractEventLoopPolicy
+ # alias, which Python 3.14 marks for removal in 3.16. Drop this when
+ # uvloop stops touching the deprecated alias.
+ ignore:'asyncio.AbstractEventLoopPolicy' is deprecated:DeprecationWarning:uvloop
junit_suite_name = aiohttp_test_suite
norecursedirs = dist docs build .tox .eggs
minversion = 3.8.2
From 0503dbd0e644718006b2f4f4e77686e1b7c521ae Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 07:38:14 -0700
Subject: [PATCH 116/191] [PR #12619/b391f7be backport][3.14] Add timeouts to
test runners to prevent CI hangs (#12622)
Co-authored-by: J. Nick Koston
---
.github/workflows/ci-cd.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 167bd6813a6..7344bbc2efe 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -167,6 +167,7 @@ jobs:
fail-fast: true
runs-on: ${{ matrix.os }}-latest
continue-on-error: ${{ matrix.experimental }}
+ timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -281,6 +282,7 @@ jobs:
os: [ubuntu]
fail-fast: true
runs-on: ${{ matrix.os }}-latest
+ timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -416,6 +418,7 @@ jobs:
matrix:
os: [ubuntu, windows]
runs-on: ${{ matrix.os }}-latest
+ timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
From cecad51bf65c144e27790e6991c1881749d16a07 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Mon, 18 May 2026 07:45:48 -0700
Subject: [PATCH 117/191] [PR #12619/b391f7be backport][3.13] Add timeouts to
test runners to prevent CI hangs (#12623)
---
.github/workflows/ci-cd.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 0ae7a7eb1ed..4963ce47324 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -167,6 +167,7 @@ jobs:
fail-fast: true
runs-on: ${{ matrix.os }}-latest
continue-on-error: ${{ matrix.experimental }}
+ timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v5
From 02558d38a94fc251ef7ca2a25b2deecfe1e3c9b7 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 15:27:32 +0000
Subject: [PATCH 118/191] [PR #12606/3db06d9c backport][3.14] Shrink three slow
tests without changing their intent (#12625)
Co-authored-by: J. Nick Koston
---
CHANGES/12606.contrib.rst | 6 ++++++
tests/test_client_functional.py | 8 ++++----
tests/test_http_parser.py | 8 +++++---
3 files changed, 15 insertions(+), 7 deletions(-)
create mode 100644 CHANGES/12606.contrib.rst
diff --git a/CHANGES/12606.contrib.rst b/CHANGES/12606.contrib.rst
new file mode 100644
index 00000000000..72925114e8c
--- /dev/null
+++ b/CHANGES/12606.contrib.rst
@@ -0,0 +1,6 @@
+Reduced runtime of several of the slowest unit tests
+(decompress size-limit payloads from 64 MiB to 2 MiB,
+``test_chunk_splits_after_pause`` chunk count from 50000
+to 20000, and ``test_set_cookies_max_age`` sleep from 2
+seconds to 1.1 seconds) without changing what they
+exercise -- by :user:`bdraco`.
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
index 4b4d12443b6..802b8ee239d 100644
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -2460,7 +2460,7 @@ async def test_payload_decompress_size_limit(aiohttp_client: AiohttpClient) -> N
we raise DecompressSizeError.
"""
# Create a highly compressible payload.
- payload_size = 64 * 2**20
+ payload_size = 2 * 2**20
original = b"A" * payload_size
compressed = zlib.compress(original)
assert len(original) > DEFAULT_CHUNK_SIZE
@@ -2492,7 +2492,7 @@ async def test_payload_decompress_size_limit_brotli(
"""Test that brotli decompression size limit triggers DecompressSizeError."""
assert brotli is not None
# Create a highly compressible payload
- payload_size = 64 * 2**20
+ payload_size = 2 * 2**20
original = b"A" * payload_size
compressed = brotli.compress(original)
assert len(original) > DEFAULT_CHUNK_SIZE
@@ -2523,7 +2523,7 @@ async def test_payload_decompress_size_limit_zstd(
"""Test that zstd decompression size limit triggers DecompressSizeError."""
assert ZstdCompressor is not None
# Create a highly compressible payload.
- payload_size = 64 * 2**20
+ payload_size = 2 * 2**20
original = b"A" * payload_size
compressor = ZstdCompressor()
compressed = compressor.compress(original) + compressor.flush()
@@ -2930,7 +2930,7 @@ async def handler(request):
assert 200 == resp.status
cookie_names = {c.key for c in client.session.cookie_jar}
assert cookie_names == {"c1", "c2", "c3"}
- await asyncio.sleep(2)
+ await asyncio.sleep(1.1)
cookie_names = {c.key for c in client.session.cookie_jar}
assert cookie_names == {"c1", "c2"}
diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
index c3a8669cb31..99e952ac882 100644
--- a/tests/test_http_parser.py
+++ b/tests/test_http_parser.py
@@ -1074,9 +1074,10 @@ def test_max_header_value_size_under_limit(parser: HttpRequestParser) -> None:
async def test_chunk_splits_after_pause(parser: HttpRequestParser) -> None:
+ num_chunks = 20000 # comfortably above the 16385 pause threshold
text = (
b"GET /test HTTP/1.1\r\nHost: a\r\nTransfer-Encoding: chunked\r\n\r\n"
- + b"1\r\nb\r\n" * 50000
+ + b"1\r\nb\r\n" * num_chunks
+ b"0\r\n\r\n"
)
@@ -1087,8 +1088,9 @@ async def test_chunk_splits_after_pause(parser: HttpRequestParser) -> None:
assert len(payload._http_chunk_splits) == 16385
# We should still get the full result after read(), as it will continue processing.
result = await payload.read()
- assert len(result) == 50000 # Compare len first, as it's easier to debug in diff.
- assert result == b"b" * 50000
+ # Compare len first, as it's easier to debug in diff.
+ assert len(result) == num_chunks
+ assert result == b"b" * num_chunks
async def test_compressed_with_tail(response: HttpResponseParser) -> None:
From 869fd3c66de7eed04e0c76a950620c73e016a4d7 Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Mon, 18 May 2026 08:34:26 -0700
Subject: [PATCH 119/191] [PR #12624/3bca18a8 backport][3.14] Add per-test
pytest-timeout to surface hung tests (#12627)
---
.github/workflows/ci-cd.yml | 4 ++--
CHANGES/12624.contrib.rst | 4 ++++
requirements/test-common.in | 1 +
requirements/test-common.txt | 2 ++
requirements/test-ft.txt | 2 ++
requirements/test.txt | 2 ++
setup.cfg | 3 +++
7 files changed, 16 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12624.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 7344bbc2efe..eea8db6665e 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -335,7 +335,7 @@ jobs:
PIP_USER: 1
run: >-
PATH="${HOME}/Library/Python/3.11/bin:${HOME}/.local/bin:${PATH}"
- pytest --junitxml=junit.xml --cov=aiohttp/ --cov=tests/ -m autobahn
+ pytest --junitxml=junit.xml --cov=aiohttp/ --cov=tests/ --timeout=0 -m autobahn
shell: bash
- name: Turn coverage into xml
env:
@@ -405,7 +405,7 @@ jobs:
uses: CodSpeedHQ/action@v4
with:
mode: instrumentation
- run: python -Im pytest --no-cov -vvvvv --codspeed --durations=30
+ run: python -Im pytest --no-cov -vvvvv --codspeed --durations=30 --timeout=0
cython-coverage:
diff --git a/CHANGES/12624.contrib.rst b/CHANGES/12624.contrib.rst
new file mode 100644
index 00000000000..0c7e2a0a548
--- /dev/null
+++ b/CHANGES/12624.contrib.rst
@@ -0,0 +1,4 @@
+Added a default 120-second per-test timeout via ``pytest-timeout`` so a
+hung test surfaces by name in CI output instead of getting hidden behind
+the job-level timeout added in :pr:`12619`. The ``autobahn`` and
+benchmark jobs opt out with ``--timeout=0`` -- by :user:`bdraco`.
diff --git a/requirements/test-common.in b/requirements/test-common.in
index b2f6dd869e1..77700881024 100644
--- a/requirements/test-common.in
+++ b/requirements/test-common.in
@@ -8,6 +8,7 @@ proxy.py >= 2.4.4rc5
pytest
pytest-cov
pytest-mock
+pytest-timeout
pytest-xdist
pytest_codspeed
python-on-whales
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index f0234b57ba7..bf54902928a 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -79,6 +79,8 @@ pytest-cov==7.1.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
+pytest-timeout==2.4.0
+ # via -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index baf5c381d3f..309efbaf5bd 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -115,6 +115,8 @@ pytest-cov==7.1.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
+pytest-timeout==2.4.0
+ # via -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
diff --git a/requirements/test.txt b/requirements/test.txt
index 4ea290d8e2c..9df6d8f9e00 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -115,6 +115,8 @@ pytest-cov==7.1.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
+pytest-timeout==2.4.0
+ # via -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
diff --git a/setup.cfg b/setup.cfg
index 6e82d8c00bd..6ea9b485db6 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -54,6 +54,9 @@ addopts =
--showlocals
-m "not dev_mode and not autobahn and not internal"
+# 2-minute per-test timeout so a hung test surfaces by name instead of taking
+# down the whole job. Autobahn and benchmark jobs override with `--timeout=0`.
+timeout = 120
filterwarnings =
error
ignore:module 'ssl' has no attribute 'OP_NO_COMPRESSION'. The Python interpreter is compiled against OpenSSL < 1.0.0. Ref. https.//docs.python.org/3/library/ssl.html#ssl.OP_NO_COMPRESSION:UserWarning
From 4ec3ac37ad25139af1d6156ecd0cfd4d3e84050e Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Mon, 18 May 2026 08:36:49 -0700
Subject: [PATCH 120/191] [PR #12624/3bca18a8 backport][3.13] Add per-test
pytest-timeout to surface hung tests (#12626)
---
.github/workflows/ci-cd.yml | 2 +-
CHANGES/12624.contrib.rst | 4 ++++
requirements/test-common.in | 1 +
requirements/test-common.txt | 2 ++
requirements/test-ft.txt | 2 ++
requirements/test.txt | 2 ++
setup.cfg | 3 +++
7 files changed, 15 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/12624.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 4963ce47324..aec0cfac0f0 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -307,7 +307,7 @@ jobs:
uses: CodSpeedHQ/action@v4
with:
mode: instrumentation
- run: python -Im pytest --no-cov --numprocesses=0 -vvvvv --codspeed
+ run: python -Im pytest --no-cov --numprocesses=0 -vvvvv --codspeed --timeout=0
check: # This job does nothing and is only used for the branch protection
diff --git a/CHANGES/12624.contrib.rst b/CHANGES/12624.contrib.rst
new file mode 100644
index 00000000000..0c7e2a0a548
--- /dev/null
+++ b/CHANGES/12624.contrib.rst
@@ -0,0 +1,4 @@
+Added a default 120-second per-test timeout via ``pytest-timeout`` so a
+hung test surfaces by name in CI output instead of getting hidden behind
+the job-level timeout added in :pr:`12619`. The ``autobahn`` and
+benchmark jobs opt out with ``--timeout=0`` -- by :user:`bdraco`.
diff --git a/requirements/test-common.in b/requirements/test-common.in
index b2f6dd869e1..77700881024 100644
--- a/requirements/test-common.in
+++ b/requirements/test-common.in
@@ -8,6 +8,7 @@ proxy.py >= 2.4.4rc5
pytest
pytest-cov
pytest-mock
+pytest-timeout
pytest-xdist
pytest_codspeed
python-on-whales
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 16c3a976898..8159a92c22c 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -77,6 +77,8 @@ pytest-cov==7.0.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
+pytest-timeout==2.4.0
+ # via -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 18abb27d9cf..b904ec6c8ed 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -110,6 +110,8 @@ pytest-cov==7.0.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
+pytest-timeout==2.4.0
+ # via -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
diff --git a/requirements/test.txt b/requirements/test.txt
index 52b74930a05..f71af7ba99c 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -110,6 +110,8 @@ pytest-cov==7.0.0
# via -r requirements/test-common.in
pytest-mock==3.15.1
# via -r requirements/test-common.in
+pytest-timeout==2.4.0
+ # via -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
diff --git a/setup.cfg b/setup.cfg
index eb3a36b9d23..76079e364a4 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -63,6 +63,9 @@ addopts =
# run tests that are not marked with dev_mode
-m "not dev_mode"
+# 2-minute per-test timeout so a hung test surfaces by name instead of taking
+# down the whole job. Autobahn and benchmark jobs override with `--timeout=0`.
+timeout = 120
filterwarnings =
error
ignore:module 'ssl' has no attribute 'OP_NO_COMPRESSION'. The Python interpreter is compiled against OpenSSL < 1.0.0. Ref. https.//docs.python.org/3/library/ssl.html#ssl.OP_NO_COMPRESSION:UserWarning
From e439180a7ec7569262ba23bf545f5c05a6ed8a84 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 17:05:46 +0000
Subject: [PATCH 121/191] Bump uvloop from 0.21.0 to 0.22.1 (#11683)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [uvloop](https://github.com/MagicStack/uvloop) from 0.21.0 to
0.22.1.
Release notes
Sourced from uvloop's
releases.
v0.22.1
This is identical to 0.22.0, re-ran with CI fixes
v0.22.0
Changes
Fixes
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base.txt | 2 +-
requirements/constraints.txt | 5 ++++-
requirements/dev.txt | 5 ++++-
requirements/lint.txt | 2 +-
requirements/test.txt | 3 ++-
5 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/requirements/base.txt b/requirements/base.txt
index 20730032d38..c1abe470372 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -47,7 +47,7 @@ typing-extensions==4.15.0 ; python_version < "3.13"
# -r requirements/runtime-deps.in
# aiosignal
# multidict
-uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpython"
+uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpython"
# via -r requirements/base.in
yarl==1.22.0
# via -r requirements/runtime-deps.in
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 95ea5248658..baa98a1a7c4 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -183,6 +183,7 @@ pytest==9.0.3
# pytest-codspeed
# pytest-cov
# pytest-mock
+ # pytest-timeout
# pytest-xdist
pytest-codspeed==5.0.2
# via
@@ -194,6 +195,8 @@ pytest-mock==3.15.1
# via
# -r requirements/lint.in
# -r requirements/test-common.in
+pytest-timeout==2.4.0
+ # via -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
@@ -280,7 +283,7 @@ typing-inspection==0.4.2
# via pydantic
urllib3==2.7.0
# via requests
-uvloop==0.21.0 ; platform_system != "Windows"
+uvloop==0.22.1 ; platform_system != "Windows"
# via
# -r requirements/base.in
# -r requirements/lint.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index eabac796cb9..40e3f3ef30f 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -178,6 +178,7 @@ pytest==9.0.3
# pytest-codspeed
# pytest-cov
# pytest-mock
+ # pytest-timeout
# pytest-xdist
pytest-codspeed==5.0.2
# via
@@ -189,6 +190,8 @@ pytest-mock==3.15.1
# via
# -r requirements/lint.in
# -r requirements/test-common.in
+pytest-timeout==2.4.0
+ # via -r requirements/test-common.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
@@ -270,7 +273,7 @@ typing-inspection==0.4.2
# via pydantic
urllib3==2.7.0
# via requests
-uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpython"
+uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpython"
# via
# -r requirements/base.in
# -r requirements/lint.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index ef45375b634..6b0be6d5a85 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -124,7 +124,7 @@ typing-extensions==4.15.0
# virtualenv
typing-inspection==0.4.2
# via pydantic
-uvloop==0.21.0 ; platform_system != "Windows"
+uvloop==0.22.1 ; platform_system != "Windows"
# via -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 9df6d8f9e00..bc065a27805 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -108,6 +108,7 @@ pytest==9.0.3
# pytest-codspeed
# pytest-cov
# pytest-mock
+ # pytest-timeout
# pytest-xdist
pytest-codspeed==5.0.2
# via -r requirements/test-common.in
@@ -154,7 +155,7 @@ typing-extensions==4.15.0 ; python_version < "3.13"
# typing-inspection
typing-inspection==0.4.2
# via pydantic
-uvloop==0.21.0 ; platform_system != "Windows" and implementation_name == "cpython"
+uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpython"
# via -r requirements/base.in
wait-for-it==2.3.0
# via -r requirements/test-common.in
From 1b5c21e0ab55be733e12d547fbb2206e9f719ed9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 20:21:20 +0000
Subject: [PATCH 122/191] Bump ast-serialize from 0.4.0 to 0.5.0 (#12634)
Bumps [ast-serialize](https://github.com/mypyc/ast_serialize) from 0.4.0
to 0.5.0.
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 3 ++-
requirements/test-ft.txt | 3 ++-
requirements/test.txt | 2 +-
6 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index baa98a1a7c4..c3c02cccd85 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -18,7 +18,7 @@ alabaster==1.0.0
# via sphinx
annotated-types==0.7.0
# via pydantic
-ast-serialize==0.4.0
+ast-serialize==0.5.0
# via mypy
async-timeout==5.0.1 ; python_version < "3.11"
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 40e3f3ef30f..48699e31765 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -18,7 +18,7 @@ alabaster==1.0.0
# via sphinx
annotated-types==0.7.0
# via pydantic
-ast-serialize==0.4.0
+ast-serialize==0.5.0
# via mypy
async-timeout==5.0.1 ; python_version < "3.11"
# via
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 6b0be6d5a85..47865d7d125 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -8,7 +8,7 @@ aiodns==4.0.0
# via -r requirements/lint.in
annotated-types==0.7.0
# via pydantic
-ast-serialize==0.4.0
+ast-serialize==0.5.0
# via mypy
async-timeout==5.0.1
# via valkey
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index bf54902928a..2fa57883159 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -6,7 +6,7 @@
#
annotated-types==0.7.0
# via pydantic
-ast-serialize==0.4.0
+ast-serialize==0.5.0
# via mypy
blockbuster==1.5.26
# via -r requirements/test-common.in
@@ -72,6 +72,7 @@ pytest==9.0.3
# pytest-codspeed
# pytest-cov
# pytest-mock
+ # pytest-timeout
# pytest-xdist
pytest-codspeed==5.0.2
# via -r requirements/test-common.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 309efbaf5bd..27b43324777 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -12,7 +12,7 @@ aiosignal==1.4.0
# via -r requirements/runtime-deps.in
annotated-types==0.7.0
# via pydantic
-ast-serialize==0.4.0
+ast-serialize==0.5.0
# via mypy
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
@@ -108,6 +108,7 @@ pytest==9.0.3
# pytest-codspeed
# pytest-cov
# pytest-mock
+ # pytest-timeout
# pytest-xdist
pytest-codspeed==5.0.2
# via -r requirements/test-common.in
diff --git a/requirements/test.txt b/requirements/test.txt
index bc065a27805..ee214f01243 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -12,7 +12,7 @@ aiosignal==1.4.0
# via -r requirements/runtime-deps.in
annotated-types==0.7.0
# via pydantic
-ast-serialize==0.4.0
+ast-serialize==0.5.0
# via mypy
async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
From 38c9057c01b383ebdcac4192e0559647f09b2039 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 20:22:45 +0000
Subject: [PATCH 123/191] Bump click from 8.3.3 to 8.4.0 (#12636)
Bumps [click](https://github.com/pallets/click) from 8.3.3 to 8.4.0.
Release notes
Sourced from click's
releases.
8.4.0
This is the Click 8.4.0 feature release. A feature release may
include new features, remove previously deprecated code, add new
deprecation, or introduce potentially breaking changes.
We encourage everyone to upgrade. You can read more about our Version Support Policy
on our website.
PyPI: https://pypi.org/project/click/8.4.0/
Changes: https://click.palletsprojects.com/page/changes/#version-8-4-0
Milestone https://github.com/pallets/click/milestone/30
-
ParamType typing improvements. #3371
- :class:
ParamType is now a generic abstract base class,
parameterized by its converted value type.
- :meth:
~ParamType.convert return types are narrowed on
all
concrete types (str for :class:STRING,
int for
:class:INT, etc.).
- :meth:
~ParamType.to_info_dict returns specific
:class:~typing.TypedDict subclasses instead of
dict[str, Any].
- :class:
CompositeParamType and the number-range base are
now
generic with abstract methods.
-
Refactor convert_type to extract type inference into a
private
_guess_type helper, and add
:func:typing.overload signatures.
#3372
-
Parameter typing improvements. #2805
- :class:
Parameter is now an abstract base class, making
explicit
that it cannot be instantiated directly.
- :attr:
Parameter.name is now str instead of
str | None.
When expose_value=False, the name is set to
"" instead
of None.
- The
ctx parameter of
:meth:Parameter.get_error_hint is now
typed as Context | None, matching the runtime
behavior.
-
Split string values from default_map for parameters with
nargs > 1
or :class:Tuple type, matching environment variable
behavior.
#2745
#3364
-
Auto-detect type=UNPROCESSED for flag_value
of non-basic types
(not str, int, float, or
bool), so programmer-provided
Python objects like classes and enum members are passed through
unchanged
instead of being stringified. Previously
type=click.UNPROCESSED had
to be set explicitly. #2012
#3363
-
The error hint now uses Command.get_help_option_names to
pick
non-shadowed help option names, so Try '... -h' no longer
points to a
subcommand option that shadows -h. All surviving names are
shown
(-h/--help). #2790
#3208
-
Fix readline functionality on non-Windows platforms. Prompt text is
now
passed directly to readline instead of being printed separately,
allowing
proper backspace, line editing, and line wrapping behavior. #2968
... (truncated)
Changelog
Sourced from click's
changelog.
Version 8.4.0
Released 2026-05-17
-
:class:ParamType typing improvements.
:pr:3371
- :class:
ParamType is now a generic abstract base class,
parameterized by its converted value type.
- :meth:
~ParamType.convert return types are narrowed on
all
concrete types (str for :class:STRING,
int for
:class:INT, etc.).
- :meth:
~ParamType.to_info_dict returns specific
:class:~typing.TypedDict subclasses instead of
dict[str, Any].
- :class:
CompositeParamType and the number-range base are
now
generic with abstract methods.
-
Refactor convert_type to extract type inference into a
private
_guess_type helper, and add
:func:typing.overload signatures.
:pr:3372
-
:class:Parameter typing improvements.
:pr:2805
- :class:
Parameter is now an abstract base class, making
explicit
that it cannot be instantiated directly.
- :attr:
Parameter.name is now str instead of
str | None.
When expose_value=False, the name is set to
"" instead
of None.
- The
ctx parameter of
:meth:Parameter.get_error_hint is now
typed as Context | None, matching the runtime
behavior.
-
Split string values from default_map for parameters with
nargs > 1
or :class:Tuple type, matching environment variable
behavior.
:issue:2745 :pr:3364
-
Auto-detect type=UNPROCESSED for flag_value
of non-basic types
(not str, int, float, or
bool), so programmer-provided
Python objects like classes and enum members are passed through
unchanged
instead of being stringified. Previously
type=click.UNPROCESSED had
to be set explicitly. :issue:2012 :pr:3363
-
The error hint now uses
:meth:Command.get_help_option_names to pick
non-shadowed help option names, so Try '... -h' no longer
points to a
subcommand option that shadows -h. All surviving names are
shown
(-h/--help). :issue:2790
:pr:3208
-
Fix readline functionality on non-Windows platforms. Prompt text is
now
passed directly to readline instead of being printed separately,
allowing
proper backspace, line editing, and line wrapping behavior.
:issue:2968
:pr:2969
-
Use :func:os.startfile on Windows to open URLs in
:func:open_url,
replacing the start built-in which cannot be invoked
without
shell=True. :issue:3164
:pr:3186
-
Fix Fish shell completion errors when option help text contains
newlines.
:issue:3043 :pr:3126
... (truncated)
Commits
41f410f
Release 8.4.0
e3e69e3
Add type annotations for instance attributes in utils (#3422)
3bb230d
WIP: Fix HelpFormatter.write_usage producing spurious
characters (#3434)
63274a7
click.get_pager_file: add tests (#1572
followup) (#3405)
0551bf5
Fix HelpFormatter.write_usage producing spurious
characters
fc41aa1
Apply class-body annotations to KeepOpenFile for
consistency
b761eda
Skip some tests on Windows
98302ac
Check PAGER usage, color preservation and edge-cases
dbdae17
Fix documentation
1aa2d53
Redesigned tests and get_pager_file branching to be more clear and not
set color
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
8 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index c3c02cccd85..8b3284bb84a 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -50,7 +50,7 @@ cfgv==3.5.0
# via pre-commit
charset-normalizer==3.4.7
# via requests
-click==8.3.3
+click==8.4.0
# via
# pip-tools
# slotscheck
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 48699e31765..402435e68f9 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -50,7 +50,7 @@ cfgv==3.5.0
# via pre-commit
charset-normalizer==3.4.7
# via requests
-click==8.3.3
+click==8.4.0
# via
# pip-tools
# slotscheck
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index ed58e6c876f..eb528786532 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -14,7 +14,7 @@ certifi==2026.4.22
# via requests
charset-normalizer==3.4.7
# via requests
-click==8.3.3
+click==8.4.0
# via towncrier
docutils==0.21.2
# via sphinx
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 660c70163e8..64ba54abc64 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -14,7 +14,7 @@ certifi==2026.4.22
# via requests
charset-normalizer==3.4.7
# via requests
-click==8.3.3
+click==8.4.0
# via towncrier
docutils==0.21.2
# via sphinx
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 47865d7d125..8b3d6525c90 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -22,7 +22,7 @@ cffi==2.0.0
# pycares
cfgv==3.5.0
# via pre-commit
-click==8.3.3
+click==8.4.0
# via slotscheck
cryptography==48.0.0
# via trustme
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 2fa57883159..dc964130606 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -12,7 +12,7 @@ blockbuster==1.5.26
# via -r requirements/test-common.in
cffi==2.0.0
# via cryptography
-click==8.3.3
+click==8.4.0
# via wait-for-it
coverage==7.14.0
# via
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 27b43324777..95d7e45450b 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -28,7 +28,7 @@ cffi==2.0.0
# via
# cryptography
# pycares
-click==8.3.3
+click==8.4.0
# via wait-for-it
coverage==7.14.0
# via
diff --git a/requirements/test.txt b/requirements/test.txt
index ee214f01243..3f1cfc91577 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -28,7 +28,7 @@ cffi==2.0.0
# via
# cryptography
# pycares
-click==8.3.3
+click==8.4.0
# via wait-for-it
coverage==7.14.0
# via
From 446f330a6f99eef650742ac422d4214cd82f0811 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 20:23:11 +0000
Subject: [PATCH 124/191] Bump aiodns from 4.0.0 to 4.0.3 (#12635)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[//]: # (dependabot-start)
⚠️ **Dependabot is rebasing this PR** ⚠️
Rebasing might not happen immediately, so don't worry if this takes some
time.
Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.
---
[//]: # (dependabot-end)
Bumps [aiodns](https://github.com/aio-libs/aiodns) from 4.0.0 to 4.0.3.
Changelog
Sourced from aiodns's
changelog.
4.0.3
- Restore license metadata that was dropped during the
pyproject.toml migration in #244,
so packaging tools again detect aiodns as MIT-licensed (#250).
4.0.2
- Re-release of 4.0.1; the 4.0.1 wheel build failed because the
release workflow still invoked
python setup.py after #244
removed setup.py, so 4.0.1 never reached PyPI. The release
workflow now uses python -m build (#248).
4.0.1
- Fix
Future exception was never retrieved when pycares
raises AresError synchronously, e.g. for malformed
hostnames (#245,
fixes #231)
- Modernized package setup using
pyproject.toml instead
of setup.py (#244)
- Updated dependencies
- Bumped mypy from 1.19.1 to 2.1.0 (#236,
#239,
#241,
#242)
- Bumped pytest from 9.0.2 to 9.0.3 (#237)
- Bumped pytest-cov from 7.0.0 to 7.1.0 (#232)
- Bumped dependabot/fetch-metadata from 2.4.0 to 3.1.0 (#227,
#234,
#240)
- Bumped actions/download-artifact from 7.0.0 to 8.0.1 (#228,
#235)
- Bumped actions/upload-artifact from 6 to 7 (#229)
- Bumped codecov/codecov-action from 5 to 6 (#233)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/runtime-deps.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
8 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 10fa8f02117..972f93541f8 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/base-ft.txt --strip-extras requirements/base-ft.in
#
-aiodns==4.0.0
+aiodns==4.0.3
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
diff --git a/requirements/base.txt b/requirements/base.txt
index c1abe470372..e0edceac79c 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/base.txt --strip-extras requirements/base.in
#
-aiodns==4.0.0
+aiodns==4.0.3
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 8b3284bb84a..184499ce80e 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/constraints.txt --strip-extras requirements/constraints.in
#
-aiodns==4.0.0
+aiodns==4.0.3
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 402435e68f9..0ecd8ce83ae 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/dev.txt --strip-extras requirements/dev.in
#
-aiodns==4.0.0
+aiodns==4.0.3
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 8b3d6525c90..ea061166130 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/lint.txt --resolver=backtracking --strip-extras requirements/lint.in
#
-aiodns==4.0.0
+aiodns==4.0.3
# via -r requirements/lint.in
annotated-types==0.7.0
# via pydantic
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index df3530154c3..4bba60d46ed 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/runtime-deps.txt --strip-extras requirements/runtime-deps.in
#
-aiodns==4.0.0
+aiodns==4.0.3
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 95d7e45450b..abbf0178d64 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/test-ft.txt --strip-extras requirements/test-ft.in
#
-aiodns==4.0.0
+aiodns==4.0.3
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 3f1cfc91577..396d0469999 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/test.txt --strip-extras requirements/test.in
#
-aiodns==4.0.0
+aiodns==4.0.3
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
From a309741d8132e76548f4371a0c64bc662b8adddc Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Mon, 18 May 2026 23:35:13 +0100
Subject: [PATCH 125/191] Deprecate auth. Add encode_basic_auth. (#12499)
(#12555)
(cherry picked from commit 5cde279c1fc46d9b3396524a14d92f9c5bdeaf04)
---------
Co-authored-by: J. Nick Koston
---
CHANGES/12499.deprecation.rst | 7 ++++
CHANGES/12499.feature.rst | 3 ++
aiohttp/__init__.py | 3 +-
aiohttp/client.py | 49 +++++++++++++++++++++++++
aiohttp/client_reqrep.py | 3 +-
aiohttp/helpers.py | 42 ++++++++++++++++++----
docs/client_advanced.rst | 35 +++++++++++++-----
docs/client_reference.rst | 26 +++++++++++++-
tests/test_client_functional.py | 10 ++++++
tests/test_client_request.py | 18 +++++-----
tests/test_client_session.py | 27 +++++++++++++-
tests/test_helpers.py | 63 +++++++++++++++++++++++++--------
tests/test_proxy.py | 8 +++--
tests/test_proxy_functional.py | 10 ++++++
14 files changed, 259 insertions(+), 45 deletions(-)
create mode 100644 CHANGES/12499.deprecation.rst
create mode 100644 CHANGES/12499.feature.rst
diff --git a/CHANGES/12499.deprecation.rst b/CHANGES/12499.deprecation.rst
new file mode 100644
index 00000000000..c9a5b533c77
--- /dev/null
+++ b/CHANGES/12499.deprecation.rst
@@ -0,0 +1,7 @@
+Deprecated :class:`~aiohttp.BasicAuth` and the ``auth`` / ``proxy_auth``
+parameters. They will be removed in aiohttp 4.0. Use the new
+:func:`~aiohttp.encode_basic_auth` helper together with
+``headers={"Authorization": ...}`` (or
+``proxy_headers={"Proxy-Authorization": ...}`` for proxies) instead.
+Note that ``encode_basic_auth()`` defaults to `utf-8`, not `latin1`
+-- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12499.feature.rst b/CHANGES/12499.feature.rst
new file mode 100644
index 00000000000..8b242432367
--- /dev/null
+++ b/CHANGES/12499.feature.rst
@@ -0,0 +1,3 @@
+Added :func:`~aiohttp.encode_basic_auth` for encoding HTTP Basic
+Authentication credentials. Replaces the now-deprecated
+:class:`~aiohttp.BasicAuth` -- by :user:`Dreamsorcerer`.
diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py
index c298b51424d..4bbf8d308ed 100644
--- a/aiohttp/__init__.py
+++ b/aiohttp/__init__.py
@@ -56,7 +56,7 @@
)
from .cookiejar import CookieJar as CookieJar, DummyCookieJar as DummyCookieJar
from .formdata import FormData as FormData
-from .helpers import BasicAuth, ChainMapProxy, ETag
+from .helpers import BasicAuth, ChainMapProxy, ETag, encode_basic_auth
from .http import (
HttpVersion as HttpVersion,
HttpVersion10 as HttpVersion10,
@@ -190,6 +190,7 @@
"ChainMapProxy",
"DigestAuthMiddleware",
"ETag",
+ "encode_basic_auth",
"set_zlib_backend",
# http
"HttpVersion",
diff --git a/aiohttp/client.py b/aiohttp/client.py
index 09aa4d154de..382679214d2 100644
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -425,6 +425,22 @@ def __init__(
if cookies:
self._cookie_jar.update_cookies(cookies)
+ if auth is not None:
+ warnings.warn(
+ "The 'auth' parameter is deprecated and will be removed in v4;"
+ " pass headers={'Authorization': "
+ "aiohttp.encode_basic_auth(login, password)} instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ if proxy_auth is not None:
+ warnings.warn(
+ "The 'proxy_auth' parameter is deprecated and will be removed in v4;"
+ " pass proxy_headers={'Proxy-Authorization': "
+ "aiohttp.encode_basic_auth(login, password)} instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
self._connector = connector
self._connector_owner = connector_owner
self._default_auth = auth
@@ -566,6 +582,23 @@ async def _request(
ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint)
+ if auth is not None:
+ warnings.warn(
+ "The 'auth' parameter is deprecated and will be removed in v4;"
+ " pass headers={'Authorization': "
+ "aiohttp.encode_basic_auth(login, password)} instead",
+ DeprecationWarning,
+ stacklevel=3,
+ )
+ if proxy_auth is not None:
+ warnings.warn(
+ "The 'proxy_auth' parameter is deprecated and will be removed in v4;"
+ " pass proxy_headers={'Proxy-Authorization': "
+ "aiohttp.encode_basic_auth(login, password)} instead",
+ DeprecationWarning,
+ stacklevel=3,
+ )
+
if data is not None and json is not None:
raise ValueError(
"data and json parameters can not be used at the same time"
@@ -1131,6 +1164,22 @@ async def _ws_connect(
max_msg_size: int = 4 * 1024 * 1024,
decode_text: bool = True,
) -> "ClientWebSocketResponse[bool]":
+ if auth is not None:
+ warnings.warn(
+ "The 'auth' parameter is deprecated and will be removed in v4;"
+ " pass headers={'Authorization': "
+ "aiohttp.encode_basic_auth(login, password)} instead",
+ DeprecationWarning,
+ stacklevel=3,
+ )
+ if proxy_auth is not None:
+ warnings.warn(
+ "The 'proxy_auth' parameter is deprecated and will be removed in v4;"
+ " pass proxy_headers={'Proxy-Authorization': "
+ "aiohttp.encode_basic_auth(login, password)} instead",
+ DeprecationWarning,
+ stacklevel=3,
+ )
if timeout is not sentinel:
if isinstance(timeout, ClientWSTimeout):
ws_timeout = timeout
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
index 92b3529bf13..0353ec34f03 100644
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -40,6 +40,7 @@
BasicAuth,
HeadersMixin,
TimerNoop,
+ _basic_auth_no_warn,
noop,
reify,
sentinel,
@@ -1019,7 +1020,7 @@ def update_host(self, url: URL) -> None:
# basic auth info
if url.raw_user or url.raw_password:
- self.auth = helpers.BasicAuth(url.user or "", url.password or "")
+ self.auth = _basic_auth_no_warn(url.user or "", url.password or "")
def update_version(self, version: http.HttpVersion | str) -> None:
"""Convert request version to two elements tuple.
diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py
index ebc73e19205..5988aab4aa8 100644
--- a/aiohttp/helpers.py
+++ b/aiohttp/helpers.py
@@ -14,6 +14,7 @@
import re
import sys
import time
+import warnings
import weakref
from collections import namedtuple
from collections.abc import Callable, Generator, Iterable, Iterator, Mapping
@@ -115,6 +116,18 @@ def __await__(self) -> Generator[None, None, None]:
yield
+def encode_basic_auth(login: str, password: str = "", encoding: str = "utf-8") -> str:
+ """Encode HTTP Basic Authentication credentials as an Authorization header value.
+
+ Returns a string of the form ``"Basic "`` suitable for use as the
+ value of the ``Authorization`` (or ``Proxy-Authorization``) header.
+ """
+ if ":" in login:
+ raise ValueError('A ":" is not allowed in login (RFC 7617#section-2)')
+ creds = f"{login}:{password}".encode(encoding)
+ return "Basic " + base64.b64encode(creds).decode(encoding)
+
+
class BasicAuth(namedtuple("BasicAuth", ["login", "password", "encoding"])):
"""Http basic authentication helper."""
@@ -130,6 +143,13 @@ def __new__(
if ":" in login:
raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)')
+ warnings.warn(
+ "BasicAuth is deprecated and will be removed in aiohttp 4.0; "
+ "use aiohttp.encode_basic_auth() with "
+ "headers={'Authorization': ...} instead",
+ DeprecationWarning,
+ stacklevel=2,
+ )
return super().__new__(cls, login, password, encoding)
@classmethod
@@ -159,7 +179,7 @@ def decode(cls, auth_header: str, encoding: str = "latin1") -> "BasicAuth":
except ValueError:
raise ValueError("Invalid credentials.")
- return cls(username, password, encoding=encoding)
+ return _basic_auth_no_warn(username, password, encoding)
@classmethod
def from_url(cls, url: URL, *, encoding: str = "latin1") -> Optional["BasicAuth"]:
@@ -170,12 +190,22 @@ def from_url(cls, url: URL, *, encoding: str = "latin1") -> Optional["BasicAuth"
# to already have these values parsed from the netloc in the cache.
if url.raw_user is None and url.raw_password is None:
return None
- return cls(url.user or "", url.password or "", encoding=encoding)
+ return _basic_auth_no_warn(url.user or "", url.password or "", encoding)
def encode(self) -> str:
"""Encode credentials."""
- creds = (f"{self.login}:{self.password}").encode(self.encoding)
- return "Basic %s" % base64.b64encode(creds).decode(self.encoding)
+ return encode_basic_auth(self.login, self.password, self.encoding)
+
+
+def _basic_auth_no_warn(
+ login: str, password: str = "", encoding: str = "latin1"
+) -> BasicAuth:
+ """Construct a BasicAuth without emitting the deprecation warning.
+
+ For internal use only. Bypasses BasicAuth.__new__ so that aiohttp's own
+ machinery doesn't trigger deprecation warnings in user code.
+ """
+ return tuple.__new__(BasicAuth, (login, password, encoding))
def strip_auth_from_url(url: URL) -> tuple[URL, BasicAuth | None]:
@@ -184,7 +214,7 @@ def strip_auth_from_url(url: URL) -> tuple[URL, BasicAuth | None]:
# to already have these values parsed from the netloc in the cache.
if url.raw_user is None and url.raw_password is None:
return url, None
- return url.with_user(None), BasicAuth(url.user or "", url.password or "")
+ return url.with_user(None), _basic_auth_no_warn(url.user or "", url.password or "")
def netrc_from_env() -> netrc.netrc | None:
@@ -262,7 +292,7 @@ def basicauth_from_netrc(netrc_obj: netrc.netrc | None, host: str) -> BasicAuth:
if password is None:
password = ""
- return BasicAuth(username, password)
+ return _basic_auth_no_warn(username, password)
def proxies_from_env() -> dict[str, ProxyInfo]:
diff --git a/docs/client_advanced.rst b/docs/client_advanced.rst
index 18c274ca7f5..e4a94f50942 100644
--- a/docs/client_advanced.rst
+++ b/docs/client_advanced.rst
@@ -59,14 +59,21 @@ For *text/plain* ::
Authentication
--------------
-Instead of setting the ``Authorization`` header directly,
-:class:`ClientSession` and individual request methods provide an ``auth``
-argument. An instance of :class:`BasicAuth` can be passed in like this::
+For HTTP Basic Authentication, build the ``Authorization`` header using
+:func:`encode_basic_auth` and pass it via ``headers``::
- auth = BasicAuth(login="...", password="...")
- async with ClientSession(auth=auth) as session:
+ from aiohttp import ClientSession, encode_basic_auth
+
+ headers = {"Authorization": encode_basic_auth("user", "pass")}
+ async with ClientSession(headers=headers) as session:
...
+.. deprecated:: 3.14
+
+ The ``auth`` parameter and the :class:`BasicAuth` class are deprecated and
+ will be removed in 4.0. Use :func:`encode_basic_auth` together with the
+ ``headers`` parameter as shown above.
+
For HTTP digest authentication, use the :class:`DigestAuthMiddleware` client middleware::
from aiohttp import ClientSession, DigestAuthMiddleware
@@ -722,11 +729,13 @@ To connect, use the *proxy* parameter::
It also supports proxy authorization::
+ from aiohttp import encode_basic_auth
+
async with aiohttp.ClientSession() as session:
- proxy_auth = aiohttp.BasicAuth('user', 'pass')
+ proxy_headers = {"Proxy-Authorization": encode_basic_auth("user", "pass")}
async with session.get("http://python.org",
proxy="http://proxy.com",
- proxy_auth=proxy_auth) as resp:
+ proxy_headers=proxy_headers) as resp:
print(resp.status)
Authentication credentials can be passed in proxy URL::
@@ -736,11 +745,19 @@ Authentication credentials can be passed in proxy URL::
And you may set default proxy::
- proxy_auth = aiohttp.BasicAuth('user', 'pass')
- async with aiohttp.ClientSession(proxy="http://proxy.com", proxy_auth=proxy_auth) as session:
+ proxy_headers = {"Proxy-Authorization": encode_basic_auth("user", "pass")}
+ async with aiohttp.ClientSession(
+ proxy="http://proxy.com", proxy_headers=proxy_headers
+ ) as session:
async with session.get("http://python.org") as resp:
print(resp.status)
+.. deprecated:: 3.14
+
+ The ``proxy_auth`` parameter is deprecated and will be removed in 4.0. Use
+ :func:`encode_basic_auth` with ``proxy_headers={"Proxy-Authorization": ...}``
+ as shown above.
+
Contrary to the ``requests`` library, it won't read environment
variables by default. But you can do so by passing
``trust_env=True`` into :class:`aiohttp.ClientSession`
diff --git a/docs/client_reference.rst b/docs/client_reference.rst
index 651190c3cdf..f9f3dfcfef8 100644
--- a/docs/client_reference.rst
+++ b/docs/client_reference.rst
@@ -2327,6 +2327,22 @@ Utilities
+.. function:: encode_basic_auth(login, password='', encoding='utf-8')
+
+ Encode HTTP Basic Authentication credentials as a value suitable for the
+ ``Authorization`` (or ``Proxy-Authorization``) header::
+
+ headers = {"Authorization": encode_basic_auth("user", "pass")}
+
+ :param str login: login
+ :param str password: password (``''`` by default)
+ :param str encoding: encoding (``'utf-8'`` by default)
+ :return: a string of the form ``"Basic "``
+ :rtype: str
+
+ .. versionadded:: 3.14
+
+
.. class:: BasicAuth(login, password='', encoding='latin1')
HTTP basic authentication helper.
@@ -2336,9 +2352,17 @@ Utilities
:param str encoding: encoding (``'latin1'`` by default)
- Should be used for specifying authorization data in client API,
+ Previously this was used for specifying authorization data in client API,
e.g. *auth* parameter for :meth:`ClientSession.request() `.
+ .. deprecated:: 3.14
+
+ Constructing :class:`BasicAuth` is deprecated and will be removed in
+ 4.0. Use :func:`encode_basic_auth` together with the ``headers``
+ parameter (or ``proxy_headers`` for proxies) instead. The
+ :meth:`decode` and :meth:`from_url` class methods remain available for
+ parsing.
+
.. classmethod:: decode(auth_header, encoding='latin1')
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
index 802b8ee239d..3853f88dd9e 100644
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -66,6 +66,16 @@
from aiohttp.test_utils import TestClient, TestServer, unused_port
from aiohttp.typedefs import Handler
+pytestmark = [
+ pytest.mark.filterwarnings(r"ignore:BasicAuth is deprecated:DeprecationWarning"),
+ pytest.mark.filterwarnings(
+ r"ignore:The 'auth' parameter is deprecated:DeprecationWarning"
+ ),
+ pytest.mark.filterwarnings(
+ r"ignore:The 'proxy_auth' parameter is deprecated:DeprecationWarning"
+ ),
+]
+
@pytest.fixture(autouse=True)
def cleanup(
diff --git a/tests/test_client_request.py b/tests/test_client_request.py
index 5e23f085d45..5f5cf4a849f 100644
--- a/tests/test_client_request.py
+++ b/tests/test_client_request.py
@@ -412,17 +412,17 @@ def test_ipv6_nondefault_https_port(make_request) -> None:
def test_basic_auth(make_request) -> None:
- req = make_request(
- "get", "http://python.org", auth=aiohttp.BasicAuth("nkim", "1234")
- )
+ with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"):
+ auth = aiohttp.BasicAuth("nkim", "1234")
+ req = make_request("get", "http://python.org", auth=auth)
assert "AUTHORIZATION" in req.headers
assert "Basic bmtpbToxMjM0" == req.headers["AUTHORIZATION"]
def test_basic_auth_utf8(make_request) -> None:
- req = make_request(
- "get", "http://python.org", auth=aiohttp.BasicAuth("nkim", "секрет", "utf-8")
- )
+ with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"):
+ auth = aiohttp.BasicAuth("nkim", "секрет", "utf-8")
+ req = make_request("get", "http://python.org", auth=auth)
assert "AUTHORIZATION" in req.headers
assert "Basic bmtpbTrRgdC10LrRgNC10YI=" == req.headers["AUTHORIZATION"]
@@ -447,9 +447,9 @@ def test_basic_auth_no_user_from_url(make_request) -> None:
def test_basic_auth_from_url_overridden(make_request) -> None:
- req = make_request(
- "get", "http://garbage@python.org", auth=aiohttp.BasicAuth("nkim", "1234")
- )
+ with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"):
+ auth = aiohttp.BasicAuth("nkim", "1234")
+ req = make_request("get", "http://garbage@python.org", auth=auth)
assert "AUTHORIZATION" in req.headers
assert "Basic bmtpbToxMjM0" == req.headers["AUTHORIZATION"]
assert "python.org" == req.host
diff --git a/tests/test_client_session.py b/tests/test_client_session.py
index c316170d4a0..1060d1f2516 100644
--- a/tests/test_client_session.py
+++ b/tests/test_client_session.py
@@ -882,7 +882,10 @@ def test_proxy_str(session, params) -> None:
]
-async def test_default_proxy(loop: asyncio.AbstractEventLoop) -> None:
+@pytest.mark.filterwarnings(
+ r"ignore:The 'proxy_auth' parameter is deprecated:DeprecationWarning"
+)
+async def test_default_proxy() -> None:
proxy_url = URL("http://proxy.example.com")
proxy_auth = mock.Mock()
proxy_url2 = URL("http://proxy.example2.com")
@@ -1414,6 +1417,10 @@ async def test_netrc_auth_from_home_directory(auth_server: TestServer) -> None:
@pytest.mark.usefixtures("netrc_default_contents")
+@pytest.mark.filterwarnings(
+ r"ignore:The 'auth' parameter is deprecated:DeprecationWarning",
+ r"ignore:BasicAuth is deprecated:DeprecationWarning",
+)
async def test_netrc_auth_overridden_by_explicit_auth(auth_server: TestServer) -> None:
"""Test that explicit auth parameter overrides netrc authentication."""
async with (
@@ -1428,6 +1435,24 @@ async def test_netrc_auth_overridden_by_explicit_auth(auth_server: TestServer) -
assert text == "auth:Basic ZXhwbGljaXRfdXNlcjpleHBsaWNpdF9wYXNz"
+async def test_client_session_auth_deprecated() -> None:
+ """ClientSession(auth=...) emits a DeprecationWarning."""
+ with pytest.warns(DeprecationWarning, match="'auth' parameter is deprecated"):
+ session = ClientSession(
+ auth=aiohttp.helpers._basic_auth_no_warn("user", "pass")
+ )
+ await session.close()
+
+
+async def test_client_session_proxy_auth_deprecated() -> None:
+ """ClientSession(proxy_auth=...) emits a DeprecationWarning."""
+ with pytest.warns(DeprecationWarning, match="'proxy_auth' parameter is deprecated"):
+ session = ClientSession(
+ proxy_auth=aiohttp.helpers._basic_auth_no_warn("user", "pass")
+ )
+ await session.close()
+
+
@pytest.mark.usefixtures("netrc_other_host")
async def test_netrc_auth_host_not_in_netrc(auth_server: TestServer) -> None:
"""Test that netrc lookup returns None when host is not in netrc file."""
diff --git a/tests/test_helpers.py b/tests/test_helpers.py
index 4baf6b8c0cd..3ddf79c7666 100644
--- a/tests/test_helpers.py
+++ b/tests/test_helpers.py
@@ -3,6 +3,7 @@
import datetime
import gc
import sys
+import warnings
import weakref
from math import ceil, modf
from pathlib import Path
@@ -128,18 +129,56 @@ def test_basic_with_auth_colon_in_login() -> None:
def test_basic_auth3() -> None:
- auth = helpers.BasicAuth("nkim")
+ with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"):
+ auth = helpers.BasicAuth("nkim")
assert auth.login == "nkim"
assert auth.password == ""
def test_basic_auth4() -> None:
- auth = helpers.BasicAuth("nkim", "pwd")
+ with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"):
+ auth = helpers.BasicAuth("nkim", "pwd")
assert auth.login == "nkim"
assert auth.password == "pwd"
assert auth.encode() == "Basic bmtpbTpwd2Q="
+def test_basic_auth_deprecated() -> None:
+ with pytest.warns(
+ DeprecationWarning,
+ match=(
+ "BasicAuth is deprecated and will be removed in aiohttp 4.0; "
+ "use aiohttp.encode_basic_auth"
+ ),
+ ):
+ helpers.BasicAuth("user", "pass")
+
+
+def test_encode_basic_auth() -> None:
+ assert helpers.encode_basic_auth("nkim", "pwd") == "Basic bmtpbTpwd2Q="
+ assert helpers.encode_basic_auth("") == "Basic Og=="
+ assert (
+ helpers.encode_basic_auth("usér", "pàss", encoding="utf-8")
+ == "Basic dXPDqXI6cMOgc3M="
+ )
+
+
+def test_encode_basic_auth_rejects_colon_in_login() -> None:
+ with pytest.raises(ValueError):
+ helpers.encode_basic_auth("user:1", "pwd")
+
+
+def test_basic_auth_no_warn_helpers_silent() -> None:
+ """Internal aiohttp paths must not raise BasicAuth's deprecation warning."""
+ with warnings.catch_warnings():
+ warnings.simplefilter("error", DeprecationWarning)
+ url = URL("http://user:pass@example.com/")
+ helpers.strip_auth_from_url(url)
+ helpers.BasicAuth.decode("Basic dXNlcjpwYXNz")
+ helpers.BasicAuth.from_url(url)
+ helpers._basic_auth_no_warn("user", "pass")
+
+
@pytest.mark.parametrize(
"header",
(
@@ -183,18 +222,12 @@ def test_basic_auth_decode_invalid_credentials() -> None:
@pytest.mark.parametrize(
"credentials, expected_auth",
(
- (":", helpers.BasicAuth(login="", password="", encoding="latin1")),
- (
- "username:",
- helpers.BasicAuth(login="username", password="", encoding="latin1"),
- ),
- (
- ":password",
- helpers.BasicAuth(login="", password="password", encoding="latin1"),
- ),
+ (":", helpers._basic_auth_no_warn("", "", "latin1")),
+ ("username:", helpers._basic_auth_no_warn("username", "", "latin1")),
+ (":password", helpers._basic_auth_no_warn("", "password", "latin1")),
(
"username:password",
- helpers.BasicAuth(login="username", password="password", encoding="latin1"),
+ helpers._basic_auth_no_warn("username", "password", "latin1"),
),
),
)
@@ -878,15 +911,15 @@ def test_netrc_from_home_does_not_raise_if_access_denied(
[
(
"machine example.com login username password pass\n",
- helpers.BasicAuth("username", "pass"),
+ helpers._basic_auth_no_warn("username", "pass", "latin1"),
),
(
"machine example.com account username password pass\n",
- helpers.BasicAuth("username", "pass"),
+ helpers._basic_auth_no_warn("username", "pass", "latin1"),
),
(
"machine example.com password pass\n",
- helpers.BasicAuth("", "pass"),
+ helpers._basic_auth_no_warn("", "pass", "latin1"),
),
],
indirect=("netrc_contents",),
diff --git a/tests/test_proxy.py b/tests/test_proxy.py
index 4c506cc5730..5599bf804f4 100644
--- a/tests/test_proxy.py
+++ b/tests/test_proxy.py
@@ -859,11 +859,13 @@ async def make_conn():
self.assertEqual(req.url, URL("http://localhost:1234/path"))
def test_proxy_auth_property(self) -> None:
+ with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"):
+ proxy_auth = aiohttp.helpers.BasicAuth("user", "pass")
req = aiohttp.ClientRequest(
"GET",
URL("http://localhost:1234/path"),
proxy=URL("http://proxy.example.com"),
- proxy_auth=aiohttp.helpers.BasicAuth("user", "pass"),
+ proxy_auth=proxy_auth,
loop=self.loop,
)
self.assertEqual(("user", "pass", "latin1"), req.proxy_auth)
@@ -959,10 +961,12 @@ async def make_conn():
spec_set=True,
)
def test_https_auth(self, start_connection: Any, ClientRequestMock: Any) -> None:
+ with pytest.warns(DeprecationWarning, match="BasicAuth is deprecated"):
+ auth = aiohttp.helpers.BasicAuth("user", "pass")
proxy_req = ClientRequest(
"GET",
URL("http://proxy.example.com"),
- auth=aiohttp.helpers.BasicAuth("user", "pass"),
+ auth=auth,
loop=self.loop,
)
ClientRequestMock.return_value = proxy_req
diff --git a/tests/test_proxy_functional.py b/tests/test_proxy_functional.py
index bf6ecc9a933..a240692d0d8 100644
--- a/tests/test_proxy_functional.py
+++ b/tests/test_proxy_functional.py
@@ -23,6 +23,16 @@
ASYNCIO_SUPPORTS_TLS_IN_TLS = sys.version_info >= (3, 11)
+pytestmark = [
+ pytest.mark.filterwarnings(r"ignore:BasicAuth is deprecated:DeprecationWarning"),
+ pytest.mark.filterwarnings(
+ r"ignore:The 'auth' parameter is deprecated:DeprecationWarning"
+ ),
+ pytest.mark.filterwarnings(
+ r"ignore:The 'proxy_auth' parameter is deprecated:DeprecationWarning"
+ ),
+]
+
@pytest.fixture
def secure_proxy_url(tls_certificate_pem_path):
From fd037ee27d2e393940b7cf1b73bca834a7d92e84 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Mon, 18 May 2026 23:01:35 +0000
Subject: [PATCH 126/191] [PR #12629/66cc7b68 backport][3.14] Install Python
via astral-sh/setup-uv in test/autobahn jobs (#12637)
Co-authored-by: J. Nick Koston
---
.github/workflows/ci-cd.yml | 52 +++++++++++++------------------------
CHANGES/12629.contrib.rst | 7 +++++
2 files changed, 25 insertions(+), 34 deletions(-)
create mode 100644 CHANGES/12629.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index eea8db6665e..27708170b49 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -175,30 +175,22 @@ jobs:
submodules: true
- name: Setup Python ${{ matrix.pyver }}
id: python-install
- uses: actions/setup-python@v6
+ # important: do not use system python
+ env:
+ UV_PYTHON_PREFERENCE: only-managed
+ uses: astral-sh/setup-uv@v8.1.0
with:
- allow-prereleases: true
python-version: ${{ matrix.pyver }}
- - name: Get pip cache dir
- id: pip-cache
- run: |
- echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}"
- shell: bash
- - name: Cache PyPI
- uses: actions/cache@v5.0.5
- with:
- key: pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}-${{ hashFiles('requirements/*.txt') }}
- path: ${{ steps.pip-cache.outputs.dir }}
- restore-keys: |
- pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}-
+ activate-environment: true
+ enable-cache: true
- name: Update pip, wheel, setuptools, build, twine
run: |
- python -m pip install -U pip wheel setuptools build twine
+ uv pip install -U pip wheel setuptools build twine
- name: Install dependencies
env:
DEPENDENCY_GROUP: test${{ endsWith(matrix.pyver, 't') && '-ft' || '' }}
run: |
- python -Im pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt
+ uv pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt
- name: Set PYTHON_GIL=0 for free-threading builds
if: ${{ endsWith(matrix.pyver, 't') }}
run: echo "PYTHON_GIL=0" >> $GITHUB_ENV
@@ -215,7 +207,7 @@ jobs:
- name: Install self
env:
AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }}
- run: python -m pip install -e .
+ run: uv pip install -e .
- name: Run unittests
env:
COLOR: yes
@@ -290,30 +282,22 @@ jobs:
submodules: true
- name: Setup Python ${{ matrix.pyver }}
id: python-install
- uses: actions/setup-python@v6
+ # important: do not use system python
+ env:
+ UV_PYTHON_PREFERENCE: only-managed
+ uses: astral-sh/setup-uv@v8.1.0
with:
- allow-prereleases: true
python-version: ${{ matrix.pyver }}
- - name: Get pip cache dir
- id: pip-cache
- run: |
- echo "dir=$(pip cache dir)" >> "${GITHUB_OUTPUT}"
- shell: bash
- - name: Cache PyPI
- uses: actions/cache@v5.0.5
- with:
- key: pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}-${{ hashFiles('requirements/*.txt') }}
- path: ${{ steps.pip-cache.outputs.dir }}
- restore-keys: |
- pip-ci-${{ runner.os }}-${{ matrix.pyver }}-${{ matrix.no-extensions }}-
+ activate-environment: true
+ enable-cache: true
- name: Update pip, wheel, setuptools, build, twine
run: |
- python -m pip install -U pip wheel setuptools build twine
+ uv pip install -U pip wheel setuptools build twine
- name: Install dependencies
env:
DEPENDENCY_GROUP: test${{ endsWith(matrix.pyver, 't') && '-ft' || '' }}
run: |
- python -Im pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt
+ uv pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt
- name: Restore llhttp generated files
if: ${{ matrix.no-extensions == '' }}
uses: actions/download-artifact@v8
@@ -327,7 +311,7 @@ jobs:
- name: Install self
env:
AIOHTTP_NO_EXTENSIONS: ${{ matrix.no-extensions }}
- run: python -m pip install -e .
+ run: uv pip install -e .
- name: Run unittests
env:
COLOR: yes
diff --git a/CHANGES/12629.contrib.rst b/CHANGES/12629.contrib.rst
new file mode 100644
index 00000000000..8c10b198425
--- /dev/null
+++ b/CHANGES/12629.contrib.rst
@@ -0,0 +1,7 @@
+Switched the CI ``test`` and ``autobahn`` jobs from
+``actions/setup-python`` to ``astral-sh/setup-uv`` for installing
+interpreters, cutting the ``Setup Python`` step from 40-58s to a
+few seconds on ``macos-latest`` and ``windows-latest`` runners for
+variants not in the hosted tool-cache (notably the free-threaded
+``3.14t``)
+-- by :user:`bdraco`.
From f54c40851b0d6c4bbdab97ba518a223adda32478 Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Tue, 19 May 2026 01:23:00 +0100
Subject: [PATCH 127/191] Drop cookies on redirect (#12550) (#12640)
(cherry picked from commit d57efb05f5073071ceb2d3b35d72d9d0bc4512a2)
---
CHANGES/12540.bugfix.rst | 1 +
aiohttp/client.py | 1 +
tests/test_client_functional.py | 16 ++++++++++++++--
3 files changed, 16 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12540.bugfix.rst
diff --git a/CHANGES/12540.bugfix.rst b/CHANGES/12540.bugfix.rst
new file mode 100644
index 00000000000..dfd98129e4b
--- /dev/null
+++ b/CHANGES/12540.bugfix.rst
@@ -0,0 +1 @@
+Fixed per-request ``cookies`` not being dropped on cross-origin redirects -- by :user:`Dreamsorcerer`.
diff --git a/aiohttp/client.py b/aiohttp/client.py
index 382679214d2..d9d8dfd5db7 100644
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -971,6 +971,7 @@ async def _connect_and_send_request(
if url.origin() != redirect_origin:
auth = None
+ cookies = None
headers.pop(hdrs.AUTHORIZATION, None)
headers.pop(hdrs.COOKIE, None)
headers.pop(hdrs.PROXY_AUTHORIZATION, None)
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
index 3853f88dd9e..66b86eb64ac 100644
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -3593,8 +3593,20 @@ async def close(self) -> None:
async with aiohttp.ClientSession(
connector=connector, auth=aiohttp.BasicAuth("user", "pass")
) as client:
- resp = await client.get(url_from)
- assert resp.status == 200
+ async with client.get(
+ url_from,
+ headers={
+ "Proxy-Authorization": "Basic dXNlcjpwYXNz",
+ "Cookie": "a=b",
+ },
+ ) as resp:
+ assert resp.status == 200
+ async with client.get(
+ url_from,
+ headers={"Proxy-Authorization": "Basic dXNlcjpwYXNz"},
+ cookies={"a": "b"},
+ ) as resp:
+ assert resp.status == 200
async def test_drop_auth_on_redirect_to_other_host_with_global_auth_and_base_url(
From 63ff485578f051f92fadf4e6ea8563bd7add8d7c Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Mon, 18 May 2026 19:40:30 -0700
Subject: [PATCH 128/191] [PR #12641/1aef5d70 backport][3.13] Make pip command
configurable via PIP variable in Makefile (#12644)
---
CHANGES/12641.contrib.rst | 5 +++++
Makefile | 11 ++++++-----
2 files changed, 11 insertions(+), 5 deletions(-)
create mode 100644 CHANGES/12641.contrib.rst
diff --git a/CHANGES/12641.contrib.rst b/CHANGES/12641.contrib.rst
new file mode 100644
index 00000000000..f431e0fd1e3
--- /dev/null
+++ b/CHANGES/12641.contrib.rst
@@ -0,0 +1,5 @@
+Made the ``pip`` command used by the :file:`Makefile` configurable via a
+``PIP`` variable; downstream consumers can now run, for example,
+``make .develop PIP="uv pip"`` to install via ``uv`` without us
+maintaining a parallel target
+-- by :user:`bdraco`.
diff --git a/Makefile b/Makefile
index 893565cc4e0..27db4fe9327 100644
--- a/Makefile
+++ b/Makefile
@@ -3,6 +3,7 @@
to-hash-one = $(dir $1).hash/$(addsuffix .hash,$(notdir $1))
to-hash = $(foreach fname,$1,$(call to-hash-one,$(fname)))
+PIP ?= python -m pip
CYS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/*.pyi) $(wildcard aiohttp/*.pxd) $(wildcard aiohttp/_websocket/*.pyx) $(wildcard aiohttp/_websocket/*.pyi) $(wildcard aiohttp/_websocket/*.pxd)
PYXS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/_websocket/*.pyx)
CS := $(wildcard aiohttp/*.c) $(wildcard aiohttp/_websocket/*.c)
@@ -47,10 +48,10 @@ endif
.SECONDARY: $(call to-hash,$(ALLS))
.update-pip:
- @python -m pip install --upgrade pip
+ @$(PIP) install --upgrade pip
.install-cython: .update-pip $(call to-hash,requirements/cython.txt)
- @python -m pip install -r requirements/cython.in -c requirements/cython.txt
+ @$(PIP) install -r requirements/cython.in -c requirements/cython.txt
@touch .install-cython
aiohttp/_find_header.c: $(call to-hash,aiohttp/hdrs.py ./tools/gen.py)
@@ -85,7 +86,7 @@ cythonize: .install-cython $(PYXS:.pyx=.c) aiohttp/_websocket/reader_c.c
cythonize-nodeps: $(PYXS:.pyx=.c) aiohttp/_websocket/reader_c.c
.install-deps: .install-cython $(PYXS:.pyx=.c) aiohttp/_websocket/reader_c.c $(call to-hash,$(CYS) $(REQS))
- @python -m pip install -r requirements/dev.in -c requirements/dev.txt
+ @$(PIP) install -r requirements/dev.in -c requirements/dev.txt
@touch .install-deps
.PHONY: lint
@@ -100,7 +101,7 @@ mypy:
mypy
.develop: .install-deps generate-llhttp $(call to-hash,$(PYS) $(CYS) $(CS))
- python -m pip install -e . -c requirements/runtime-deps.txt
+ $(PIP) install -e . -c requirements/runtime-deps.txt
@touch .develop
.PHONY: test
@@ -182,7 +183,7 @@ doc-spelling:
.PHONY: install
install: .update-pip
- @python -m pip install -r requirements/dev.in -c requirements/dev.txt
+ @$(PIP) install -r requirements/dev.in -c requirements/dev.txt
.PHONY: install-dev
install-dev: .develop
From 7978c715c3a6d0a8bc5759b53bab1923355f84b9 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 02:50:29 +0000
Subject: [PATCH 129/191] [PR #12641/1aef5d70 backport][3.14] Make pip command
configurable via PIP variable in Makefile (#12642)
Co-authored-by: J. Nick Koston
---
CHANGES/12641.contrib.rst | 5 +++++
Makefile | 11 ++++++-----
2 files changed, 11 insertions(+), 5 deletions(-)
create mode 100644 CHANGES/12641.contrib.rst
diff --git a/CHANGES/12641.contrib.rst b/CHANGES/12641.contrib.rst
new file mode 100644
index 00000000000..f431e0fd1e3
--- /dev/null
+++ b/CHANGES/12641.contrib.rst
@@ -0,0 +1,5 @@
+Made the ``pip`` command used by the :file:`Makefile` configurable via a
+``PIP`` variable; downstream consumers can now run, for example,
+``make .develop PIP="uv pip"`` to install via ``uv`` without us
+maintaining a parallel target
+-- by :user:`bdraco`.
diff --git a/Makefile b/Makefile
index 1d85f8cdb23..502db01218e 100644
--- a/Makefile
+++ b/Makefile
@@ -4,6 +4,7 @@ to-hash-one = $(dir $1).hash/$(addsuffix .hash,$(notdir $1))
to-hash = $(foreach fname,$1,$(call to-hash-one,$(fname)))
CYTHON_EXTRA ?=
+PIP ?= python -m pip
CYS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/*.pyi) $(wildcard aiohttp/*.pxd) $(wildcard aiohttp/_websocket/*.pyx) $(wildcard aiohttp/_websocket/*.pyi) $(wildcard aiohttp/_websocket/*.pxd)
PYXS := $(wildcard aiohttp/*.pyx) $(wildcard aiohttp/_websocket/*.pyx)
CS := $(wildcard aiohttp/*.c) $(wildcard aiohttp/_websocket/*.c)
@@ -48,10 +49,10 @@ endif
.SECONDARY: $(call to-hash,$(ALLS))
.update-pip:
- @python -m pip install --upgrade pip
+ @$(PIP) install --upgrade pip
.install-cython: .update-pip $(call to-hash,requirements/cython.txt)
- @python -m pip install -r requirements/cython.in -c requirements/cython.txt
+ @$(PIP) install -r requirements/cython.in -c requirements/cython.txt
@touch .install-cython
aiohttp/_find_header.c: $(call to-hash,aiohttp/hdrs.py ./tools/gen.py)
@@ -86,7 +87,7 @@ cythonize: .install-cython $(PYXS:.pyx=.c) aiohttp/_websocket/reader_c.c
cythonize-nodeps: $(PYXS:.pyx=.c) aiohttp/_websocket/reader_c.c
.install-deps: .install-cython $(PYXS:.pyx=.c) aiohttp/_websocket/reader_c.c $(call to-hash,$(CYS) $(REQS))
- @python -m pip install -r requirements/dev.in -c requirements/dev.txt
+ @$(PIP) install -r requirements/dev.in -c requirements/dev.txt
@touch .install-deps
.PHONY: lint
@@ -101,7 +102,7 @@ mypy:
mypy
.develop: .install-deps generate-llhttp $(call to-hash,$(PYS) $(CYS) $(CS))
- python -m pip install -e . -c requirements/runtime-deps.txt
+ $(PIP) install -e . -c requirements/runtime-deps.txt
@touch .develop
.PHONY: test
@@ -173,7 +174,7 @@ doc-spelling:
.PHONY: install
install: .update-pip
- @python -m pip install -r requirements/dev.in -c requirements/dev.txt
+ @$(PIP) install -r requirements/dev.in -c requirements/dev.txt
.PHONY: install-dev
install-dev: .develop
From e1ec3c563d2e59e958b6855bca816ec1126d5f11 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 03:11:59 +0000
Subject: [PATCH 130/191] [PR #12643/348f0a1e backport][3.14] Combine
build-tool update and dependency install into one CI step per job (#12645)
---
.github/workflows/ci-cd.yml | 39 +++++++++----------------------------
CHANGES/12643.contrib.rst | 5 +++++
2 files changed, 14 insertions(+), 30 deletions(-)
create mode 100644 CHANGES/12643.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 27708170b49..c954482a635 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -77,12 +77,9 @@ jobs:
path: ~/.cache/pip
restore-keys: |
pip-lint-
- - name: Update pip, wheel, setuptools, build, twine
- run: |
- python -m pip install -U pip wheel setuptools build twine
- name: Install dependencies
run: |
- python -m pip install -r requirements/lint.in -c requirements/lint.txt
+ python -m pip install -U pip wheel setuptools build twine -r requirements/lint.in -c requirements/lint.txt
- name: Install self
run: |
python -m pip install . -c requirements/runtime-deps.txt
@@ -183,14 +180,11 @@ jobs:
python-version: ${{ matrix.pyver }}
activate-environment: true
enable-cache: true
- - name: Update pip, wheel, setuptools, build, twine
- run: |
- uv pip install -U pip wheel setuptools build twine
- name: Install dependencies
env:
DEPENDENCY_GROUP: test${{ endsWith(matrix.pyver, 't') && '-ft' || '' }}
run: |
- uv pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt
+ uv pip install -U pip wheel setuptools build twine -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt
- name: Set PYTHON_GIL=0 for free-threading builds
if: ${{ endsWith(matrix.pyver, 't') }}
run: echo "PYTHON_GIL=0" >> $GITHUB_ENV
@@ -290,14 +284,11 @@ jobs:
python-version: ${{ matrix.pyver }}
activate-environment: true
enable-cache: true
- - name: Update pip, wheel, setuptools, build, twine
- run: |
- uv pip install -U pip wheel setuptools build twine
- name: Install dependencies
env:
DEPENDENCY_GROUP: test${{ endsWith(matrix.pyver, 't') && '-ft' || '' }}
run: |
- uv pip install -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt
+ uv pip install -U pip wheel setuptools build twine -r requirements/${{ env.DEPENDENCY_GROUP }}.in -c requirements/${{ env.DEPENDENCY_GROUP }}.txt
- name: Restore llhttp generated files
if: ${{ matrix.no-extensions == '' }}
uses: actions/download-artifact@v8
@@ -369,12 +360,9 @@ jobs:
python-version: 3.13.2
cache: pip
cache-dependency-path: requirements/*.txt
- - name: Update pip, wheel, setuptools, build, twine
- run: |
- python -m pip install -U pip wheel setuptools build twine
- name: Install dependencies
run: |
- python -m pip install -r requirements/test.in -c requirements/test.txt
+ python -m pip install -U pip wheel setuptools build twine -r requirements/test.in -c requirements/test.txt
- name: Restore llhttp generated files
uses: actions/download-artifact@v8
with:
@@ -413,12 +401,9 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: '3.12'
- - name: Update pip, wheel, setuptools, build, twine
- run: |
- python -m pip install -U pip wheel setuptools build twine
- name: Install dependencies
run: |
- python -Im pip install -r requirements/test.in -c requirements/test.txt
+ python -Im pip install -U pip wheel setuptools build twine -r requirements/test.in -c requirements/test.txt
- name: Uninstall blocbuster
run: python -m pip uninstall blockbuster -y
- name: Restore llhttp generated files
@@ -499,13 +484,10 @@ jobs:
submodules: true
- name: Setup Python
uses: actions/setup-python@v6
- - name: Update pip, wheel, setuptools, build, twine
- run: |
- python -m pip install -U pip wheel setuptools build twine
- - name: Install cython
+ - name: Install build tooling and cython
run: >-
python -m
- pip install -r requirements/cython.in -c requirements/cython.txt
+ pip install -U pip wheel setuptools build twine -r requirements/cython.in -c requirements/cython.txt
- name: Restore llhttp generated files
uses: actions/download-artifact@v8
with:
@@ -589,13 +571,10 @@ jobs:
uses: actions/setup-python@v6
with:
python-version: 3.x
- - name: Update pip, wheel, setuptools, build, twine
- run: |
- python -m pip install -U pip wheel setuptools build twine
- - name: Install cython
+ - name: Install build tooling and cython
run: >-
python -m
- pip install -r requirements/cython.in -c requirements/cython.txt
+ pip install -U pip wheel setuptools build twine -r requirements/cython.in -c requirements/cython.txt
- name: Restore llhttp generated files
uses: actions/download-artifact@v8
with:
diff --git a/CHANGES/12643.contrib.rst b/CHANGES/12643.contrib.rst
new file mode 100644
index 00000000000..8477457ce16
--- /dev/null
+++ b/CHANGES/12643.contrib.rst
@@ -0,0 +1,5 @@
+Merged the "Update pip, wheel, setuptools, build, twine" step into
+the following ``Install dependencies`` (or ``Install cython``) step
+in every job in :file:`.github/workflows/ci-cd.yml`, so each job now
+runs a single ``pip``/``uv pip install`` invocation instead of two
+-- by :user:`bdraco`.
From c291b26fdc59fcbb56edb07077d1b59fcdfa7fd1 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 17:34:54 +0000
Subject: [PATCH 131/191] [PR #12647/3c608498 backport][3.13] ci: fall back to
plain build frontend for odd-arch wheels (#12648)
Co-authored-by: J. Nick Koston
---
.github/workflows/ci-cd.yml | 10 ++++++++--
CHANGES/12647.contrib.rst | 4 ++++
docs/spelling_wordlist.txt | 2 ++
3 files changed, 14 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12647.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index aec0cfac0f0..704cf815c0a 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -438,6 +438,9 @@ jobs:
# Build emulated architectures only if QEMU is set,
# use default "auto" otherwise
echo "CIBW_ARCHS_LINUX=${{ matrix.qemu }}" >> $GITHUB_ENV
+ # Override pyproject.toml's `build[uv]`: the pypa odd-arch
+ # manylinux/musllinux containers do not ship `uv` preinstalled.
+ echo "CIBW_BUILD_FRONTEND=build" >> $GITHUB_ENV
fi
shell: bash
- name: Setup Python
@@ -464,8 +467,11 @@ jobs:
with:
# `build-frontend = "build[uv]"` (pyproject.toml) requires uv to be
# available on the runner for Windows and macOS. Installing
- # cibuildwheel with the `uv` extra bundles uv with it; Linux
- # already has uv inside the manylinux/musllinux container.
+ # cibuildwheel with the `uv` extra bundles uv with it; the
+ # tested-arch manylinux/musllinux containers also ship uv
+ # preinstalled. The odd-arch containers do not, so the
+ # `Prepare emulation` step above sets `CIBW_BUILD_FRONTEND=build`
+ # for those QEMU matrix cells.
extras: uv
env:
CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }}
diff --git a/CHANGES/12647.contrib.rst b/CHANGES/12647.contrib.rst
new file mode 100644
index 00000000000..5203ba0a395
--- /dev/null
+++ b/CHANGES/12647.contrib.rst
@@ -0,0 +1,4 @@
+Override ``CIBW_BUILD_FRONTEND=build`` on the QEMU-emulated odd-arch wheel
+jobs so cibuildwheel falls back to plain pip, because the pypa
+``manylinux``/``musllinux`` containers for those arches do not ship ``uv``
+preinstalled -- by :user:`bdraco`.
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 77c421e3e79..416e4f34615 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -67,6 +67,7 @@ charset
charsetdetect
chunked
chunking
+cibuildwheel
CIMultiDict
ClientSession
cls
@@ -255,6 +256,7 @@ py
pydantic
pyenv
pyflakes
+pypa
pyright
pytest
Pytest
From 730c54a0f71e8eb5c8678ea7b0b453c6733b138e Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 17:40:19 +0000
Subject: [PATCH 132/191] [PR #12647/3c608498 backport][3.14] ci: fall back to
plain build frontend for odd-arch wheels (#12649)
Co-authored-by: J. Nick Koston
---
.github/workflows/ci-cd.yml | 10 ++++++++--
CHANGES/12647.contrib.rst | 4 ++++
docs/spelling_wordlist.txt | 2 ++
3 files changed, 14 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12647.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index c954482a635..44246de8533 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -565,6 +565,9 @@ jobs:
# Build emulated architectures only if QEMU is set,
# use default "auto" otherwise
echo "CIBW_ARCHS_LINUX=${{ matrix.qemu }}" >> $GITHUB_ENV
+ # Override pyproject.toml's `build[uv]`: the pypa odd-arch
+ # manylinux/musllinux containers do not ship `uv` preinstalled.
+ echo "CIBW_BUILD_FRONTEND=build" >> $GITHUB_ENV
fi
shell: bash
- name: Setup Python
@@ -588,8 +591,11 @@ jobs:
with:
# `build-frontend = "build[uv]"` (pyproject.toml) requires uv to be
# available on the runner for Windows and macOS. Installing
- # cibuildwheel with the `uv` extra bundles uv with it; Linux
- # already has uv inside the manylinux/musllinux container.
+ # cibuildwheel with the `uv` extra bundles uv with it; the
+ # tested-arch manylinux/musllinux containers also ship uv
+ # preinstalled. The odd-arch containers do not, so the
+ # `Prepare emulation` step above sets `CIBW_BUILD_FRONTEND=build`
+ # for those QEMU matrix cells.
extras: uv
env:
CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }}
diff --git a/CHANGES/12647.contrib.rst b/CHANGES/12647.contrib.rst
new file mode 100644
index 00000000000..5203ba0a395
--- /dev/null
+++ b/CHANGES/12647.contrib.rst
@@ -0,0 +1,4 @@
+Override ``CIBW_BUILD_FRONTEND=build`` on the QEMU-emulated odd-arch wheel
+jobs so cibuildwheel falls back to plain pip, because the pypa
+``manylinux``/``musllinux`` containers for those arches do not ship ``uv``
+preinstalled -- by :user:`bdraco`.
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 16f7da5ebe4..b6323e39882 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -68,6 +68,7 @@ charset
charsetdetect
chunked
chunking
+cibuildwheel
CIMultiDict
ClientSession
cls
@@ -263,6 +264,7 @@ py
pydantic
pyenv
pyflakes
+pypa
pyright
pytest
Pytest
From 7baa5923253b866649c6e1a58ca554fbf3033b25 Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Tue, 19 May 2026 20:02:44 +0100
Subject: [PATCH 133/191] Add output_size property (#12452) (#12650)
(cherry picked from commit 37cd4a8edcbfe543067a96e36f4da3e9395328a3)
---
CHANGES/12452.feature.rst | 2 +
aiohttp/client_reqrep.py | 39 ++++++-
docs/client_reference.rst | 24 ++++
tests/test_client_functional.py | 170 ++++++++++++++++++++++++++++
tests/test_client_proto.py | 10 ++
tests/test_client_request.py | 3 +
tests/test_client_response.py | 190 ++++++++++++++++++++++++++++++++
tests/test_proxy.py | 31 ++++++
8 files changed, 467 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12452.feature.rst
diff --git a/CHANGES/12452.feature.rst b/CHANGES/12452.feature.rst
new file mode 100644
index 00000000000..ad4936323da
--- /dev/null
+++ b/CHANGES/12452.feature.rst
@@ -0,0 +1,2 @@
+Added :attr:`~aiohttp.ClientResponse.output_size` and
+:attr:`~aiohttp.ClientResponse.upload_complete` -- by :user:`Dreamsorcerer`.
diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py
index 0353ec34f03..4aeae08ac19 100644
--- a/aiohttp/client_reqrep.py
+++ b/aiohttp/client_reqrep.py
@@ -304,6 +304,9 @@ class ClientResponse(HeadersMixin):
_resolve_charset: Callable[["ClientResponse", bytes], str] = lambda *_: "utf-8"
__writer: Optional["asyncio.Task[None]"] = None
+ _stream_writer: Optional[AbstractStreamWriter] = None
+ _output_size: int = 0
+ _upload_complete: Optional[asyncio.Future[None]] = None
def __init__(
self,
@@ -317,6 +320,7 @@ def __init__(
traces: list["Trace"],
loop: asyncio.AbstractEventLoop,
session: "ClientSession",
+ stream_writer: AbstractStreamWriter,
) -> None:
# URL forbids subclasses, so a simple type check is enough.
assert type(url) is URL
@@ -325,7 +329,10 @@ def __init__(
self._real_url = url
self._url = url.with_fragment(None) if url.raw_fragment else url
- if writer is not None:
+ if writer is None: # Request already sent
+ self._output_size = stream_writer.output_size
+ else:
+ self._stream_writer = stream_writer
self._writer = writer
if continue100 is not None:
self._continue = continue100
@@ -346,6 +353,11 @@ def __init__(
def __reset_writer(self, _: object = None) -> None:
self.__writer = None
+ if self._stream_writer is not None:
+ self._output_size = self._stream_writer.output_size
+ self._stream_writer = None
+ if self._upload_complete is not None and not self._upload_complete.done():
+ self._upload_complete.set_result(None)
@property
def _writer(self) -> Optional["asyncio.Task[None]"]:
@@ -366,10 +378,29 @@ def _writer(self, writer: Optional["asyncio.Task[None]"]) -> None:
return
if writer.done():
# The writer is already done, so we can clear it immediately.
- self.__writer = None
+ self.__reset_writer()
else:
writer.add_done_callback(self.__reset_writer)
+ @property
+ def output_size(self) -> int:
+ """Number of bytes sent for this request."""
+ if self._stream_writer is not None:
+ return self._stream_writer.output_size
+ return self._output_size
+
+ @property
+ def upload_complete(self) -> "asyncio.Future[None]":
+ """Future set when the request body has been fully sent.
+
+ Already done when the request had no body or was written eagerly.
+ """
+ if self._upload_complete is None:
+ self._upload_complete = self._loop.create_future()
+ if self._stream_writer is None: # upload already finished
+ self._upload_complete.set_result(None)
+ return self._upload_complete
+
@property
def cookies(self) -> SimpleCookie:
if self._cookies is None:
@@ -652,6 +683,9 @@ async def _wait_released(self) -> None:
def _cleanup_writer(self) -> None:
if self.__writer is not None:
self.__writer.cancel()
+ if self._stream_writer is not None:
+ self._output_size = self._stream_writer.output_size
+ self._stream_writer = None
self._session = None
def _notify_content(self) -> None:
@@ -1487,6 +1521,7 @@ async def send(self, conn: "Connection") -> "ClientResponse":
traces=self._traces,
loop=self.loop,
session=self._session,
+ stream_writer=writer,
)
return self.response
diff --git a/docs/client_reference.rst b/docs/client_reference.rst
index f9f3dfcfef8..1b1a29072c7 100644
--- a/docs/client_reference.rst
+++ b/docs/client_reference.rst
@@ -1579,6 +1579,30 @@ Response object
.. versionadded:: 3.2
+ .. attribute:: output_size
+
+ Number of bytes sent for this request.
+
+ Pair with :attr:`upload_complete` to display upload progress::
+
+ async with session.post(url, data=mpwriter) as resp:
+ while not resp.upload_complete.done():
+ print(f"uploaded {resp.output_size} bytes")
+ await asyncio.sleep(0.5)
+ print(f"upload complete: {resp.output_size} bytes")
+
+ .. versionadded:: 3.14
+
+ .. attribute:: upload_complete
+
+ An :class:`asyncio.Future` set when the request body has been fully sent.
+
+ Use ``await resp.upload_complete`` to block until the upload finishes, or
+ ``resp.upload_complete.done()`` to poll from a progress-sampling loop
+ (see :attr:`output_size`).
+
+ .. versionadded:: 3.14
+
.. attribute:: content_type
Read-only property with *content* part of *Content-Type* header.
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
index 66b86eb64ac..972de1e0e1c 100644
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -5876,3 +5876,173 @@ async def handler(request: web.Request) -> web.Response:
data = await resp.content.read()
assert resp.content.total_raw_bytes == len(data)
assert resp.content.total_raw_bytes == int(resp.headers["Content-Length"])
+
+
+async def test_output_size_bytes(aiohttp_client: AiohttpClient) -> None:
+ async def handler(request: web.Request) -> web.Response:
+ await request.read()
+ return web.Response()
+
+ app = web.Application()
+ app.router.add_post("/", handler)
+ client = await aiohttp_client(app)
+
+ body = b"x" * 1024
+ async with client.post("/", data=body) as resp:
+ assert resp.output_size >= len(body)
+
+
+async def test_output_size_multipart(aiohttp_client: AiohttpClient) -> None:
+ async def handler(request: web.Request) -> web.Response:
+ await request.read()
+ return web.Response()
+
+ app = web.Application()
+ app.router.add_post("/", handler)
+ client = await aiohttp_client(app)
+
+ mpwriter = aiohttp.MultipartWriter("form-data")
+ mpwriter.append(b"x" * 4096)
+ mpwriter.append(b"y" * 2048)
+ expected_body_size = mpwriter.size
+ assert expected_body_size is not None
+
+ async with client.post("/", data=mpwriter) as resp:
+ assert resp.output_size >= expected_body_size
+
+
+async def test_output_size_keepalive_isolated(
+ aiohttp_client: AiohttpClient,
+) -> None:
+ """Each request on a keep-alive connection has its own counter."""
+ transports: set[object] = set()
+
+ async def handler(request: web.Request) -> web.Response:
+ transports.add(request.transport)
+ await request.read()
+ return web.Response()
+
+ app = web.Application()
+ app.router.add_post("/", handler)
+ connector = aiohttp.TCPConnector(limit=1, force_close=False)
+ client = await aiohttp_client(app, connector=connector)
+ body = b"x" * 65536
+
+ async with client.post("/", data=body) as resp1:
+ size1 = resp1.output_size
+
+ async with client.post("/", data=body) as resp2:
+ size2 = resp2.output_size
+
+ assert len(transports) == 1 # Check keep-alive worked.
+ assert size1 >= len(body)
+ assert size1 == size2
+
+
+async def test_output_size_progress(aiohttp_client: AiohttpClient) -> None:
+ """output_size advances by exactly one chunk per yield."""
+
+ async def handler(request: web.Request) -> web.StreamResponse:
+ response = web.StreamResponse()
+ await response.prepare(request)
+ # Flush headers + a chunk so resp.start() returns on the client
+ # side before we read the body.
+ await response.write(b"x")
+ await request.read()
+ return response
+
+ app = web.Application()
+ app.router.add_post("/", handler)
+ client = await aiohttp_client(app)
+
+ chunk_size = 4096
+ chunk = b"z" * chunk_size
+ num_chunks = 8
+ sample_taken = asyncio.Event()
+ next_chunk = asyncio.Event()
+
+ async def gated_body() -> AsyncIterator[bytes]:
+ for _ in range(num_chunks):
+ yield chunk
+ sample_taken.clear()
+ next_chunk.set()
+ await sample_taken.wait()
+
+ async with client.post("/", data=gated_body()) as resp:
+ samples: list[int] = []
+ for _ in range(num_chunks):
+ await next_chunk.wait()
+ next_chunk.clear()
+ samples.append(resp.output_size)
+ assert not resp.upload_complete.done()
+ sample_taken.set()
+ await resp.upload_complete
+ assert resp.upload_complete.done()
+ await resp.read()
+
+ # Each sample after the first reflects exactly one more chunk on the wire.
+ chunked_framing = len(f"{chunk_size:x}".encode()) + 4
+ deltas = [samples[i] - samples[i - 1] for i in range(1, len(samples))]
+ assert deltas == [chunk_size + chunked_framing] * (num_chunks - 1)
+
+
+async def test_output_size_get_request(aiohttp_client: AiohttpClient) -> None:
+ """GET request with no body still reports the request header byte count."""
+
+ async def handler(request: web.Request) -> web.Response:
+ return web.Response()
+
+ app = web.Application()
+ app.router.add_get("/", handler)
+ client = await aiohttp_client(app)
+
+ async with client.get("/") as resp:
+ assert resp.output_size >= 0
+
+
+async def test_output_size_writer_released(aiohttp_client: AiohttpClient) -> None:
+ """Writer is dropped once body upload completes; output_size survives."""
+
+ async def handler(request: web.Request) -> web.Response:
+ await request.read()
+ return web.Response()
+
+ app = web.Application()
+ app.router.add_post("/", handler)
+ client = await aiohttp_client(app)
+
+ body = b"x" * 1024
+ async with client.post("/", data=body) as resp:
+ await resp.read()
+ assert resp._stream_writer is None
+ assert resp.output_size >= len(body)
+
+
+async def test_upload_complete_no_body(aiohttp_client: AiohttpClient) -> None:
+ async def handler(request: web.Request) -> web.Response:
+ return web.Response()
+
+ app = web.Application()
+ app.router.add_get("/", handler)
+ client = await aiohttp_client(app)
+
+ async with client.get("/") as resp:
+ assert resp.upload_complete.done()
+
+
+async def test_upload_complete_late_access(aiohttp_client: AiohttpClient) -> None:
+ """Accessing upload_complete after the upload finished returns a done future."""
+
+ async def handler(request: web.Request) -> web.Response:
+ await request.read()
+ return web.Response()
+
+ app = web.Application()
+ app.router.add_post("/", handler)
+ client = await aiohttp_client(app)
+
+ async with client.post("/", data=b"x" * 1024) as resp:
+ await resp.read()
+ # Writer task is done; future is created lazily on this first access.
+ assert resp._upload_complete is None
+ assert resp.upload_complete.done()
diff --git a/tests/test_client_proto.py b/tests/test_client_proto.py
index 2936aab021a..ed6e602b0d5 100644
--- a/tests/test_client_proto.py
+++ b/tests/test_client_proto.py
@@ -4,6 +4,7 @@
from yarl import URL
from aiohttp import http
+from aiohttp.abc import AbstractStreamWriter
from aiohttp.client_exceptions import ClientOSError, ServerDisconnectedError
from aiohttp.client_proto import ResponseHandler
from aiohttp.client_reqrep import ClientResponse
@@ -118,6 +119,9 @@ async def test_multiple_responses_one_byte_at_a_time(
traces=[],
loop=loop,
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
await response.start(conn)
await response.read() == payload
@@ -148,6 +152,9 @@ class PatchableHttpResponseParser(http.HttpResponseParser):
traces=[],
loop=loop,
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
await response.start(conn)
await response.read() == b"ab"
@@ -176,6 +183,9 @@ async def test_client_protocol_readuntil_eof(loop: asyncio.AbstractEventLoop) ->
traces=[],
loop=loop,
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
proto.set_response_params(read_until_eof=True)
await response.start(conn)
diff --git a/tests/test_client_request.py b/tests/test_client_request.py
index 5f5cf4a849f..66240258620 100644
--- a/tests/test_client_request.py
+++ b/tests/test_client_request.py
@@ -1462,6 +1462,9 @@ async def send(self, conn):
traces=self._traces,
loop=self.loop,
session=self._session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
self.response = resp
nonlocal called
diff --git a/tests/test_client_response.py b/tests/test_client_response.py
index 4e7ae2fb1a6..7e683a8b9e8 100644
--- a/tests/test_client_response.py
+++ b/tests/test_client_response.py
@@ -13,6 +13,7 @@
import aiohttp
from aiohttp import ClientSession, hdrs, http
+from aiohttp.abc import AbstractStreamWriter
from aiohttp.client_reqrep import ClientResponse, RequestInfo
from aiohttp.helpers import TimerNoop
from aiohttp.multipart import BadContentDispositionHeader
@@ -47,6 +48,9 @@ async def test_http_processing_error(session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
loop.get_debug = mock.Mock()
loop.get_debug.return_value = True
@@ -75,6 +79,9 @@ def test_del(session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
loop.get_debug = mock.Mock()
loop.get_debug.return_value = True
@@ -102,6 +109,9 @@ def test_close(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._closed = False
response._connection = mock.Mock()
@@ -122,6 +132,9 @@ def test_wait_for_100_1(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
assert response._continue is not None
response.close()
@@ -138,6 +151,9 @@ def test_wait_for_100_2(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
assert response._continue is None
response.close()
@@ -154,6 +170,9 @@ def test_repr(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response.status = 200
response.reason = "Ok"
@@ -171,6 +190,9 @@ def test_repr_non_ascii_url() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
assert "" in repr(response)
@@ -186,6 +208,9 @@ def test_repr_non_ascii_reason() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response.reason = "\u03bb"
assert "" in repr(
@@ -204,6 +229,9 @@ def test_url_obj_deprecated() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
with pytest.warns(DeprecationWarning):
response.url_obj
@@ -220,6 +248,9 @@ async def test_read_and_release_connection(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args, **kwargs):
@@ -246,6 +277,9 @@ async def test_read_and_release_connection_with_error(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
content = response.content = mock.Mock()
content.read.return_value = loop.create_future()
@@ -267,6 +301,9 @@ async def test_release(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
fut = loop.create_future()
fut.set_result(b"")
@@ -296,6 +333,9 @@ def run(conn):
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._closed = False
response._connection = conn
@@ -316,6 +356,9 @@ async def test_response_eof(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._closed = False
conn = response._connection = mock.Mock()
@@ -337,6 +380,9 @@ async def test_response_eof_upgraded(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
conn = response._connection = mock.Mock()
@@ -358,6 +404,9 @@ async def test_response_eof_after_connection_detach(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._closed = False
conn = response._connection = mock.Mock()
@@ -379,6 +428,9 @@ async def test_text(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args, **kwargs):
@@ -406,6 +458,9 @@ async def test_text_bad_encoding(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args, **kwargs):
@@ -437,6 +492,9 @@ async def test_text_badly_encoded_encoding_header(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args: object, **kwargs: object):
@@ -466,6 +524,9 @@ async def test_text_custom_encoding(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args, **kwargs):
@@ -497,6 +558,9 @@ async def test_text_charset_resolver(content_type: str, loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args, **kwargs):
@@ -526,6 +590,9 @@ async def test_get_encoding_body_none(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args, **kwargs):
@@ -556,6 +623,9 @@ async def test_text_after_read(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args, **kwargs):
@@ -583,6 +653,9 @@ async def test_json(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args, **kwargs):
@@ -610,6 +683,9 @@ async def test_json_extended_content_type(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args, **kwargs):
@@ -639,6 +715,9 @@ async def test_json_custom_content_type(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args, **kwargs):
@@ -666,6 +745,9 @@ async def test_json_custom_loader(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = {"Content-Type": "application/json;charset=cp1251"}
response._body = b"data"
@@ -688,6 +770,9 @@ async def test_json_invalid_content_type(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = {"Content-Type": "data/octet-stream"}
response._body = b""
@@ -711,6 +796,9 @@ async def test_json_no_content(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = {"Content-Type": "data/octet-stream"}
response._body = b""
@@ -730,6 +818,9 @@ async def test_json_override_encoding(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args, **kwargs):
@@ -759,6 +850,9 @@ def test_get_encoding_unknown(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = {"Content-Type": "application/json"}
@@ -776,6 +870,9 @@ def test_raise_for_status_2xx() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response.status = 200
response.reason = "OK"
@@ -793,6 +890,9 @@ def test_raise_for_status_4xx() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response.status = 409
response.reason = "CONFLICT"
@@ -814,6 +914,9 @@ def test_raise_for_status_4xx_without_reason() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response.status = 404
response.reason = ""
@@ -835,6 +938,9 @@ def test_resp_host() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
assert "del-cl-resp.org" == response.host
@@ -850,6 +956,9 @@ def test_content_type() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = {"Content-Type": "application/json;charset=cp1251"}
@@ -867,6 +976,9 @@ def test_content_type_no_header() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = {}
@@ -884,6 +996,9 @@ def test_charset() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = {"Content-Type": "application/json;charset=cp1251"}
@@ -901,6 +1016,9 @@ def test_charset_no_header() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = {}
@@ -918,6 +1036,9 @@ def test_charset_no_charset() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = {"Content-Type": "application/json"}
@@ -935,6 +1056,9 @@ def test_content_disposition_full() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = {
"Content-Disposition": 'attachment; filename="archive.tar.gz"; foo=bar'
@@ -958,6 +1082,9 @@ def test_content_disposition_no_parameters() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = {"Content-Disposition": "attachment"}
@@ -984,6 +1111,9 @@ def test_content_disposition_empty_parts(content_disposition: str) -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
h = {"Content-Disposition": content_disposition}
response._headers = CIMultiDictProxy(CIMultiDict(h))
@@ -1005,6 +1135,9 @@ def test_content_disposition_no_header() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = {}
@@ -1022,6 +1155,9 @@ def test_default_encoding_is_utf8() -> None:
traces=[],
loop=mock.Mock(),
session=None, # type: ignore[arg-type]
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = CIMultiDictProxy(CIMultiDict({}))
response._body = b""
@@ -1042,6 +1178,9 @@ def test_response_request_info() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
assert url == response.request_info.url
assert "get" == response.request_info.method
@@ -1061,6 +1200,9 @@ def test_request_info_in_exception() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response.status = 409
response.reason = "CONFLICT"
@@ -1082,6 +1224,9 @@ def test_no_redirect_history_in_exception() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response.status = 409
response.reason = "CONFLICT"
@@ -1105,6 +1250,9 @@ def test_redirect_history_in_exception() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response.status = 409
response.reason = "CONFLICT"
@@ -1119,6 +1267,9 @@ def test_redirect_history_in_exception() -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
hist_response._headers = hist_headers
@@ -1148,6 +1299,9 @@ async def test_response_read_triggers_callback(loop, session) -> None:
loop=loop,
session=session,
traces=[trace],
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
def side_effect(*args, **kwargs):
@@ -1182,6 +1336,9 @@ def test_response_cookies(
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
cookies = response.cookies
# Ensure the same cookies object is returned each time
@@ -1202,6 +1359,9 @@ def test_response_real_url(
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
assert response.url == url.with_fragment(None)
assert response.real_url == url
@@ -1219,6 +1379,9 @@ def test_response_links_comma_separated(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = CIMultiDict(
[
@@ -1249,6 +1412,9 @@ def test_response_links_multiple_headers(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = CIMultiDict(
[
@@ -1274,6 +1440,9 @@ def test_response_links_no_rel(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = CIMultiDict([("Link", "")])
assert response.links == {
@@ -1293,6 +1462,9 @@ def test_response_links_quoted(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = CIMultiDict(
[
@@ -1316,6 +1488,9 @@ def test_response_links_relative(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = CIMultiDict(
[
@@ -1339,6 +1514,9 @@ def test_response_links_empty(loop, session) -> None:
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response._headers = CIMultiDict()
assert response.links == {}
@@ -1355,6 +1533,9 @@ def test_response_not_closed_after_get_ok(mocker) -> None:
traces=[],
loop=mock.Mock(),
session=mock.Mock(),
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
response.status = 400
response.reason = "Bad Request"
@@ -1389,6 +1570,9 @@ def test_response_duplicate_cookie_names(
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
# Set headers with duplicate cookie names but different domains
@@ -1428,6 +1612,9 @@ def test_response_raw_cookie_headers_preserved(
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
# Set headers with multiple cookies
@@ -1468,6 +1655,9 @@ def test_response_cookies_setter_updates_raw_headers(
traces=[],
loop=loop,
session=session,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
)
# Create a SimpleCookie with some cookies
diff --git a/tests/test_proxy.py b/tests/test_proxy.py
index 5599bf804f4..6f4b80c37b1 100644
--- a/tests/test_proxy.py
+++ b/tests/test_proxy.py
@@ -11,6 +11,7 @@
from yarl import URL
import aiohttp
+from aiohttp.abc import AbstractStreamWriter
from aiohttp.client_reqrep import ClientRequest, ClientResponse, Fingerprint
from aiohttp.connector import _SSL_CONTEXT_VERIFIED
from aiohttp.helpers import TimerNoop
@@ -269,6 +270,9 @@ def test_proxy_server_hostname_default(
timer=TimerNoop(),
traces=[],
loop=self.loop,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
session=mock.Mock(),
)
proxy_req.send = mock.AsyncMock(return_value=proxy_resp)
@@ -338,6 +342,9 @@ def test_proxy_server_hostname_override(
timer=TimerNoop(),
traces=[],
loop=self.loop,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
session=mock.Mock(),
)
proxy_req.send = mock.AsyncMock(return_value=proxy_resp)
@@ -417,6 +424,9 @@ def close(self) -> None:
timer=TimerNoop(),
traces=[],
loop=self.loop,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
session=mock.Mock(),
)
fingerprint_mock = mock.Mock(spec=Fingerprint, auto_spec=True)
@@ -516,6 +526,9 @@ def test_https_connect(
timer=TimerNoop(),
traces=[],
loop=self.loop,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
session=mock.Mock(),
)
proxy_req.send = mock.AsyncMock(return_value=proxy_resp)
@@ -583,6 +596,9 @@ def test_https_connect_certificate_error(
timer=TimerNoop(),
traces=[],
loop=self.loop,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
session=mock.Mock(),
)
proxy_req.send = mock.AsyncMock(return_value=proxy_resp)
@@ -646,6 +662,9 @@ def test_https_connect_ssl_error(
timer=TimerNoop(),
traces=[],
loop=self.loop,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
session=mock.Mock(),
)
proxy_req.send = mock.AsyncMock(return_value=proxy_resp)
@@ -709,6 +728,9 @@ def test_https_connect_http_proxy_error(
timer=TimerNoop(),
traces=[],
loop=self.loop,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
session=mock.Mock(),
)
proxy_req.send = mock.AsyncMock(return_value=proxy_resp)
@@ -777,6 +799,9 @@ def test_https_connect_resp_start_error(
timer=TimerNoop(),
traces=[],
loop=self.loop,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
session=mock.Mock(),
)
proxy_req.send = mock.AsyncMock(return_value=proxy_resp)
@@ -902,6 +927,9 @@ def test_https_connect_pass_ssl_context(
timer=TimerNoop(),
traces=[],
loop=self.loop,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
session=mock.Mock(),
)
proxy_req.send = mock.AsyncMock(return_value=proxy_resp)
@@ -980,6 +1008,9 @@ def test_https_auth(self, start_connection: Any, ClientRequestMock: Any) -> None
timer=TimerNoop(),
traces=[],
loop=self.loop,
+ stream_writer=mock.create_autospec(
+ AbstractStreamWriter, spec_set=True, instance=True
+ ),
session=mock.Mock(),
)
proxy_req.send = mock.AsyncMock(return_value=proxy_resp)
From 3c9ede21c1571366eec7fd85711beae538fe80cb Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 15:15:30 -0500
Subject: [PATCH 134/191] [PR #12651/f6912583 backport][3.13] ci: make the
deploy job re-runnable after a partial failure (#12652)
Co-authored-by: J. Nick Koston
---
.github/workflows/ci-cd.yml | 26 ++++++++++++++++++++++++++
CHANGES/12651.contrib.rst | 5 +++++
docs/spelling_wordlist.txt | 2 ++
3 files changed, 33 insertions(+)
create mode 100644 CHANGES/12651.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 704cf815c0a..058bd5234c7 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -522,7 +522,29 @@ jobs:
- name: Collected dists
run: |
tree dist
+ - name: Check whether the GitHub Release already exists
+ # Allows re-running the deploy job after a partial failure (e.g. PyPI
+ # upload error) without the Make Release step failing with HTTP 422
+ # because the tag/release was created on a prior attempt. Treat
+ # only the literal `release not found` reply as "does not exist";
+ # other failures (auth, rate-limit, network) re-raise so the job
+ # fails loudly instead of falling through to Make Release.
+ id: gh-release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ TAG: ${{ github.ref_name }}
+ run: |
+ if gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" \
+ >/dev/null 2>err; then
+ echo 'exists=true' >> "${GITHUB_OUTPUT}"
+ elif grep -qx 'release not found' err; then
+ echo 'exists=false' >> "${GITHUB_OUTPUT}"
+ else
+ cat err >&2
+ exit 1
+ fi
- name: Make Release
+ if: steps.gh-release.outputs.exists != 'true'
uses: aio-libs/create-release@v1.6.6
with:
changes_file: CHANGES.rst
@@ -538,6 +560,10 @@ jobs:
- name: >-
Publish 🐍📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ # Allow re-running the deploy job after a partial PyPI upload
+ # without failing on dists that were already published.
+ skip-existing: true
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.0.1
diff --git a/CHANGES/12651.contrib.rst b/CHANGES/12651.contrib.rst
new file mode 100644
index 00000000000..0318ed0d084
--- /dev/null
+++ b/CHANGES/12651.contrib.rst
@@ -0,0 +1,5 @@
+Allowed re-running the ``deploy`` job in ``.github/workflows/ci-cd.yml``
+after a partial release failure: the ``Make Release`` step now skips
+when the GitHub Release already exists, and the PyPI publish step uses
+``skip-existing`` so dists that were already uploaded on a prior
+attempt do not break the retry -- by :user:`bdraco`.
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 416e4f34615..8fab90d3f7b 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -106,6 +106,7 @@ Dev
dict
Dict
Discord
+dists
django
Django
dns
@@ -257,6 +258,7 @@ pydantic
pyenv
pyflakes
pypa
+PyPI
pyright
pytest
Pytest
From 33c4789b7c08a6d121091615a9cda738b328b69a Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 15:18:51 -0500
Subject: [PATCH 135/191] [PR #12651/f6912583 backport][3.14] ci: make the
deploy job re-runnable after a partial failure (#12653)
Co-authored-by: J. Nick Koston
---
.github/workflows/ci-cd.yml | 26 ++++++++++++++++++++++++++
CHANGES/12651.contrib.rst | 5 +++++
docs/spelling_wordlist.txt | 2 ++
3 files changed, 33 insertions(+)
create mode 100644 CHANGES/12651.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 44246de8533..626731fcf08 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -646,7 +646,29 @@ jobs:
- name: Collected dists
run: |
tree dist
+ - name: Check whether the GitHub Release already exists
+ # Allows re-running the deploy job after a partial failure (e.g. PyPI
+ # upload error) without the Make Release step failing with HTTP 422
+ # because the tag/release was created on a prior attempt. Treat
+ # only the literal `release not found` reply as "does not exist";
+ # other failures (auth, rate-limit, network) re-raise so the job
+ # fails loudly instead of falling through to Make Release.
+ id: gh-release
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ TAG: ${{ github.ref_name }}
+ run: |
+ if gh release view "${TAG}" --repo "${GITHUB_REPOSITORY}" \
+ >/dev/null 2>err; then
+ echo 'exists=true' >> "${GITHUB_OUTPUT}"
+ elif grep -qx 'release not found' err; then
+ echo 'exists=false' >> "${GITHUB_OUTPUT}"
+ else
+ cat err >&2
+ exit 1
+ fi
- name: Make Release
+ if: steps.gh-release.outputs.exists != 'true'
uses: aio-libs/create-release@v1.6.6
with:
changes_file: CHANGES.rst
@@ -662,6 +684,10 @@ jobs:
- name: >-
Publish 🐍📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ # Allow re-running the deploy job after a partial PyPI upload
+ # without failing on dists that were already published.
+ skip-existing: true
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.3.0
diff --git a/CHANGES/12651.contrib.rst b/CHANGES/12651.contrib.rst
new file mode 100644
index 00000000000..0318ed0d084
--- /dev/null
+++ b/CHANGES/12651.contrib.rst
@@ -0,0 +1,5 @@
+Allowed re-running the ``deploy`` job in ``.github/workflows/ci-cd.yml``
+after a partial release failure: the ``Make Release`` step now skips
+when the GitHub Release already exists, and the PyPI publish step uses
+``skip-existing`` so dists that were already uploaded on a prior
+attempt do not break the retry -- by :user:`bdraco`.
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index b6323e39882..75719ce0318 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -108,6 +108,7 @@ Dev
dict
Dict
Discord
+dists
django
Django
dns
@@ -265,6 +266,7 @@ pydantic
pyenv
pyflakes
pypa
+PyPI
pyright
pytest
Pytest
From 2ab51fa50fe407fc297ae00fe5f373f7d571d531 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 21:57:48 +0000
Subject: [PATCH 136/191] [PR #12655/35a310d5 backport][3.13] ci: build armv7l
wheels on native ARM runners (#12656)
Co-authored-by: J. Nick Koston
---
.github/workflows/ci-cd.yml | 9 +++++++--
CHANGES/12655.contrib.rst | 4 ++++
docs/spelling_wordlist.txt | 1 +
3 files changed, 12 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12655.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 058bd5234c7..aea2635dcc6 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -407,10 +407,15 @@ jobs:
- os: ubuntu-latest
qemu: s390x
musl: musllinux
- - os: ubuntu-latest
+ # armv7l builds on aarch64 hosts. We still register QEMU so
+ # binfmt picks up the 32-bit ARM userspace handler regardless of
+ # whether the host kernel has CONFIG_COMPAT enabled. Even with
+ # emulation, aarch64-on-aarch64 hosting beats x86_64 by a wide
+ # margin.
+ - os: ubuntu-24.04-arm
qemu: armv7l
musl: ""
- - os: ubuntu-latest
+ - os: ubuntu-24.04-arm
qemu: armv7l
musl: musllinux
- os: ubuntu-latest
diff --git a/CHANGES/12655.contrib.rst b/CHANGES/12655.contrib.rst
new file mode 100644
index 00000000000..f5c76cd4105
--- /dev/null
+++ b/CHANGES/12655.contrib.rst
@@ -0,0 +1,4 @@
+Switched the armv7l wheel builds onto GitHub's hosted ARM runners. The
+32-bit ARM build still runs under QEMU, but the host is now aarch64
+rather than x86_64, so the emulation overhead drops sharply
+-- by :user:`bdraco`.
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 8fab90d3f7b..9773d61ab3e 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -1,3 +1,4 @@
+aarch
abc
ABI
addons
From ec5c3fc2054041f9c30d7a89682c0f4c7dcb854f Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 22:09:39 +0000
Subject: [PATCH 137/191] [PR #12655/35a310d5 backport][3.14] ci: build armv7l
wheels on native ARM runners (#12657)
Co-authored-by: J. Nick Koston
---
.github/workflows/ci-cd.yml | 9 +++++++--
CHANGES/12655.contrib.rst | 4 ++++
docs/spelling_wordlist.txt | 1 +
3 files changed, 12 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12655.contrib.rst
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 626731fcf08..67923da42c0 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -534,10 +534,15 @@ jobs:
- os: ubuntu-latest
qemu: s390x
musl: musllinux
- - os: ubuntu-latest
+ # armv7l builds on aarch64 hosts. We still register QEMU so
+ # binfmt picks up the 32-bit ARM userspace handler regardless of
+ # whether the host kernel has CONFIG_COMPAT enabled. Even with
+ # emulation, aarch64-on-aarch64 hosting beats x86_64 by a wide
+ # margin.
+ - os: ubuntu-24.04-arm
qemu: armv7l
musl: ""
- - os: ubuntu-latest
+ - os: ubuntu-24.04-arm
qemu: armv7l
musl: musllinux
- os: ubuntu-latest
diff --git a/CHANGES/12655.contrib.rst b/CHANGES/12655.contrib.rst
new file mode 100644
index 00000000000..f5c76cd4105
--- /dev/null
+++ b/CHANGES/12655.contrib.rst
@@ -0,0 +1,4 @@
+Switched the armv7l wheel builds onto GitHub's hosted ARM runners. The
+32-bit ARM build still runs under QEMU, but the host is now aarch64
+rather than x86_64, so the emulation overhead drops sharply
+-- by :user:`bdraco`.
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index 75719ce0318..c10f10fdce4 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -1,3 +1,4 @@
+aarch
abc
ABI
addons
From 79d80b58a9ce03e35527a6031e41838a3f24a53d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 19 May 2026 22:12:18 +0000
Subject: [PATCH 138/191] Bump yarl from 1.22.0 to 1.24.2 (#12659)
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/runtime-deps.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
7 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 972f93541f8..d15daa320f9 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -47,5 +47,5 @@ typing-extensions==4.15.0 ; python_version < "3.13"
# -r requirements/runtime-deps.in
# aiosignal
# multidict
-yarl==1.22.0
+yarl==1.24.2
# via -r requirements/runtime-deps.in
diff --git a/requirements/base.txt b/requirements/base.txt
index e0edceac79c..eaa71549020 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -49,5 +49,5 @@ typing-extensions==4.15.0 ; python_version < "3.13"
# multidict
uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpython"
# via -r requirements/base.in
-yarl==1.22.0
+yarl==1.24.2
# via -r requirements/runtime-deps.in
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 184499ce80e..676f24d00be 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -295,7 +295,7 @@ wait-for-it==2.3.0
# via -r requirements/test-common.in
wheel==0.47.0
# via pip-tools
-yarl==1.22.0
+yarl==1.24.2
# via -r requirements/runtime-deps.in
zlib-ng==1.0.0
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 0ecd8ce83ae..69f0bc8d4e5 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -285,7 +285,7 @@ wait-for-it==2.3.0
# via -r requirements/test-common.in
wheel==0.47.0
# via pip-tools
-yarl==1.22.0
+yarl==1.24.2
# via -r requirements/runtime-deps.in
zlib-ng==1.0.0
# via
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index 4bba60d46ed..549c5576f4d 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -43,5 +43,5 @@ typing-extensions==4.15.0 ; python_version < "3.13"
# -r requirements/runtime-deps.in
# aiosignal
# multidict
-yarl==1.22.0
+yarl==1.24.2
# via -r requirements/runtime-deps.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index abbf0178d64..a103d39b26b 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -157,7 +157,7 @@ typing-inspection==0.4.2
# via pydantic
wait-for-it==2.3.0
# via -r requirements/test-common.in
-yarl==1.22.0
+yarl==1.24.2
# via -r requirements/runtime-deps.in
zlib-ng==1.0.0
# via -r requirements/test-common.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 396d0469999..6449ddcfb14 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -159,7 +159,7 @@ uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpytho
# via -r requirements/base.in
wait-for-it==2.3.0
# via -r requirements/test-common.in
-yarl==1.22.0
+yarl==1.24.2
# via -r requirements/runtime-deps.in
zlib-ng==1.0.0
# via -r requirements/test-common.in
From 491a818bc42a73862fc2e6d019bb295cf52a343b Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Wed, 20 May 2026 01:52:06 +0100
Subject: [PATCH 139/191] Fix C parser not rejecting response body when none
expected (#10587) (#12660)
(cherry picked from commit f69522cbfe6191bb60a926b5c313590f00349e9a)
---
CHANGES/10587.bugfix.rst | 1 +
aiohttp/_http_parser.pyx | 27 ++++++++++++++-------------
tests/test_http_parser.py | 16 +++++++++++++++-
3 files changed, 30 insertions(+), 14 deletions(-)
create mode 100644 CHANGES/10587.bugfix.rst
diff --git a/CHANGES/10587.bugfix.rst b/CHANGES/10587.bugfix.rst
new file mode 100644
index 00000000000..f60e1d17c5f
--- /dev/null
+++ b/CHANGES/10587.bugfix.rst
@@ -0,0 +1 @@
+Fixed the C parser failing to reject a response with a body when none was expected -- by :user:`Dreamsorcerer`.
diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx
index 146822b4887..344be3123b0 100644
--- a/aiohttp/_http_parser.pyx
+++ b/aiohttp/_http_parser.pyx
@@ -502,11 +502,14 @@ cdef class HttpParser:
upgrade, chunked)
if (
- ULLONG_MAX > self._cparser.content_length > 0 or chunked or
- self._cparser.method == cparser.HTTP_CONNECT or
- (self._cparser.status_code >= 199 and
- self._cparser.content_length == 0 and
- self._read_until_eof)
+ self._response_with_body
+ and (
+ ULLONG_MAX > self._cparser.content_length > 0 or chunked or
+ self._cparser.method == cparser.HTTP_CONNECT or
+ (self._cparser.status_code >= 199 and
+ self._cparser.content_length == 0 and
+ self._read_until_eof)
+ )
):
payload = StreamReader(
self.protocol, timer=self._timer, loop=self._loop,
@@ -518,9 +521,6 @@ cdef class HttpParser:
if encoding is not None and self._auto_decompress:
self._payload = DeflateBuffer(payload, encoding, max_decompress_size=self._limit)
- if not self._response_with_body:
- payload = EMPTY_PAYLOAD
-
self._messages.append((msg, payload))
cdef _on_message_complete(self):
@@ -847,8 +847,9 @@ cdef int cb_on_headers_complete(cparser.llhttp_t* parser) except -1:
else:
if pyparser._upgraded or pyparser._cparser.method == cparser.HTTP_CONNECT:
return 2
- else:
- return 0
+ if not pyparser._response_with_body:
+ return 1
+ return 0
cdef int cb_on_body(cparser.llhttp_t* parser,
@@ -923,7 +924,6 @@ cdef parser_error_from_errno(cparser.llhttp_t* parser, data, pointer):
cparser.HPE_CB_MESSAGE_COMPLETE,
cparser.HPE_CB_CHUNK_HEADER,
cparser.HPE_CB_CHUNK_COMPLETE,
- cparser.HPE_INVALID_CONSTANT,
cparser.HPE_INVALID_HEADER_TOKEN,
cparser.HPE_INVALID_CONTENT_LENGTH,
cparser.HPE_INVALID_CHUNK_SIZE,
@@ -933,8 +933,9 @@ cdef parser_error_from_errno(cparser.llhttp_t* parser, data, pointer):
elif errno == cparser.HPE_INVALID_METHOD:
return BadHttpMethod(error=err_msg)
elif errno in {cparser.HPE_INVALID_STATUS,
- cparser.HPE_INVALID_VERSION}:
- return BadStatusLine(error=err_msg)
+ cparser.HPE_INVALID_VERSION,
+ cparser.HPE_INVALID_CONSTANT}:
+ return BadStatusLine(error=f"Bad status line:\n {err_msg}")
elif errno == cparser.HPE_INVALID_URL:
return InvalidURLError(err_msg)
diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
index 99e952ac882..625c34533bc 100644
--- a/tests/test_http_parser.py
+++ b/tests/test_http_parser.py
@@ -1910,7 +1910,21 @@ def test_parse_payload_response_without_body(
assert payload.is_eof()
-def test_parse_length_payload(response) -> None:
+async def test_parse_payload_response_with_invalid_body(
+ protocol: BaseProtocol,
+ response_cls: type[HttpResponseParser],
+) -> None:
+ loop = asyncio.get_running_loop()
+ parser = response_cls(protocol, loop, 2**16, response_with_body=False)
+ text = (
+ b"HTTP/1.1 200 Ok\r\nTransfer-Encoding: chunked\r\n\r\n"
+ b"7\r\nchunked\r\n0\r\n\r\n"
+ )
+ with pytest.raises(http_exceptions.BadHttpMessage, match="status line"):
+ parser.feed_data(text)[0][0]
+
+
+def test_parse_length_payload(response: HttpResponseParser) -> None:
text = b"HTTP/1.1 200 Ok\r\ncontent-length: 4\r\n\r\n"
msg, payload = response.feed_data(text)[0][0]
assert not payload.is_eof()
From 7dde3a295ed4b4a22cb52cc2580bd3ac91fc1ae7 Mon Sep 17 00:00:00 2001
From: Omkar Kabde
Date: Wed, 20 May 2026 18:24:42 +0530
Subject: [PATCH 140/191] [PR #12615/421eb60cc backport][3.14] Render the
threat model in the docs (#12661)
Co-authored-by: Sam Bull
---
CHANGES/12549.doc.rst | 1 +
CONTRIBUTORS.txt | 1 +
THREAT_MODEL.md | 4 +---
docs/conf.py | 19 +++++++++++++++++++
docs/index.rst | 1 +
docs/threat_model.md | 2 ++
requirements/constraints.txt | 30 +++++++++++++++++++++++++-----
requirements/dev.txt | 30 +++++++++++++++++++++++++-----
requirements/doc-spelling.txt | 24 +++++++++++++++++++++++-
requirements/doc.in | 2 ++
requirements/doc.txt | 24 +++++++++++++++++++++++-
11 files changed, 123 insertions(+), 15 deletions(-)
create mode 100644 CHANGES/12549.doc.rst
create mode 100644 docs/threat_model.md
diff --git a/CHANGES/12549.doc.rst b/CHANGES/12549.doc.rst
new file mode 100644
index 00000000000..0288c33cba5
--- /dev/null
+++ b/CHANGES/12549.doc.rst
@@ -0,0 +1 @@
+Added the :doc:`threat_model` to the Sphinx documentation -- by :user:`omkar-334`.
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 530b6683d54..4180c590f44 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -283,6 +283,7 @@ Nikolay Tiunov
Nándor Mátravölgyi
Oisin Aylward
Olaf Conradi
+Omkar Kabde
Pahaz Blinov
Panagiotis Kolokotronis
Pankaj Pandey
diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
index 1c10ce1e219..3f48765e249 100644
--- a/THREAT_MODEL.md
+++ b/THREAT_MODEL.md
@@ -1,4 +1,4 @@
-# aiohttp Threat Model
+# Threat Model
This document is a STRIDE-based threat model for the
[aiohttp](https://github.com/aio-libs/aiohttp) library. It is a living document
@@ -310,5 +310,3 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and
request parser (strict).
These are all currently in place; this section assumes no regression.
-
----
diff --git a/docs/conf.py b/docs/conf.py
index 184ad7816cf..c5a4b921a40 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -59,6 +59,8 @@
"sphinx.ext.intersphinx",
"sphinx.ext.viewcode",
# Third-party extensions:
+ "myst_parser", # renders Markdown sources (e.g. ``THREAT_MODEL.md``)
+ "sphinxcontrib.mermaid", # renders the Mermaid flowcharts in ``THREAT_MODEL.md``
"sphinxcontrib.towncrier.ext", # provides `towncrier-draft-entries` directive
]
@@ -70,6 +72,23 @@
except ImportError:
pass
+spelling_exclude_patterns = [
+ # THREAT_MODEL.md is already spell-checked by the codespell pre-commit hook.
+ # The spelling builder additionally mis-tokenises its ``**S**poofing`` STRIDE
+ # list into non-words, so skip the threat model here to avoid double coverage.
+ "threat_model.md",
+]
+
+
+# -- MyST (Markdown) configuration ----------------------------------------
+
+# ``THREAT_MODEL.md`` lives at the repo root and is surfaced through
+# ``docs/threat_model.md``.
+myst_heading_anchors = 3 # anchors for h1-h3 so the in-page section links work
+myst_fence_as_directive = ["mermaid"] # render ```mermaid fences as diagrams
+
+# TODO: Remove this option once THREAT_MODEL.md is complete.
+suppress_warnings = ["myst.xref_missing"]
intersphinx_mapping = {
"pytest": ("http://docs.pytest.org/en/latest/", None),
diff --git a/docs/index.rst b/docs/index.rst
index f9c4a4b2c54..8387ae186b5 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -233,3 +233,4 @@ Table Of Contents
misc
external
contributing
+ threat_model
diff --git a/docs/threat_model.md b/docs/threat_model.md
new file mode 100644
index 00000000000..f585a70ff9e
--- /dev/null
+++ b/docs/threat_model.md
@@ -0,0 +1,2 @@
+```{include} ../THREAT_MODEL.md
+```
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 676f24d00be..7187b799f9f 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -25,7 +25,9 @@ async-timeout==5.0.1 ; python_version < "3.11"
# -r requirements/runtime-deps.in
# valkey
attrs==26.1.0
- # via -r requirements/runtime-deps.in
+ # via
+ # -r requirements/runtime-deps.in
+ # myst-parser
babel==2.18.0
# via sphinx
backports-zstd==1.3.0 ; implementation_name == "cpython" and python_version < "3.14"
@@ -67,7 +69,9 @@ cython==3.2.4
distlib==0.4.0
# via virtualenv
docutils==0.21.2
- # via sphinx
+ # via
+ # myst-parser
+ # sphinx
exceptiongroup==1.3.1
# via pytest
execnet==2.1.2
@@ -105,14 +109,21 @@ isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython"
# -r requirements/test-common.in
jinja2==3.1.6
# via
+ # myst-parser
# sphinx
+ # sphinxcontrib-mermaid
# towncrier
librt==0.11.0
# via mypy
-markdown-it-py==4.2.0
- # via rich
+markdown-it-py==3.0.0
+ # via
+ # mdit-py-plugins
+ # myst-parser
+ # rich
markupsafe==3.0.3
# via jinja2
+mdit-py-plugins==0.6.1
+ # via myst-parser
mdurl==0.1.2
# via markdown-it-py
multidict==6.7.1
@@ -126,6 +137,8 @@ mypy==2.1.0 ; implementation_name == "cpython"
# -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
+myst-parser==4.0.1
+ # via -r requirements/doc.in
nodeenv==1.10.0
# via pre-commit
packaging==26.2
@@ -208,7 +221,10 @@ python-on-whales==0.81.0
# -r requirements/lint.in
# -r requirements/test-common.in
pyyaml==6.0.3
- # via pre-commit
+ # via
+ # myst-parser
+ # pre-commit
+ # sphinxcontrib-mermaid
re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.5.9
@@ -230,6 +246,8 @@ snowballstemmer==3.0.1
sphinx==8.1.3
# via
# -r requirements/doc.in
+ # myst-parser
+ # sphinxcontrib-mermaid
# sphinxcontrib-spelling
# sphinxcontrib-towncrier
sphinxcontrib-applehelp==2.0.0
@@ -240,6 +258,8 @@ sphinxcontrib-htmlhelp==2.1.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
+sphinxcontrib-mermaid==2.0.2
+ # via -r requirements/doc.in
sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 69f0bc8d4e5..fbc7bcf0649 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -25,7 +25,9 @@ async-timeout==5.0.1 ; python_version < "3.11"
# -r requirements/runtime-deps.in
# valkey
attrs==26.1.0
- # via -r requirements/runtime-deps.in
+ # via
+ # -r requirements/runtime-deps.in
+ # myst-parser
babel==2.18.0
# via sphinx
backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
@@ -65,7 +67,9 @@ cryptography==48.0.0
distlib==0.4.0
# via virtualenv
docutils==0.21.2
- # via sphinx
+ # via
+ # myst-parser
+ # sphinx
exceptiongroup==1.3.1
# via pytest
execnet==2.1.2
@@ -103,14 +107,21 @@ isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython"
# -r requirements/test-common.in
jinja2==3.1.6
# via
+ # myst-parser
# sphinx
+ # sphinxcontrib-mermaid
# towncrier
librt==0.11.0
# via mypy
-markdown-it-py==4.2.0
- # via rich
+markdown-it-py==3.0.0
+ # via
+ # mdit-py-plugins
+ # myst-parser
+ # rich
markupsafe==3.0.3
# via jinja2
+mdit-py-plugins==0.6.1
+ # via myst-parser
mdurl==0.1.2
# via markdown-it-py
multidict==6.7.1
@@ -123,6 +134,8 @@ mypy==2.1.0 ; implementation_name == "cpython"
# -r requirements/test-common.in
mypy-extensions==1.1.0
# via mypy
+myst-parser==4.0.1
+ # via -r requirements/doc.in
nodeenv==1.10.0
# via pre-commit
packaging==26.2
@@ -203,7 +216,10 @@ python-on-whales==0.81.0
# -r requirements/lint.in
# -r requirements/test-common.in
pyyaml==6.0.3
- # via pre-commit
+ # via
+ # myst-parser
+ # pre-commit
+ # sphinxcontrib-mermaid
re-assert==1.1.0
# via -r requirements/test-common.in
regex==2026.5.9
@@ -223,6 +239,8 @@ snowballstemmer==3.0.1
sphinx==8.1.3
# via
# -r requirements/doc.in
+ # myst-parser
+ # sphinxcontrib-mermaid
# sphinxcontrib-towncrier
sphinxcontrib-applehelp==2.0.0
# via sphinx
@@ -232,6 +250,8 @@ sphinxcontrib-htmlhelp==2.1.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
+sphinxcontrib-mermaid==2.0.2
+ # via -r requirements/doc.in
sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index eb528786532..e653ba3adea 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -17,23 +17,41 @@ charset-normalizer==3.4.7
click==8.4.0
# via towncrier
docutils==0.21.2
- # via sphinx
+ # via
+ # myst-parser
+ # sphinx
idna==3.15
# via requests
imagesize==2.0.0
# via sphinx
jinja2==3.1.6
# via
+ # myst-parser
# sphinx
+ # sphinxcontrib-mermaid
# towncrier
+markdown-it-py==3.0.0
+ # via
+ # mdit-py-plugins
+ # myst-parser
markupsafe==3.0.3
# via jinja2
+mdit-py-plugins==0.6.1
+ # via myst-parser
+mdurl==0.1.2
+ # via markdown-it-py
+myst-parser==4.0.1
+ # via -r requirements/doc.in
packaging==26.2
# via sphinx
pyenchant==3.3.0
# via sphinxcontrib-spelling
pygments==2.20.0
# via sphinx
+pyyaml==6.0.3
+ # via
+ # myst-parser
+ # sphinxcontrib-mermaid
requests==2.34.2
# via
# sphinx
@@ -43,6 +61,8 @@ snowballstemmer==3.0.1
sphinx==8.1.3
# via
# -r requirements/doc.in
+ # myst-parser
+ # sphinxcontrib-mermaid
# sphinxcontrib-spelling
# sphinxcontrib-towncrier
sphinxcontrib-applehelp==2.0.0
@@ -53,6 +73,8 @@ sphinxcontrib-htmlhelp==2.1.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
+sphinxcontrib-mermaid==2.0.2
+ # via -r requirements/doc.in
sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
diff --git a/requirements/doc.in b/requirements/doc.in
index 15017b083d3..7106c3808d9 100644
--- a/requirements/doc.in
+++ b/requirements/doc.in
@@ -1,4 +1,6 @@
aiohttp-theme
+myst-parser
sphinx
+sphinxcontrib-mermaid
sphinxcontrib-towncrier
towncrier
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 64ba54abc64..8466dd55845 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -17,21 +17,39 @@ charset-normalizer==3.4.7
click==8.4.0
# via towncrier
docutils==0.21.2
- # via sphinx
+ # via
+ # myst-parser
+ # sphinx
idna==3.15
# via requests
imagesize==2.0.0
# via sphinx
jinja2==3.1.6
# via
+ # myst-parser
# sphinx
+ # sphinxcontrib-mermaid
# towncrier
+markdown-it-py==3.0.0
+ # via
+ # mdit-py-plugins
+ # myst-parser
markupsafe==3.0.3
# via jinja2
+mdit-py-plugins==0.6.1
+ # via myst-parser
+mdurl==0.1.2
+ # via markdown-it-py
+myst-parser==4.0.1
+ # via -r requirements/doc.in
packaging==26.2
# via sphinx
pygments==2.20.0
# via sphinx
+pyyaml==6.0.3
+ # via
+ # myst-parser
+ # sphinxcontrib-mermaid
requests==2.34.2
# via sphinx
snowballstemmer==3.0.1
@@ -39,6 +57,8 @@ snowballstemmer==3.0.1
sphinx==8.1.3
# via
# -r requirements/doc.in
+ # myst-parser
+ # sphinxcontrib-mermaid
# sphinxcontrib-towncrier
sphinxcontrib-applehelp==2.0.0
# via sphinx
@@ -48,6 +68,8 @@ sphinxcontrib-htmlhelp==2.1.0
# via sphinx
sphinxcontrib-jsmath==1.0.1
# via sphinx
+sphinxcontrib-mermaid==2.0.2
+ # via -r requirements/doc.in
sphinxcontrib-qthelp==2.0.0
# via sphinx
sphinxcontrib-serializinghtml==2.0.0
From 79bdecdbe330b6a7925fc81d12d1cdb11a0af02f Mon Sep 17 00:00:00 2001
From: "J. Nick Koston"
Date: Wed, 20 May 2026 11:56:28 -0500
Subject: [PATCH 141/191] [PR #12174/ca9d73dd backport][3.14] feat: add cookies
and host_only_cookies properties to CookieJar (#12662)
Co-authored-by: Br1an <932039080@qq.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Fixes #3951
---
CHANGES/3951.feature.rst | 1 +
CONTRIBUTORS.txt | 1 +
aiohttp/abc.py | 13 +++++++++-
aiohttp/cookiejar.py | 21 ++++++++++++++++
docs/client_reference.rst | 15 +++++++++++
tests/test_client_session.py | 11 ++++++++
tests/test_cookiejar.py | 49 ++++++++++++++++++++++++++++++++++++
7 files changed, 110 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/3951.feature.rst
diff --git a/CHANGES/3951.feature.rst b/CHANGES/3951.feature.rst
new file mode 100644
index 00000000000..ba127c20a83
--- /dev/null
+++ b/CHANGES/3951.feature.rst
@@ -0,0 +1 @@
+Added :py:attr:`~aiohttp.CookieJar.cookies` and :py:attr:`~aiohttp.CookieJar.host_only_cookies` read-only properties to :py:class:`~aiohttp.CookieJar` exposing the stored cookies with their full attributes -- by :user:`Br1an67`.
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 4180c590f44..ba7ce91cec1 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -65,6 +65,7 @@ Benedikt Reinartz
Bob Haddleton
Boris Feld
Boyi Chen
+Br1an67
Brett Cannon
Brett Higgins
Brian Bouterse
diff --git a/aiohttp/abc.py b/aiohttp/abc.py
index d5dcdc00233..b67287318a9 100644
--- a/aiohttp/abc.py
+++ b/aiohttp/abc.py
@@ -3,7 +3,8 @@
import socket
from abc import ABC, abstractmethod
from collections.abc import Awaitable, Callable, Generator, Iterable, Sequence, Sized
-from http.cookies import BaseCookie, Morsel
+from http.cookies import BaseCookie, Morsel, SimpleCookie
+from types import MappingProxyType
from typing import TYPE_CHECKING, Any, TypedDict
from multidict import CIMultiDict
@@ -173,6 +174,16 @@ def unsafe(self) -> bool:
def quote_cookie(self) -> bool:
"""Return True if cookies should be quoted."""
+ @property
+ @abstractmethod
+ def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]:
+ """Return the cookies stored in this jar."""
+
+ @property
+ @abstractmethod
+ def host_only_cookies(self) -> frozenset[tuple[str, str]]:
+ """Return the host-only cookies stored in this jar."""
+
@abstractmethod
def clear(self, predicate: ClearCookiePredicate | None = None) -> None:
"""Clear all cookies if no predicate is passed."""
diff --git a/aiohttp/cookiejar.py b/aiohttp/cookiejar.py
index cd19c9e79cc..e1579c0ed4c 100644
--- a/aiohttp/cookiejar.py
+++ b/aiohttp/cookiejar.py
@@ -14,6 +14,7 @@
from collections import defaultdict
from collections.abc import Iterable, Iterator, Mapping
from http.cookies import BaseCookie, Morsel, SimpleCookie
+from types import MappingProxyType
from typing import Union
from yarl import URL
@@ -156,6 +157,16 @@ def unsafe(self) -> bool:
def quote_cookie(self) -> bool:
return self._quote_cookie
+ @property
+ def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]:
+ """Return the cookies stored in this jar."""
+ return MappingProxyType(self._cookies)
+
+ @property
+ def host_only_cookies(self) -> frozenset[tuple[str, str]]:
+ """Return the host-only cookies stored in this jar."""
+ return frozenset(self._host_only_cookies)
+
def save(self, file_path: PathLike) -> None:
"""Save cookies to a file using JSON format.
@@ -626,6 +637,16 @@ def unsafe(self) -> bool:
def quote_cookie(self) -> bool:
return True
+ @property
+ def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]:
+ """Return an empty mapping."""
+ return MappingProxyType({})
+
+ @property
+ def host_only_cookies(self) -> frozenset[tuple[str, str]]:
+ """Return an empty frozenset."""
+ return frozenset()
+
def clear(self, predicate: ClearCookiePredicate | None = None) -> None:
pass
diff --git a/docs/client_reference.rst b/docs/client_reference.rst
index 1b1a29072c7..860e6fba119 100644
--- a/docs/client_reference.rst
+++ b/docs/client_reference.rst
@@ -2586,6 +2586,21 @@ Utilities
.. versionadded:: 4.0
+ .. attribute:: cookies
+
+ A read-only view of the jar's cookies as a
+ :class:`~types.MappingProxyType` mapping ``(domain, path)`` tuples
+ to :class:`~http.cookies.SimpleCookie` instances.
+
+ .. versionadded:: 3.14
+
+ .. attribute:: host_only_cookies
+
+ A :class:`frozenset` of ``(domain, name)`` tuples indicating which
+ cookies are host-only (not sent to subdomains).
+
+ .. versionadded:: 3.14
+
.. class:: DummyCookieJar(*, loop=None)
diff --git a/tests/test_client_session.py b/tests/test_client_session.py
index 1060d1f2516..be9e77dfcc0 100644
--- a/tests/test_client_session.py
+++ b/tests/test_client_session.py
@@ -8,6 +8,7 @@
from collections import deque
from collections.abc import Awaitable, Callable, Iterator
from http.cookies import BaseCookie, SimpleCookie
+from types import MappingProxyType
from typing import Any, cast
from unittest import mock
from uuid import uuid4
@@ -755,6 +756,14 @@ def unsafe(self) -> bool:
def quote_cookie(self) -> bool:
return True
+ @property
+ def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]:
+ return MappingProxyType({})
+
+ @property
+ def host_only_cookies(self) -> frozenset[tuple[str, str]]:
+ return frozenset()
+
def clear(self, predicate: abc.ClearCookiePredicate | None = None) -> None:
self._clear_mock(predicate)
@@ -777,6 +786,8 @@ def __iter__(self) -> Iterator[Any]:
assert jar.quote_cookie is True
assert jar.unsafe is False
+ assert jar.cookies == MappingProxyType({})
+ assert jar.host_only_cookies == frozenset()
assert len(jar) == 0
assert list(jar) == []
jar.clear()
diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py
index d3001191fbd..6dac3a0adb4 100644
--- a/tests/test_cookiejar.py
+++ b/tests/test_cookiejar.py
@@ -12,6 +12,7 @@
from http.cookies import BaseCookie, Morsel, SimpleCookie
from operator import not_
from pathlib import Path
+from types import MappingProxyType
from unittest import mock
import pytest
@@ -840,6 +841,54 @@ async def test_dummy_cookie_jar() -> None:
dummy_jar.clear()
+async def test_dummy_cookie_jar_cookies_property() -> None:
+ dummy_jar = DummyCookieJar()
+ assert dict(dummy_jar.cookies) == {}
+ assert dummy_jar.host_only_cookies == frozenset()
+
+
+async def test_cookie_jar_cookies_property() -> None:
+ jar = CookieJar()
+ cookie = SimpleCookie(
+ "shared-cookie=first; domain-cookie=second; Domain=example.com; Path=/; "
+ )
+ jar.update_cookies(cookie, URL("http://example.com/"))
+
+ cookies = jar.cookies
+ # Should be a read-only view
+ assert isinstance(cookies, MappingProxyType)
+ # Should contain the stored cookies with their full attributes
+ found_names = {name for simple_cookie in cookies.values() for name in simple_cookie}
+ assert "shared-cookie" in found_names
+ assert "domain-cookie" in found_names
+ # Verify that domain attribute is preserved
+ for key, simple_cookie in cookies.items():
+ for name, morsel in simple_cookie.items():
+ if name == "domain-cookie":
+ assert morsel["domain"] == "example.com"
+ assert morsel["path"] == "/"
+
+
+async def test_cookie_jar_host_only_cookies_property() -> None:
+ jar = CookieJar()
+ # Cookies without an explicit Domain attribute are host-only
+ cookie = SimpleCookie("hostonly=value;")
+ jar.update_cookies(cookie, URL("http://example.com/"))
+
+ host_only = jar.host_only_cookies
+ assert isinstance(host_only, frozenset)
+ assert ("example.com", "hostonly") in host_only
+
+
+async def test_cookie_jar_cookies_property_immutable() -> None:
+ jar = CookieJar()
+ cookie = SimpleCookie("foo=bar;")
+ jar.update_cookies(cookie, URL("http://example.com/"))
+ cookies = jar.cookies
+ with pytest.raises(TypeError):
+ cookies[("new", "key")] = SimpleCookie() # type: ignore[index]
+
+
async def test_loose_cookies_types() -> None:
jar = CookieJar()
From 56562d025cd5dea41e213735b4892cce8fdaa5b1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 20 May 2026 17:10:45 +0000
Subject: [PATCH 142/191] Bump aiodns from 4.0.3 to 4.0.4 (#12663)
Bumps [aiodns](https://github.com/aio-libs/aiodns) from 4.0.3 to 4.0.4.
Changelog
Sourced from aiodns's
changelog.
4.0.4
- Raise
DNSError(ARES_ENODATA) from query()
when the answer section has no records of the requested qtype, restoring
the pycares 4.x NODATA contract and avoiding AttributeError
for CNAME/SOA/PTR callers (#254).
- Add the missing
build-backend entry to
pyproject.toml so PEP 517 builds from the sdist work
without falling back to the deprecated legacy setuptools backend (#252).
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 6 ++----
requirements/dev.txt | 6 ++----
requirements/lint.txt | 2 +-
requirements/runtime-deps.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
8 files changed, 10 insertions(+), 14 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index d15daa320f9..55b6049b556 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/base-ft.txt --strip-extras requirements/base-ft.in
#
-aiodns==4.0.3
+aiodns==4.0.4
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
diff --git a/requirements/base.txt b/requirements/base.txt
index eaa71549020..640c58a664d 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/base.txt --strip-extras requirements/base.in
#
-aiodns==4.0.3
+aiodns==4.0.4
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 7187b799f9f..9b1c4426589 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/constraints.txt --strip-extras requirements/constraints.in
#
-aiodns==4.0.3
+aiodns==4.0.4
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
@@ -25,9 +25,7 @@ async-timeout==5.0.1 ; python_version < "3.11"
# -r requirements/runtime-deps.in
# valkey
attrs==26.1.0
- # via
- # -r requirements/runtime-deps.in
- # myst-parser
+ # via -r requirements/runtime-deps.in
babel==2.18.0
# via sphinx
backports-zstd==1.3.0 ; implementation_name == "cpython" and python_version < "3.14"
diff --git a/requirements/dev.txt b/requirements/dev.txt
index fbc7bcf0649..b4b7a5d52f3 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/dev.txt --strip-extras requirements/dev.in
#
-aiodns==4.0.3
+aiodns==4.0.4
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
@@ -25,9 +25,7 @@ async-timeout==5.0.1 ; python_version < "3.11"
# -r requirements/runtime-deps.in
# valkey
attrs==26.1.0
- # via
- # -r requirements/runtime-deps.in
- # myst-parser
+ # via -r requirements/runtime-deps.in
babel==2.18.0
# via sphinx
backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
diff --git a/requirements/lint.txt b/requirements/lint.txt
index ea061166130..c212712be39 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/lint.txt --resolver=backtracking --strip-extras requirements/lint.in
#
-aiodns==4.0.3
+aiodns==4.0.4
# via -r requirements/lint.in
annotated-types==0.7.0
# via pydantic
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index 549c5576f4d..8d2d7508455 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/runtime-deps.txt --strip-extras requirements/runtime-deps.in
#
-aiodns==4.0.3
+aiodns==4.0.4
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index a103d39b26b..86a2d7bf908 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/test-ft.txt --strip-extras requirements/test-ft.in
#
-aiodns==4.0.3
+aiodns==4.0.4
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 6449ddcfb14..6237c964ef8 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/test.txt --strip-extras requirements/test.in
#
-aiodns==4.0.3
+aiodns==4.0.4
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.1
# via -r requirements/runtime-deps.in
From 40a4ddcdb140dd85b82da16a1b4305c7a4f472f1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 20 May 2026 17:29:10 +0000
Subject: [PATCH 143/191] Bump certifi from 2026.4.22 to 2026.5.20 (#12668)
Bumps [certifi](https://github.com/certifi/python-certifi) from
2026.4.22 to 2026.5.20.
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 9b1c4426589..c4bb0a97c8a 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -40,7 +40,7 @@ brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
build==1.5.0
# via pip-tools
-certifi==2026.4.22
+certifi==2026.5.20
# via requests
cffi==2.0.0
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index b4b7a5d52f3..6a33929dfae 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -40,7 +40,7 @@ brotli==1.2.0 ; platform_python_implementation == "CPython"
# via -r requirements/runtime-deps.in
build==1.5.0
# via pip-tools
-certifi==2026.4.22
+certifi==2026.5.20
# via requests
cffi==2.0.0
# via
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index e653ba3adea..b196c4967d9 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -10,7 +10,7 @@ alabaster==1.0.0
# via sphinx
babel==2.18.0
# via sphinx
-certifi==2026.4.22
+certifi==2026.5.20
# via requests
charset-normalizer==3.4.7
# via requests
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 8466dd55845..8ae2412c091 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -10,7 +10,7 @@ alabaster==1.0.0
# via sphinx
babel==2.18.0
# via sphinx
-certifi==2026.4.22
+certifi==2026.5.20
# via requests
charset-normalizer==3.4.7
# via requests
From 1401ebd0c5a666a39ceddcb84b1fa3cadd36f047 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 20 May 2026 17:50:28 +0000
Subject: [PATCH 144/191] Bump aiohappyeyeballs from 2.6.1 to 2.6.2 (#12665)
Bumps [aiohappyeyeballs](https://github.com/aio-libs/aiohappyeyeballs)
from 2.6.1 to 2.6.2.
Release notes
Sourced from aiohappyeyeballs's
releases.
v2.6.2 (2026-05-20)
Bug Fixes
- Clear error on empty addr_infos in start_connection (#222,
55dd76a)
Refactoring
- Optimize obtaining event-loop down to 1 line (#185,
02a7029)
Testing
- Stop verify_no_lingering_tasks from leaking an event loop (#221,
ce43beb)
Detailed Changes: v2.6.1...v2.6.2
Changelog
Sourced from aiohappyeyeballs's
changelog.
v2.6.2 (2026-05-20)
Bug fixes
- Clear error on empty addr_infos in start_connection (
55dd76a)
Testing
- Stop verify_no_lingering_tasks from leaking an event loop (
ce43beb)
Refactoring
- Optimize obtaining event-loop down to 1 line (
02a7029)
Commits
d779d62
2.6.2
55dd76a
fix: clear error on empty addr_infos in start_connection (#222)
ce43beb
test: stop verify_no_lingering_tasks from leaking an event loop (#221)
184079b
chore(deps-ci): bump the github-actions group across 1 directory with 7
updat...
d9b0fa6
chore(deps-dev): bump requests from 2.32.4 to 2.33.0 (#219)
09dcc92
chore(deps-dev): bump urllib3 from 2.5.0 to 2.7.0 (#220)
a0a313a
chore(deps-dev): bump cryptography from 43.0.3 to 46.0.7 (#216)
b5dec0c
ci: replace per-commit conventional commits check with pr title check
(#218)
363b722
chore(pre-commit.ci): pre-commit autoupdate (#202)
cc07391
chore: drop Python 3.9 support (#215)
- Additional commits viewable in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/runtime-deps.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
7 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 55b6049b556..2592806157c 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -6,7 +6,7 @@
#
aiodns==4.0.4
# via -r requirements/runtime-deps.in
-aiohappyeyeballs==2.6.1
+aiohappyeyeballs==2.6.2
# via -r requirements/runtime-deps.in
aiosignal==1.4.0
# via -r requirements/runtime-deps.in
diff --git a/requirements/base.txt b/requirements/base.txt
index 640c58a664d..e3c17b4f01d 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -6,7 +6,7 @@
#
aiodns==4.0.4
# via -r requirements/runtime-deps.in
-aiohappyeyeballs==2.6.1
+aiohappyeyeballs==2.6.2
# via -r requirements/runtime-deps.in
aiosignal==1.4.0
# via -r requirements/runtime-deps.in
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index c4bb0a97c8a..bb974de91b7 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -8,7 +8,7 @@ aiodns==4.0.4
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
-aiohappyeyeballs==2.6.1
+aiohappyeyeballs==2.6.2
# via -r requirements/runtime-deps.in
aiohttp-theme==0.1.7
# via -r requirements/doc.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 6a33929dfae..9a67a45909e 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -8,7 +8,7 @@ aiodns==4.0.4
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
-aiohappyeyeballs==2.6.1
+aiohappyeyeballs==2.6.2
# via -r requirements/runtime-deps.in
aiohttp-theme==0.1.7
# via -r requirements/doc.in
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index 8d2d7508455..d83995a61af 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -6,7 +6,7 @@
#
aiodns==4.0.4
# via -r requirements/runtime-deps.in
-aiohappyeyeballs==2.6.1
+aiohappyeyeballs==2.6.2
# via -r requirements/runtime-deps.in
aiosignal==1.4.0
# via -r requirements/runtime-deps.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 86a2d7bf908..825a2767520 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -6,7 +6,7 @@
#
aiodns==4.0.4
# via -r requirements/runtime-deps.in
-aiohappyeyeballs==2.6.1
+aiohappyeyeballs==2.6.2
# via -r requirements/runtime-deps.in
aiosignal==1.4.0
# via -r requirements/runtime-deps.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 6237c964ef8..9c51a06bba0 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -6,7 +6,7 @@
#
aiodns==4.0.4
# via -r requirements/runtime-deps.in
-aiohappyeyeballs==2.6.1
+aiohappyeyeballs==2.6.2
# via -r requirements/runtime-deps.in
aiosignal==1.4.0
# via -r requirements/runtime-deps.in
From 677797fb1d6e3e37ab3c5829baf4f7c2bc3dc620 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Thu, 21 May 2026 21:45:58 +0100
Subject: [PATCH 145/191] [PR #7049/cbf25531 backport][3.14] add full apache
license (#12675)
**This is a backport of PR #7049 as merged into master
(cbf255319921ba84a115d123f7304f9f813e1139).**
Co-authored-by: Lukas Kahwe Smith
---
LICENSE.txt | 190 +++++++++++++++++++++++++++++++++++++++++++++++++++-
README.rst | 5 --
2 files changed, 189 insertions(+), 6 deletions(-)
diff --git a/LICENSE.txt b/LICENSE.txt
index e497a322f20..0b2f7b0655f 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,192 @@
- Copyright aio-libs contributors.
+Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright aio-libs contributors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.rst b/README.rst
index e6f428640da..d5c8dd835bc 100644
--- a/README.rst
+++ b/README.rst
@@ -171,11 +171,6 @@ Optionally you may install the aiodns_ library (highly recommended for sake of s
.. _yarl: https://pypi.python.org/pypi/yarl
.. _async-timeout: https://pypi.python.org/pypi/async_timeout
-License
-=======
-
-``aiohttp`` is offered under the Apache 2 license.
-
Keepsafe
========
From 18be3b9abe71fe1fde99596ba2cdfe3612960529 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Fri, 22 May 2026 00:40:57 +0100
Subject: [PATCH 146/191] [PR #12551/feb0405a backport][3.14] Threat model
chapter 2 (#12676)
**This is a backport of PR #12551 as merged into master
(feb0405a8e93a016cf352a9adbdf6487ae1b6b92).**
Co-authored-by: Sam Bull
---
THREAT_MODEL.md | 133 +++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 126 insertions(+), 7 deletions(-)
diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
index 3f48765e249..cb50d264489 100644
--- a/THREAT_MODEL.md
+++ b/THREAT_MODEL.md
@@ -6,6 +6,11 @@ intended to (a) make explicit the implicit security assumptions baked into the
codebase, (b) catalogue known classes of threat against each subsystem, and
(c) record the existing and recommended mitigations.
+Some mitigations are expected to be in the application code built on top
+of aiohttp. Recommendations addressed to application authors rather than
+to aiohttp maintainers are prefixed **User:** to make the responsibility
+explicit.
+
---
## 1. Library Overview
@@ -255,14 +260,14 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and
| 1.1 | Smuggling via duplicate framing headers | llhttp rejects conflicting `Content-Length`. `http_parser.py:HttpRequestParserPy.parse_headers` rejects coexistence of CL + `Transfer-Encoding: chunked`. The full `SINGLETON_HEADERS` set (CL, CT, Host, TE, ETag, etc.) is duplicate-rejected by the request parser (strict mode); `#12302` disabled this check on the response parser (lax mode), since real-world servers commonly send duplicate `Content-Type` / `Server`. | If new singleton-sensitive headers emerge in HTTP/1.1 RFC errata, add to `SINGLETON_HEADERS`. |
| 1.2 | Lenient response parsing | Lenient flags (`llhttp_set_lenient_headers`, `llhttp_set_lenient_optional_cr_before_lf`, `llhttp_set_lenient_spaces_after_chunk_size`) are only enabled on the **response** parser and only when `DEBUG` is False (set in `HttpResponseParser.__init__`). The **request** parser is strict. | Documented design decision: keep lenient response parsing for real-world server interop |
| 1.3 | CRLF / NUL in header values | Bytes `\r`, `\n`, `\x00` rejected in header values (`_http_parser.pyx` callbacks; `http_parser.py:HeadersParser.parse_headers`). | Keep regression tests in `tests/test_http_parser.py` covering each forbidden byte both in name and value, and across both Cython and pure-Python parsers. |
-| 1.4 | Non-UTF-8 round-trip | None at parser layer (intentional — preserving original bytes is required for some use cases). | **Document in user-facing docs that header values are bytes-preserving; warn against reflecting headers verbatim into responses, logs, or sub-requests without re-validation.** |
+| 1.4 | Non-UTF-8 round-trip | None at parser layer (intentional — preserving original bytes is required for some use cases). | **Document in user-facing docs that header values are bytes-preserving.** **User**: Re-validate any header value before reflecting it into responses, logs, or sub-requests. |
| 1.5 | HTTP version regex accepts 0.9 / 2.0 | None (regex is permissive). | **Tighten `VERSRE` (and llhttp configuration if possible) to reject anything outside `HTTP/1.0` and `HTTP/1.1`.** |
-| 1.6 | Method-case round-trip | Method token validated by regex; not canonicalised. | **Document that user route handlers / authorization checks should compare methods case-sensitively to the canonical RFC tokens, or use the framework's `web.RouteTableDef` decorators which already match canonical methods.** |
+| 1.6 | Method-case round-trip | Method token validated by regex; not canonicalised. | **Document the asymmetry.** **User**: Compare HTTP methods case-sensitively to canonical RFC tokens, or use `web.RouteTableDef` decorators (which already match canonical methods). |
| 1.7 | `Content-Length` parsing | llhttp validates CL is decimal and non-negative; pure-Python parser validates via `DIGITS.fullmatch(r"\d+")` before `int(...)`, rejecting `+`/`-`/non-ASCII-digit forms (`test_bad_headers`, `test_headers_content_length_err_*` cover these). | None. Cross-backend parity is covered by the shared parser tests. |
| 1.8 | `Transfer-Encoding` lenience | `_is_chunked_te` requires `chunked` to be the last value; duplicate `chunked` rejected (`#10611`). Request parser strict. | None. |
| 1.9 | Chunk-size DoS | The parser doesn't cap chunk size, but **server-side body length is bounded by `client_max_size` (default `1 MiB`)** in `web_request.py:BaseRequest.read`. Client-side responses are bounded by user-supplied `max_body_size` / streaming reads. | None. If a cap is ever needed at the parser level, plumb it through `HttpPayloadParser`. |
| 1.10 | Chunk-extension DoS | Chunk-extension content is bounded by the same wire-level size constraints (it shares the chunk-size line with `max_line_size`). | **Add an explicit test that chunk-extension flooding cannot blow past `max_line_size`.** |
-| 1.11 | Parser error reflection | `http_parser.py` truncates to `[:100]` for line errors. Server-side error path renders 4xx with the exception message; tracebacks only when `DEBUG=True`. | **Audit any path where `BadHttpMessage` content is reflected to the client unsanitised (especially in custom `web_log` configurations).** |
+| 1.11 | Parser error reflection | `http_parser.py` truncates to `[:100]` for line errors. Server-side error path renders 4xx with the exception message; tracebacks only when `DEBUG=True`. | **Audit any aiohttp path where `BadHttpMessage` content is reflected to the client unsanitised.** **User**: Review custom `web_log` configurations and any middleware that reflects parser exception messages back to the peer. |
| 1.12 | Cython ⇄ pure-Python divergence | `tests/test_http_parser.py` parameterises tests over `REQUEST_PARSERS` / `RESPONSE_PARSERS` (pure-Python always; Cython when the extension imports). The high-leverage attack vectors are already covered under both backends: CL+TE (`test_content_length_transfer_encoding`), CL×N (`test_duplicate_singleton_header_rejected`), obs-fold (`test_reject_obsolete_line_folding`, `test_http_response_parser_obs_line_folding*`), CR/LF/NUL (`test_bad_headers`, `test_http_response_parser_null_byte_in_header_value`, `test_http_response_parser_bad_crlf`), version regex (`test_http_request_parser_bad_version*`, `test_http_response_parser_bad_version*`). | None. When new attack vectors emerge, add them to the parameterised tests. |
| 1.13 | llhttp version drift | Manual upgrade via `make generate-llhttp`; vendor pinned in `vendor/llhttp/package.json`. | Track upstream releases (e.g. via Dependabot rule for `vendor/llhttp/package.json`), bump on every llhttp release, regenerate in CI. |
| 1.14 | npm-side compromise of `llhttp` | The vendored output is checked into git, so a compromise during a future regen would be detectable in PR review. See [§5.19](#519-build--release-supply-chain). | **Make the llhttp build reproducible: pin Node.js version, commit the npm lockfile, and on every bump verify the regenerated C against upstream's release tarballs before committing.** |
@@ -272,12 +277,12 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and
- **GHSA-xx9p-xxvh-7g8j (CVE-2023-47641)** (3.8.0) — CL-vs-TE divergence
between the Cython and pure-Python parsers, allowing request smuggling
against deployments that switched backends.
-- **CVE-2023-37276 / GHSA-45c4-8wx5-qw6w** (3.8.5) — HTTP request smuggling
+- **GHSA-45c4-8wx5-qw6w (CVE-2023-37276)** (3.8.5) — HTTP request smuggling
via CR/LF/NUL in header values. Both parsers reject these bytes at the
byte level.
- **GHSA-pjjw-qhg8-p2p9** (3.8.6) — smuggling pair in vendored llhttp 8.1.1;
fixed by bumping llhttp to 9.
-- **GHSA-gfw2-4jvh-wgfg / GHSA-8qpw-xqxj-h4r2** (3.8.6 / 3.9.2) — pure-Python
+- **GHSA-gfw2-4jvh-wgfg (CVE-2023-47627) / GHSA-8qpw-xqxj-h4r2 (CVE-2024-23829)** (3.8.6 / 3.9.2) — pure-Python
parser accepted lenient separators / weak RFC validation that llhttp
rejected.
- **GHSA-8495-4g3g-x7pr (CVE-2024-52304)** (3.10.11) — chunk-extension
@@ -287,12 +292,12 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and
- **GHSA-69f9-5gxw-wvc2 (CVE-2025-69224)** (3.13.3) — Unicode codepoints
matched by `\d` in the pure-Python parser's regexes were treated as
digits.
-- **GHSA-g84x-mcqj-x9qq** (3.13.3) — CPU-DoS on `request.read()` when
+- **GHSA-g84x-mcqj-x9qq (CVE-2025-69229)** (3.13.3) — CPU-DoS on `request.read()` when
the body arrives as a very large number of small chunks.
- **PR #12137** (3.13.4) — precautionary hardening: pure-Python parser
now explicitly rejects duplicate `Transfer-Encoding: chunked` on
the request parser.
-- **GHSA-c427-h43c-vf67** (3.13.4) — duplicate `Host` header accepted
+- **GHSA-c427-h43c-vf67 (CVE-2026-34525)** (3.13.4) — duplicate `Host` header accepted
in request parser, bypassing `Application.add_domain()` host-based
routing / authorisation. Fixed by adding `Host` to the strict
request-parser singleton rejection set.
@@ -310,3 +315,117 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and
request parser (strict).
These are all currently in place; this section assumes no regression.
+
+---
+
+### 5.2. HTTP/1 writer
+
+**Scope.** Serialisation of outbound HTTP/1.x messages — request lines, status
+lines, header blocks, chunked / fixed-length / EOF-terminated bodies, drain /
+backpressure behaviour. Both server-side response emission and client-side
+request emission share the same `StreamWriter`. Out of scope: WebSocket frame
+emission ([§5.3](#53-websocket-framing--per-message-deflate)), payload generation for multipart ([§5.4](#54-multipart-parsing--encoding)), compression codecs
+([§5.5](#55-compression-codecs)), the user-handler-facing parts of `web.Response` and
+`ClientRequest` (covered in [§5.9](#59-server-requestresponse-objects) and [§5.12](#512-client-api--request-lifecycle) respectively, but
+called out where the writer's safety depends on them).
+
+**Components covered.**
+
+- `aiohttp/_http_writer.pyx` — Cython `_serialize_headers` and
+ `_write_str_raise_on_nlcr` (the CR/LF/NUL bytewise rejector).
+- `aiohttp/http_writer.py` — `StreamWriter` (the `AbstractStreamWriter`
+ implementation) plus the pure-Python `_py_serialize_headers` /
+ `_safe_header` fallback and the Cython/pure-Python switch at
+ `http_writer.py:_py_serialize_headers`.
+- `aiohttp/abc.py` — `AbstractStreamWriter` interface.
+- Header-source feeders: `aiohttp/web_response.py` (server),
+ `aiohttp/client_reqrep.py` (client), `aiohttp/helpers.py:populate_with_cookies`.
+
+**Selection.** `_serialize_headers` defaults to the pure-Python
+implementation; if `_http_writer` (Cython) imports successfully and
+`AIOHTTP_NO_EXTENSIONS` is unset, the Cython implementation replaces it
+(`http_writer.py:_py_serialize_headers`). Both implementations apply the same
+CR / LF / NUL rejection on names *and* values *and* the status/request line.
+
+**Trust boundaries & data flow.**
+
+```mermaid
+flowchart LR
+ Handler([User handler / ClientRequest]) -->|status_line, headers, body| SW[StreamWriter]
+ SW --> Serialize[_serialize_headers]
+ Serialize -->|reject CR/LF/NUL| Bytes[Wire bytes]
+ SW --> Body[write / write_eof / write_chunked]
+ Body --> Bytes
+ Bytes --> Transport[(asyncio Transport)]
+```
+
+The writer's input is **trusted** in the threat-model sense — i.e., it comes
+from in-process Python code that ran the user's handler or constructed the
+client request. The writer's job is therefore **structural integrity**: ensure
+that whatever bytes a handler attempts to emit cannot escape the framing of a
+single HTTP message and inject new headers, new status lines, or new requests
+on the wire. The wire-side consumer is the **untrusted** counterparty
+(arbitrary peer or intermediary).
+
+**Assets at risk (chunk-specific).**
+
+- **Outbound framing integrity** — one logical message ↔ one well-framed wire
+ message; no smuggling on the egress side.
+- **Header integrity** — no name/value can introduce additional headers,
+ status lines, or chunk markers.
+- **Liveness of the connection** — a slow / hostile reader cannot drive the
+ server (or client) into unbounded memory growth via writer buffering.
+
+**Threats (STRIDE).**
+
+| # | Component / Vector | STRIDE | Threat | Risk |
+| :--- | :--- | :--- | :--- | :--- |
+| 2.1 | Header name/value with CR / LF / NUL | T / I | Response-splitting / header injection allowing the next "header" or even a complete second response/request to be appended on the wire. | High |
+| 2.2 | Status-line `reason` with CR / LF | T | Same family as 2.1 but on the status line; could let an attacker-controlled reason inject a body or a second status line. | High |
+| 2.3 | Request-line path/method | T | Path-side smuggling via CR / LF / NUL or whitespace inside the path the writer emits. | Medium |
+| 2.4 | `Content-Length` ≠ actual body length | T | If a handler / ClientRequest emits a body whose length disagrees with declared `Content-Length`, an intermediary may interpret framing differently from the writer's peer (smuggling). | Medium |
+| 2.5 | `Content-Length` *and* `Transfer-Encoding: chunked` | T | Both headers reach the wire if user code constructs them via the raw headers dict; intermediaries disagree on which wins. | Medium |
+| 2.6 | Body emission on HEAD / 1xx / 204 / 304 | T | Writer strips CL/TE for empty-body responses but **does not block the application from writing a body**; bytes after the `\r\n\r\n` confuse the next pipelined request. | Medium |
+| 2.7 | `Set-Cookie` / `Cookie` value | T | Cookie name or value containing CR / LF / NUL passes through `SimpleCookie.output()` unchanged; only caught by writer's header validation. | Medium |
+| 2.8 | Compression / `Content-Encoding` | T | Body double-compression when user sets `Content-Encoding` manually and also enables `compress=...`. Intermediaries may reject or mis-decode a doubly-compressed body. | Low |
+| 2.9 | Drain / backpressure on slow readers | D | Slow consumer (or `Sec-WebSocket-Key`-style hold) keeps `transport.write()` queued; writer drains at 64 KiB threshold (`http_writer.py:StreamWriter.write`). A handler that doesn't await `drain()` can blow up. | Medium |
+| 2.10 | Single oversized chunk | D | `write(b)` with a multi-GB blob is handed straight to `transport.write`; memory pressure shifts to asyncio's buffer. | Low |
+| 2.11 | Chunked encoding hex framing | T | Malformed chunk-size lines (negative values, leading-`+`, leading zeros, hex obfuscation) would let a non-aiohttp peer reframe the body differently and smuggle. | Low |
+| 2.12 | Header insertion validation timing | T | CR/LF/NUL rejection is *write-time*, not *insert-time*. A handler that sets a malicious header and then aborts before `write_headers()` will not raise. (Documented; not a recommended change.) | Low |
+| 2.13 | Cython ⇄ pure-Python parity | T | Divergence between the two `_serialize_headers` implementations could let one backend silently pass CR/LF/NUL that the other rejects, weakening egress safety asymmetrically. | Low |
+| 2.14 | Trailers asymmetry | T | The writer never emits trailers, but the parser accepts incoming trailers; not a writer-side threat in itself, just a documentation point for completeness. | Low |
+
+**Mitigations.**
+
+| # | Threat | Existing | Recommended |
+| :--- | :--- | :--- | :--- |
+| 2.1 | Header CR / LF / NUL injection | Both backends reject these bytes via `_write_str_raise_on_nlcr` (`_http_writer.pyx:_write_str_raise_on_nlcr`) and `_safe_header` (`http_writer.py:_safe_header`), raising `ValueError` from `_serialize_headers` before any byte hits the transport. Applied symmetrically to names, values, and the status line. | **The current tests import whichever `_serialize_headers` won the import, so only one backend is exercised. Parameterise like `tests/test_http_parser.py` does (cross-cuts [§6.1](#61-highest-leverage-recommendations) #3).** |
+| 2.2 | Status-line `reason` injection | `web_response.Response._set_status` (`web_response.py:StreamResponse._set_status`) rejects `\r` / `\n` in `reason` *at set-time*. The writer also rejects them at write-time as part of the status-line validation. | None. |
+| 2.3 | Request-line path / method | The full status line (`{method} {path} HTTP/{v}.{v}`) goes through `_write_str_raise_on_nlcr` / `_safe_header`, so CR / LF / NUL are caught regardless of whether `path` came from `yarl` or `method` was a caller-supplied string. yarl additionally rejects these bytes earlier per RFC 3986. | None. |
+| 2.4 | CL / body-length mismatch | None at write-time. `web.Response.write_eof` and the chunked writer write what they're given. | **Recommended hardening: in DEBUG mode, assert / warn when actual bytes-written disagrees with declared `Content-Length` at `write_eof()`. Useful for catching smuggling-adjacent bugs in user handlers.** |
+| 2.5 | CL + TE simultaneous | Server-side `enable_chunked_encoding()` (`web_response.py:StreamResponse.enable_chunked_encoding`) raises if `Content-Length` is already set; client-side `_update_transfer_encoding()` (`client_reqrep.py:ClientRequest._update_transfer_encoding`) raises if user sets `chunked=True` while `Content-Length` is in headers. Manual user injection into the raw headers dict is *not* caught. | **Consider a write-time assert in `StreamWriter` that rejects `Content-Length` and `Transfer-Encoding: chunked` coexisting.** |
+| 2.6 | Body-suppression edge cases | `web_response.py:StreamResponse._prepare_headers` strips `Content-Length` and `Transfer-Encoding` for HEAD / 1xx / 204 / 304 (`EMPTY_BODY_STATUS_CODES`, `helpers.py:EMPTY_BODY_METHODS`). The framework's own machinery doesn't write a body for these. | **User**: Do not call `resp.write(...)` in a handler responding HEAD / 1xx / 204 / 304 — framing strips CL / TE but does not block the byte write. **Optional aiohttp change: have `StreamWriter` short-circuit body writes when `length == 0` and the response was framed as empty-body.** |
+| 2.7 | Cookie injection | `populate_with_cookies` (`helpers.py:populate_with_cookies`) routes the cookie through `SimpleCookie.output()` and then into a regular header, where the CR / LF / NUL check at write-time catches anything `SimpleCookie` happened to pass through. | Documented design decision: rely on writer-level validation rather than tightening `set_cookie` / `populate_with_cookies` further. Keep regression tests covering cookie name/value with CR / LF / NUL across both backends. |
+| 2.8 | Manual `Content-Encoding` | Server side: `enable_compression()` (`web_response.py:StreamResponse.enable_compression`) returns early if `Content-Encoding` already present, so the body is not double-compressed. Client side: `ClientRequest._update_content_encoding` raises `ValueError("compress can not be set if Content-Encoding header is set")` — symmetric guard. | None. |
+| 2.9 | Drain / backpressure | `StreamWriter.write` drains at `LIMIT = 0x10000` bytes (`http_writer.py:StreamWriter.write`) when `drain=True` is set by the caller. Application code is expected to `await write(...)` to honour backpressure. | **User**: `await write(...)` in handlers; tight `for` loops without `await` can starve the event loop. Cross-reference [§5.7](#57-server-connection-lifecycle) for connection-level read/write timeouts that mitigate slow consumers. |
+| 2.10 | Oversized single chunk | None at the writer layer — bytes go straight to `transport.write`. asyncio applies its own high-water marks via the transport. | **User**: Relies on application-level bounds (use streaming, generators, `FileResponse`, etc., for large bodies). |
+| 2.11 | Chunked hex framing | The writer always uses `f"{len(chunk):x}\r\n"` followed by the chunk and `\r\n` (`http_writer.py:StreamWriter._write_chunked_payload`). | None. |
+| 2.12 | Insert-time vs write-time validation | Headers are validated at write-time only; `set_status` validates `reason` at set-time. | Documented design decision: late validation is acceptable; keep behaviour as-is. |
+| 2.13 | Cython ⇄ pure-Python parity | Both backends share the same logic and test surface; the Cython version uses a fast bytewise check, the Python version uses `in` on three sentinel characters. | **Parameterise the writer tests over both backends so egress equivalence on malicious inputs is exercised under both (see [§6.1](#61-highest-leverage-recommendations) #3).** |
+| 2.14 | Trailers asymmetry | Writer does not emit trailers; parser accepts trailers on incoming. Documented for completeness. | None. |
+
+**Past advisories / hardening (recap).**
+
+- **GHSA-q3qx-c6g2-7pw2 (CVE-2023-49081)** (3.9.0) — `ClientSession`
+ CRLF injection via the HTTP `version` argument.
+- **GHSA-qvrw-v9rv-5rjx (CVE-2023-49082)** (3.9.0) — `ClientSession` CRLF injection via
+ the `method` argument (request-line injection).
+- **GHSA-mwh4-6h8g-pg8w (CVE-2026-34519)** (3.13.4) — response-splitting via `\r` in
+ the status-line `reason`. Fixed by rejecting CR/LF in `reason` at
+ `_set_status` set-time, on top of the existing writer-side check
+ (threat 2.2).
+
+Writer-level CR / LF / NUL rejection via `_safe_header` and
+`_write_str_raise_on_nlcr` has been in place since the header-injection
+family of issues was first surfaced (well before CVE-2023-37276, which
+was a parser-side fix).
From ad6dbf89b4c7541fb406385ca62b1760da714e8e Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Fri, 22 May 2026 02:43:54 +0100
Subject: [PATCH 147/191] [PR #12678/b9280223 backport][3.14] Submit llhttp for
Github SBOM (#12679)
**This is a backport of PR #12678 as merged into master
(b92802234d84d98e617cd42a9862cd8697f2f571).**
Co-authored-by: Sam Bull
---
.github/workflows/dependency-submission.yml | 78 +++++++++++++++++++++
CHANGES/12678.packaging.rst | 1 +
2 files changed, 79 insertions(+)
create mode 100644 .github/workflows/dependency-submission.yml
create mode 100644 CHANGES/12678.packaging.rst
diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml
new file mode 100644
index 00000000000..323a5971cb9
--- /dev/null
+++ b/.github/workflows/dependency-submission.yml
@@ -0,0 +1,78 @@
+name: Dependency submission
+
+# GitHub's automatically generated dependency graph (and the SBOM exported from
+# it) only covers dependencies declared in recognised package manifests. llhttp
+# is vendored as a git submodule under vendor/llhttp, so that scan never sees
+# it. This workflow submits llhttp explicitly through the Dependency submission
+# API so it shows up in the dependency graph, the exported SBOM and Dependabot
+# alerts.
+
+on:
+ push:
+ branches:
+ - master
+ paths:
+ - .gitmodules
+ - vendor/llhttp
+ - .github/workflows/dependency-submission.yml
+
+permissions: {}
+
+jobs:
+ llhttp:
+ name: Submit vendored llhttp
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ permissions:
+ contents: write # Required by the Dependency submission API
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ submodules: true
+ - name: Read vendored llhttp version
+ id: llhttp
+ run: echo "version=$(jq -re .version vendor/llhttp/package.json)" >> "$GITHUB_OUTPUT"
+ - name: Submit llhttp to the dependency graph
+ uses: actions/github-script@v9
+ env:
+ LLHTTP_VERSION: ${{ steps.llhttp.outputs.version }}
+ with:
+ script: |
+ const version = process.env.LLHTTP_VERSION;
+ const response = await github.request(
+ 'POST /repos/{owner}/{repo}/dependency-graph/snapshots',
+ {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ version: 0,
+ sha: context.sha,
+ ref: context.ref,
+ job: {
+ correlator: context.workflow,
+ id: context.runId.toString(),
+ },
+ detector: {
+ name: 'aiohttp vendored dependency submission',
+ version: '1.0.0',
+ url: `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}`,
+ },
+ scanned: new Date().toISOString(),
+ manifests: {
+ 'vendor/llhttp': {
+ name: 'vendor/llhttp',
+ file: {
+ source_location: '.gitmodules',
+ },
+ resolved: {
+ llhttp: {
+ package_url: `pkg:npm/llhttp@${version}`,
+ relationship: 'direct',
+ scope: 'runtime',
+ },
+ },
+ },
+ },
+ },
+ );
+ core.info(`Submitted llhttp ${version}: ${response.data.message}`);
diff --git a/CHANGES/12678.packaging.rst b/CHANGES/12678.packaging.rst
new file mode 100644
index 00000000000..7e459cf9ff5
--- /dev/null
+++ b/CHANGES/12678.packaging.rst
@@ -0,0 +1 @@
+Submitted vendored `llhttp` to Github's SBOM -- by :user:`Dreamsorcerer`.
From 9e703c735e1a387d3a06802714ed666d003a596b Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Fri, 22 May 2026 03:23:43 +0100
Subject: [PATCH 148/191] [PR #12680/e37ab8be backport][3.14] Drop paths
restriction on dep submission (#12682)
**This is a backport of PR #12680 as merged into master
(e37ab8be3cfc89a9ca6f9508428382630ce3d6b5).**
Co-authored-by: Sam Bull
---
.github/workflows/dependency-submission.yml | 4 ----
1 file changed, 4 deletions(-)
diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml
index 323a5971cb9..9a499f22629 100644
--- a/.github/workflows/dependency-submission.yml
+++ b/.github/workflows/dependency-submission.yml
@@ -11,10 +11,6 @@ on:
push:
branches:
- master
- paths:
- - .gitmodules
- - vendor/llhttp
- - .github/workflows/dependency-submission.yml
permissions: {}
From 3be82a043d6e013c1a7600ab12a93577a2508c53 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 22 May 2026 10:59:26 +0000
Subject: [PATCH 149/191] Bump click from 8.4.0 to 8.4.1 (#12684)
Bumps [click](https://github.com/pallets/click) from 8.4.0 to 8.4.1.
Release notes
Sourced from click's
releases.
8.4.1
This is the Click 8.4.1 fix release, which fixes bugs but does not
otherwise change behavior and should not result in breaking changes
compared to the latest feature release.
PyPI: https://pypi.org/project/click/8.4.1/
Changes: https://click.palletsprojects.com/page/changes/#version-8-4-1
Milestone: https://github.com/pallets/click/milestone/32?closed=1
get_parameter_source() is available during eager
callbacks and type conversion again. #3458
#3484
- Zsh completion scripts parse correctly on Windows. #3277 #
3466
- Shell completion of
Choice Enum values
produces a valid completion result. #3015
- Fix empty byte-string handling in echo. #3487
- Fix closed file error with
echo_via_pager. #3449
Changelog
Sourced from click's
changelog.
Version 8.4.1
Released 2026-05-21
get_parameter_source() is available during eager
callbacks and type
conversion again. :issue:3458 :issue:3484
- Zsh completion scripts parse correctly on Windows.
:issue:
3277 :pr:3466
- Shell completion of
Choice Enum values
produces a valid completion
result. :issue:3015
- Fix empty byte-string handling in echo.
:issue:
3487
- Fix closed file error with
echo_via_pager.
:issue:3449
Commits
6eeb50e
release version 8.4.1
67921d5
change log and doc fixes (#3495)
9c41f46
Fix changelog and version admonitions
6cb3477
fix skip condition
5ee8e31
fix I/O operation on closed file error with CliRunner and echo_via_pager
(#3482)
becbde5
pager doesn't close std streams
a5f5aa6
Handle empty bytes in echo (#3493)
4d3db84
handle empty bytes in echo
d42f15b
Fix get_parameter_source() during type conversion and eager
callbacks (#3484)
0baa8db
Document ctx.params bypass with test and doc
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
8 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index bb974de91b7..fae88d992e5 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -50,7 +50,7 @@ cfgv==3.5.0
# via pre-commit
charset-normalizer==3.4.7
# via requests
-click==8.4.0
+click==8.4.1
# via
# pip-tools
# slotscheck
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 9a67a45909e..5b16877d993 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -50,7 +50,7 @@ cfgv==3.5.0
# via pre-commit
charset-normalizer==3.4.7
# via requests
-click==8.4.0
+click==8.4.1
# via
# pip-tools
# slotscheck
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index b196c4967d9..8c92b8157cb 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -14,7 +14,7 @@ certifi==2026.5.20
# via requests
charset-normalizer==3.4.7
# via requests
-click==8.4.0
+click==8.4.1
# via towncrier
docutils==0.21.2
# via
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 8ae2412c091..8d6f78a500c 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -14,7 +14,7 @@ certifi==2026.5.20
# via requests
charset-normalizer==3.4.7
# via requests
-click==8.4.0
+click==8.4.1
# via towncrier
docutils==0.21.2
# via
diff --git a/requirements/lint.txt b/requirements/lint.txt
index c212712be39..1d05336f9f5 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -22,7 +22,7 @@ cffi==2.0.0
# pycares
cfgv==3.5.0
# via pre-commit
-click==8.4.0
+click==8.4.1
# via slotscheck
cryptography==48.0.0
# via trustme
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index dc964130606..ee87da7fc03 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -12,7 +12,7 @@ blockbuster==1.5.26
# via -r requirements/test-common.in
cffi==2.0.0
# via cryptography
-click==8.4.0
+click==8.4.1
# via wait-for-it
coverage==7.14.0
# via
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 825a2767520..8385768b547 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -28,7 +28,7 @@ cffi==2.0.0
# via
# cryptography
# pycares
-click==8.4.0
+click==8.4.1
# via wait-for-it
coverage==7.14.0
# via
diff --git a/requirements/test.txt b/requirements/test.txt
index 9c51a06bba0..30587100a57 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -28,7 +28,7 @@ cffi==2.0.0
# via
# cryptography
# pycares
-click==8.4.0
+click==8.4.1
# via wait-for-it
coverage==7.14.0
# via
From 9e6092a42a481580757f84789f1507e5fa131055 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Fri, 22 May 2026 14:00:49 +0100
Subject: [PATCH 150/191] [PR #12681/1f005f12 backport][3.14] Update llhttp to
v9.4.1 (#12683)
**This is a backport of PR #12681 as merged into master
(1f005f123956833b64d047fdc713138ae1893f56).**
Co-authored-by: Sam Bull
---
CHANGES/12681.packaging.rst | 1 +
vendor/llhttp | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/12681.packaging.rst
diff --git a/CHANGES/12681.packaging.rst b/CHANGES/12681.packaging.rst
new file mode 100644
index 00000000000..1cd12837ecb
--- /dev/null
+++ b/CHANGES/12681.packaging.rst
@@ -0,0 +1 @@
+Updated ``llhttp`` to v9.4.1 -- by :user:`Dreamsorcerer`.
diff --git a/vendor/llhttp b/vendor/llhttp
index 06b12e87f20..96b15fb3bc0 160000
--- a/vendor/llhttp
+++ b/vendor/llhttp
@@ -1 +1 @@
-Subproject commit 06b12e87f209da43e3e9e0f958b7464a4a218896
+Subproject commit 96b15fb3bc00117d2db3df8e87fa6d3e9bcff328
From adb26e229fef4b83b569c7d17f038358f95079b5 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 25 May 2026 16:41:10 +0000
Subject: [PATCH 151/191] Bump cython from 3.2.4 to 3.2.5 (#12694)
Bumps [cython](https://github.com/cython/cython) from 3.2.4 to 3.2.5.
Changelog
Sourced from cython's
changelog.
3.2.5 (2026-05-23)
Bugs fixed
-
A compile failure was fixed when using the walrus operator inside of
try-except.
(Github issue :issue:7462)
-
Expressions with side-effects as object argument to
isinstance() could get
evaluated multiple times, e.g. when they use the walrus operator.
(Github issue :issue:7670)
-
Several problems generating the shared utility module were resolved,
including
a performance regression with memory views.
(Github issues :issue:7487, :issue:7497,
:issue:7504, :issue:7558)
-
Some GC and refcounting issues were resolved for Cython functions in
the Limited API.
(Github issue :issue:7594)
-
Refcounting errors and error handling issues were resolved in some
rare error handling cases.
(Github issues :issue:7597, :issue:7599,
:issue:7612, :issue:7673)
-
Using cython.pymutex in an extension type with
cdef methods generated
invalid C code missing the required PyMutex declarations.
(Github issue :issue:6995)
-
Calling .get_frame() on Cython coroutines could crash in
freethreading Python.
(Github issue :issue:7632)
-
The vectorcall protocol was not used correctly in
.throw() of Cython coroutines
when raising the exception only by type (without value or traceback).
(Github issue :issue:7677)
-
A problem with cpdef enums in the Limited API of Python 3.11+ was
resolved.
(Github issue :issue:7503)
-
Unicode predicates like .isdigit() are now allowed to
fail in the Limited API.
(Github issue :issue:7602)
-
Conditional expressions mixing Python float and int object types
could accidentally
infer float as the common result type, instead of treating both
independently.
-
Using sizeof() in the size declarations of
extern arrays failed.
(Github issue :issue:7451)
-
Enabling profiling generated invalid C code for non-Python return
tuples.
(Github issue :issue:7580)
-
abs() on C long long values could generate
invalid C code.
... (truncated)
Commits
ec15209
Tests: Fix test in Py3.16, following cython/cython#7709
aa576c4
Fix test.
2398ddd
Prepare release of 3.2.5.
abb261f
Update changelog.
a4bae70
Small cleanup of memoryview assertion (GH-7635)
80d9e7e
Prevent walrus operator from being re-evaluated multiple times in
isinstance(...
0c69532
CI: Pip PyPy 3.11 version to avoid CI failures.
f7d6b7a
CI: Allow longer PyPy version strings than "major.minor".
b7a1d43
Update changelog.
f02df0a
Build: Remove outdated license identifier (long replaced by
license kw-opti...
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/cython.in | 2 +-
requirements/cython.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index fae88d992e5..669e0ce1dab 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -62,7 +62,7 @@ coverage==7.14.0
# pytest-cov
cryptography==48.0.0
# via trustme
-cython==3.2.4
+cython==3.2.5
# via -r requirements/cython.in
distlib==0.4.0
# via virtualenv
diff --git a/requirements/cython.in b/requirements/cython.in
index 6b848f6df9e..c8fcfa99220 100644
--- a/requirements/cython.in
+++ b/requirements/cython.in
@@ -1,3 +1,3 @@
-r multidict.in
-Cython >= 3.1.1
+Cython >= 3.2.5
diff --git a/requirements/cython.txt b/requirements/cython.txt
index 193a2299871..d6e2a89bf4d 100644
--- a/requirements/cython.txt
+++ b/requirements/cython.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/cython.txt --resolver=backtracking --strip-extras requirements/cython.in
#
-cython==3.2.4
+cython==3.2.5
# via -r requirements/cython.in
multidict==6.7.1
# via -r requirements/multidict.in
From 20346dd60022c7a79fcf0486255a9f994323f751 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 25 May 2026 17:01:09 +0000
Subject: [PATCH 152/191] Bump snowballstemmer from 3.0.1 to 3.1.0 (#12697)
Bumps [snowballstemmer](https://github.com/snowballstem/snowball) from
3.0.1 to 3.1.0.
Changelog
Sourced from snowballstemmer's
changelog.
Snowball 3.1.0 (2026-05-22)
Compiler changes
... (truncated)
Commits
77e07c9
Update for 3.1.0
4d37f9c
Finalise NEWS entry for 3.1.0
2d38e20
make update_version now also updates README.rst
1596c74
Go: Fix code generated for non-constant hop
34d1214
NEWS: Update draft entry
65885b5
finnish: Rename things to match algo description
5c05f53
finnish: Accept apostrophe instead of VI
df25742
JS: Generate simpler code for hop by constant
27c9355
Add runtime test of hop/next
2508d20
Add test coverage for hop 1->next
canonicalisation
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 669e0ce1dab..033a6a7badc 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -239,7 +239,7 @@ six==1.17.0
# via python-dateutil
slotscheck==0.19.1
# via -r requirements/lint.in
-snowballstemmer==3.0.1
+snowballstemmer==3.1.0
# via sphinx
sphinx==8.1.3
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 5b16877d993..280f50b2e74 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -232,7 +232,7 @@ six==1.17.0
# via python-dateutil
slotscheck==0.19.1
# via -r requirements/lint.in
-snowballstemmer==3.0.1
+snowballstemmer==3.1.0
# via sphinx
sphinx==8.1.3
# via
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index 8c92b8157cb..38e8808825c 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -56,7 +56,7 @@ requests==2.34.2
# via
# sphinx
# sphinxcontrib-spelling
-snowballstemmer==3.0.1
+snowballstemmer==3.1.0
# via sphinx
sphinx==8.1.3
# via
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 8d6f78a500c..ac53f3db591 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -52,7 +52,7 @@ pyyaml==6.0.3
# sphinxcontrib-mermaid
requests==2.34.2
# via sphinx
-snowballstemmer==3.0.1
+snowballstemmer==3.1.0
# via sphinx
sphinx==8.1.3
# via
From 400088ee767cd184e0257c0e9fd88940c0b6ea2d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 25 May 2026 17:01:54 +0000
Subject: [PATCH 153/191] Bump pytest-codspeed from 5.0.2 to 5.0.3 (#12698)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [pytest-codspeed](https://github.com/CodSpeedHQ/pytest-codspeed)
from 5.0.2 to 5.0.3.
Release notes
Sourced from pytest-codspeed's
releases.
v5.0.3
What's Changed
Full Changelog: https://github.com/CodSpeedHQ/pytest-codspeed/compare/v5.0.2...v5.0.3
Changelog
Sourced from pytest-codspeed's
changelog.
[5.0.3] - 2026-05-22
🏗️ Refactor
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test.txt | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 033a6a7badc..2c8aaf7e349 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -196,7 +196,7 @@ pytest==9.0.3
# pytest-mock
# pytest-timeout
# pytest-xdist
-pytest-codspeed==5.0.2
+pytest-codspeed==5.0.3
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 280f50b2e74..842ccb71917 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -191,7 +191,7 @@ pytest==9.0.3
# pytest-mock
# pytest-timeout
# pytest-xdist
-pytest-codspeed==5.0.2
+pytest-codspeed==5.0.3
# via
# -r requirements/lint.in
# -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 1d05336f9f5..9d2c282b04b 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -87,7 +87,7 @@ pytest==9.0.3
# -r requirements/lint.in
# pytest-codspeed
# pytest-mock
-pytest-codspeed==5.0.2
+pytest-codspeed==5.0.3
# via -r requirements/lint.in
pytest-mock==3.15.1
# via -r requirements/lint.in
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index ee87da7fc03..40117fc2b88 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -74,7 +74,7 @@ pytest==9.0.3
# pytest-mock
# pytest-timeout
# pytest-xdist
-pytest-codspeed==5.0.2
+pytest-codspeed==5.0.3
# via -r requirements/test-common.in
pytest-cov==7.1.0
# via -r requirements/test-common.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 8385768b547..b73e0f51fe2 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -110,7 +110,7 @@ pytest==9.0.3
# pytest-mock
# pytest-timeout
# pytest-xdist
-pytest-codspeed==5.0.2
+pytest-codspeed==5.0.3
# via -r requirements/test-common.in
pytest-cov==7.1.0
# via -r requirements/test-common.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 30587100a57..1b6dfdfb332 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -110,7 +110,7 @@ pytest==9.0.3
# pytest-mock
# pytest-timeout
# pytest-xdist
-pytest-codspeed==5.0.2
+pytest-codspeed==5.0.3
# via -r requirements/test-common.in
pytest-cov==7.1.0
# via -r requirements/test-common.in
From b4153bab74481f431ed9305721f98f97447fc0dc Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Mon, 25 May 2026 17:12:56 +0000
Subject: [PATCH 154/191] [PR #12677/7f0532c1 backport][3.14] Threat model
chapter 3 (#12699)
**This is a backport of PR #12677 as merged into master
(7f0532c1df1915e4367ff4af25cd8453d2df9fc0).**
Co-authored-by: Sam Bull
---
THREAT_MODEL.md | 117 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 117 insertions(+)
diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
index cb50d264489..3b6648685d5 100644
--- a/THREAT_MODEL.md
+++ b/THREAT_MODEL.md
@@ -429,3 +429,120 @@ Writer-level CR / LF / NUL rejection via `_safe_header` and
`_write_str_raise_on_nlcr` has been in place since the header-injection
family of issues was first surfaced (well before CVE-2023-37276, which
was a parser-side fix).
+
+---
+
+### 5.3. WebSocket framing & per-message deflate
+
+**Scope.** RFC 6455 frame parsing and serialisation, masking, fragmentation
+and continuation, control frames (close / ping / pong), and the
+permessage-deflate (PMCE; RFC 7692) extension. Both server-side
+(`web_ws.WebSocketResponse`) and client-side (`client_ws.ClientWebSocketResponse`)
+share this layer. Out of scope: the WebSocket *handshake* and per-side
+lifecycle (covered in [§5.11](#511-server-side-websocket-handler) server, [§5.14](#514-client-side-websocket) client) and the underlying
+compression codecs themselves ([§5.5](#55-compression-codecs)).
+
+**Components covered.**
+
+- `aiohttp/http_websocket.py` — public re-export shim.
+- `aiohttp/_websocket/`:
+ - `mask.pyx` / `mask.pxd` — Cython SIMD-style XOR.
+ - `helpers.py` — pure-Python masking fallback (`bytearray.translate`),
+ extension parameter parsing (`ws_ext_parse`), close-code unpacking.
+ - `models.py` — `WSCloseCode`, `WSMsgType`, message dataclasses.
+ - `reader.py` / `reader_c.pyx` / `reader_c.py` / `reader_py.py` — frame
+ reader (Cython preferred, pure-Python fallback).
+ - `writer.py` — frame writer.
+ - `__init__.py`.
+
+**Selection.** The reader's import dance (`reader.py`) prefers
+`reader_c` (Cython) and falls back to `reader_py` on `ImportError`. Both
+implementations apply the same validation rules.
+
+**Trust boundaries & data flow.**
+
+```mermaid
+flowchart LR
+ Wire([Untrusted peer]) --> Feed[reader.feed_data]
+ Feed --> Parse[Frame state machine]
+ Parse -->|opcode/RSV/length checks| Mask[websocket_mask XOR]
+ Mask --> Inflate[(zlib inflate if RSV1)]
+ Inflate --> Validate[max_msg_size + UTF-8]
+ Validate -->|WSMessage| App[(user code)]
+ App -->|send_*| Writer[writer.send_frame]
+ Writer --> Deflate[(zlib deflate if compress)]
+ Deflate --> Frame[Frame builder + mask]
+ Frame --> Wire
+```
+
+The reader's input is fully attacker-controlled. Output (`WSMessage` with
+`data`, `extra`, `type`) is consumed by user handler code. Server-side, mask
+bytes from the peer's frames are XORed before reaching the application;
+client-side, the writer adds masks to outgoing frames.
+
+**Assets at risk (chunk-specific).**
+
+- **Frame integrity** — opcode, RSV bits, FIN, length, mask all parsed
+ consistently; no path can let a peer smuggle a control frame inside a data
+ frame or coerce the reader into accepting a malformed frame.
+- **Decompression safety** — PMCE-compressed frames cannot drive memory or
+ CPU to denial of service via a small input expanding to a huge output.
+- **Memory bounds across messages** — a peer holding the connection open and
+ drip-feeding fragments cannot grow memory unboundedly.
+
+**Threats (STRIDE).**
+
+| # | Component / Vector | STRIDE | Threat | Risk |
+| :--- | :--- | :--- | :--- | :--- |
+| 3.1 | Server reader accepting unmasked client frames | T / S | The reader does not enforce RFC 6455 §5.1 ("server MUST fail on unmasked client frame"). A non-conformant or malicious client can send unmasked frames; the spec rationale is preventing cache-poisoning of intermediaries. | Medium |
+| 3.2 | Mask key generation | I | Outbound masks come from `random.getrandbits(32)` (`writer.py:WebSocketWriter.__init__`), not `secrets`. RFC 6455 §5.3 says masks must be "unpredictable"; PRNG is not cryptographic. | Low |
+| 3.3 | Reserved bits (RSV1/2/3) | T | RSV2/3 always rejected; RSV1 only allowed when PMCE is negotiated (`reader_py.py:WebSocketReader._feed_data`). Misalignment between negotiation state and frame would let a peer toggle decompression. | Low |
+| 3.4 | Unknown opcode | T | Peer-controlled opcode outside the defined range (`0x0`–`0xA`) could route to an unexpected handler path if accepted. | Low |
+| 3.5 | Control-frame size > 125 bytes | T | Oversized control frame would violate RFC 6455 framing and could mis-frame against a non-aiohttp peer. | Low |
+| 3.6 | Fragmented control frame (FIN=0) | T | Fragmented control frame is a protocol violation; accepting one would let a peer interleave control state across the fragment sequence. | Low |
+| 3.7 | Continuation without preceding text/binary | T | Continuation frame without an initial data frame leaves assembly state ambiguous. | Low |
+| 3.8 | Unbounded fragmentation memory growth | D | A peer streams many continuation fragments without ever setting FIN; the reassembly buffer grows with each fragment until memory is exhausted. | Low |
+| 3.9 | PMCE decompression bomb | D | Compressed frame expanding to >`max_msg_size`. Mitigated by post-decompress check; some zlib backends (e.g. isal) may overshoot the per-call `max_length` by a chunk before the post-check rejects it. | Medium |
+| 3.10 | PMCE context retention memory | D / I | When `server_no_context_takeover` / `client_no_context_takeover` is *not* negotiated, the zlib context persists across messages on each side. No explicit per-message reset; long-lived sessions accumulate state. | Low–Med |
+| 3.11 | UTF-8 validation on text frames | T | Invalid UTF-8 in a text frame (or close reason) reaching the handler as `str` would surface as bytes via `surrogateescape` and confuse caller code that assumes valid Unicode. | Low |
+| 3.12 | Close-frame handling | T | Out-of-range close codes from a peer would let a non-aiohttp consumer of the close reason mis-interpret the disconnect reason. | Low |
+| 3.13 | Writer-side: large outbound message as single frame | D | Writer does not auto-fragment; a single `send_str(big_blob)` becomes one frame. Memory pressure on the local side and on intermediaries. | Low |
+| 3.14 | Mask-on-send keys (Cython vs Python parity) | T | Divergence between `mask.pyx` and `helpers.py` `websocket_mask` would silently break receivers (one peer XORs with a different key than the other expects). | Low |
+| 3.15 | Reader Cython vs pure-Python parity | T | Divergence between the two reader backends could let one silently accept a frame the other rejects, weakening protocol enforcement asymmetrically. | Low |
+
+**Mitigations.**
+
+| # | Threat | Existing | Recommended |
+| :--- | :--- | :--- | :--- |
+| 3.1 | Unmasked client frames accepted | None — the reader is direction-agnostic; `web_ws.py` does not enforce client-mask either. | **Recommended hardening: Enforce RFC 6455 §5.1 mask direction in strict mode only (gated on `DEBUG`, mirroring the HTTP parser's lenient-default / strict-DEBUG asymmetry): server reader rejects frames with `has_mask == 0`, client reader rejects masked server frames, both with a `PROTOCOL_ERROR`-style close. Production default stays lenient for interop.** |
+| 3.2 | Non-cryptographic mask RNG | `partial(random.getrandbits, 32)` per writer instance. | Documented design decision: WebSocket masking exists for cache-poisoning resistance against intermediaries, not as a confidentiality primitive. The mask needs to be performant — called once per outbound frame on a hot path — and does not need to be cryptographically unpredictable. `random.getrandbits(32)` is the deliberate choice. |
+| 3.3 | RSV bits | `reader_py.py:WebSocketReader._feed_data` ties RSV1 acceptance to the PMCE-negotiated `_compress` flag; RSV2/3 always rejected. | None. |
+| 3.4 | Unknown opcode | Rejected. | None. |
+| 3.5–3.7 | Control-frame and fragmentation rules | All enforced at reader. | None. |
+| 3.8 | Fragment memory bound | `max_msg_size` enforced pre-FIN and at assembly. Default 4 MiB. | **User**: set a smaller `max_msg_size` for protocols where messages are bounded (e.g. chat); the 4 MiB default suits arbitrary payloads. |
+| 3.9 | PMCE decompression bomb | `WebSocketReader._handle_frame` decompresses with a `max_length` of `max_msg_size + 1` and checks the result; on overflow, raises `MESSAGE_TOO_BIG` (1009). This `max_length` post-decompress check was introduced by PR #11898 (v3.13.3). | **Documented known limitation.** Some backends (notably `isal_zlib`) do not strictly honour `max_length` in `decompress()` and may overshoot by up to one zlib block before the post-decompress size check fires. The post-check still catches it before the bytes reach the application, but a transient over-allocation is possible. Document and monitor. |
+| 3.10 | PMCE context retention | Default extensions request context takeover (per RFC 7692 default); user can negotiate `server_no_context_takeover` / `client_no_context_takeover` via handshake. | Documented design decision: keep the RFC 7692 default (context takeover). **Document the memory tradeoff in user-facing WebSocket docs.** **User**: configure no-context-takeover on long-lived sessions running on memory-constrained hosts. |
+| 3.11 | UTF-8 validation | Strict `bytes.decode("utf-8")` post-assembly. | None. |
+| 3.12 | Close-code validation | `reader_py.py:WebSocketReader._handle_frame` validates codes < 3000 against `ALLOWED_CLOSE_CODES`; codes ≥ 3000 accepted (RFC reserved for libraries / private use, correct). | None. |
+| 3.13 | Writer single-frame size | None — caller-controlled. | **User**: chunk very large outbound payloads (beyond a few MiB) via fragmented messages; a single `send_*` becomes one frame and can pressure intermediaries. |
+| 3.14 | Cython vs pure-Python mask parity | Both implement XOR on the same key cycling; behaviour identical. | Add a parameterised test that runs the mask helper against both backends side-by-side (see [§6.1](#61-highest-leverage-recommendations) #3). |
+| 3.15 | Reader backend parity | `tests/test_websocket_parser.py` imports the single `WebSocketReader` symbol (whichever backend won the import), so each CI run only exercises one. | Parameterise like `tests/test_http_parser.py` does — explicitly import `WebSocketReaderPython` and `WebSocketReaderCython` (when available) and fixture-parametrise over both (see [§6.1](#61-highest-leverage-recommendations) #3). |
+
+**Past advisories / hardening (recap).**
+
+- **PR #11898** (3.13.3) — PMCE decompression DoS hardening:
+ `WebSocketReader._handle_frame` decompresses with a `max_length` cap of
+ `max_msg_size + 1` and rejects with `MESSAGE_TOO_BIG` (1009) on overflow.
+ This is the primary mitigation for zip-bomb-style attacks against
+ WebSocket peers.
+- No formal CVE has been published against the WebSocket framing layer to
+ date.
+
+**Open questions.**
+
+1. Should server-side reader reject unmasked frames (and client-side reject
+ masked ones) per RFC 6455 §5.1? (Threat 3.1 — recommended.)
+2. Is the PRNG mask source (`random.getrandbits`) sufficient, or should it be
+ migrated to `secrets`/`os.urandom`? (Threat 3.2.)
+3. For long-lived WebSocket sessions, is there a use case for *forcing*
+ `no_context_takeover` defaults to limit memory growth? (Threat 3.10.)
From afbc6bd3987121b142bf73b2331b022512b638f2 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Tue, 26 May 2026 21:25:50 +0100
Subject: [PATCH 155/191] [PR #12673/93de128a backport][3.14] Reduce client
timeout test runtime (#12705)
**This is a backport of PR #12673 as merged into master
(93de128a2ffa265e6f697dec11998745553977fa).**
Co-authored-by: nightcityblade
---
CHANGES/9705.contrib.rst | 2 ++
CONTRIBUTORS.txt | 1 +
tests/test_client_functional.py | 8 ++++----
3 files changed, 7 insertions(+), 4 deletions(-)
create mode 100644 CHANGES/9705.contrib.rst
diff --git a/CHANGES/9705.contrib.rst b/CHANGES/9705.contrib.rst
new file mode 100644
index 00000000000..5eaef0c4398
--- /dev/null
+++ b/CHANGES/9705.contrib.rst
@@ -0,0 +1,2 @@
+Reduced the runtime of a client timeout regression test by shortening its artificial response delay.
+-- by :user:`nightcityblade`.
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index ba7ce91cec1..01b6805cf6d 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -278,6 +278,7 @@ Moss Collum
Mun Gwan-gyeong
Navid Sheikhol
Nicolas Braem
+Night Cityblade
Nikolay Kim
Nikolay Novik
Nikolay Tiunov
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
index 972de1e0e1c..ee07cbf1697 100644
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -1158,17 +1158,17 @@ async def test_read_timeout_between_chunks(aiohttp_client, mocker) -> None:
async def handler(request):
resp = aiohttp.web.StreamResponse()
await resp.prepare(request)
- # write data 4 times, with pauses. Total time 2 seconds.
+ # write data 4 times, with pauses. Total time 0.4 seconds.
for _ in range(4):
- await asyncio.sleep(0.5)
+ await asyncio.sleep(0.1)
await resp.write(b"data\n")
return resp
app = web.Application()
app.add_routes([web.get("/", handler)])
- # A timeout of 0.2 seconds should apply per read.
- timeout = aiohttp.ClientTimeout(sock_read=1)
+ # The read timeout should apply per read, not to the whole response.
+ timeout = aiohttp.ClientTimeout(sock_read=0.2)
client = await aiohttp_client(app, timeout=timeout)
res = b""
From 3a875d8e719a47c46443c5a93f99ebbc9ef8fb26 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 28 May 2026 11:45:44 +0000
Subject: [PATCH 156/191] Bump python-discovery from 1.3.1 to 1.4.0 (#12712)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [python-discovery](https://github.com/tox-dev/python-discovery)
from 1.3.1 to 1.4.0.
Release notes
Sourced from python-discovery's
releases.
v1.4.0
What's Changed
Full Changelog: https://github.com/tox-dev/python-discovery/compare/1.3.2...1.4.0
v1.3.2
What's Changed
Full Changelog: https://github.com/tox-dev/python-discovery/compare/1.3.1...1.3.2
Changelog
Sourced from python-discovery's
changelog.
Features - 1.4.0
- Add
debug_build attribute to
:class:PythonInfo exposing whether the interpreter is a
debug build
(Py_DEBUG) - by :user:gaborbernat.
(:issue:80)
v1.3.2 (2026-05-27)
No significant changes.
v1.3.1 (2026-05-12)
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 2c8aaf7e349..99853e1a743 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -212,7 +212,7 @@ pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
-python-discovery==1.3.1
+python-discovery==1.4.0
# via virtualenv
python-on-whales==0.81.0
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 842ccb71917..f843a593cbc 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -207,7 +207,7 @@ pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
-python-discovery==1.3.1
+python-discovery==1.4.0
# via virtualenv
python-on-whales==0.81.0
# via
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 9d2c282b04b..2ad0cda2e37 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -93,7 +93,7 @@ pytest-mock==3.15.1
# via -r requirements/lint.in
python-dateutil==2.9.0.post0
# via freezegun
-python-discovery==1.3.1
+python-discovery==1.4.0
# via virtualenv
python-on-whales==0.81.0
# via -r requirements/lint.in
From a35b450adb41a7645908ec7c3911f7eeb07e104b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 28 May 2026 12:00:55 +0000
Subject: [PATCH 157/191] Bump platformdirs from 4.9.6 to 4.10.0 (#12713)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [platformdirs](https://github.com/tox-dev/platformdirs) from 4.9.6
to 4.10.0.
Release notes
Sourced from platformdirs's
releases.
4.10.0
What's Changed
New Contributors
Full Changelog: https://github.com/tox-dev/platformdirs/compare/4.9.6...4.10.0
Changelog
Sourced from platformdirs's
changelog.
###########
Changelog
###########
4.10.0 (2026-05-28)
- ✨ feat: add user_publicshare_dir, user_templates_dir,
user_fonts_dir, user_preference_dir :pr:
491
- ✨ feat: add user_projects_dir for $XDG_PROJECTS_DIR
:pr:
490
- chore: improve platformdirs maintenance path :pr:
488 -
by :user:lphuc2250gma
4.9.6 (2026-04-09)
- 🐛 fix(release): use double quotes for tag variable expansion
:pr:
477
4.9.5 (2026-04-06)
- 📝 docs(appauthor): clarify None vs False on Windows
:pr:
476
- Separates implementations of macOS dirs that share a default
:pr:
473 - by :user:Goddesen
- Remove persist-credentials: false from release job
:pr:
472
- fix: do not duplicate site dirs in Unix.iter_{config,site}_dirs()
when use_site_for_root is active :pr:
469 - by
:user:viccie30
- 🔧 fix(type): resolve ty 0.0.25 type errors :pr:
468
- 🔒 ci(workflows): add zizmor security auditing
:pr:
467
- 🐛 fix(release): generate docstrfmt-compatible changelog entries
:pr:
463
4.9.4 (2026-03-05)
- [pre-commit.ci] pre-commit autoupdate :pr:
461 - by
:user:pre-commit-ci[bot]
- Update README.md
- 📝 docs: add project logo to documentation :pr:
459
- Standardize .github files to .yaml suffix
- build(deps): bump the all group with 2 updates :pr:
457
- by :user:dependabot[bot]
- Move SECURITY.md to .github/SECURITY.md
- Add permissions to workflows :pr:
455
- Add security policy
- [pre-commit.ci] pre-commit autoupdate :pr:
454 - by
:user:pre-commit-ci[bot]
4.9.2 (2026-02-16)
- 📝 docs: restructure following Diataxis framework
:pr:
448
... (truncated)
Commits
04cb136
Release 4.10.0
078bc61
✨ feat: add user_publicshare_dir, user_templates_dir, user_fonts_dir,
user_pr...
d279747
✨ feat: add user_projects_dir for $XDG_PROJECTS_DIR (#490)
4116391
[pre-commit.ci] pre-commit autoupdate (#489)
dbc63f5
chore: improve platformdirs maintenance path (#488)
9265108
[pre-commit.ci] pre-commit autoupdate (#487)
9f857ec
[pre-commit.ci] pre-commit autoupdate (#486)
a76e777
[pre-commit.ci] pre-commit autoupdate (#484)
903fd9f
[pre-commit.ci] pre-commit autoupdate (#483)
a5da35d
build(deps): bump astral-sh/setup-uv from 8.0.0 to 8.1.0 in the all
group (#482)
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 99853e1a743..1f81db53c19 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -152,7 +152,7 @@ pip-tools==7.5.3
# via -r requirements/dev.in
pkgconfig==1.6.0
# via -r requirements/test-common.in
-platformdirs==4.9.6
+platformdirs==4.10.0
# via
# python-discovery
# virtualenv
diff --git a/requirements/dev.txt b/requirements/dev.txt
index f843a593cbc..df3ac1d182a 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -149,7 +149,7 @@ pip-tools==7.5.3
# via -r requirements/dev.in
pkgconfig==1.6.0
# via -r requirements/test-common.in
-platformdirs==4.9.6
+platformdirs==4.10.0
# via
# python-discovery
# virtualenv
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 2ad0cda2e37..cf361940101 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -62,7 +62,7 @@ packaging==26.2
# via pytest
pathspec==1.1.1
# via mypy
-platformdirs==4.9.6
+platformdirs==4.10.0
# via
# python-discovery
# virtualenv
From 2d9fe794f3242a7bd4b9e130ff534d5c656a99f8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Thu, 28 May 2026 12:19:04 +0000
Subject: [PATCH 158/191] Bump virtualenv from 21.3.3 to 21.4.1 (#12716)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.3.3 to
21.4.1.
Release notes
Sourced from virtualenv's
releases.
21.4.1
What's Changed
Full Changelog: https://github.com/pypa/virtualenv/compare/21.4.0...21.4.1
21.4.0
What's Changed
New Contributors
Full Changelog: https://github.com/pypa/virtualenv/compare/21.3.3...21.4.0
Changelog
Sourced from virtualenv's
changelog.
Bugfixes - 21.4.1
- Fix Windows debug build
venvlauncher_d.exe substitution
never triggering because executables() compared the
source executable name instead of the target name, and fix
AttributeError on debug_build attribute for
interpreter info objects missing the field - by
:user:gaborbernat. (:issue:3151)
v21.4.0 (2026-05-28)
Features - 21.4.0
- Remove dead code targeting Python versions below the supported
target range (PyPy 3.6, deprecated importlib APIs) and
simplify the runtime import hook in
_virtualenv.py - by
:user:gaborbernat. (:issue:3149)
- Support Windows debug builds (
python_d.exe,
venvlauncher_d.exe) matching CPython venv behavior, remove
dead
__SCRIPT_DIR__ replacement and has_shim
version guard, drop unreachable Python 3.7 branch from
pyvenv_launch_patch_active, and fix wheel deprecation
message to say >= 3.9 - by
:user:gaborbernat.
(:issue:3150)
v21.3.3 (2026-05-13)
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 1f81db53c19..f7016461b45 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -307,7 +307,7 @@ uvloop==0.22.1 ; platform_system != "Windows"
# -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.3.3
+virtualenv==21.4.1
# via pre-commit
wait-for-it==2.3.0
# via -r requirements/test-common.in
diff --git a/requirements/dev.txt b/requirements/dev.txt
index df3ac1d182a..0a882012a66 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -297,7 +297,7 @@ uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpytho
# -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.3.3
+virtualenv==21.4.1
# via pre-commit
wait-for-it==2.3.0
# via -r requirements/test-common.in
diff --git a/requirements/lint.txt b/requirements/lint.txt
index cf361940101..a0a6a505ce4 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -128,7 +128,7 @@ uvloop==0.22.1 ; platform_system != "Windows"
# via -r requirements/lint.in
valkey==6.1.1
# via -r requirements/lint.in
-virtualenv==21.3.3
+virtualenv==21.4.1
# via pre-commit
zlib-ng==1.0.0
# via -r requirements/lint.in
From 1e3ecd48ec423c1fcc729c98adff31172faae1ef Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Thu, 28 May 2026 15:20:43 +0100
Subject: [PATCH 159/191] [PR #12689/07ed41bc backport][3.14] Reject all RFC
9110 forbidden control characters in outbound headers (#12717)
**This is a backport of PR #12689 as merged into master
(07ed41bc3bc0fe27316b168d2631fffaecba7d65).**
Co-authored-by: Rodrigo Nogueira
---
CHANGES/12689.breaking.rst | 7 ++++
THREAT_MODEL.md | 43 +++++++++++++++----------
aiohttp/_http_writer.pyx | 6 ++--
aiohttp/http_writer.py | 10 ++++--
tests/test_http_writer.py | 65 +++++++++++++++++++++++++++++++++-----
5 files changed, 103 insertions(+), 28 deletions(-)
create mode 100644 CHANGES/12689.breaking.rst
diff --git a/CHANGES/12689.breaking.rst b/CHANGES/12689.breaking.rst
new file mode 100644
index 00000000000..9d807e00769
--- /dev/null
+++ b/CHANGES/12689.breaking.rst
@@ -0,0 +1,7 @@
+Tightened outbound header serialization to reject all ASCII control
+characters forbidden by :rfc:`9110#section-5.5` and :rfc:`9112#section-4`
+(``0x00``\-``0x08``, ``0x0A``\-``0x1F``, ``0x7F``) in status lines,
+header field-names, and field-values. Previously only CR, LF and NUL were
+rejected. HTAB (``0x09``) remains permitted in field values. Applications
+that placed bare control characters in outbound headers will now raise
+:exc:`ValueError` instead of emitting non-RFC-compliant bytes -- by :user:`rodrigobnogueira`.
diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
index 3b6648685d5..2834202427f 100644
--- a/THREAT_MODEL.md
+++ b/THREAT_MODEL.md
@@ -332,7 +332,8 @@ called out where the writer's safety depends on them).
**Components covered.**
- `aiohttp/_http_writer.pyx` — Cython `_serialize_headers` and
- `_write_str_raise_on_nlcr` (the CR/LF/NUL bytewise rejector).
+ `_write_str_raise_on_nlcr` (the forbidden-CTL bytewise rejector: rejects
+ `0x00-0x08`, `0x0A-0x1F`, `0x7F`; HTAB and SP remain permitted).
- `aiohttp/http_writer.py` — `StreamWriter` (the `AbstractStreamWriter`
implementation) plus the pure-Python `_py_serialize_headers` /
`_safe_header` fallback and the Cython/pure-Python switch at
@@ -345,7 +346,9 @@ called out where the writer's safety depends on them).
implementation; if `_http_writer` (Cython) imports successfully and
`AIOHTTP_NO_EXTENSIONS` is unset, the Cython implementation replaces it
(`http_writer.py:_py_serialize_headers`). Both implementations apply the same
-CR / LF / NUL rejection on names *and* values *and* the status/request line.
+RFC 9110 §5.5 / RFC 9112 §4 forbidden-CTL rejection (`0x00-0x08`,
+`0x0A-0x1F`, `0x7F`; HTAB and SP permitted) on names *and* values *and*
+the status/request line.
**Trust boundaries & data flow.**
@@ -353,7 +356,7 @@ CR / LF / NUL rejection on names *and* values *and* the status/request line.
flowchart LR
Handler([User handler / ClientRequest]) -->|status_line, headers, body| SW[StreamWriter]
SW --> Serialize[_serialize_headers]
- Serialize -->|reject CR/LF/NUL| Bytes[Wire bytes]
+ Serialize -->|reject forbidden CTLs| Bytes[Wire bytes]
SW --> Body[write / write_eof / write_chunked]
Body --> Bytes
Bytes --> Transport[(asyncio Transport)]
@@ -380,38 +383,38 @@ on the wire. The wire-side consumer is the **untrusted** counterparty
| # | Component / Vector | STRIDE | Threat | Risk |
| :--- | :--- | :--- | :--- | :--- |
-| 2.1 | Header name/value with CR / LF / NUL | T / I | Response-splitting / header injection allowing the next "header" or even a complete second response/request to be appended on the wire. | High |
+| 2.1 | Header name/value with forbidden CTL | T / I | Response-splitting / header injection (CR / LF) or non-RFC-compliant CTLs (`0x01-0x08`, `0x0B-0x1F`, `0x7F`) that downstream agents historically treat inconsistently. | High |
| 2.2 | Status-line `reason` with CR / LF | T | Same family as 2.1 but on the status line; could let an attacker-controlled reason inject a body or a second status line. | High |
-| 2.3 | Request-line path/method | T | Path-side smuggling via CR / LF / NUL or whitespace inside the path the writer emits. | Medium |
+| 2.3 | Request-line path/method | T | Path-side smuggling via forbidden CTLs or whitespace inside the path the writer emits. | Medium |
| 2.4 | `Content-Length` ≠ actual body length | T | If a handler / ClientRequest emits a body whose length disagrees with declared `Content-Length`, an intermediary may interpret framing differently from the writer's peer (smuggling). | Medium |
| 2.5 | `Content-Length` *and* `Transfer-Encoding: chunked` | T | Both headers reach the wire if user code constructs them via the raw headers dict; intermediaries disagree on which wins. | Medium |
| 2.6 | Body emission on HEAD / 1xx / 204 / 304 | T | Writer strips CL/TE for empty-body responses but **does not block the application from writing a body**; bytes after the `\r\n\r\n` confuse the next pipelined request. | Medium |
-| 2.7 | `Set-Cookie` / `Cookie` value | T | Cookie name or value containing CR / LF / NUL passes through `SimpleCookie.output()` unchanged; only caught by writer's header validation. | Medium |
+| 2.7 | `Set-Cookie` / `Cookie` value | T | Cookie name or value containing forbidden CTLs passes through `SimpleCookie.output()` unchanged; only caught by writer's header validation. | Medium |
| 2.8 | Compression / `Content-Encoding` | T | Body double-compression when user sets `Content-Encoding` manually and also enables `compress=...`. Intermediaries may reject or mis-decode a doubly-compressed body. | Low |
| 2.9 | Drain / backpressure on slow readers | D | Slow consumer (or `Sec-WebSocket-Key`-style hold) keeps `transport.write()` queued; writer drains at 64 KiB threshold (`http_writer.py:StreamWriter.write`). A handler that doesn't await `drain()` can blow up. | Medium |
| 2.10 | Single oversized chunk | D | `write(b)` with a multi-GB blob is handed straight to `transport.write`; memory pressure shifts to asyncio's buffer. | Low |
| 2.11 | Chunked encoding hex framing | T | Malformed chunk-size lines (negative values, leading-`+`, leading zeros, hex obfuscation) would let a non-aiohttp peer reframe the body differently and smuggle. | Low |
-| 2.12 | Header insertion validation timing | T | CR/LF/NUL rejection is *write-time*, not *insert-time*. A handler that sets a malicious header and then aborts before `write_headers()` will not raise. (Documented; not a recommended change.) | Low |
-| 2.13 | Cython ⇄ pure-Python parity | T | Divergence between the two `_serialize_headers` implementations could let one backend silently pass CR/LF/NUL that the other rejects, weakening egress safety asymmetrically. | Low |
+| 2.12 | Header insertion validation timing | T | Forbidden-CTL rejection is *write-time*, not *insert-time*. A handler that sets a malicious header and then aborts before `write_headers()` will not raise. (Documented; not a recommended change.) | Low |
+| 2.13 | Cython ⇄ pure-Python parity | T | Divergence between the two `_serialize_headers` implementations could let one backend silently pass a forbidden CTL that the other rejects, weakening egress safety asymmetrically. | Low |
| 2.14 | Trailers asymmetry | T | The writer never emits trailers, but the parser accepts incoming trailers; not a writer-side threat in itself, just a documentation point for completeness. | Low |
**Mitigations.**
| # | Threat | Existing | Recommended |
| :--- | :--- | :--- | :--- |
-| 2.1 | Header CR / LF / NUL injection | Both backends reject these bytes via `_write_str_raise_on_nlcr` (`_http_writer.pyx:_write_str_raise_on_nlcr`) and `_safe_header` (`http_writer.py:_safe_header`), raising `ValueError` from `_serialize_headers` before any byte hits the transport. Applied symmetrically to names, values, and the status line. | **The current tests import whichever `_serialize_headers` won the import, so only one backend is exercised. Parameterise like `tests/test_http_parser.py` does (cross-cuts [§6.1](#61-highest-leverage-recommendations) #3).** |
-| 2.2 | Status-line `reason` injection | `web_response.Response._set_status` (`web_response.py:StreamResponse._set_status`) rejects `\r` / `\n` in `reason` *at set-time*. The writer also rejects them at write-time as part of the status-line validation. | None. |
-| 2.3 | Request-line path / method | The full status line (`{method} {path} HTTP/{v}.{v}`) goes through `_write_str_raise_on_nlcr` / `_safe_header`, so CR / LF / NUL are caught regardless of whether `path` came from `yarl` or `method` was a caller-supplied string. yarl additionally rejects these bytes earlier per RFC 3986. | None. |
+| 2.1 | Header forbidden-CTL injection | Both backends reject the full RFC 9110 §5.5 / RFC 9112 §4 forbidden set (`0x00-0x08`, `0x0A-0x1F`, `0x7F`; HTAB and SP permitted) via `_write_str_raise_on_nlcr` (`_http_writer.pyx:_write_str_raise_on_nlcr`) and `_safe_header` (`http_writer.py:_safe_header`), raising `ValueError` from `_serialize_headers` before any byte hits the transport. Applied symmetrically to names, values, and the status line. | **The current tests import whichever `_serialize_headers` won the import, so only one backend is exercised. Parameterise like `tests/test_http_parser.py` does (cross-cuts [§6.1](#61-highest-leverage-recommendations) #3).** |
+| 2.2 | Status-line `reason` injection | `web_response.Response._set_status` (`web_response.py:StreamResponse._set_status`) rejects `\r` / `\n` in `reason` *at set-time*. The writer also rejects the full forbidden-CTL set at write-time as part of the status-line validation. | None. |
+| 2.3 | Request-line path / method | The full status line (`{method} {path} HTTP/{v}.{v}`) goes through `_write_str_raise_on_nlcr` / `_safe_header`, so forbidden CTLs are caught regardless of whether `path` came from `yarl` or `method` was a caller-supplied string. yarl additionally rejects CR/LF/NUL earlier per RFC 3986. | None. |
| 2.4 | CL / body-length mismatch | None at write-time. `web.Response.write_eof` and the chunked writer write what they're given. | **Recommended hardening: in DEBUG mode, assert / warn when actual bytes-written disagrees with declared `Content-Length` at `write_eof()`. Useful for catching smuggling-adjacent bugs in user handlers.** |
| 2.5 | CL + TE simultaneous | Server-side `enable_chunked_encoding()` (`web_response.py:StreamResponse.enable_chunked_encoding`) raises if `Content-Length` is already set; client-side `_update_transfer_encoding()` (`client_reqrep.py:ClientRequest._update_transfer_encoding`) raises if user sets `chunked=True` while `Content-Length` is in headers. Manual user injection into the raw headers dict is *not* caught. | **Consider a write-time assert in `StreamWriter` that rejects `Content-Length` and `Transfer-Encoding: chunked` coexisting.** |
| 2.6 | Body-suppression edge cases | `web_response.py:StreamResponse._prepare_headers` strips `Content-Length` and `Transfer-Encoding` for HEAD / 1xx / 204 / 304 (`EMPTY_BODY_STATUS_CODES`, `helpers.py:EMPTY_BODY_METHODS`). The framework's own machinery doesn't write a body for these. | **User**: Do not call `resp.write(...)` in a handler responding HEAD / 1xx / 204 / 304 — framing strips CL / TE but does not block the byte write. **Optional aiohttp change: have `StreamWriter` short-circuit body writes when `length == 0` and the response was framed as empty-body.** |
-| 2.7 | Cookie injection | `populate_with_cookies` (`helpers.py:populate_with_cookies`) routes the cookie through `SimpleCookie.output()` and then into a regular header, where the CR / LF / NUL check at write-time catches anything `SimpleCookie` happened to pass through. | Documented design decision: rely on writer-level validation rather than tightening `set_cookie` / `populate_with_cookies` further. Keep regression tests covering cookie name/value with CR / LF / NUL across both backends. |
+| 2.7 | Cookie injection | `populate_with_cookies` (`helpers.py:populate_with_cookies`) routes the cookie through `SimpleCookie.output()` and then into a regular header, where the forbidden-CTL check at write-time catches anything `SimpleCookie` happened to pass through. | Documented design decision: rely on writer-level validation rather than tightening `set_cookie` / `populate_with_cookies` further. Keep regression tests covering cookie name/value with forbidden CTLs across both backends. |
| 2.8 | Manual `Content-Encoding` | Server side: `enable_compression()` (`web_response.py:StreamResponse.enable_compression`) returns early if `Content-Encoding` already present, so the body is not double-compressed. Client side: `ClientRequest._update_content_encoding` raises `ValueError("compress can not be set if Content-Encoding header is set")` — symmetric guard. | None. |
| 2.9 | Drain / backpressure | `StreamWriter.write` drains at `LIMIT = 0x10000` bytes (`http_writer.py:StreamWriter.write`) when `drain=True` is set by the caller. Application code is expected to `await write(...)` to honour backpressure. | **User**: `await write(...)` in handlers; tight `for` loops without `await` can starve the event loop. Cross-reference [§5.7](#57-server-connection-lifecycle) for connection-level read/write timeouts that mitigate slow consumers. |
| 2.10 | Oversized single chunk | None at the writer layer — bytes go straight to `transport.write`. asyncio applies its own high-water marks via the transport. | **User**: Relies on application-level bounds (use streaming, generators, `FileResponse`, etc., for large bodies). |
| 2.11 | Chunked hex framing | The writer always uses `f"{len(chunk):x}\r\n"` followed by the chunk and `\r\n` (`http_writer.py:StreamWriter._write_chunked_payload`). | None. |
| 2.12 | Insert-time vs write-time validation | Headers are validated at write-time only; `set_status` validates `reason` at set-time. | Documented design decision: late validation is acceptable; keep behaviour as-is. |
-| 2.13 | Cython ⇄ pure-Python parity | Both backends share the same logic and test surface; the Cython version uses a fast bytewise check, the Python version uses `in` on three sentinel characters. | **Parameterise the writer tests over both backends so egress equivalence on malicious inputs is exercised under both (see [§6.1](#61-highest-leverage-recommendations) #3).** |
+| 2.13 | Cython ⇄ pure-Python parity | Both backends share the same logic and test surface; the Cython version uses a fast per-codepoint range check (`ch < 0x20 and ch != 0x09`, plus `0x7F`), the Python version uses a precompiled `re` over the same forbidden set. | **Parameterise the writer tests over both backends so egress equivalence on malicious inputs is exercised under both (see [§6.1](#61-highest-leverage-recommendations) #3).** |
| 2.14 | Trailers asymmetry | Writer does not emit trailers; parser accepts trailers on incoming. Documented for completeness. | None. |
**Past advisories / hardening (recap).**
@@ -424,11 +427,19 @@ on the wire. The wire-side consumer is the **untrusted** counterparty
the status-line `reason`. Fixed by rejecting CR/LF in `reason` at
`_set_status` set-time, on top of the existing writer-side check
(threat 2.2).
-
-Writer-level CR / LF / NUL rejection via `_safe_header` and
+- **[#12689](https://github.com/aio-libs/aiohttp/pull/12689)** (hardening, no
+ CVE) — outbound header serialization only rejected CR/LF/NUL; other
+ RFC 9110 §5.5 / RFC 9112 §4 forbidden CTLs (`0x01-0x08`, `0x0B-0x1F`,
+ `0x7F`) could be emitted on the wire if a handler placed them into a
+ header. Tightened `_safe_header` and `_write_str_raise_on_nlcr` to
+ reject the full forbidden set (threat 2.1).
+
+Writer-level forbidden-CTL rejection via `_safe_header` and
`_write_str_raise_on_nlcr` has been in place since the header-injection
family of issues was first surfaced (well before CVE-2023-37276, which
-was a parser-side fix).
+was a parser-side fix); the rejected set was broadened from
+{CR, LF, NUL} to the full RFC 9110 forbidden set in
+[#12689](https://github.com/aio-libs/aiohttp/pull/12689).
---
diff --git a/aiohttp/_http_writer.pyx b/aiohttp/_http_writer.pyx
index ee8dcd7d2e9..7859ebd6682 100644
--- a/aiohttp/_http_writer.pyx
+++ b/aiohttp/_http_writer.pyx
@@ -111,9 +111,11 @@ cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s):
out_str = str(s)
for ch in out_str:
- if ch in {0x0D, 0x0A, 0x00}:
+ # https://www.rfc-editor.org/info/rfc9110/#section-5.5-5
+ # https://www.rfc-editor.org/info/rfc9112/#section-4-3
+ if (ch < 0x20 and ch != 0x09) or ch == 0x7F:
raise ValueError(
- "Newline, carriage return, or null byte detected in headers. "
+ "Forbidden control character detected in headers. "
"Potential header injection attack."
)
if _write_utf8(writer, ch) < 0:
diff --git a/aiohttp/http_writer.py b/aiohttp/http_writer.py
index 411a2aae882..a252d92effe 100644
--- a/aiohttp/http_writer.py
+++ b/aiohttp/http_writer.py
@@ -1,6 +1,7 @@
"""Http related parsers and protocol."""
import asyncio
+import re
import sys
from typing import ( # noqa
TYPE_CHECKING,
@@ -347,10 +348,15 @@ async def drain(self) -> None:
await protocol._drain_helper()
+# https://www.rfc-editor.org/info/rfc9110/#section-5.5-5
+# https://www.rfc-editor.org/info/rfc9112/#section-4-3
+_FORBIDDEN_HEADER_CHARS_RE = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]")
+
+
def _safe_header(string: str) -> str:
- if "\r" in string or "\n" in string or "\x00" in string:
+ if _FORBIDDEN_HEADER_CHARS_RE.search(string) is not None:
raise ValueError(
- "Newline, carriage return, or null byte detected in headers. "
+ "Forbidden control character detected in headers. "
"Potential header injection attack."
)
return string
diff --git a/tests/test_http_writer.py b/tests/test_http_writer.py
index 3c667bb7344..0d052110242 100644
--- a/tests/test_http_writer.py
+++ b/tests/test_http_writer.py
@@ -984,10 +984,11 @@ async def test_write_headers_with_compression_coalescing(
[
"\n",
"\r",
+ "\x00",
],
)
def test_serialize_headers_raises_on_new_line_or_carriage_return(char: str) -> None:
- """Verify serialize_headers raises on cr or nl in the headers."""
+ """Verify serialize_headers raises on cr, nl, or null byte in the headers."""
status_line = "HTTP/1.1 200 OK"
headers = CIMultiDict(
{
@@ -1002,21 +1003,69 @@ def test_serialize_headers_raises_on_new_line_or_carriage_return(char: str) -> N
_serialize_headers(status_line, headers)
-def test_serialize_headers_raises_on_null_byte() -> None:
+@pytest.mark.parametrize(
+ "char",
+ [chr(c) for c in (*range(0x01, 0x09), *range(0x0B, 0x20), 0x7F)],
+)
+def test_serialize_headers_raises_on_forbidden_control_chars_in_value(
+ char: str,
+) -> None:
+ """Verify serialize_headers rejects RFC 9110-forbidden CTLs in values."""
status_line = "HTTP/1.1 200 OK"
- headers = CIMultiDict(
- {
- hdrs.CONTENT_TYPE: "text/plain\x00",
- }
- )
+ headers = CIMultiDict({hdrs.CONTENT_TYPE: f"text/plain{char}"})
with pytest.raises(
ValueError,
- match="null byte detected in headers",
+ match="Forbidden control character detected in headers",
):
_serialize_headers(status_line, headers)
+@pytest.mark.parametrize(
+ "char",
+ [chr(c) for c in (*range(0x01, 0x09), *range(0x0B, 0x20), 0x7F)],
+)
+def test_serialize_headers_raises_on_forbidden_control_chars_in_name(
+ char: str,
+) -> None:
+ """Verify serialize_headers rejects RFC 9110-forbidden CTLs in names."""
+ status_line = "HTTP/1.1 200 OK"
+ headers = CIMultiDict({f"X-Bad{char}Header": "value"})
+
+ with pytest.raises(
+ ValueError,
+ match="Forbidden control character detected in headers",
+ ):
+ _serialize_headers(status_line, headers)
+
+
+@pytest.mark.parametrize(
+ "char",
+ [chr(c) for c in (*range(0x01, 0x09), *range(0x0B, 0x20), 0x7F)],
+)
+def test_serialize_headers_raises_on_forbidden_control_chars_in_status_line(
+ char: str,
+) -> None:
+ """Verify serialize_headers rejects RFC 9110-forbidden CTLs in status line."""
+ status_line = f"HTTP/1.1 200 OK{char}"
+ headers: CIMultiDict[str] = CIMultiDict()
+
+ with pytest.raises(
+ ValueError,
+ match="Forbidden control character detected in headers",
+ ):
+ _serialize_headers(status_line, headers)
+
+
+def test_serialize_headers_allows_htab_in_value() -> None:
+ """Verify HTAB (0x09) remains permitted in field values per RFC 9110."""
+ status_line = "HTTP/1.1 200 OK"
+ headers = CIMultiDict({hdrs.CONTENT_TYPE: "text/plain\tcharset=utf-8"})
+
+ result = _serialize_headers(status_line, headers)
+ assert b"text/plain\tcharset=utf-8" in result
+
+
async def test_write_compressed_data_with_headers_coalescing(
buf: bytearray,
protocol: BaseProtocol,
From bf88077ebb14f4c29924b8e8904cba20c55c28b8 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Thu, 28 May 2026 23:57:41 +0100
Subject: [PATCH 160/191] [PR #12719/879d48d1 backport][3.14] Reject invalid
bytes in multipart/payload headers (#12720)
**This is a backport of PR #12719 as merged into master
(879d48d1619b9bc3662037afcb8bfa790a205e9b).**
Co-authored-by: Sam Bull
---
CHANGES/12706.bugfix.rst | 1 +
aiohttp/payload.py | 8 +++++---
tests/test_payload.py | 15 +++++++++++++++
3 files changed, 21 insertions(+), 3 deletions(-)
create mode 100644 CHANGES/12706.bugfix.rst
diff --git a/CHANGES/12706.bugfix.rst b/CHANGES/12706.bugfix.rst
new file mode 100644
index 00000000000..9248585f99f
--- /dev/null
+++ b/CHANGES/12706.bugfix.rst
@@ -0,0 +1 @@
+Fixed invalid bytes being allowed in multipart/payload headers -- by :user:`Dreamsorcerer`.
diff --git a/aiohttp/payload.py b/aiohttp/payload.py
index dfc51831dad..b337bcf7795 100644
--- a/aiohttp/payload.py
+++ b/aiohttp/payload.py
@@ -23,6 +23,7 @@
parse_mimetype,
sentinel,
)
+from .http_writer import _safe_header
from .streams import StreamReader
from .typedefs import JSONBytesEncoder, JSONEncoder, _CIMultiDict
@@ -197,9 +198,10 @@ def headers(self) -> _CIMultiDict:
@property
def _binary_headers(self) -> bytes:
return (
- "".join([k + ": " + v + "\r\n" for k, v in self.headers.items()]).encode(
- "utf-8"
- )
+ "".join(
+ _safe_header(k) + ": " + _safe_header(v) + "\r\n"
+ for k, v in self.headers.items()
+ ).encode("utf-8")
+ b"\r\n"
)
diff --git a/tests/test_payload.py b/tests/test_payload.py
index 3284c213d55..0fe57eeb679 100644
--- a/tests/test_payload.py
+++ b/tests/test_payload.py
@@ -101,6 +101,21 @@ def test_payload_content_type() -> None:
assert p.content_type == "application/json"
+@pytest.mark.parametrize("bad_byte", ("\r", "\n", "\x00"))
+def test_binary_headers_reject_injection_in_value(bad_byte: str) -> None:
+ p = Payload("test", headers={"X-Custom": f"value{bad_byte}Injected: bad"})
+ with pytest.raises(ValueError, match="header injection"):
+ p._binary_headers
+
+
+@pytest.mark.parametrize("bad_byte", ("\r", "\n", "\x00"))
+def test_binary_headers_reject_injection_in_name(bad_byte: str) -> None:
+ p = Payload("test")
+ p.headers[f"X-Custom{bad_byte}Injected"] = "value"
+ with pytest.raises(ValueError, match="header injection"):
+ p._binary_headers
+
+
def test_bytes_payload_default_content_type() -> None:
p = payload.BytesPayload(b"data")
assert p.content_type == "application/octet-stream"
From 221d02b882a162eee2b050692720aef6e1fa5865 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Fri, 29 May 2026 02:04:34 +0100
Subject: [PATCH 161/191] [PR #12721/5a9777d0 backport][3.14] Reject invalid
bytes in add_field() name/filename (#12722)
**This is a backport of PR #12721 as merged into master
(5a9777d09b460996f91f14520ce1a8056c06477c).**
Co-authored-by: Sam Bull
---
CHANGES/12722.bugfix.rst | 1 +
aiohttp/formdata.py | 9 ++++-----
tests/test_formdata.py | 16 +++++++++++++++-
3 files changed, 20 insertions(+), 6 deletions(-)
create mode 100644 CHANGES/12722.bugfix.rst
diff --git a/CHANGES/12722.bugfix.rst b/CHANGES/12722.bugfix.rst
new file mode 100644
index 00000000000..af8b103f566
--- /dev/null
+++ b/CHANGES/12722.bugfix.rst
@@ -0,0 +1 @@
+Fixed :py:meth:`~aiohttp.FormData.add_field` accepting invalid bytes in ``name`` and ``filename`` -- by :user:`Dreamsorcerer`.
diff --git a/aiohttp/formdata.py b/aiohttp/formdata.py
index 5fe37f62dce..f602cdf2357 100644
--- a/aiohttp/formdata.py
+++ b/aiohttp/formdata.py
@@ -8,6 +8,7 @@
from . import hdrs, multipart, payload
from .helpers import guess_filename
+from .http_writer import _safe_header
from .payload import Payload
__all__ = ("FormData",)
@@ -64,12 +65,14 @@ def add_field(
warnings.warn(msg, DeprecationWarning)
filename = name
+ _safe_header(name)
type_options: MultiDict[str] = MultiDict({"name": name})
if filename is not None and not isinstance(filename, str):
raise TypeError("filename must be an instance of str. Got: %s" % filename)
if filename is None and isinstance(value, io.IOBase):
filename = guess_filename(value, name)
if filename is not None:
+ _safe_header(filename)
type_options["filename"] = filename
self._is_multipart = True
@@ -79,11 +82,7 @@ def add_field(
raise TypeError(
"content_type must be an instance of str. Got: %s" % content_type
)
- if "\r" in content_type or "\n" in content_type:
- raise ValueError(
- "Newline or carriage return detected in headers. "
- "Potential header injection attack."
- )
+ _safe_header(content_type)
headers[hdrs.CONTENT_TYPE] = content_type
self._is_multipart = True
if content_transfer_encoding is not None:
diff --git a/tests/test_formdata.py b/tests/test_formdata.py
index d826c92e67d..16839aa69a3 100644
--- a/tests/test_formdata.py
+++ b/tests/test_formdata.py
@@ -74,13 +74,27 @@ def test_invalid_type_formdata_content_type(val: object) -> None:
form.add_field("foo", "bar", content_type=val) # type: ignore[arg-type]
-@pytest.mark.parametrize("val", ("\r", "\n", "a\ra\n", "a\na\r"))
+@pytest.mark.parametrize("val", ("\r", "\n", "a\ra\n", "a\na\r", "a\x00b"))
def test_invalid_value_formdata_content_type(val: str) -> None:
form = FormData()
with pytest.raises(ValueError):
form.add_field("foo", "bar", content_type=val)
+@pytest.mark.parametrize("val", ("\r", "\n", "a\ra\n", "a\na\r", "a\x00b"))
+def test_invalid_value_formdata_name(val: str) -> None:
+ form = FormData()
+ with pytest.raises(ValueError):
+ form.add_field(val, "bar")
+
+
+@pytest.mark.parametrize("val", ("\r", "\n", "a\ra\n", "a\na\r", "a\x00b"))
+def test_invalid_value_formdata_filename(val: str) -> None:
+ form = FormData()
+ with pytest.raises(ValueError):
+ form.add_field("foo", "bar", filename=val)
+
+
def test_invalid_formdata_filename() -> None:
form = FormData()
invalid_vals = [0, 0.1, {}, [], b"foo"]
From 9962023f625743c31004eb29b80eebb7819e207f Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Fri, 29 May 2026 03:21:05 +0100
Subject: [PATCH 162/191] Fix incorrect websocket upgrade (#12723) (#12724)
(cherry picked from commit 2a4247b1adba3c9a6b1cb0bd6135f332f8ff2f3a)
---
CHANGES/12727.bugfix.rst | 1 +
aiohttp/test_utils.py | 5 ++++-
aiohttp/web_ws.py | 2 +-
tests/test_web_websocket_functional.py | 29 ++++++++++++++++++++++++--
4 files changed, 33 insertions(+), 4 deletions(-)
create mode 100644 CHANGES/12727.bugfix.rst
diff --git a/CHANGES/12727.bugfix.rst b/CHANGES/12727.bugfix.rst
new file mode 100644
index 00000000000..d74b5eae2d3
--- /dev/null
+++ b/CHANGES/12727.bugfix.rst
@@ -0,0 +1 @@
+Fixed websocket upgrade occurring when header contained a value like `notupgrade` -- by :user:`Dreamsorcerer`.
diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py
index 92a49b975ec..6b50e23b810 100644
--- a/aiohttp/test_utils.py
+++ b/aiohttp/test_utils.py
@@ -733,6 +733,9 @@ def make_mocked_request(
raw_hdrs = ()
chunked = "chunked" in headers.get(hdrs.TRANSFER_ENCODING, "").lower()
+ upgrade = headers.get(hdrs.CONNECTION, "").lower() == "upgrade" and bool(
+ headers.get(hdrs.UPGRADE)
+ )
message = RawRequestMessage(
method,
@@ -742,7 +745,7 @@ def make_mocked_request(
raw_hdrs,
closing,
None,
- False,
+ upgrade,
chunked,
URL(path),
)
diff --git a/aiohttp/web_ws.py b/aiohttp/web_ws.py
index c01d95637cd..f4cef84db53 100644
--- a/aiohttp/web_ws.py
+++ b/aiohttp/web_ws.py
@@ -287,7 +287,7 @@ def _handshake(
)
)
- if "upgrade" not in headers.get(hdrs.CONNECTION, "").lower():
+ if not request._message.upgrade:
raise HTTPBadRequest(
text=f"No CONNECTION upgrade hdr: {headers.get(hdrs.CONNECTION)}"
)
diff --git a/tests/test_web_websocket_functional.py b/tests/test_web_websocket_functional.py
index 1f34631279d..7fd084af588 100644
--- a/tests/test_web_websocket_functional.py
+++ b/tests/test_web_websocket_functional.py
@@ -32,8 +32,33 @@ async def handler(request):
assert resp.status == 426
-async def test_websocket_json(loop, aiohttp_client) -> None:
- async def handler(request):
+async def test_handshake_connection_header_substring_not_a_token(
+ aiohttp_client: AiohttpClient,
+) -> None:
+ async def handler(request: web.Request) -> web.WebSocketResponse:
+ ws = web.WebSocketResponse()
+ await ws.prepare(request)
+ await ws.close()
+ return ws
+
+ app = web.Application()
+ app.router.add_route("GET", "/", handler)
+ client = await aiohttp_client(app)
+
+ resp = await client.get(
+ "/",
+ headers={
+ "Upgrade": "websocket",
+ "Connection": "keep-alive, notupgrade",
+ "Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
+ "Sec-WebSocket-Version": "13",
+ },
+ )
+ assert resp.status == 400
+
+
+async def test_websocket_json(aiohttp_client: AiohttpClient) -> None:
+ async def handler(request: web.Request) -> web.WebSocketResponse:
ws = web.WebSocketResponse()
if not ws.can_prepare(request):
return web.HTTPUpgradeRequired()
From 8bd9765fd7f0d9aac09ef6f30b908b276061e14c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 29 May 2026 11:58:58 +0000
Subject: [PATCH 163/191] Bump slotscheck from 0.19.1 to 0.20.0 (#12731)
Bumps [slotscheck](https://github.com/ariebovenberg/slotscheck) from
0.19.1 to 0.20.0.
Release notes
Sourced from slotscheck's
releases.
0.20.0
- Improve speed and accuracy of detecting of "slotless"
classes, even if they are not pure Python (e.g. C extension
modules).
- Remove support for alternative Python implementations (e.g. PyPy)
since they treat slots differently and running slotscheck wouldn't be
useful. It also allows us to use CPython-specific features.
- Improve accuracy of benchmarks on "errors and violations"
page.
- Add experimental
--detect-unused-slots flag (Python
3.13+ only) to detect slots that are declared but never assigned within
the class body. Disabled by default. Use --exclude-slots to
suppress false positives.
Changelog
Sourced from slotscheck's
changelog.
0.20.0 (2026-05-29)
- Improve speed and accuracy of detecting of "slotless"
classes,
even if they are not pure Python (e.g. C extension modules).
- Remove support for alternative Python implementations (e.g. PyPy)
since
they treat slots differently and running slotscheck wouldn't be useful.
It also allows us to use CPython-specific features.
- Improve accuracy of benchmarks on "errors and violations"
page.
- Add experimental
--detect-unused-slots flag (Python
3.13+ only) to
detect slots that are declared but never assigned within the class body.
Disabled by default. Use --exclude-slots to suppress false
positives.
Abstract classes are automatically excluded from this check.
Commits
26f749d
Release 0.20 (#294)
17d803e
Merge pull request #293
from ariebovenberg/dependabot/pip/furo-gte-2025.12.19...
b9713b9
Update furo requirement from <2026,>=2024.8.6 to
>=2025.12.19,<2026
669a5eb
Merge pull request #292
from ariebovenberg/dependabot/pip/tomli-2.4.1
05909f4
Bump tomli from 2.4.0 to 2.4.1
80b4169
Merge pull request #291
from ariebovenberg/dependabot/pip/tomli-2.4.0
a4b8340
Bump tomli from 2.3.0 to 2.4.0
c4f72c0
Merge pull request #290
from ariebovenberg/dependabot/pip/sphinx-click-approx...
1c743d0
Update sphinx-click requirement from ~=6.0.0 to ~=6.2.0
41f715c
Merge pull request #289
from ariebovenberg/dependabot/pip/sphinx-gt-8-and-lt-10
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index f7016461b45..d84fdbdcbfb 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -237,7 +237,7 @@ setuptools-git==1.2
# via -r requirements/test-common.in
six==1.17.0
# via python-dateutil
-slotscheck==0.19.1
+slotscheck==0.20.0
# via -r requirements/lint.in
snowballstemmer==3.1.0
# via sphinx
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 0a882012a66..8e75c8bb8a5 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -230,7 +230,7 @@ setuptools-git==1.2
# via -r requirements/test-common.in
six==1.17.0
# via python-dateutil
-slotscheck==0.19.1
+slotscheck==0.20.0
# via -r requirements/lint.in
snowballstemmer==3.1.0
# via sphinx
diff --git a/requirements/lint.txt b/requirements/lint.txt
index a0a6a505ce4..055e397d7df 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -103,7 +103,7 @@ rich==15.0.0
# via pytest-codspeed
six==1.17.0
# via python-dateutil
-slotscheck==0.19.1
+slotscheck==0.20.0
# via -r requirements/lint.in
tomli==2.4.1
# via
From 193257dbb570f78072b67a32b0531fb6aae98577 Mon Sep 17 00:00:00 2001
From: timrid <6593626+timrid@users.noreply.github.com>
Date: Fri, 29 May 2026 21:30:39 +0200
Subject: [PATCH 164/191] Add support for Android and iOS platforms (#12220)
(#12733)
---
.coveragerc-cython.toml | 3 +
.coveragerc.toml | 3 +
.github/workflows/ci-cd.yml | 68 +++++++++++++-
CHANGES/11750.packaging.rst | 1 +
pyproject.toml | 10 +-
requirements/base-ft.txt | 6 +-
requirements/base.txt | 6 +-
requirements/constraints.txt | 26 +++---
requirements/dev.txt | 28 +++---
requirements/lint.txt | 2 +-
requirements/runtime-deps.in | 6 +-
requirements/runtime-deps.txt | 6 +-
requirements/test-common-base.in | 9 ++
requirements/test-common-base.txt | 54 +++++++++++
requirements/test-common.in | 14 +--
requirements/test-common.txt | 22 ++---
requirements/test-ft.txt | 28 +++---
requirements/test-mobile.in | 8 ++
requirements/test-mobile.txt | 104 +++++++++++++++++++++
requirements/test.txt | 28 +++---
setup.cfg | 2 +
tests/test_benchmarks_client.py | 8 +-
tests/test_benchmarks_client_request.py | 11 ++-
tests/test_benchmarks_client_ws.py | 8 +-
tests/test_benchmarks_cookiejar.py | 9 +-
tests/test_benchmarks_http_websocket.py | 8 +-
tests/test_benchmarks_http_writer.py | 10 +-
tests/test_benchmarks_web_fileresponse.py | 9 +-
tests/test_benchmarks_web_middleware.py | 9 +-
tests/test_benchmarks_web_request.py | 8 +-
tests/test_benchmarks_web_response.py | 10 +-
tests/test_benchmarks_web_urldispatcher.py | 9 +-
tests/test_circular_imports.py | 3 +
tests/test_client_functional.py | 3 +-
tests/test_client_session.py | 7 +-
tests/test_cookiejar.py | 3 +
tests/test_http_parser.py | 3 +
tests/test_leaks.py | 3 +
tests/test_loop.py | 7 ++
tests/test_multipart.py | 2 +-
tests/test_proxy_functional.py | 7 +-
tests/test_run_app.py | 6 ++
tests/test_streams.py | 3 +-
tests/test_urldispatch.py | 13 ++-
tests/test_web_functional.py | 6 +-
tests/test_web_response.py | 72 +++++++-------
tests/test_web_sendfile_functional.py | 14 ++-
47 files changed, 507 insertions(+), 178 deletions(-)
create mode 100644 CHANGES/11750.packaging.rst
create mode 100644 requirements/test-common-base.in
create mode 100644 requirements/test-common-base.txt
create mode 100644 requirements/test-mobile.in
create mode 100644 requirements/test-mobile.txt
diff --git a/.coveragerc-cython.toml b/.coveragerc-cython.toml
index 586dc937786..91336d47dd9 100644
--- a/.coveragerc-cython.toml
+++ b/.coveragerc-cython.toml
@@ -8,6 +8,9 @@ omit = [
]
[report]
+partial_also = [
+ 'if not TYPE_CHECKING',
+]
exclude_also = [
'if TYPE_CHECKING',
'assert False',
diff --git a/.coveragerc.toml b/.coveragerc.toml
index 4ca5d2808bd..e08c0f24b8c 100644
--- a/.coveragerc.toml
+++ b/.coveragerc.toml
@@ -14,6 +14,9 @@ omit = [
]
[report]
+partial_also = [
+ 'if not TYPE_CHECKING',
+]
exclude_also = [
'if TYPE_CHECKING',
'assert False',
diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml
index 67923da42c0..9bb01cb551c 100644
--- a/.github/workflows/ci-cd.yml
+++ b/.github/workflows/ci-cd.yml
@@ -255,6 +255,59 @@ jobs:
report_type: test_results
token: ${{ secrets.CODECOV_TOKEN }}
+ test-mobile:
+ permissions:
+ contents: read # to fetch code (actions/checkout)
+
+ name: Test (${{ matrix.config.platform }}, ${{ matrix.pyver }}, ${{ matrix.config.os }})
+ runs-on: ${{ matrix.config.os }}
+ needs: gen_llhttp
+ strategy:
+ matrix:
+ pyver: ["cp313", "cp314"]
+ config:
+ - os: ubuntu-latest
+ platform: android
+ archs: x86_64
+ - os: macos-14
+ platform: ios
+ archs: arm64_iphonesimulator
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6
+ with:
+ submodules: true
+ - name: Setup Python ${{ matrix.pyver }}
+ id: python-install
+ # important: do not use system python
+ env:
+ UV_PYTHON_PREFERENCE: only-managed
+ uses: astral-sh/setup-uv@v8.1.0
+ with:
+ python-version: ${{ matrix.pyver }}
+ activate-environment: true
+ enable-cache: true
+ - name: Install build tooling and cython
+ run: |
+ uv pip install -U pip wheel setuptools build twine -r requirements/cython.in -c requirements/cython.txt
+ - name: Restore llhttp generated files
+ uses: actions/download-artifact@v8
+ with:
+ name: llhttp
+ path: vendor/llhttp/build/
+ - name: Cythonize
+ run: |
+ make cythonize
+ - name: Build wheels and test
+ uses: pypa/cibuildwheel@v3.4.1
+ env:
+ CIBW_BUILD: ${{ matrix.pyver }}-*
+ CIBW_PLATFORM: ${{ matrix.config.platform }}
+ CIBW_ARCHS: ${{ matrix.config.archs }}
+ CIBW_TEST_REQUIRES: -r requirements/test-mobile.txt
+ CIBW_TEST_SOURCES: setup.cfg README.rst tests
+ CIBW_TEST_COMMAND: python -m pytest
+
autobahn:
permissions:
contents: read # to fetch code (actions/checkout)
@@ -445,6 +498,7 @@ jobs:
needs:
- lint
- test
+ - test-mobile
- autobahn
runs-on: ubuntu-latest
@@ -506,7 +560,7 @@ jobs:
path: dist
build-wheels:
- name: Build wheels on ${{ matrix.os }} ${{ matrix.qemu }} ${{ matrix.musl }}
+ name: Build wheels on ${{ matrix.os }} ${{ matrix.qemu }} ${{ matrix.musl }} ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
needs: pre-deploy
strategy:
@@ -514,6 +568,7 @@ jobs:
os: ["ubuntu-latest", "windows-latest", "windows-11-arm", "macos-latest", "ubuntu-24.04-arm"]
qemu: ['']
musl: [""]
+ platform: [""]
include:
# Split ubuntu/musl jobs for the sake of speed-up
- os: ubuntu-latest
@@ -549,6 +604,10 @@ jobs:
musl: musllinux
- os: ubuntu-24.04-arm
musl: musllinux
+ - os: ubuntu-latest
+ platform: android
+ - os: macos-14
+ platform: ios
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -603,14 +662,19 @@ jobs:
# for those QEMU matrix cells.
extras: uv
env:
+ CIBW_PLATFORM: ${{ matrix.platform || 'auto' }}
CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }}
CIBW_ARCHS_MACOS: x86_64 arm64 universal2
+ CIBW_ARCHS_IOS: arm64_iphoneos arm64_iphonesimulator x86_64_iphonesimulator
+ CIBW_ARCHS_ANDROID: arm64_v8a x86_64
- name: Upload wheels
uses: actions/upload-artifact@v7
with:
name: >-
dist-${{ matrix.os }}-${{ matrix.musl }}-${{
- matrix.qemu
+ matrix.platform
+ && matrix.platform
+ || matrix.qemu
&& matrix.qemu
|| 'native'
}}
diff --git a/CHANGES/11750.packaging.rst b/CHANGES/11750.packaging.rst
new file mode 100644
index 00000000000..2af5e11cf45
--- /dev/null
+++ b/CHANGES/11750.packaging.rst
@@ -0,0 +1 @@
+Added wheels for Android and iOS platforms -- by :user:`timrid`.
diff --git a/pyproject.toml b/pyproject.toml
index c74565e4902..bf490bf7cdc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -49,10 +49,10 @@ dynamic = [
[project.optional-dependencies]
speedups = [
- "aiodns >= 3.3.0",
- "Brotli >= 1.2; platform_python_implementation == 'CPython'",
+ "aiodns >= 3.3.0; sys_platform != 'android' and sys_platform != 'ios'",
+ "Brotli >= 1.2; platform_python_implementation == 'CPython' and sys_platform != 'android' and sys_platform != 'ios'",
"brotlicffi >= 1.2; platform_python_implementation != 'CPython'",
- "backports.zstd; platform_python_implementation == 'CPython' and python_version < '3.14'",
+ "backports.zstd; platform_python_implementation == 'CPython' and python_version < '3.14' and sys_platform != 'android' and sys_platform != 'ios'",
]
[[project.maintainers]]
@@ -171,6 +171,10 @@ test-command = ""
# don't build PyPy wheels, install from source instead
skip = "pp*"
+[tool.cibuildwheel.ios]
+# iOS currently does not support build[uv]
+build-frontend = "build"
+
[tool.codespell]
skip = '.git,*.pdf,*.svg,Makefile,CONTRIBUTORS.txt,venvs,_build'
ignore-words-list = 'te,ue'
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 2592806157c..4a320fa2e85 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/base-ft.txt --strip-extras requirements/base-ft.in
#
-aiodns==4.0.4
+aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.2
# via -r requirements/runtime-deps.in
@@ -14,9 +14,9 @@ async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
attrs==26.1.0
# via -r requirements/runtime-deps.in
-backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
+backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
-brotli==1.2.0 ; platform_python_implementation == "CPython"
+brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
cffi==2.0.0
# via pycares
diff --git a/requirements/base.txt b/requirements/base.txt
index e3c17b4f01d..5b50ef94765 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/base.txt --strip-extras requirements/base.in
#
-aiodns==4.0.4
+aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.2
# via -r requirements/runtime-deps.in
@@ -14,9 +14,9 @@ async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
attrs==26.1.0
# via -r requirements/runtime-deps.in
-backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
+backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
-brotli==1.2.0 ; platform_python_implementation == "CPython"
+brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
cffi==2.0.0
# via pycares
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index d84fdbdcbfb..5745d103182 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/constraints.txt --strip-extras requirements/constraints.in
#
-aiodns==4.0.4
+aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios"
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
@@ -36,7 +36,7 @@ blockbuster==1.5.26
# via
# -r requirements/lint.in
# -r requirements/test-common.in
-brotli==1.2.0 ; platform_python_implementation == "CPython"
+brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
build==1.5.0
# via pip-tools
@@ -83,7 +83,7 @@ forbiddenfruit==0.1.4
freezegun==1.5.5
# via
# -r requirements/lint.in
- # -r requirements/test-common.in
+ # -r requirements/test-common-base.in
frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
@@ -151,7 +151,7 @@ pathspec==1.1.1
pip-tools==7.5.3
# via -r requirements/dev.in
pkgconfig==1.6.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
platformdirs==4.10.0
# via
# python-discovery
@@ -167,7 +167,7 @@ propcache==0.5.2
# -r requirements/runtime-deps.in
# yarl
proxy-py==2.4.10
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pycares==5.0.1
# via aiodns
pycparser==3.0
@@ -190,7 +190,7 @@ pyproject-hooks==1.2.0
pytest==9.0.3
# via
# -r requirements/lint.in
- # -r requirements/test-common.in
+ # -r requirements/test-common-base.in
# pytest-codspeed
# pytest-cov
# pytest-mock
@@ -201,13 +201,13 @@ pytest-codspeed==5.0.3
# -r requirements/lint.in
# -r requirements/test-common.in
pytest-cov==7.1.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-mock==3.15.1
# via
# -r requirements/lint.in
- # -r requirements/test-common.in
+ # -r requirements/test-common-base.in
pytest-timeout==2.4.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
@@ -223,10 +223,6 @@ pyyaml==6.0.3
# myst-parser
# pre-commit
# sphinxcontrib-mermaid
-re-assert==1.1.0
- # via -r requirements/test-common.in
-regex==2026.5.9
- # via re-assert
requests==2.34.2
# via
# sphinx
@@ -234,7 +230,7 @@ requests==2.34.2
rich==15.0.0
# via pytest-codspeed
setuptools-git==1.2
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
six==1.17.0
# via python-dateutil
slotscheck==0.20.0
@@ -310,7 +306,7 @@ valkey==6.1.1
virtualenv==21.4.1
# via pre-commit
wait-for-it==2.3.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
wheel==0.47.0
# via pip-tools
yarl==1.24.2
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 8e75c8bb8a5..98ede6ba8d3 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/dev.txt --strip-extras requirements/dev.in
#
-aiodns==4.0.4
+aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios"
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
@@ -28,7 +28,7 @@ attrs==26.1.0
# via -r requirements/runtime-deps.in
babel==2.18.0
# via sphinx
-backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
+backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios"
# via
# -r requirements/lint.in
# -r requirements/runtime-deps.in
@@ -36,7 +36,7 @@ blockbuster==1.5.26
# via
# -r requirements/lint.in
# -r requirements/test-common.in
-brotli==1.2.0 ; platform_python_implementation == "CPython"
+brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
build==1.5.0
# via pip-tools
@@ -81,7 +81,7 @@ forbiddenfruit==0.1.4
freezegun==1.5.5
# via
# -r requirements/lint.in
- # -r requirements/test-common.in
+ # -r requirements/test-common-base.in
frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
@@ -148,7 +148,7 @@ pathspec==1.1.1
pip-tools==7.5.3
# via -r requirements/dev.in
pkgconfig==1.6.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
platformdirs==4.10.0
# via
# python-discovery
@@ -164,7 +164,7 @@ propcache==0.5.2
# -r requirements/runtime-deps.in
# yarl
proxy-py==2.4.10
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pycares==5.0.1
# via aiodns
pycparser==3.0
@@ -185,7 +185,7 @@ pyproject-hooks==1.2.0
pytest==9.0.3
# via
# -r requirements/lint.in
- # -r requirements/test-common.in
+ # -r requirements/test-common-base.in
# pytest-codspeed
# pytest-cov
# pytest-mock
@@ -196,13 +196,13 @@ pytest-codspeed==5.0.3
# -r requirements/lint.in
# -r requirements/test-common.in
pytest-cov==7.1.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-mock==3.15.1
# via
# -r requirements/lint.in
- # -r requirements/test-common.in
+ # -r requirements/test-common-base.in
pytest-timeout==2.4.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
@@ -218,16 +218,12 @@ pyyaml==6.0.3
# myst-parser
# pre-commit
# sphinxcontrib-mermaid
-re-assert==1.1.0
- # via -r requirements/test-common.in
-regex==2026.5.9
- # via re-assert
requests==2.34.2
# via sphinx
rich==15.0.0
# via pytest-codspeed
setuptools-git==1.2
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
six==1.17.0
# via python-dateutil
slotscheck==0.20.0
@@ -300,7 +296,7 @@ valkey==6.1.1
virtualenv==21.4.1
# via pre-commit
wait-for-it==2.3.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
wheel==0.47.0
# via pip-tools
yarl==1.24.2
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 055e397d7df..c294530dd86 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -1,5 +1,5 @@
#
-# This file is autogenerated by pip-compile with Python 3.12
+# This file is autogenerated by pip-compile with Python 3.10
# by the following command:
#
# pip-compile --allow-unsafe --output-file=requirements/lint.txt --resolver=backtracking --strip-extras requirements/lint.in
diff --git a/requirements/runtime-deps.in b/requirements/runtime-deps.in
index cf8f209b4ac..49ba703ef65 100644
--- a/requirements/runtime-deps.in
+++ b/requirements/runtime-deps.in
@@ -1,12 +1,12 @@
# Extracted from `pyproject.toml` via `make sync-direct-runtime-deps`
-aiodns >= 3.3.0
+aiodns >= 3.3.0; sys_platform != 'android' and sys_platform != 'ios'
aiohappyeyeballs >= 2.5.0
aiosignal >= 1.4.0
async-timeout >= 4.0, < 6.0 ; python_version < '3.11'
attrs >= 17.3.0
-backports.zstd; platform_python_implementation == 'CPython' and python_version < '3.14'
-Brotli >= 1.2; platform_python_implementation == 'CPython'
+backports.zstd; platform_python_implementation == 'CPython' and python_version < '3.14' and sys_platform != 'android' and sys_platform != 'ios'
+Brotli >= 1.2; platform_python_implementation == 'CPython' and sys_platform != 'android' and sys_platform != 'ios'
brotlicffi >= 1.2; platform_python_implementation != 'CPython'
frozenlist >= 1.1.1
multidict >=4.5, < 7.0
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index d83995a61af..12fca0e33ef 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/runtime-deps.txt --strip-extras requirements/runtime-deps.in
#
-aiodns==4.0.4
+aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.2
# via -r requirements/runtime-deps.in
@@ -14,9 +14,9 @@ async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
attrs==26.1.0
# via -r requirements/runtime-deps.in
-backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
+backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
-brotli==1.2.0 ; platform_python_implementation == "CPython"
+brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
cffi==2.0.0
# via pycares
diff --git a/requirements/test-common-base.in b/requirements/test-common-base.in
new file mode 100644
index 00000000000..7d7f58a13b0
--- /dev/null
+++ b/requirements/test-common-base.in
@@ -0,0 +1,9 @@
+freezegun
+pkgconfig
+proxy.py >= 2.4.4rc5
+pytest
+pytest-cov
+pytest-mock
+pytest-timeout
+setuptools-git
+wait-for-it
diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt
new file mode 100644
index 00000000000..421a9cf1149
--- /dev/null
+++ b/requirements/test-common-base.txt
@@ -0,0 +1,54 @@
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+# pip-compile --allow-unsafe --output-file=requirements/test-common-base.txt --strip-extras requirements/test-common-base.in
+#
+click==8.3.1
+ # via wait-for-it
+coverage==7.13.4
+ # via pytest-cov
+exceptiongroup==1.3.1
+ # via pytest
+freezegun==1.5.5
+ # via -r requirements/test-common-base.in
+iniconfig==2.3.0
+ # via pytest
+packaging==26.0
+ # via pytest
+pkgconfig==1.6.0
+ # via -r requirements/test-common-base.in
+pluggy==1.6.0
+ # via
+ # pytest
+ # pytest-cov
+proxy-py==2.4.10
+ # via -r requirements/test-common-base.in
+pygments==2.19.2
+ # via pytest
+pytest==9.0.2
+ # via
+ # -r requirements/test-common-base.in
+ # pytest-cov
+ # pytest-mock
+ # pytest-timeout
+pytest-cov==7.0.0
+ # via -r requirements/test-common-base.in
+pytest-mock==3.15.1
+ # via -r requirements/test-common-base.in
+pytest-timeout==2.4.0
+ # via -r requirements/test-common-base.in
+python-dateutil==2.9.0.post0
+ # via freezegun
+setuptools-git==1.2
+ # via -r requirements/test-common-base.in
+six==1.17.0
+ # via python-dateutil
+tomli==2.4.0
+ # via
+ # coverage
+ # pytest
+typing-extensions==4.15.0
+ # via exceptiongroup
+wait-for-it==2.3.0
+ # via -r requirements/test-common-base.in
diff --git a/requirements/test-common.in b/requirements/test-common.in
index 77700881024..d3aa5d156b0 100644
--- a/requirements/test-common.in
+++ b/requirements/test-common.in
@@ -1,19 +1,11 @@
+-r test-common-base.in
+
blockbuster
coverage
-freezegun
isal; python_version < "3.14" and implementation_name == "cpython" # no wheel for 3.14, no PyPy wheel for 1.8.0+
mypy; implementation_name == "cpython"
-pkgconfig
-proxy.py >= 2.4.4rc5
-pytest
-pytest-cov
-pytest-mock
-pytest-timeout
pytest-xdist
pytest_codspeed
python-on-whales
-re-assert
-setuptools-git
-trustme; platform_machine != "i686" # no 32-bit wheels
-wait-for-it
+trustme; platform_machine != "i686" # no 32-bit wheels
zlib_ng
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 40117fc2b88..6f9603b63b2 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -27,7 +27,7 @@ execnet==2.1.2
forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.5
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
idna==3.15
# via trustme
iniconfig==2.3.0
@@ -49,13 +49,13 @@ packaging==26.2
pathspec==1.1.1
# via mypy
pkgconfig==1.6.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pluggy==1.6.0
# via
# pytest
# pytest-cov
proxy-py==2.4.10
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pycparser==3.0
# via cffi
pydantic==2.13.4
@@ -68,7 +68,7 @@ pygments==2.20.0
# rich
pytest==9.0.3
# via
- # -r requirements/test-common.in
+ # -r requirements/test-common-base.in
# pytest-codspeed
# pytest-cov
# pytest-mock
@@ -77,25 +77,21 @@ pytest==9.0.3
pytest-codspeed==5.0.3
# via -r requirements/test-common.in
pytest-cov==7.1.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-mock==3.15.1
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-timeout==2.4.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
python-on-whales==0.81.0
# via -r requirements/test-common.in
-re-assert==1.1.0
- # via -r requirements/test-common.in
-regex==2026.5.9
- # via re-assert
rich==15.0.0
# via pytest-codspeed
setuptools-git==1.2
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
six==1.17.0
# via python-dateutil
tomli==2.4.1
@@ -117,6 +113,6 @@ typing-extensions==4.15.0
typing-inspection==0.4.2
# via pydantic
wait-for-it==2.3.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
zlib-ng==1.0.0
# via -r requirements/test-common.in
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index b73e0f51fe2..e36986edaef 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/test-ft.txt --strip-extras requirements/test-ft.in
#
-aiodns==4.0.4
+aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.2
# via -r requirements/runtime-deps.in
@@ -18,11 +18,11 @@ async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
attrs==26.1.0
# via -r requirements/runtime-deps.in
-backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
+backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
blockbuster==1.5.26
# via -r requirements/test-common.in
-brotli==1.2.0 ; platform_python_implementation == "CPython"
+brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
cffi==2.0.0
# via
@@ -43,7 +43,7 @@ execnet==2.1.2
forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.5
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
@@ -79,7 +79,7 @@ packaging==26.2
pathspec==1.1.1
# via mypy
pkgconfig==1.6.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pluggy==1.6.0
# via
# pytest
@@ -89,7 +89,7 @@ propcache==0.5.2
# -r requirements/runtime-deps.in
# yarl
proxy-py==2.4.10
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pycares==5.0.1
# via aiodns
pycparser==3.0
@@ -104,7 +104,7 @@ pygments==2.20.0
# rich
pytest==9.0.3
# via
- # -r requirements/test-common.in
+ # -r requirements/test-common-base.in
# pytest-codspeed
# pytest-cov
# pytest-mock
@@ -113,25 +113,21 @@ pytest==9.0.3
pytest-codspeed==5.0.3
# via -r requirements/test-common.in
pytest-cov==7.1.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-mock==3.15.1
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-timeout==2.4.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
python-on-whales==0.81.0
# via -r requirements/test-common.in
-re-assert==1.1.0
- # via -r requirements/test-common.in
-regex==2026.5.9
- # via re-assert
rich==15.0.0
# via pytest-codspeed
setuptools-git==1.2
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
six==1.17.0
# via python-dateutil
tomli==2.4.1
@@ -156,7 +152,7 @@ typing-extensions==4.15.0 ; python_version < "3.13"
typing-inspection==0.4.2
# via pydantic
wait-for-it==2.3.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
yarl==1.24.2
# via -r requirements/runtime-deps.in
zlib-ng==1.0.0
diff --git a/requirements/test-mobile.in b/requirements/test-mobile.in
new file mode 100644
index 00000000000..72238fd941e
--- /dev/null
+++ b/requirements/test-mobile.in
@@ -0,0 +1,8 @@
+-r base-ft.in
+-r test-common-base.in
+
+# pip-compile does not support environment markers for transitive dependencies. So
+# some packages that are transitive dependencies have to be explicitly excluded here:
+backports-asyncio-runner; python_version < "3.11" # transitive dependency of "pytest-asyncio"; not installable on Python >= 3.11 using pip (uv works though)
+cffi; sys_platform != 'android' and sys_platform != 'ios' # transitive dependency of "pycares"
+pycares; sys_platform != 'android' and sys_platform != 'ios' # transitive dependency of "aiodns"
diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt
new file mode 100644
index 00000000000..f874c03ece9
--- /dev/null
+++ b/requirements/test-mobile.txt
@@ -0,0 +1,104 @@
+#
+# This file is autogenerated by pip-compile with Python 3.10
+# by the following command:
+#
+# pip-compile --allow-unsafe --output-file=requirements/test-mobile.txt --strip-extras requirements/test-mobile.in
+#
+aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios"
+ # via -r requirements/runtime-deps.in
+aiohappyeyeballs==2.6.2
+ # via -r requirements/runtime-deps.in
+aiosignal==1.4.0
+ # via -r requirements/runtime-deps.in
+async-timeout==5.0.1 ; python_version < "3.11"
+ # via -r requirements/runtime-deps.in
+attrs==26.1.0
+ # via -r requirements/runtime-deps.in
+backports-asyncio-runner==1.2.0 ; python_version < "3.11"
+ # via -r requirements/test-mobile.in
+backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios"
+ # via -r requirements/runtime-deps.in
+brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios"
+ # via -r requirements/runtime-deps.in
+cffi==2.0.0 ; sys_platform != "android" and sys_platform != "ios"
+ # via
+ # -r requirements/test-mobile.in
+ # pycares
+click==8.4.0
+ # via wait-for-it
+coverage==7.14.0
+ # via pytest-cov
+exceptiongroup==1.3.1
+ # via pytest
+freezegun==1.5.5
+ # via -r requirements/test-common-base.in
+frozenlist==1.8.0
+ # via
+ # -r requirements/runtime-deps.in
+ # aiosignal
+gunicorn==26.0.0
+ # via -r requirements/base-ft.in
+idna==3.15
+ # via yarl
+iniconfig==2.3.0
+ # via pytest
+multidict==6.7.1
+ # via
+ # -r requirements/runtime-deps.in
+ # yarl
+packaging==26.2
+ # via
+ # gunicorn
+ # pytest
+pkgconfig==1.6.0
+ # via -r requirements/test-common-base.in
+pluggy==1.6.0
+ # via
+ # pytest
+ # pytest-cov
+propcache==0.5.2
+ # via
+ # -r requirements/runtime-deps.in
+ # yarl
+proxy-py==2.4.10
+ # via -r requirements/test-common-base.in
+pycares==5.0.1 ; sys_platform != "android" and sys_platform != "ios"
+ # via
+ # -r requirements/test-mobile.in
+ # aiodns
+pycparser==3.0
+ # via cffi
+pygments==2.20.0
+ # via pytest
+pytest==9.0.3
+ # via
+ # -r requirements/test-common-base.in
+ # pytest-cov
+ # pytest-mock
+ # pytest-timeout
+pytest-cov==7.1.0
+ # via -r requirements/test-common-base.in
+pytest-mock==3.15.1
+ # via -r requirements/test-common-base.in
+pytest-timeout==2.4.0
+ # via -r requirements/test-common-base.in
+python-dateutil==2.9.0.post0
+ # via freezegun
+setuptools-git==1.2
+ # via -r requirements/test-common-base.in
+six==1.17.0
+ # via python-dateutil
+tomli==2.4.1
+ # via
+ # coverage
+ # pytest
+typing-extensions==4.15.0 ; python_version < "3.13"
+ # via
+ # -r requirements/runtime-deps.in
+ # aiosignal
+ # exceptiongroup
+ # multidict
+wait-for-it==2.3.0
+ # via -r requirements/test-common-base.in
+yarl==1.24.2
+ # via -r requirements/runtime-deps.in
diff --git a/requirements/test.txt b/requirements/test.txt
index 1b6dfdfb332..3cd82be4d49 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -4,7 +4,7 @@
#
# pip-compile --allow-unsafe --output-file=requirements/test.txt --strip-extras requirements/test.in
#
-aiodns==4.0.4
+aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
aiohappyeyeballs==2.6.2
# via -r requirements/runtime-deps.in
@@ -18,11 +18,11 @@ async-timeout==5.0.1 ; python_version < "3.11"
# via -r requirements/runtime-deps.in
attrs==26.1.0
# via -r requirements/runtime-deps.in
-backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14"
+backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
blockbuster==1.5.26
# via -r requirements/test-common.in
-brotli==1.2.0 ; platform_python_implementation == "CPython"
+brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios"
# via -r requirements/runtime-deps.in
cffi==2.0.0
# via
@@ -43,7 +43,7 @@ execnet==2.1.2
forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.5
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
@@ -79,7 +79,7 @@ packaging==26.2
pathspec==1.1.1
# via mypy
pkgconfig==1.6.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pluggy==1.6.0
# via
# pytest
@@ -89,7 +89,7 @@ propcache==0.5.2
# -r requirements/runtime-deps.in
# yarl
proxy-py==2.4.10
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pycares==5.0.1
# via aiodns
pycparser==3.0
@@ -104,7 +104,7 @@ pygments==2.20.0
# rich
pytest==9.0.3
# via
- # -r requirements/test-common.in
+ # -r requirements/test-common-base.in
# pytest-codspeed
# pytest-cov
# pytest-mock
@@ -113,25 +113,21 @@ pytest==9.0.3
pytest-codspeed==5.0.3
# via -r requirements/test-common.in
pytest-cov==7.1.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-mock==3.15.1
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-timeout==2.4.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
pytest-xdist==3.8.0
# via -r requirements/test-common.in
python-dateutil==2.9.0.post0
# via freezegun
python-on-whales==0.81.0
# via -r requirements/test-common.in
-re-assert==1.1.0
- # via -r requirements/test-common.in
-regex==2026.5.9
- # via re-assert
rich==15.0.0
# via pytest-codspeed
setuptools-git==1.2
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
six==1.17.0
# via python-dateutil
tomli==2.4.1
@@ -158,7 +154,7 @@ typing-inspection==0.4.2
uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpython"
# via -r requirements/base.in
wait-for-it==2.3.0
- # via -r requirements/test-common.in
+ # via -r requirements/test-common-base.in
yarl==1.24.2
# via -r requirements/runtime-deps.in
zlib-ng==1.0.0
diff --git a/setup.cfg b/setup.cfg
index 6ea9b485db6..0f77c005a70 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -81,6 +81,8 @@ filterwarnings =
# https://github.com/spulec/freezegun/issues/508
# https://github.com/spulec/freezegun/pull/511
ignore:datetime.*utcnow\(\) is deprecated and scheduled for removal:DeprecationWarning:freezegun.api
+ # coverage's C tracer is not available on iOS/Android (pure-Python fallback is used instead)
+ ignore:Couldn't import C tracer:coverage.exceptions.CoverageWarning
# Weird issue in Python 3.13+ triggered in test_multipart.py
ignore:coroutine method 'aclose' of 'BodyPartReader._decode_content_async' was never awaited:RuntimeWarning
# Our own warning
diff --git a/tests/test_benchmarks_client.py b/tests/test_benchmarks_client.py
index 8ebedcc4776..d33936b5c8b 100644
--- a/tests/test_benchmarks_client.py
+++ b/tests/test_benchmarks_client.py
@@ -1,14 +1,20 @@
"""codspeed benchmarks for HTTP client."""
import asyncio
+from typing import TYPE_CHECKING
import pytest
-from pytest_codspeed import BenchmarkFixture
from yarl import URL
from aiohttp import hdrs, request, web
from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer
+if TYPE_CHECKING:
+ from pytest_codspeed import BenchmarkFixture
+else:
+ pytest_codspeed = pytest.importorskip("pytest_codspeed")
+ BenchmarkFixture = pytest_codspeed.BenchmarkFixture
+
def test_one_hundred_simple_get_requests(
loop: asyncio.AbstractEventLoop,
diff --git a/tests/test_benchmarks_client_request.py b/tests/test_benchmarks_client_request.py
index 3a56e21d448..eeaf7329fea 100644
--- a/tests/test_benchmarks_client_request.py
+++ b/tests/test_benchmarks_client_request.py
@@ -2,9 +2,10 @@
import asyncio
from http.cookies import BaseCookie
+from typing import TYPE_CHECKING
+import pytest
from multidict import CIMultiDict
-from pytest_codspeed import BenchmarkFixture
from yarl import URL
from aiohttp.client_reqrep import ClientRequest, ClientResponse
@@ -13,6 +14,12 @@
from aiohttp.http_writer import HttpVersion11
from aiohttp.tracing import Trace
+if TYPE_CHECKING:
+ from pytest_codspeed import BenchmarkFixture
+else:
+ pytest_codspeed = pytest.importorskip("pytest_codspeed")
+ BenchmarkFixture = pytest_codspeed.BenchmarkFixture
+
def test_client_request_update_cookies(
loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture
@@ -135,7 +142,6 @@ def write(self, data: bytes | bytearray | memoryview) -> None:
"""Swallow writes."""
class MockProtocol(asyncio.BaseProtocol):
-
def __init__(self) -> None:
self.transport = MockTransport()
self._paused = False
@@ -151,7 +157,6 @@ def start_timeout(self) -> None:
"""Swallow start_timeout."""
class MockConnector:
-
def __init__(self) -> None:
self.force_close = False
diff --git a/tests/test_benchmarks_client_ws.py b/tests/test_benchmarks_client_ws.py
index 0338b52fb9d..60b0a027133 100644
--- a/tests/test_benchmarks_client_ws.py
+++ b/tests/test_benchmarks_client_ws.py
@@ -1,14 +1,20 @@
"""codspeed benchmarks for websocket client."""
import asyncio
+from typing import TYPE_CHECKING
import pytest
-from pytest_codspeed import BenchmarkFixture
from aiohttp import web
from aiohttp._websocket.helpers import MSG_SIZE
from aiohttp.pytest_plugin import AiohttpClient
+if TYPE_CHECKING:
+ from pytest_codspeed import BenchmarkFixture
+else:
+ pytest_codspeed = pytest.importorskip("pytest_codspeed")
+ BenchmarkFixture = pytest_codspeed.BenchmarkFixture
+
def test_one_thousand_round_trip_websocket_text_messages(
loop: asyncio.AbstractEventLoop,
diff --git a/tests/test_benchmarks_cookiejar.py b/tests/test_benchmarks_cookiejar.py
index 78566151ef4..1f820cc061d 100644
--- a/tests/test_benchmarks_cookiejar.py
+++ b/tests/test_benchmarks_cookiejar.py
@@ -1,12 +1,19 @@
"""codspeed benchmarks for cookies."""
from http.cookies import BaseCookie
+from typing import TYPE_CHECKING
-from pytest_codspeed import BenchmarkFixture
+import pytest
from yarl import URL
from aiohttp.cookiejar import CookieJar
+if TYPE_CHECKING:
+ from pytest_codspeed import BenchmarkFixture
+else:
+ pytest_codspeed = pytest.importorskip("pytest_codspeed")
+ BenchmarkFixture = pytest_codspeed.BenchmarkFixture
+
async def test_load_cookies_into_temp_cookiejar(benchmark: BenchmarkFixture) -> None:
"""Benchmark for creating a temp CookieJar and filtering by URL.
diff --git a/tests/test_benchmarks_http_websocket.py b/tests/test_benchmarks_http_websocket.py
index 61b23125460..a5e506f8e9a 100644
--- a/tests/test_benchmarks_http_websocket.py
+++ b/tests/test_benchmarks_http_websocket.py
@@ -1,9 +1,9 @@
"""codspeed benchmarks for http websocket."""
import asyncio
+from typing import TYPE_CHECKING
import pytest
-from pytest_codspeed import BenchmarkFixture
from aiohttp._websocket.helpers import MSG_SIZE, PACK_LEN3
from aiohttp._websocket.reader import WebSocketDataQueue
@@ -11,6 +11,12 @@
from aiohttp.helpers import DEFAULT_CHUNK_SIZE
from aiohttp.http_websocket import WebSocketReader, WebSocketWriter, WSMsgType
+if TYPE_CHECKING:
+ from pytest_codspeed import BenchmarkFixture
+else:
+ pytest_codspeed = pytest.importorskip("pytest_codspeed")
+ BenchmarkFixture = pytest_codspeed.BenchmarkFixture
+
def test_read_large_binary_websocket_messages(
loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture
diff --git a/tests/test_benchmarks_http_writer.py b/tests/test_benchmarks_http_writer.py
index 0d52ca875e6..d3cd1a72ea6 100644
--- a/tests/test_benchmarks_http_writer.py
+++ b/tests/test_benchmarks_http_writer.py
@@ -1,11 +1,19 @@
"""codspeed benchmarks for http writer."""
+from typing import TYPE_CHECKING
+
+import pytest
from multidict import CIMultiDict
-from pytest_codspeed import BenchmarkFixture
from aiohttp import hdrs
from aiohttp.http_writer import _serialize_headers
+if TYPE_CHECKING:
+ from pytest_codspeed import BenchmarkFixture
+else:
+ pytest_codspeed = pytest.importorskip("pytest_codspeed")
+ BenchmarkFixture = pytest_codspeed.BenchmarkFixture
+
def test_serialize_headers(benchmark: BenchmarkFixture) -> None:
"""Benchmark 100 calls to _serialize_headers."""
diff --git a/tests/test_benchmarks_web_fileresponse.py b/tests/test_benchmarks_web_fileresponse.py
index 01aa7448c86..ea7ef6c5ab0 100644
--- a/tests/test_benchmarks_web_fileresponse.py
+++ b/tests/test_benchmarks_web_fileresponse.py
@@ -2,13 +2,20 @@
import asyncio
import pathlib
+from typing import TYPE_CHECKING
+import pytest
from multidict import CIMultiDict
-from pytest_codspeed import BenchmarkFixture
from aiohttp import ClientResponse, web
from aiohttp.pytest_plugin import AiohttpClient
+if TYPE_CHECKING:
+ from pytest_codspeed import BenchmarkFixture
+else:
+ pytest_codspeed = pytest.importorskip("pytest_codspeed")
+ BenchmarkFixture = pytest_codspeed.BenchmarkFixture
+
def test_simple_web_file_response(
loop: asyncio.AbstractEventLoop,
diff --git a/tests/test_benchmarks_web_middleware.py b/tests/test_benchmarks_web_middleware.py
index 14aa269e360..b2d7b5035eb 100644
--- a/tests/test_benchmarks_web_middleware.py
+++ b/tests/test_benchmarks_web_middleware.py
@@ -1,13 +1,20 @@
"""codspeed benchmarks for web middlewares."""
import asyncio
+from typing import TYPE_CHECKING
-from pytest_codspeed import BenchmarkFixture
+import pytest
from aiohttp import web
from aiohttp.pytest_plugin import AiohttpClient
from aiohttp.typedefs import Handler
+if TYPE_CHECKING:
+ from pytest_codspeed import BenchmarkFixture
+else:
+ pytest_codspeed = pytest.importorskip("pytest_codspeed")
+ BenchmarkFixture = pytest_codspeed.BenchmarkFixture
+
def test_ten_web_middlewares(
benchmark: BenchmarkFixture,
diff --git a/tests/test_benchmarks_web_request.py b/tests/test_benchmarks_web_request.py
index 81afe7824e4..c27694d85b8 100644
--- a/tests/test_benchmarks_web_request.py
+++ b/tests/test_benchmarks_web_request.py
@@ -2,13 +2,19 @@
import asyncio
import zlib
+from typing import TYPE_CHECKING
import pytest
-from pytest_codspeed import BenchmarkFixture
from aiohttp import web
from aiohttp.pytest_plugin import AiohttpClient
+if TYPE_CHECKING:
+ from pytest_codspeed import BenchmarkFixture
+else:
+ pytest_codspeed = pytest.importorskip("pytest_codspeed")
+ BenchmarkFixture = pytest_codspeed.BenchmarkFixture
+
@pytest.mark.usefixtures("parametrize_zlib_backend")
def test_read_compressed_post_body(
diff --git a/tests/test_benchmarks_web_response.py b/tests/test_benchmarks_web_response.py
index fbf1fadf1e1..a4246cea9ae 100644
--- a/tests/test_benchmarks_web_response.py
+++ b/tests/test_benchmarks_web_response.py
@@ -1,9 +1,17 @@
"""codspeed benchmarks for the web responses."""
-from pytest_codspeed import BenchmarkFixture
+from typing import TYPE_CHECKING
+
+import pytest
from aiohttp import web
+if TYPE_CHECKING:
+ from pytest_codspeed import BenchmarkFixture
+else:
+ pytest_codspeed = pytest.importorskip("pytest_codspeed")
+ BenchmarkFixture = pytest_codspeed.BenchmarkFixture
+
def test_simple_web_response(benchmark: BenchmarkFixture) -> None:
"""Benchmark creating 100 simple web.Response."""
diff --git a/tests/test_benchmarks_web_urldispatcher.py b/tests/test_benchmarks_web_urldispatcher.py
index f01adb6da5c..45f339b3274 100644
--- a/tests/test_benchmarks_web_urldispatcher.py
+++ b/tests/test_benchmarks_web_urldispatcher.py
@@ -6,18 +6,23 @@
import random
import string
from pathlib import Path
-from typing import NoReturn, cast
+from typing import TYPE_CHECKING, NoReturn, cast
from unittest import mock
import pytest
from multidict import CIMultiDict, CIMultiDictProxy
-from pytest_codspeed import BenchmarkFixture
from yarl import URL
import aiohttp
from aiohttp import web
from aiohttp.http import HttpVersion, RawRequestMessage
+if TYPE_CHECKING:
+ from pytest_codspeed import BenchmarkFixture
+else:
+ pytest_codspeed = pytest.importorskip("pytest_codspeed")
+ BenchmarkFixture = pytest_codspeed.BenchmarkFixture
+
@pytest.fixture
def github_urls() -> list[str]:
diff --git a/tests/test_circular_imports.py b/tests/test_circular_imports.py
index 9b5d7ed2697..227da10e53b 100644
--- a/tests/test_circular_imports.py
+++ b/tests/test_circular_imports.py
@@ -85,6 +85,9 @@ def _discover_path_importables(
)
+@pytest.mark.skipif(
+ sys.platform in ("android", "ios"), reason="subprocess not supported"
+)
@pytest.mark.parametrize(
"import_path",
_mark_aiohttp_worker_for_skipping(_find_all_importables(aiohttp)),
diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py
index ee07cbf1697..aa3a6925c2f 100644
--- a/tests/test_client_functional.py
+++ b/tests/test_client_functional.py
@@ -1,4 +1,5 @@
# HTTP client functional tests against aiohttp.web server
+from __future__ import annotations # TODO(PY311): Remove
import asyncio
import datetime
@@ -25,7 +26,7 @@
except ImportError:
import brotli
except ImportError:
- brotli = None # pragma: no cover
+ brotli = None
try:
from backports.zstd import ZstdCompressor
diff --git a/tests/test_client_session.py b/tests/test_client_session.py
index be9e77dfcc0..6b351687995 100644
--- a/tests/test_client_session.py
+++ b/tests/test_client_session.py
@@ -15,7 +15,6 @@
import pytest
from multidict import CIMultiDict, MultiDict
-from re_assert import Matches
from yarl import URL
import aiohttp
@@ -435,10 +434,8 @@ async def make_sess():
return ClientSession(connector=connector, loop=loop)
loop.run_until_complete(make_sess())
- assert (
- Matches("Session and connector has to use same event loop")
- == str(ctx.value).strip()
- )
+ expected = "Session and connector has to use same event loop"
+ assert str(ctx.value).startswith(expected)
another_loop.run_until_complete(connector.close())
diff --git a/tests/test_cookiejar.py b/tests/test_cookiejar.py
index 6dac3a0adb4..fa1e31cd9f6 100644
--- a/tests/test_cookiejar.py
+++ b/tests/test_cookiejar.py
@@ -1678,6 +1678,9 @@ async def test_shared_cookie_with_multiple_domains() -> None:
# === Security tests for restricted unpickler and JSON save/load ===
+@pytest.mark.skipif(
+ sys.platform in ("android", "ios"), reason="os.system not supported"
+)
async def test_load_rejects_malicious_pickle(tmp_path: Path) -> None:
"""Verify CookieJar.load() blocks arbitrary code execution via pickle.
diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
index 625c34533bc..d0fc90e6fc7 100644
--- a/tests/test_http_parser.py
+++ b/tests/test_http_parser.py
@@ -2605,6 +2605,9 @@ async def test_feed_eof_no_err_gzip(self, protocol: BaseProtocol) -> None:
dbuf.feed_eof()
assert buf._eof
+ @pytest.mark.skipif(
+ sys.platform in ("android", "ios"), reason="brotli not available"
+ )
async def test_feed_eof_no_err_brotli(self, protocol: BaseProtocol) -> None:
buf = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
dbuf = DeflateBuffer(buf, "br")
diff --git a/tests/test_leaks.py b/tests/test_leaks.py
index 07b506bdb99..742d6f9aa65 100644
--- a/tests/test_leaks.py
+++ b/tests/test_leaks.py
@@ -8,6 +8,9 @@
IS_PYPY = platform.python_implementation() == "PyPy"
+@pytest.mark.skipif(
+ sys.platform in ("android", "ios"), reason="subprocess not supported"
+)
@pytest.mark.skipif(IS_PYPY, reason="gc.DEBUG_LEAK not available on PyPy")
@pytest.mark.parametrize(
("script", "message"),
diff --git a/tests/test_loop.py b/tests/test_loop.py
index a3520b457e4..aceec5a3998 100644
--- a/tests/test_loop.py
+++ b/tests/test_loop.py
@@ -1,5 +1,6 @@
import asyncio
import platform
+import sys
import threading
import pytest
@@ -11,6 +12,9 @@
@pytest.mark.skipif(
platform.system() == "Windows", reason="the test is not valid for Windows"
)
+@pytest.mark.skipif(
+ sys.platform in ("android", "ios"), reason="subprocess not supported"
+)
async def test_subprocess_co(loop) -> None:
proc = await asyncio.create_subprocess_shell(
"exit 0",
@@ -38,6 +42,9 @@ def test_default_loop(loop: asyncio.AbstractEventLoop) -> None:
assert asyncio.get_event_loop() is loop
+@pytest.mark.skipif(
+ sys.platform in ("android", "ios"), reason="subprocess not supported"
+)
def test_setup_loop_non_main_thread() -> None:
child_exc = None
diff --git a/tests/test_multipart.py b/tests/test_multipart.py
index 1f80f9c2a21..fb5bbe562d5 100644
--- a/tests/test_multipart.py
+++ b/tests/test_multipart.py
@@ -113,7 +113,7 @@ async def read(self, size=None):
class TestMultipartResponseWrapper:
- def test_at_eof(self) -> None:
+ async def test_at_eof(self) -> None:
wrapper = MultipartResponseWrapper(mock.Mock(), mock.Mock())
wrapper.at_eof()
assert wrapper.resp.content.at_eof.called
diff --git a/tests/test_proxy_functional.py b/tests/test_proxy_functional.py
index a240692d0d8..85309dc5cea 100644
--- a/tests/test_proxy_functional.py
+++ b/tests/test_proxy_functional.py
@@ -7,10 +7,10 @@
from collections.abc import Awaitable, Callable
from contextlib import suppress
from re import match as match_regex
+from typing import TYPE_CHECKING
from unittest import mock
from uuid import uuid4
-import proxy
import pytest
from yarl import URL
@@ -21,6 +21,11 @@
from aiohttp.pytest_plugin import AiohttpServer
from aiohttp.test_utils import TestServer
+if TYPE_CHECKING:
+ import proxy
+else:
+ proxy = pytest.importorskip("proxy")
+
ASYNCIO_SUPPORTS_TLS_IN_TLS = sys.version_info >= (3, 11)
pytestmark = [
diff --git a/tests/test_run_app.py b/tests/test_run_app.py
index d9e21c696c0..e4acbb2cd21 100644
--- a/tests/test_run_app.py
+++ b/tests/test_run_app.py
@@ -686,6 +686,9 @@ def test_run_app_multiple_preexisting_sockets(patched_loop) -> None:
"""
+@pytest.mark.skipif(
+ sys.platform in ("android", "ios"), reason="subprocess not supported"
+)
def test_sigint() -> None:
skip_if_on_windows()
@@ -700,6 +703,9 @@ def test_sigint() -> None:
assert proc.wait() == 0
+@pytest.mark.skipif(
+ sys.platform in ("android", "ios"), reason="subprocess not supported"
+)
def test_sigterm() -> None:
skip_if_on_windows()
diff --git a/tests/test_streams.py b/tests/test_streams.py
index aa50b5ca2d3..ae3c731e477 100644
--- a/tests/test_streams.py
+++ b/tests/test_streams.py
@@ -9,7 +9,6 @@
from unittest import mock
import pytest
-from re_assert import Matches
from aiohttp import streams
from aiohttp.helpers import DEFAULT_CHUNK_SIZE
@@ -1104,7 +1103,7 @@ async def test___repr__waiter(self) -> None:
loop = asyncio.get_event_loop()
stream = self._make_one()
stream._waiter = loop.create_future()
- assert Matches(r">") == repr(stream)
+ assert repr(stream).startswith(" None:
handler2 = make_handler()
router.add_route("GET", "/get", handler1, name="name")
- regexp = "Duplicate 'name', already handled by"
with pytest.raises(ValueError) as ctx:
router.add_route("GET", "/get_other", handler2, name="name")
- assert Matches(regexp) == str(ctx.value)
+ assert str(ctx.value).startswith("Duplicate 'name', already handled by")
def test_route_plain(router) -> None:
@@ -561,7 +559,7 @@ def test_contains(router) -> None:
def test_static_repr(router) -> None:
router.add_static("/get", pathlib.Path(aiohttp.__file__).parent, name="name")
- assert Matches(r" None:
@@ -688,7 +686,8 @@ async def test_regular_match_info(router) -> None:
req = make_mocked_request("GET", "/get/john")
match_info = await router.resolve(req)
assert {"name": "john"} == match_info
- assert Matches(">") == repr(match_info)
+ assert repr(match_info).startswith(" None:
@@ -1021,7 +1020,7 @@ def test_static_route_user_home(router) -> None:
except ValueError: # pragma: no cover
pytest.skip("aiohttp folder is not placed in user's HOME")
route = router.add_static("/st", str(static_dir))
- assert here == route.get_info()["directory"]
+ assert here.resolve() == route.get_info()["directory"]
def test_static_route_points_to_file(router) -> None:
@@ -1045,7 +1044,7 @@ async def test_405_for_resource_adapter(router) -> None:
@pytest.mark.skipif(platform.system() == "Windows", reason="Different path formats")
async def test_static_resource_outside_traversal(router: web.UrlDispatcher) -> None:
"""Test relative path traversing outside root does not resolve."""
- static_file = pathlib.Path(aiohttp.__file__)
+ static_file = pathlib.Path(aiohttp.__file__).resolve()
request_path = "/st" + "/.." * (len(static_file.parts) - 2) + str(static_file)
assert pathlib.Path(request_path).resolve() == static_file
diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py
index c50ceaad4c9..f054f71db21 100644
--- a/tests/test_web_functional.py
+++ b/tests/test_web_functional.py
@@ -31,7 +31,10 @@
try:
import brotlicffi as brotli
except ImportError:
- import brotli
+ try:
+ import brotli
+ except ImportError:
+ brotli = None
try:
import ssl
@@ -1199,6 +1202,7 @@ async def handler(request):
await resp.release()
+@pytest.mark.skipif(brotli is None, reason="brotli not available")
async def test_response_with_precompressed_body_brotli(aiohttp_client) -> None:
async def handler(request):
headers = {"Content-Encoding": "br"}
diff --git a/tests/test_web_response.py b/tests/test_web_response.py
index 5299f1c8a3d..52a574f1c0c 100644
--- a/tests/test_web_response.py
+++ b/tests/test_web_response.py
@@ -11,7 +11,6 @@
import aiosignal
import pytest
from multidict import CIMultiDict, CIMultiDictProxy, MultiDict
-from re_assert import Matches
from aiohttp import HttpVersion, HttpVersion10, HttpVersion11, hdrs, web
from aiohttp.abc import AbstractStreamWriter
@@ -504,7 +503,7 @@ async def test_chunked_encoding_forbidden_for_http_10() -> None:
with pytest.raises(RuntimeError) as ctx:
await resp.prepare(req)
- assert Matches("Using chunked encoding is forbidden for HTTP/1.0") == str(ctx.value)
+ assert str(ctx.value) == "Using chunked encoding is forbidden for HTTP/1.0"
@pytest.mark.usefixtures("parametrize_zlib_backend")
@@ -980,12 +979,12 @@ def test_response_cookies() -> None:
resp.del_cookie("name")
expected = (
- 'Set-Cookie: name=("")?; '
+ 'Set-Cookie: name=""; '
"expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/"
)
- assert Matches(expected) == str(resp.cookies)
+ assert str(resp.cookies) == expected
resp.del_cookie("name")
- assert str(resp.cookies) == Matches(expected)
+ assert str(resp.cookies) == expected
resp.set_cookie("name", "value", domain="local.host")
expected = "Set-Cookie: name=value; Domain=local.host; Path=/"
@@ -1047,10 +1046,10 @@ def test_response_cookie__issue_del_cookie() -> None:
resp.del_cookie("name")
expected = (
- 'Set-Cookie: name=("")?; '
+ 'Set-Cookie: name=""; '
"expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/"
)
- assert Matches(expected) == str(resp.cookies)
+ assert str(resp.cookies) == expected
def test_cookie_set_after_del() -> None:
@@ -1368,15 +1367,14 @@ async def test_send_headers_for_empty_body(buf, writer) -> None:
await resp.prepare(req)
await resp.write_eof()
txt = buf.decode("utf8")
- assert (
- Matches(
- "HTTP/1.1 200 OK\r\n"
- "Content-Length: 0\r\n"
- "Date: .+\r\n"
- "Server: .+\r\n\r\n"
- )
- == txt
- )
+
+ lines = txt.split("\r\n")
+ assert len(lines) == 6
+ assert lines[0] == "HTTP/1.1 200 OK"
+ assert lines[1] == "Content-Length: 0"
+ assert lines[2].startswith("Date: ")
+ assert lines[3].startswith("Server: ")
+ assert lines[4] == lines[5] == ""
async def test_render_with_body(buf, writer) -> None:
@@ -1385,19 +1383,17 @@ async def test_render_with_body(buf, writer) -> None:
await resp.prepare(req)
await resp.write_eof()
-
txt = buf.decode("utf8")
- assert (
- Matches(
- "HTTP/1.1 200 OK\r\n"
- "Content-Length: 4\r\n"
- "Content-Type: application/octet-stream\r\n"
- "Date: .+\r\n"
- "Server: .+\r\n\r\n"
- "data"
- )
- == txt
- )
+
+ lines = txt.split("\r\n")
+ assert len(lines) == 7
+ assert lines[0] == "HTTP/1.1 200 OK"
+ assert lines[1] == "Content-Length: 4"
+ assert lines[2] == "Content-Type: application/octet-stream"
+ assert lines[3].startswith("Date: ")
+ assert lines[4].startswith("Server: ")
+ assert lines[5] == ""
+ assert lines[6] == "data"
async def test_multiline_reason(buf: bytearray, writer: AbstractStreamWriter) -> None:
@@ -1412,18 +1408,16 @@ async def test_send_set_cookie_header(buf, writer) -> None:
await resp.prepare(req)
await resp.write_eof()
-
txt = buf.decode("utf8")
- assert (
- Matches(
- "HTTP/1.1 200 OK\r\n"
- "Content-Length: 0\r\n"
- "Set-Cookie: name=value\r\n"
- "Date: .+\r\n"
- "Server: .+\r\n\r\n"
- )
- == txt
- )
+
+ lines = txt.split("\r\n")
+ assert len(lines) == 7
+ assert lines[0] == "HTTP/1.1 200 OK"
+ assert lines[1] == "Content-Length: 0"
+ assert lines[2] == "Set-Cookie: name=value"
+ assert lines[3].startswith("Date: ")
+ assert lines[4].startswith("Server: ")
+ assert lines[5] == lines[6] == ""
async def test_consecutive_write_eof() -> None:
diff --git a/tests/test_web_sendfile_functional.py b/tests/test_web_sendfile_functional.py
index b0a51b08dbb..5c6fd637944 100644
--- a/tests/test_web_sendfile_functional.py
+++ b/tests/test_web_sendfile_functional.py
@@ -19,7 +19,10 @@
try:
import brotlicffi as brotli
except ImportError:
- import brotli
+ try:
+ import brotli
+ except ImportError:
+ brotli = None
try:
import ssl
@@ -46,9 +49,12 @@ def hello_txt(request, tmp_path_factory) -> pathlib.Path:
}
# Uncompressed file is not actually written to test it is not required.
hello["gzip"].write_bytes(gzip.compress(HELLO_AIOHTTP))
- hello["br"].write_bytes(brotli.compress(HELLO_AIOHTTP))
+ if brotli is not None:
+ hello["br"].write_bytes(brotli.compress(HELLO_AIOHTTP))
hello["bzip2"].write_bytes(bz2.compress(HELLO_AIOHTTP))
encoding = getattr(request, "param", None)
+ if encoding == "br" and brotli is None:
+ pytest.skip("brotli not available")
return hello[encoding]
@@ -279,6 +285,8 @@ async def test_static_file_custom_content_type_compress(
expect_encoding: str,
):
"""Test that custom type with encoding is returned for unencoded requests."""
+ if expect_encoding == "br" and brotli is None:
+ pytest.skip("brotli not available")
async def handler(request):
resp = sender(hello_txt, chunk_size=16)
@@ -314,6 +322,8 @@ async def test_static_file_with_encoding_and_enable_compression(
forced_compression: web.ContentCoding | None,
):
"""Test that enable_compression does not double compress when an encoded file is also present."""
+ if expect_encoding == "br" and brotli is None:
+ pytest.skip("brotli not available")
async def handler(request):
resp = sender(hello_txt)
From a9ae4ae1637b61494f9222022c5ae5d7c9d834e7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 29 May 2026 19:55:51 +0000
Subject: [PATCH 165/191] Bump coverage from 7.13.4 to 7.14.1 (#12708)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.13.4
to 7.14.1.
Changelog
Sourced from coverage's
changelog.
Version 7.14.1 — 2026-05-26
-
Fix: the HTML report used typographic niceties to make file paths
more
readable by adding a small amount of space around slashes. Those spaces
interfered with searching the page for file paths of interest. Now the
report
uses CSS to accomplish the same visual tweak so that searches with
slashes
work correctly. Closes issue 2170_.
-
Add a 3.16 PyPI classifier <hugo-316_>_ since we
test on the 3.16 main
branch.
.. _issue 2170: coveragepy/coveragepy#2170
.. _hugo-316: https://mastodon.social/@hugovk/116588523571204490
.. _changes_7-14-0:
Version 7.14.0 — 2026-05-10
-
Feature: now when running one of the reporting commands, if there are
parallel data files that need combining, they will be implicitly
combined
before creating the report. There is no option to avoid the combination;
let
us know if you have a use case that requires it. Thanks, Tim Hatch
<pull 2162_>. Closes issue 1781.
-
Fix: the output from combine was too verbose, listing
each file
considered. Now it shows a single line with the counts of files
combined,
files skipped, and files with errors. The -q flag
suppresses this line.
The old detailed lines are available with the new
--debug=combine option.
-
Fix: running a Python file through a symlink now sets the sys.path
correctly,
matching regular Python behavior. Fixes issue 2157_.
-
Fix: Collector.flush_data could fail with
"RuntimeError: Set changed
size during iteration" when a tracer in another thread added a line
to the
per-file set that add_lines (or add_arcs) was
iterating. The values
passed to CoverageData are now snapshotted via
dict.copy() and
set.copy(), which are atomic under the GIL. Thanks,
Alex Vandiver <pull 2165_>_.
-
Fix: the soft keyword lazy is now bolded in HTML
reports.
-
We are no longer testing eventlet support. Eventlet started issuing
stern
deprecation warnings that break our tests. Our support code is still
there.
.. _issue 1781: coveragepy/coveragepy#1781
.. _issue 2157: coveragepy/coveragepy#2157
.. _pull 2162: coveragepy/coveragepy#2162
... (truncated)
Commits
64d9b66
docs: correct the date for 7.14.1
6fa7dd4
chore: bump actions/dependency-review-action (#2181)
078afae
docs: sample HTML for 7.14.1
cb4f028
docs: prep for 7.14.1
ae2d09f
Merge branch 'nedbat/classifire-316-kits'
2c3568b
build: declare 3.16 compatibility
faa68f8
chore: bump github/codeql-action in the action-dependencies group (#2173)
eb55fee
test: we don't need PyPy < 7.3.22 anymore
ac168fe
test: the text summary should show missing
fed4bd2
chore: upgrade virtualenv
- Additional commits viewable in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/test-common-base.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test-mobile.txt | 2 +-
requirements/test.txt | 2 +-
7 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 5745d103182..8a7f2f7e298 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -56,7 +56,7 @@ click==8.4.1
# slotscheck
# towncrier
# wait-for-it
-coverage==7.14.0
+coverage==7.14.1
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/dev.txt b/requirements/dev.txt
index 98ede6ba8d3..d7ae2cd8d5e 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -56,7 +56,7 @@ click==8.4.1
# slotscheck
# towncrier
# wait-for-it
-coverage==7.14.0
+coverage==7.14.1
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt
index 421a9cf1149..9065c1c2340 100644
--- a/requirements/test-common-base.txt
+++ b/requirements/test-common-base.txt
@@ -6,7 +6,7 @@
#
click==8.3.1
# via wait-for-it
-coverage==7.13.4
+coverage==7.14.1
# via pytest-cov
exceptiongroup==1.3.1
# via pytest
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 6f9603b63b2..4d09da4cf73 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -14,7 +14,7 @@ cffi==2.0.0
# via cryptography
click==8.4.1
# via wait-for-it
-coverage==7.14.0
+coverage==7.14.1
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index e36986edaef..326a1acca56 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -30,7 +30,7 @@ cffi==2.0.0
# pycares
click==8.4.1
# via wait-for-it
-coverage==7.14.0
+coverage==7.14.1
# via
# -r requirements/test-common.in
# pytest-cov
diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt
index f874c03ece9..e12c7e3fab0 100644
--- a/requirements/test-mobile.txt
+++ b/requirements/test-mobile.txt
@@ -26,7 +26,7 @@ cffi==2.0.0 ; sys_platform != "android" and sys_platform != "ios"
# pycares
click==8.4.0
# via wait-for-it
-coverage==7.14.0
+coverage==7.14.1
# via pytest-cov
exceptiongroup==1.3.1
# via pytest
diff --git a/requirements/test.txt b/requirements/test.txt
index 3cd82be4d49..39a4b01e8d8 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -30,7 +30,7 @@ cffi==2.0.0
# pycares
click==8.4.1
# via wait-for-it
-coverage==7.14.0
+coverage==7.14.1
# via
# -r requirements/test-common.in
# pytest-cov
From db3c18cc29f1b870367996c68ceced14f561cbac Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 29 May 2026 19:59:47 +0000
Subject: [PATCH 166/191] Bump idna from 3.15 to 3.17 (#12729)
Bumps [idna](https://github.com/kjd/idna) from 3.15 to 3.17.
Changelog
Sourced from idna's
changelog.
3.17 (2026-05-28)
- Substantial 75% reduction in memory usage through new data
structures and some optimization in processing speed.
- Added a general 1024-character input length cap to the public
validation, conversion, and codec entry points. This is well above
any legitimate domain or label and guards against pathological
inputs.
3.16 (2026-05-22)
- Add a command-line interface (
python -m idna, also
available as
the idna script). Encodes or decodes one or more domains
supplied
as arguments or on standard input, with options to select A-label
or U-label output and control error handling.
- Raise the minimum supported Python version to 3.9
- Various code quality improvements
Commits
f48619c
Release 3.17
7421ba8
Pre-release 3.17rc0
22ebb73
Merge pull request #251 from
kjd/structure-optimizations
2a7ac0a
Drop redundant parallel-arrays comment from uts46data
354eee9
Apply ruff format to uts46data.py
8c34ffc
Refactor uts46data into parallel arrays
1189629
Range-encode joining_types for compact representation
f90b87a
Generic length limit for functions
d6ffd28
Merge pull request #247 from
kjd/release-3.16
6d1a0de
Release 3.16
- Additional commits viewable in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/runtime-deps.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test-mobile.txt | 2 +-
requirements/test.txt | 2 +-
12 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 4a320fa2e85..26f04fd4011 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -26,7 +26,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base-ft.in
-idna==3.15
+idna==3.17
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/base.txt b/requirements/base.txt
index 5b50ef94765..8713fc6262c 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -26,7 +26,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base.in
-idna==3.15
+idna==3.17
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 8a7f2f7e298..89b95c66ca4 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -92,7 +92,7 @@ gunicorn==26.0.0
# via -r requirements/base.in
identify==2.6.19
# via pre-commit
-idna==3.15
+idna==3.17
# via
# requests
# trustme
diff --git a/requirements/dev.txt b/requirements/dev.txt
index d7ae2cd8d5e..a1525e6247d 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -90,7 +90,7 @@ gunicorn==26.0.0
# via -r requirements/base.in
identify==2.6.19
# via pre-commit
-idna==3.15
+idna==3.17
# via
# requests
# trustme
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index 38e8808825c..2878edd94f6 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -20,7 +20,7 @@ docutils==0.21.2
# via
# myst-parser
# sphinx
-idna==3.15
+idna==3.17
# via requests
imagesize==2.0.0
# via sphinx
diff --git a/requirements/doc.txt b/requirements/doc.txt
index ac53f3db591..2503191af64 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -20,7 +20,7 @@ docutils==0.21.2
# via
# myst-parser
# sphinx
-idna==3.15
+idna==3.17
# via requests
imagesize==2.0.0
# via sphinx
diff --git a/requirements/lint.txt b/requirements/lint.txt
index c294530dd86..036cb2d6a8b 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -40,7 +40,7 @@ freezegun==1.5.5
# via -r requirements/lint.in
identify==2.6.19
# via pre-commit
-idna==3.15
+idna==3.17
# via trustme
iniconfig==2.3.0
# via pytest
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index 12fca0e33ef..e86f12283b8 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -24,7 +24,7 @@ frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
-idna==3.15
+idna==3.17
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 4d09da4cf73..8d3bc4b479b 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -28,7 +28,7 @@ forbiddenfruit==0.1.4
# via blockbuster
freezegun==1.5.5
# via -r requirements/test-common-base.in
-idna==3.15
+idna==3.17
# via trustme
iniconfig==2.3.0
# via pytest
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 326a1acca56..f2fd489e532 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -50,7 +50,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base-ft.in
-idna==3.15
+idna==3.17
# via
# trustme
# yarl
diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt
index e12c7e3fab0..9f18c58e8f6 100644
--- a/requirements/test-mobile.txt
+++ b/requirements/test-mobile.txt
@@ -38,7 +38,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base-ft.in
-idna==3.15
+idna==3.17
# via yarl
iniconfig==2.3.0
# via pytest
diff --git a/requirements/test.txt b/requirements/test.txt
index 39a4b01e8d8..dcf60194f9f 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -50,7 +50,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base.in
-idna==3.15
+idna==3.17
# via
# trustme
# yarl
From b00fa465c9018f9856b8a3051a96cc15612754ca Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 30 May 2026 15:04:15 +0100
Subject: [PATCH 167/191] [PR #12703/e221fdb2 backport][3.14] Fix irrelevant
URLs to Gunicorn docs (#12735)
**This is a backport of PR #12703 as merged into master
(e221fdb2626336070c78d31932d55250bec523b4).**
Co-authored-by: serhiiur
---
CHANGES/12703.bugfix.rst | 1 +
docs/deployment.rst | 10 +++++-----
docs/logging.rst | 4 ++--
3 files changed, 8 insertions(+), 7 deletions(-)
create mode 100644 CHANGES/12703.bugfix.rst
diff --git a/CHANGES/12703.bugfix.rst b/CHANGES/12703.bugfix.rst
new file mode 100644
index 00000000000..7cfca2415c3
--- /dev/null
+++ b/CHANGES/12703.bugfix.rst
@@ -0,0 +1 @@
+Fixed irrelevant URLs pointing to the corresponding sections in the documentation of Gunicorn -- by :user:`serhiiur`.
diff --git a/docs/deployment.rst b/docs/deployment.rst
index 60f218e848d..16c55b4567e 100644
--- a/docs/deployment.rst
+++ b/docs/deployment.rst
@@ -192,7 +192,7 @@ Nginx+Gunicorn
==============
aiohttp can be deployed using `Gunicorn
-`_, which is based on a
+`_, which is based on a
pre-fork worker model. Gunicorn launches your app as worker processes
for handling incoming requests.
@@ -264,10 +264,10 @@ Start Gunicorn
--------------
When `Running Gunicorn
-`_, you provide the name
+`_, you provide the name
of the module, i.e. *my_app_module*, and the name of the app or
application factory, i.e. *my_web_app*, along with other `Gunicorn
-Settings `_ provided
+Settings `_ provided
as command line flags or in your config file.
In this case, we will use:
@@ -279,7 +279,7 @@ In this case, we will use:
* you may also want to use the ``--workers`` flag to tell Gunicorn how
many worker processes to use for handling requests. (See the
documentation for recommendations on `How Many Workers?
- `_)
+ `_)
* you may also want to use the ``--accesslog`` flag to enable the access
log to be populated. (See :ref:`logging ` for more information.)
@@ -390,7 +390,7 @@ More information
----------------
See the `official documentation
-`_ for more
+`_ for more
information about suggested nginx configuration. You can also find out more about
`configuring for secure https connections as well. `_
diff --git a/docs/logging.rst b/docs/logging.rst
index 3f04c345112..b2a2a91dda4 100644
--- a/docs/logging.rst
+++ b/docs/logging.rst
@@ -160,7 +160,7 @@ instance) to the :meth:`aiohttp.web.AppRunner` constructor.
.. _access_logformat:
- http://docs.gunicorn.org/en/stable/settings.html#access-log-format
+ https://gunicorn.org/reference/settings/#access_log_format
.. _accesslog:
- http://docs.gunicorn.org/en/stable/settings.html#accesslog
+ https://gunicorn.org/reference/settings/#accesslog
From fd7a4ac1afe3be3d89a3b5d8e0b95f5ce773952e Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 30 May 2026 18:46:23 +0100
Subject: [PATCH 168/191] [PR #12737/05129a00 backport][3.14] Fix pip-tools
config (#12738)
**This is a backport of PR #12737 as merged into master
(05129a00e6037794d196ae11bd5f607770fe49bc).**
Co-authored-by: Sam Bull
---
.pip-tools.toml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.pip-tools.toml b/.pip-tools.toml
index 82737a2d4f6..86969d6eda2 100644
--- a/.pip-tools.toml
+++ b/.pip-tools.toml
@@ -1,4 +1,5 @@
[pip-tools]
-allow-unsafe = true
+allow-unsafe = false
resolver = "backtracking"
strip-extras = true
+unsafe-package = "aiohttp"
From 2da99140b25de07d37ce75a4dcaf12b059502539 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sat, 30 May 2026 21:00:40 +0100
Subject: [PATCH 169/191] [PR #12739/b3f2ff4e backport][3.14] Move pip-tools
into pyproject.toml (#12740)
**This is a backport of PR #12739 as merged into master
(b3f2ff4e718a50a5636809c89059c045ce945a9b).**
Co-authored-by: Sam Bull
---
.pip-tools.toml | 5 -----
pyproject.toml | 7 +++++++
2 files changed, 7 insertions(+), 5 deletions(-)
delete mode 100644 .pip-tools.toml
diff --git a/.pip-tools.toml b/.pip-tools.toml
deleted file mode 100644
index 86969d6eda2..00000000000
--- a/.pip-tools.toml
+++ /dev/null
@@ -1,5 +0,0 @@
-[pip-tools]
-allow-unsafe = false
-resolver = "backtracking"
-strip-extras = true
-unsafe-package = "aiohttp"
diff --git a/pyproject.toml b/pyproject.toml
index bf490bf7cdc..4011ba49ce2 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -175,6 +175,13 @@ skip = "pp*"
# iOS currently does not support build[uv]
build-frontend = "build"
+[tool.pip-tools]
+# Dependabot won't pick up .pip-tools.toml, so must be defined here.
+allow-unsafe = false
+resolver = "backtracking"
+strip-extras = true
+unsafe-package = ["aiohttp"]
+
[tool.codespell]
skip = '.git,*.pdf,*.svg,Makefile,CONTRIBUTORS.txt,venvs,_build'
ignore-words-list = 'te,ue'
From 8e2f2325cf02812760214884a2bcdbe671c52da3 Mon Sep 17 00:00:00 2001
From: Puneet Dixit
Date: Sun, 31 May 2026 20:37:17 +0530
Subject: [PATCH 170/191] [3.14] Improve HTTPS-on-HTTP parser error (#12743)
---
CHANGES/10142.bugfix.rst | 1 +
CONTRIBUTORS.txt | 1 +
aiohttp/_http_parser.pyx | 2 ++
aiohttp/http_exceptions.py | 2 ++
tests/test_http_parser.py | 11 ++++++++++-
5 files changed, 16 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/10142.bugfix.rst
diff --git a/CHANGES/10142.bugfix.rst b/CHANGES/10142.bugfix.rst
new file mode 100644
index 00000000000..821db59818d
--- /dev/null
+++ b/CHANGES/10142.bugfix.rst
@@ -0,0 +1 @@
+Improved the parser error message shown when TLS handshake bytes are received on an HTTP port -- by :user:`puneetdixit200`.
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 01b6805cf6d..1f94bac5c84 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -308,6 +308,7 @@ Phebe Polk
Philipp A.
Pierre-Louis Peeters
Pieter van Beek
+Puneet Dixit
Qiao Han
Rafael Viotti
Rahul Nahata
diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx
index 344be3123b0..8c2141fe51f 100644
--- a/aiohttp/_http_parser.pyx
+++ b/aiohttp/_http_parser.pyx
@@ -931,6 +931,8 @@ cdef parser_error_from_errno(cparser.llhttp_t* parser, data, pointer):
cparser.HPE_INVALID_TRANSFER_ENCODING}:
return BadHttpMessage(err_msg)
elif errno == cparser.HPE_INVALID_METHOD:
+ if data.startswith(b"\x16\x03"):
+ return BadHttpMethod(error="Received HTTPS traffic on an HTTP port")
return BadHttpMethod(error=err_msg)
elif errno in {cparser.HPE_INVALID_STATUS,
cparser.HPE_INVALID_VERSION,
diff --git a/aiohttp/http_exceptions.py b/aiohttp/http_exceptions.py
index f21a97f0170..54cc4af36be 100644
--- a/aiohttp/http_exceptions.py
+++ b/aiohttp/http_exceptions.py
@@ -109,6 +109,8 @@ class BadHttpMethod(BadStatusLine):
"""Invalid HTTP method in status line."""
def __init__(self, line: str = "", error: str | None = None) -> None:
+ if error is None and line.startswith("\x16\x03"):
+ error = "Received HTTPS traffic on an HTTP port"
super().__init__(line, error or f"Bad HTTP method in status line {line!r}")
diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
index d0fc90e6fc7..011edbf912a 100644
--- a/tests/test_http_parser.py
+++ b/tests/test_http_parser.py
@@ -1517,7 +1517,16 @@ def test_http_request_parser_bad_method(
)
-def test_http_request_parser_bad_version(parser) -> None:
+def test_http_request_parser_tls_handshake_on_http_port(
+ parser: HttpRequestParser,
+) -> None:
+ with pytest.raises(http_exceptions.BadHttpMethod) as ctx:
+ parser.feed_data(b"\x16\x03\x03\x01F\x01\r\n\r\n")
+
+ assert "Received HTTPS traffic on an HTTP port" in str(ctx.value)
+
+
+def test_http_request_parser_bad_version(parser: HttpRequestParser) -> None:
with pytest.raises(http_exceptions.BadHttpMessage):
parser.feed_data(b"GET //get HT/11\r\nHost: a\r\n\r\n")
From 2049ba85701586fb065378787a74f99eb2b91108 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sun, 31 May 2026 18:10:08 +0100
Subject: [PATCH 171/191] [PR #12674/e8832aee backport][3.14] Fix
ZLibDecompressor dropping data past the first gzip member (#12745)
**This is a backport of PR #12674 as merged into master
(e8832aee7f694979292740b9b56a9c98fb0afc1a).**
Co-authored-by: Ashutosh Kumar Singh <144926351+Ashutosh-177@users.noreply.github.com>
---
CHANGES/7157.bugfix.rst | 6 +++
CONTRIBUTORS.txt | 1 +
aiohttp/compression_utils.py | 36 +++++++++++++++-
docs/spelling_wordlist.txt | 2 +
tests/test_compression_utils.py | 36 ++++++++++++++++
tests/test_http_parser.py | 76 +++++++++++++++++++++++++++++++++
6 files changed, 156 insertions(+), 1 deletion(-)
create mode 100644 CHANGES/7157.bugfix.rst
diff --git a/CHANGES/7157.bugfix.rst b/CHANGES/7157.bugfix.rst
new file mode 100644
index 00000000000..60f06d8da9f
--- /dev/null
+++ b/CHANGES/7157.bugfix.rst
@@ -0,0 +1,6 @@
+Fixed ``ZLibDecompressor`` silently dropping data past the first
+member when decompressing concatenated gzip/deflate streams. Each subsequent
+member is now handed to a fresh decompressor, matching the behaviour already
+implemented for ZSTD multi-frame streams.
+
+-- by :user:`Ashutosh-177`
diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt
index 1f94bac5c84..24139ce2cda 100644
--- a/CONTRIBUTORS.txt
+++ b/CONTRIBUTORS.txt
@@ -54,6 +54,7 @@ Arcadiy Ivanov
Arseny Timoniq
Artem Yushkovskiy
Arthur Darcet
+Ashutosh Kumar Singh
Austin Scola
Bai Haoran
Ben Bader
diff --git a/aiohttp/compression_utils.py b/aiohttp/compression_utils.py
index 75b24d1cbbf..9313d21e009 100644
--- a/aiohttp/compression_utils.py
+++ b/aiohttp/compression_utils.py
@@ -55,6 +55,9 @@ def eof(self) -> bool: ...
@property
def unconsumed_tail(self) -> bytes: ...
+ @property
+ def unused_data(self) -> bytes: ...
+
class ZLibBackendProtocol(Protocol):
MAX_WBITS: int
@@ -275,15 +278,42 @@ def __init__(
self._zlib_backend: Final = ZLibBackendWrapper(ZLibBackend._zlib_backend)
self._decompressor = self._zlib_backend.decompressobj(wbits=self._mode)
self._last_empty = False
+ self._pending_unused_data: bytes | None = None
def decompress_sync(
self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED
) -> bytes:
+ if self._pending_unused_data is not None:
+ data = self._pending_unused_data + bytes(data)
+ self._pending_unused_data = None
result = self._decompressor.decompress(
self._decompressor.unconsumed_tail + data, max_length
)
# Only way to know that isal has no further data is checking we get no output
self._last_empty = result == b""
+
+ # Handle concatenated gzip/deflate streams (multi-member).
+ # After a member ends, unused_data holds the start of the next member.
+ # Create a fresh decompressor for each subsequent member.
+ while self._decompressor.eof and self._decompressor.unused_data:
+ unused = self._decompressor.unused_data
+ self._decompressor = self._zlib_backend.decompressobj(wbits=self._mode)
+ if max_length != ZLIB_MAX_LENGTH_UNLIMITED:
+ max_length -= len(result)
+ if max_length <= 0:
+ self._pending_unused_data = unused
+ break
+ chunk = self._decompressor.decompress(unused, max_length)
+ self._last_empty = chunk == b""
+ result += chunk
+
+ # Member ended exactly at chunk boundary — no unused_data, but the
+ # next feed_data() call would fail on the spent decompressor.
+ # Only reset for gzip; deflate's feed_eof() relies on eof=True to
+ # confirm the stream is complete.
+ if self._decompressor.eof and self._mode > self._zlib_backend.MAX_WBITS:
+ self._decompressor = self._zlib_backend.decompressobj(wbits=self._mode)
+
return result
def flush(self, length: int = 0) -> bytes:
@@ -295,7 +325,11 @@ def flush(self, length: int = 0) -> bytes:
@property
def data_available(self) -> bool:
- return bool(self._decompressor.unconsumed_tail) or not self._last_empty
+ return (
+ bool(self._decompressor.unconsumed_tail)
+ or not self._last_empty
+ or self._pending_unused_data is not None
+ )
@property
def eof(self) -> bool:
diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt
index c10f10fdce4..1de034c393b 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -98,6 +98,7 @@ Cython
Cythonize
cythonized
de
+decompressor
deduplicate
defs
Dependabot
@@ -144,6 +145,7 @@ github
google
gunicorn
gunicorn’s
+gzip
gzipped
hackish
highlevel
diff --git a/tests/test_compression_utils.py b/tests/test_compression_utils.py
index 3362b8feed0..5deebc8470d 100644
--- a/tests/test_compression_utils.py
+++ b/tests/test_compression_utils.py
@@ -1,5 +1,6 @@
"""Tests for compression utils."""
+import gzip
import sys
import pytest
@@ -87,3 +88,38 @@ def test_zstd_multi_frame_max_length_exhausted_preserves_unused_data() -> None:
assert result1 == b"AAAA"
result2 = d.decompress_sync(frame3)
assert result2 == b"BBBBCCCC"
+
+
+def test_zlib_gzip_multi_member_unlimited() -> None:
+ d = ZLibDecompressor(encoding="gzip")
+ member1 = gzip.compress(b"AAAA")
+ member2 = gzip.compress(b"BBBB")
+ result = d.decompress_sync(member1 + member2)
+ assert result == b"AAAABBBB"
+
+
+def test_zlib_gzip_multi_member_max_length_partial() -> None:
+ d = ZLibDecompressor(encoding="gzip")
+ member1 = gzip.compress(b"AAAA")
+ member2 = gzip.compress(b"BBBB")
+ result = d.decompress_sync(member1 + member2, max_length=6)
+ assert result == b"AAAABB"
+
+
+def test_zlib_gzip_multi_member_max_length_exhausted() -> None:
+ d = ZLibDecompressor(encoding="gzip")
+ member1 = gzip.compress(b"AAAA")
+ member2 = gzip.compress(b"BBBB")
+ result = d.decompress_sync(member1 + member2, max_length=4)
+ assert result == b"AAAA"
+
+
+def test_zlib_gzip_multi_member_max_length_exhausted_preserves_unused_data() -> None:
+ d = ZLibDecompressor(encoding="gzip")
+ member1 = gzip.compress(b"AAAA")
+ member2 = gzip.compress(b"BBBB")
+ member3 = gzip.compress(b"CCCC")
+ result1 = d.decompress_sync(member1 + member2, max_length=4)
+ assert result1 == b"AAAA"
+ result2 = d.decompress_sync(member3)
+ assert result2 == b"BBBBCCCC"
diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
index 011edbf912a..5b5f3af6bc0 100644
--- a/tests/test_http_parser.py
+++ b/tests/test_http_parser.py
@@ -1,6 +1,7 @@
# Tests for aiohttp/protocol.py
import asyncio
+import gzip
import platform
import re
import sys
@@ -2550,6 +2551,81 @@ async def test_http_payload_zstandard_many_small_frames(
assert b"".join(parts) == b"".join(out._buffer)
assert out.is_eof()
+ async def test_http_payload_gzip_multi_member(self, protocol: BaseProtocol) -> None:
+ member1 = gzip.compress(b"first")
+ member2 = gzip.compress(b"second")
+ payload = member1 + member2
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
+ p = HttpPayloadParser(
+ out,
+ length=len(payload),
+ compression="gzip",
+ headers_parser=HeadersParser(),
+ )
+ p.feed_data(payload)
+ assert b"firstsecond" == b"".join(out._buffer)
+ assert out.is_eof()
+
+ async def test_http_payload_gzip_multi_member_chunked(
+ self, protocol: BaseProtocol
+ ) -> None:
+ member1 = gzip.compress(b"chunk1")
+ member2 = gzip.compress(b"chunk2")
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
+ p = HttpPayloadParser(
+ out,
+ length=len(member1) + len(member2),
+ compression="gzip",
+ headers_parser=HeadersParser(),
+ )
+ p.feed_data(member1)
+ p.feed_data(member2)
+ assert b"chunk1chunk2" == b"".join(out._buffer)
+ assert out.is_eof()
+
+ async def test_http_payload_gzip_member_split_mid_chunk(
+ self, protocol: BaseProtocol
+ ) -> None:
+ member1 = gzip.compress(b"AAAA")
+ member2 = gzip.compress(b"BBBB")
+ combined = member1 + member2
+ split_point = len(member1) + 3 # 3 bytes into member2
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
+ p = HttpPayloadParser(
+ out,
+ length=len(combined),
+ compression="gzip",
+ headers_parser=HeadersParser(),
+ )
+ p.feed_data(combined[:split_point])
+ p.feed_data(combined[split_point:])
+ assert b"AAAABBBB" == b"".join(out._buffer)
+ assert out.is_eof()
+
+ async def test_http_payload_gzip_many_small_members(
+ self, protocol: BaseProtocol
+ ) -> None:
+ parts = [f"part{i}".encode() for i in range(10)]
+ payload = b"".join(gzip.compress(p) for p in parts)
+ out = aiohttp.StreamReader(
+ protocol, DEFAULT_CHUNK_SIZE, loop=asyncio.get_running_loop()
+ )
+ p = HttpPayloadParser(
+ out,
+ length=len(payload),
+ compression="gzip",
+ headers_parser=HeadersParser(),
+ )
+ p.feed_data(payload)
+ assert b"".join(parts) == b"".join(out._buffer)
+ assert out.is_eof()
+
class TestDeflateBuffer:
async def test_feed_data(self, protocol: BaseProtocol) -> None:
From 8206f2cf77bd253bf4375c1923e0d89546282526 Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Sun, 31 May 2026 21:20:16 +0100
Subject: [PATCH 172/191] Improve timing issues on shutdown tests (#12747)
(#12748)
(cherry picked from commit 1e14d35136fb17f1c92dd9ff2c555a5deff4e030)
---
tests/test_run_app.py | 188 ++++++++++++++++++++++++++++++------------
1 file changed, 133 insertions(+), 55 deletions(-)
diff --git a/tests/test_run_app.py b/tests/test_run_app.py
index e4acbb2cd21..19a5d021627 100644
--- a/tests/test_run_app.py
+++ b/tests/test_run_app.py
@@ -11,13 +11,20 @@
import time
import traceback
from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine, Iterator
-from typing import NoReturn
+from typing import Any, NoReturn
from unittest import mock
from uuid import uuid4
import pytest
-from aiohttp import ClientConnectorError, ClientSession, ClientTimeout, WSCloseCode, web
+from aiohttp import (
+ ClientConnectorError,
+ ClientSession,
+ ClientTimeout,
+ ServerDisconnectedError,
+ WSCloseCode,
+ web,
+)
from aiohttp.web_runner import BaseRunner
# Test for features of OS' socket support
@@ -1000,13 +1007,18 @@ async def stop(self, request: web.Request) -> web.Response:
def run_app(
self,
sock: socket.socket,
- timeout: int,
+ timeout: float,
task: Callable[[], Coroutine[None, None, None]],
extra_test: Callable[[ClientSession], Awaitable[None]] | None = None,
) -> tuple["asyncio.Task[None]", int]:
num_connections = -1
t = test_task = None
port = sock.getsockname()[1]
+ server_ready = asyncio.Event()
+ handler_started = asyncio.Event()
+ # Set from on_shutdown so per-test ``task()`` callbacks can complete
+ # deterministically during the shutdown_timeout window.
+ self._release_handler = asyncio.Event()
class DictRecordClear(dict):
def clear(self):
@@ -1024,25 +1036,29 @@ def __init__(self, *args, **kwargs):
self._connections = DictRecordClear()
async def test() -> None:
- await asyncio.sleep(0.5)
+ await server_ready.wait()
async with ClientSession() as sess:
- for _ in range(5): # pragma: no cover
- try:
- with pytest.raises(asyncio.TimeoutError):
- async with sess.get(
- f"http://127.0.0.1:{port}/",
- timeout=ClientTimeout(total=0.1),
- ):
- pass
- except ClientConnectorError:
- await asyncio.sleep(0.5)
- else:
- break
- async with sess.get(f"http://127.0.0.1:{port}/stop"):
- pass
+ # Disable retries (same as TestClient).
+ sess._retry_connection = False
+
+ async def long_poll() -> None:
+ async with sess.get(f"http://127.0.0.1:{port}/") as resp:
+ await resp.read()
- if extra_test:
- await extra_test(sess)
+ in_flight = asyncio.create_task(long_poll())
+ try:
+ await asyncio.wait_for(handler_started.wait(), timeout=5)
+ # Handler is guaranteed in-flight here. Trigger shutdown.
+ async with sess.get(f"http://127.0.0.1:{port}/stop"):
+ pass
+
+ if extra_test:
+ await extra_test(sess)
+ finally:
+ # If shutdown_timeout cancels the handler, the server
+ # tears down the transport mid-response.
+ with contextlib.suppress(ServerDisconnectedError):
+ await in_flight
async def run_test(app: web.Application) -> None:
nonlocal test_task
@@ -1052,18 +1068,27 @@ async def run_test(app: web.Application) -> None:
async def handler(request: web.Request) -> web.Response:
nonlocal t
+ handler_started.set()
t = asyncio.create_task(task())
await t
return web.Response(text="FOO")
- t = test_task = None
+ async def on_shutdown(app: web.Application) -> None:
+ self._release_handler.set()
+
+ def on_ready(*args: object, **kwargs: object) -> None:
+ server_ready.set()
+
app = web.Application()
app.cleanup_ctx.append(run_test)
+ app.on_shutdown.append(on_shutdown)
app.router.add_get("/", handler)
app.router.add_get("/stop", self.stop)
with mock.patch("aiohttp.web_app.Server", ServerWithRecordClear):
- web.run_app(app, sock=sock, shutdown_timeout=timeout)
+ # TODO: print is a bit of a hack, we should have a proper callback/condition
+ web.run_app(app, sock=sock, shutdown_timeout=timeout, print=on_ready)
+ assert test_task is not None
assert test_task.exception() is None
return t, num_connections
@@ -1073,7 +1098,10 @@ def test_shutdown_wait_for_handler(self, unused_port_socket: socket.socket) -> N
async def task():
nonlocal finished
- await asyncio.sleep(2)
+ await self._release_handler.wait()
+ # Still doing work after the shutdown signal: the server must
+ # wait for this to complete rather than cancelling prematurely.
+ await asyncio.sleep(0.5)
finished = True
t, connection_count = self.run_app(sock, 3, task)
@@ -1086,13 +1114,14 @@ async def task():
def test_shutdown_timeout_handler(self, unused_port_socket: socket.socket) -> None:
sock = unused_port_socket
finished = False
+ never = asyncio.Event()
async def task():
nonlocal finished
- await asyncio.sleep(2)
- finished = True
+ await never.wait()
+ finished = True # pragma: no cover
- t, connection_count = self.run_app(sock, 1, task)
+ t, connection_count = self.run_app(sock, 0.5, task)
assert finished is False
assert t.done()
@@ -1104,21 +1133,21 @@ def test_shutdown_timeout_not_reached(
) -> None:
sock = unused_port_socket
finished = False
+ t_shutdown_began = 0.0
- async def task():
- nonlocal finished
- await asyncio.sleep(1)
+ async def task() -> None:
+ nonlocal finished, t_shutdown_began
+ await self._release_handler.wait()
+ t_shutdown_began = time.monotonic()
finished = True
- start_time = time.time()
-
t, connection_count = self.run_app(sock, 15, task)
assert finished is True
assert t.done()
assert connection_count == 0
# Verify run_app has not waited for timeout.
- assert time.time() - start_time < 10
+ assert time.monotonic() - t_shutdown_began < 10
def test_shutdown_new_conn_rejected(
self, unused_port_socket: socket.socket
@@ -1126,21 +1155,25 @@ def test_shutdown_new_conn_rejected(
sock = unused_port_socket
port = sock.getsockname()[1]
finished = False
+ test_complete = asyncio.Event()
async def task() -> None:
nonlocal finished
- await asyncio.sleep(9)
+ # Stay in-flight until the test confirms new-conn rejection.
+ await test_complete.wait()
finished = True
async def test(sess: ClientSession) -> None:
- # Ensure we are in the middle of shutdown (waiting for task()).
- await asyncio.sleep(1)
+ # release_handler is set from on_shutdown, so its set state
+ # means shutdown is now in progress (sites stopped).
+ await self._release_handler.wait()
with pytest.raises(ClientConnectorError):
# Use a new session to try and open a new connection.
async with ClientSession() as sess:
async with sess.get(f"http://127.0.0.1:{port}/"):
pass
assert finished is False
+ test_complete.set()
t, connection_count = self.run_app(sock, 10, task, test)
@@ -1154,22 +1187,28 @@ def test_shutdown_pending_handler_responds(
sock = unused_port_socket
port = sock.getsockname()[1]
finished = False
+ t = None
+ server_ready = asyncio.Event()
+ handler_started = asyncio.Event()
+ release_handler = asyncio.Event()
async def test() -> None:
+ await server_ready.wait()
+
async def test_resp(sess: ClientSession) -> None:
async with sess.get(f"http://127.0.0.1:{port}/") as resp:
assert await resp.text() == "FOO"
- await asyncio.sleep(1)
async with ClientSession() as sess:
t = asyncio.create_task(test_resp(sess))
- await asyncio.sleep(1)
+ await asyncio.wait_for(handler_started.wait(), timeout=5)
# Handler is in-progress while we trigger server shutdown.
async with sess.get(f"http://127.0.0.1:{port}/stop"):
pass
assert finished is False
- # Handler should still complete and produce a response.
+ # Handler should still complete and produce a response;
+ # release_handler is set from on_shutdown.
await t
async def run_test(app: web.Application) -> None:
@@ -1180,17 +1219,27 @@ async def run_test(app: web.Application) -> None:
async def handler(request: web.Request) -> web.Response:
nonlocal finished
- await asyncio.sleep(3)
+ handler_started.set()
+ await release_handler.wait()
+ # Still doing work after the shutdown signal: the server must
+ # wait for this to complete and deliver the response.
+ await asyncio.sleep(0.5)
finished = True
return web.Response(text="FOO")
- t = None
+ async def on_shutdown(app: web.Application) -> None:
+ release_handler.set()
+
+ def on_ready(*args: Any, **kwargs: Any) -> None:
+ server_ready.set()
+
app = web.Application()
app.cleanup_ctx.append(run_test)
+ app.on_shutdown.append(on_shutdown)
app.router.add_get("/", handler)
app.router.add_get("/stop", self.stop)
- web.run_app(app, sock=sock, shutdown_timeout=5)
+ web.run_app(app, sock=sock, shutdown_timeout=5, print=on_ready)
assert t is not None
assert t.exception() is None
assert finished is True
@@ -1201,15 +1250,16 @@ def test_shutdown_close_idle_keepalive(
sock = unused_port_socket
port = sock.getsockname()[1]
t = None
+ server_ready = asyncio.Event()
async def test() -> None:
- await asyncio.sleep(1)
+ await server_ready.wait()
async with ClientSession() as sess:
async with sess.get(f"http://127.0.0.1:{port}/stop"):
pass
# Hold on to keep-alive connection.
- await asyncio.sleep(5)
+ await asyncio.Event().wait()
async def run_test(app: web.Application) -> None:
nonlocal t
@@ -1219,14 +1269,19 @@ async def run_test(app: web.Application) -> None:
with contextlib.suppress(asyncio.CancelledError):
await t
- t = None
+ def on_ready(*args: Any, **kwargs: Any) -> None:
+ server_ready.set()
+
app = web.Application()
app.cleanup_ctx.append(run_test)
app.router.add_get("/stop", self.stop)
- web.run_app(app, sock=sock, shutdown_timeout=10)
- # If connection closed, then test() will be cancelled in cleanup_ctx.
- # If not, then shutdown_timeout will allow it to sleep until complete.
+ start = time.monotonic()
+ web.run_app(app, sock=sock, shutdown_timeout=10, print=on_ready)
+ # Server should close idle keep-alive connections immediately on
+ # shutdown rather than waiting for shutdown_timeout to expire.
+ assert time.monotonic() - start < 5
+ assert t is not None
assert t.cancelled()
def test_shutdown_close_websockets(self, unused_port_socket: socket.socket) -> None:
@@ -1234,6 +1289,8 @@ def test_shutdown_close_websockets(self, unused_port_socket: socket.socket) -> N
port = sock.getsockname()[1]
WS = web.AppKey("ws", set[web.WebSocketResponse])
client_finished = server_finished = False
+ t = None
+ server_ready = asyncio.Event()
async def ws_handler(request: web.Request) -> web.WebSocketResponse:
ws = web.WebSocketResponse()
@@ -1250,7 +1307,7 @@ async def close_websockets(app: web.Application) -> None:
await ws.close(code=WSCloseCode.GOING_AWAY)
async def test() -> None:
- await asyncio.sleep(1)
+ await server_ready.wait()
async with ClientSession() as sess:
async with sess.ws_connect(f"http://127.0.0.1:{port}/ws") as ws:
async with sess.get(f"http://127.0.0.1:{port}/stop"):
@@ -1270,7 +1327,9 @@ async def run_test(app: web.Application) -> None:
with contextlib.suppress(asyncio.CancelledError):
await t
- t = None
+ def on_ready(*args: Any, **kwargs: Any) -> None:
+ server_ready.set()
+
app = web.Application()
app[WS] = set()
app.on_shutdown.append(close_websockets)
@@ -1279,7 +1338,7 @@ async def run_test(app: web.Application) -> None:
app.router.add_get("/stop", self.stop)
start = time.time()
- web.run_app(app, sock=sock, shutdown_timeout=10)
+ web.run_app(app, sock=sock, shutdown_timeout=10, print=on_ready)
assert time.time() - start < 5
assert client_finished
assert server_finished
@@ -1290,10 +1349,15 @@ def test_shutdown_handler_cancellation_suppressed(
sock = unused_port_socket
port = sock.getsockname()[1]
actions = []
+ t = None
+ server_ready = asyncio.Event()
suppressed = asyncio.Event()
+ release_handler = asyncio.Event()
async def test() -> None:
- async def test_resp(sess):
+ await server_ready.wait()
+
+ async def test_resp(sess: ClientSession) -> None:
t = ClientTimeout(total=0.4)
with pytest.raises(asyncio.TimeoutError):
async with sess.get(f"http://127.0.0.1:{port}/", timeout=t) as resp:
@@ -1321,20 +1385,34 @@ async def run_test(app: web.Application) -> None:
async def handler(request: web.Request) -> web.Response:
try:
- await asyncio.sleep(5)
+ await asyncio.Event().wait()
except asyncio.CancelledError:
actions.append("SUPPRESSED")
suppressed.set()
- await asyncio.sleep(2)
+ await release_handler.wait()
+ await asyncio.sleep(0.5)
actions.append("DONE")
return web.Response(text="FOO")
- t = None
+ async def on_shutdown(app: web.Application) -> None:
+ release_handler.set()
+
+ def on_ready(*args: Any, **kwargs: Any) -> None:
+ server_ready.set()
+
app = web.Application()
app.cleanup_ctx.append(run_test)
+ app.on_shutdown.append(on_shutdown)
app.router.add_get("/", handler)
app.router.add_get("/stop", self.stop)
- web.run_app(app, sock=sock, shutdown_timeout=2, handler_cancellation=True)
+ web.run_app(
+ app,
+ sock=sock,
+ shutdown_timeout=2,
+ handler_cancellation=True,
+ print=on_ready,
+ )
+ assert t is not None
assert t.exception() is None
assert actions == ["CANCELLED", "SUPPRESSED", "PRESTOP", "STOPPING", "DONE"]
From f01bb0d1494ac6d657dc94af1a5efe097808c684 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sun, 31 May 2026 22:46:45 +0100
Subject: [PATCH 173/191] [PR #12727/269f03b5 backport][3.14] add aiointercept
to third party library list (#12749)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
**This is a backport of PR #12727 as merged into master
(269f03b572b9997d3e3a061cab36f259491d28e8).**
Co-authored-by: Pablo Nicolás Estevez
---
CHANGES/12727.doc.rst | 1 +
docs/third_party.rst | 3 +++
2 files changed, 4 insertions(+)
create mode 100644 CHANGES/12727.doc.rst
diff --git a/CHANGES/12727.doc.rst b/CHANGES/12727.doc.rst
new file mode 100644
index 00000000000..61ffeb82274
--- /dev/null
+++ b/CHANGES/12727.doc.rst
@@ -0,0 +1 @@
+Added ``aiointercept`` to list of third-party libraries -- by :user:`Polandia94`.
diff --git a/docs/third_party.rst b/docs/third_party.rst
index 12380d390fe..6188ab99d54 100644
--- a/docs/third_party.rst
+++ b/docs/third_party.rst
@@ -314,3 +314,6 @@ ask to raise the status.
- `wireup `_
Performant, concise, and easy-to-use dependency injection container.
+
+- `aiointercept `_
+ Mock aiohttp HTTP requests by routing them through a real aiohttp.web test server.
From 660339a54b8cb166fd93e63a6c700682afb32ae9 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sun, 31 May 2026 22:47:03 +0100
Subject: [PATCH 174/191] [PR #12726/e3a67148 backport][3.14] remove third
party archived repos from docs (#12750)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
**This is a backport of PR #12726 as merged into master
(e3a67148e14f3fe6e098738ae9f7231de88e29ac).**
Co-authored-by: Pablo Nicolás Estevez
---
CHANGES/12726.doc.rst | 1 +
docs/third_party.rst | 22 +++-------------------
2 files changed, 4 insertions(+), 19 deletions(-)
create mode 100644 CHANGES/12726.doc.rst
diff --git a/CHANGES/12726.doc.rst b/CHANGES/12726.doc.rst
new file mode 100644
index 00000000000..45e1212acd1
--- /dev/null
+++ b/CHANGES/12726.doc.rst
@@ -0,0 +1 @@
+Removed archived and deprecated repositories from third party list -- by :user:`Polandia94`.
diff --git a/docs/third_party.rst b/docs/third_party.rst
index 6188ab99d54..5beafb48788 100644
--- a/docs/third_party.rst
+++ b/docs/third_party.rst
@@ -64,6 +64,9 @@ aiohttp extensions
- `aiozipkin `_ distributed
tracing instrumentation for `aiohttp` client and server.
+- `aiocache `_ Caching for asyncio
+ with multiple backends (framework agnostic)
+
Database drivers
^^^^^^^^^^^^^^^^
@@ -146,8 +149,6 @@ We cannot vouch for the quality of these libraries, use them at your own risk.
Please add your library reference here first and after some time
ask to raise the status.
-- `pytest-aiohttp-client `_
- Pytest fixture with simpler api, payload decoding and status code assertions.
- `python-proxy-headers `_
provides ``aiohttp_proxy`` extension for receiving custom response headers from a proxy server
@@ -161,18 +162,9 @@ ask to raise the status.
- `aiohttp-cache `_ A cache
system for aiohttp server.
-- `aiocache `_ Caching for asyncio
- with multiple backends (framework agnostic)
-
-- `gain `_ Web crawling framework
- based on asyncio for everyone.
-
- `aiohttp-validate `_
Simple library that helps you validate your API endpoints requests/responses with json schema.
-- `raven-aiohttp `_ An
- aiohttp transport for raven-python (Sentry client).
-
- `webargs `_ A friendly library
for parsing HTTP request arguments, with built-in support for
popular web frameworks, including Flask, Django, Bottle, Tornado,
@@ -188,14 +180,6 @@ ask to raise the status.
- `aioresponses `_ a
helper for mock/fake web requests in python aiohttp package.
-- `aiohttp-transmute
- `_ A transmute
- implementation for aiohttp.
-
-- `aiohttp-login `_
- Registration and authorization (including social) for aiohttp
- applications.
-
- `aiohttp_utils `_ Handy
utilities for building aiohttp.web applications.
From e0337acec2f3cc0458b63a01a7872086098ac3be Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Sun, 31 May 2026 23:47:45 +0100
Subject: [PATCH 175/191] [PR #12751/af3e9507 backport][3.14] Fix Dependabot
Security (#12752)
**This is a backport of PR #12751 as merged into master
(af3e9507a5afa5630a4a77197d39fbb7a9f0e1ad).**
Co-authored-by: Sam Bull
---
requirements/pyproject.toml | 1 +
1 file changed, 1 insertion(+)
create mode 120000 requirements/pyproject.toml
diff --git a/requirements/pyproject.toml b/requirements/pyproject.toml
new file mode 120000
index 00000000000..1e11d782571
--- /dev/null
+++ b/requirements/pyproject.toml
@@ -0,0 +1 @@
+../pyproject.toml
\ No newline at end of file
From 514567a27d3aebebd356167d29c9a956c9597128 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Mon, 1 Jun 2026 16:55:04 +0100
Subject: [PATCH 176/191] [PR #12753/eeb21d08 backport][3.14] Improve
ContentLengthError messages to show expected vs received bytes (#12755)
**This is a backport of PR #12753 as merged into master
(eeb21d0819b4d6dfb81faad060254915ec3c0794).**
Co-authored-by: Sam Bull
---
CHANGES/12753.misc.rst | 2 +
aiohttp/_http_parser.pyx | 7 ++-
aiohttp/http_parser.py | 5 +-
tests/test_http_parser.py | 114 ++++++++++++++++++++++++++++++++++++++
4 files changed, 126 insertions(+), 2 deletions(-)
create mode 100644 CHANGES/12753.misc.rst
diff --git a/CHANGES/12753.misc.rst b/CHANGES/12753.misc.rst
new file mode 100644
index 00000000000..0b73044f46a
--- /dev/null
+++ b/CHANGES/12753.misc.rst
@@ -0,0 +1,2 @@
+Improved ``ContentLengthError`` exception messages to include both expected and received byte counts. This enhancement provides better diagnostics when debugging response body size mismatches
+-- by :user:`bdraco` and :user:`Dreamsorcerer`.
diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx
index 8c2141fe51f..8bf16ab16f4 100644
--- a/aiohttp/_http_parser.pyx
+++ b/aiohttp/_http_parser.pyx
@@ -325,6 +325,7 @@ cdef class HttpParser:
bint _paused
bint _eof_pending
object _payload
+ unsigned long long _content_length_expected
bint _payload_error
object _payload_exception
object _last_error
@@ -363,6 +364,7 @@ cdef class HttpParser:
cparser.llhttp_init(self._cparser, mode, self._csettings)
self._cparser.data = self
self._cparser.content_length = 0
+ self._content_length_expected = 0
self.protocol = protocol
self._loop = loop
@@ -518,6 +520,7 @@ cdef class HttpParser:
payload = EMPTY_PAYLOAD
self._payload = payload
+ self._content_length_expected = self._cparser.content_length
if encoding is not None and self._auto_decompress:
self._payload = DeflateBuffer(payload, encoding, max_decompress_size=self._limit)
@@ -561,8 +564,10 @@ cdef class HttpParser:
raise TransferEncodingError(
"Not enough data to satisfy transfer length header.")
elif self._cparser.flags & cparser.F_CONTENT_LENGTH:
+ received = self._content_length_expected - self._cparser.content_length
raise ContentLengthError(
- "Not enough data to satisfy content length header.")
+ f"Not enough data to satisfy content length header "
+ f"(received {received} of {self._content_length_expected} bytes).")
elif cparser.llhttp_get_errno(self._cparser) != cparser.HPE_OK:
desc = cparser.llhttp_get_error_reason(self._cparser)
raise PayloadEncodingError(desc.decode('latin-1'))
diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py
index 9e229cc4459..95a1a3aa8aa 100644
--- a/aiohttp/http_parser.py
+++ b/aiohttp/http_parser.py
@@ -859,6 +859,7 @@ def __init__(
elif length is not None:
self._type = ParseState.PARSE_LENGTH
self._length = length
+ self._length_expected = length
if self._length == 0:
real_payload.feed_eof()
self.done = True
@@ -880,8 +881,10 @@ def feed_eof(self) -> None:
self.done = True
self._eof_pending = False
elif self._type == ParseState.PARSE_LENGTH:
+ received = self._length_expected - self._length
raise ContentLengthError(
- "Not enough data to satisfy content length header."
+ f"Not enough data to satisfy content length header "
+ f"(received {received} of {self._length_expected} bytes)."
)
elif self._type == ParseState.PARSE_CHUNKED:
raise TransferEncodingError(
diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py
index 5b5f3af6bc0..935cf65a493 100644
--- a/tests/test_http_parser.py
+++ b/tests/test_http_parser.py
@@ -2225,6 +2225,52 @@ async def test_parse_length_payload_eof(self, protocol: BaseProtocol) -> None:
with pytest.raises(http_exceptions.ContentLengthError):
p.feed_eof()
+ async def test_parse_length_payload_eof_error_message(
+ self, protocol: BaseProtocol
+ ) -> None:
+ """Test that ContentLengthError includes expected vs received bytes."""
+ out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+
+ # Expect 10 bytes, but only send 3
+ p = HttpPayloadParser(out, length=10, headers_parser=HeadersParser())
+ p.feed_data(b"abc")
+
+ with pytest.raises(
+ http_exceptions.ContentLengthError, match=r"received 3 of 10 bytes"
+ ):
+ p.feed_eof()
+
+ async def test_parse_length_payload_eof_no_data(
+ self, protocol: BaseProtocol
+ ) -> None:
+ """Test ContentLengthError when no data is received."""
+ out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+
+ # Expect 20 bytes, but send nothing
+ p = HttpPayloadParser(out, length=20, headers_parser=HeadersParser())
+
+ with pytest.raises(
+ http_exceptions.ContentLengthError, match=r"received 0 of 20 bytes"
+ ):
+ p.feed_eof()
+
+ async def test_parse_length_payload_partial_data(
+ self, protocol: BaseProtocol
+ ) -> None:
+ """Test ContentLengthError with various amounts of partial data."""
+ out = aiohttp.StreamReader(protocol, 2**16, loop=asyncio.get_running_loop())
+
+ # Expect 100 bytes, but only send 45
+ p = HttpPayloadParser(out, length=100, headers_parser=HeadersParser())
+ p.feed_data(b"a" * 25)
+ p.feed_data(b"b" * 20)
+
+ with pytest.raises(
+ http_exceptions.ContentLengthError,
+ match=r"received 45 of 100 bytes",
+ ):
+ p.feed_eof()
+
async def test_parse_chunked_payload_size_error(
self, protocol: BaseProtocol
) -> None:
@@ -2763,3 +2809,71 @@ async def test_streaming_decompress_large_payload(
result = b"".join(buf._buffer)
assert len(result) == len(original)
assert result == original
+
+
+def test_response_parser_incomplete_body_error_message(
+ response: HttpResponseParser,
+) -> None:
+ """Test response parser error message for incomplete body."""
+ # Response expects 50 bytes
+ response.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 50\r\n\r\n")
+ # Send only 15 bytes
+ response.feed_data(b"partial content")
+
+ with pytest.raises(
+ http_exceptions.ContentLengthError, match=r"received 15 of 50 bytes"
+ ):
+ response.feed_eof()
+
+
+def test_response_parser_no_body_error_message(response: HttpResponseParser) -> None:
+ """Test response parser error when no body is received."""
+ # Response expects 25 bytes
+ response.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 25\r\n\r\n")
+ # Send no body data
+
+ with pytest.raises(
+ http_exceptions.ContentLengthError, match=r"received 0 of 25 bytes"
+ ):
+ response.feed_eof()
+
+
+def test_response_parser_partial_chunks_error_message(
+ response: HttpResponseParser,
+) -> None:
+ """Test error message when body is sent in multiple chunks."""
+ # Response expects 100 bytes
+ response.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 100\r\n\r\n")
+ # Send data in chunks totaling 60 bytes
+ response.feed_data(b"a" * 20)
+ response.feed_data(b"b" * 20)
+ response.feed_data(b"c" * 20)
+
+ with pytest.raises(
+ http_exceptions.ContentLengthError, match=r"received 60 of 100 bytes"
+ ):
+ response.feed_eof()
+
+
+def test_request_parser_incomplete_body_error_message(
+ parser: HttpRequestParser,
+) -> None:
+ """Test request parser error message for incomplete body."""
+ # Request with Content-Length but incomplete body
+ parser.feed_data(b"POST /test HTTP/1.1\r\nHost: a\r\nContent-Length: 30\r\n\r\n")
+ # Send only 10 bytes
+ parser.feed_data(b"incomplete")
+
+ with pytest.raises(
+ http_exceptions.ContentLengthError, match=r"received 10 of 30 bytes"
+ ):
+ parser.feed_eof()
+
+
+def test_response_content_length_zero_no_error(response: HttpResponseParser) -> None:
+ """Test that Content-Length: 0 does not raise error on feed_eof."""
+ # Response with Content-Length: 0
+ response.feed_data(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n")
+
+ # This should NOT raise an error
+ response.feed_eof() # Should complete without exception
From 236d2c147bf20fa91f89567d6ddb5c20895b5e34 Mon Sep 17 00:00:00 2001
From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com>
Date: Mon, 1 Jun 2026 17:20:45 +0100
Subject: [PATCH 177/191] [PR #12754/0d864ff9 backport][3.14] Fix sendfile test
(#12756)
**This is a backport of PR #12754 as merged into master
(0d864ff9644205e5aa860e7598e4984aff6e07a9).**
Co-authored-by: Sam Bull
---
tests/test_web_sendfile.py | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/tests/test_web_sendfile.py b/tests/test_web_sendfile.py
index dbb2b902897..86bccb06d99 100644
--- a/tests/test_web_sendfile.py
+++ b/tests/test_web_sendfile.py
@@ -146,13 +146,19 @@ async def test_file_response_sends_headers_immediately() -> None:
file_sender = FileResponse(filepath)
file_sender._path = filepath
- file_sender._sendfile = mock.AsyncMock(return_value=None) # type: ignore[method-assign]
# FileResponse inherits from StreamResponse, so should send immediately
assert file_sender._send_headers_immediately is True
- # Prepare the response
- await file_sender.prepare(request)
+ # Mock the actual transfer but let _sendfile call super().prepare() to write headers
+ with (
+ mock.patch.object(
+ file_sender, "_sendfile_fallback", autospec=True, spec_set=True
+ ) as sendfile_fallback,
+ mock.patch("aiohttp.web_fileresponse.NOSENDFILE", True),
+ ):
+ sendfile_fallback.return_value = writer
+ await file_sender.prepare(request)
# Headers should be sent immediately
writer.send_headers.assert_called_once()
From edb2724f481ba05ef889accc76bbe0089068e8eb Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Mon, 1 Jun 2026 19:21:38 +0100
Subject: [PATCH 178/191] Release v3.14.0 (#12757)
---
CHANGES.rst | 692 ++++++++++++++++++++++++++++++++++
CHANGES/10142.bugfix.rst | 1 -
CHANGES/10587.bugfix.rst | 1 -
CHANGES/10600.bugfix.rst | 2 -
CHANGES/10665.feature.rst | 1 -
CHANGES/10683.bugfix.rst | 1 -
CHANGES/10753.bugfix.rst | 1 -
CHANGES/10785.misc.rst | 1 -
CHANGES/10795.doc.rst | 4 -
CHANGES/11601.breaking.rst | 1 -
CHANGES/11681.feature.rst | 6 -
CHANGES/11750.packaging.rst | 1 -
CHANGES/11761.bugfix.rst | 4 -
CHANGES/11763.feature.rst | 1 -
CHANGES/11764.feature.rst | 1 -
CHANGES/11766.feature.rst | 4 -
CHANGES/11826.contrib.rst | 7 -
CHANGES/11937.misc.rst | 2 -
CHANGES/11966.feature.rst | 8 -
CHANGES/11989.feature.rst | 7 -
CHANGES/12011.bugfix.rst | 1 -
CHANGES/12030.bugfix.rst | 2 -
CHANGES/12091.bugfix.rst | 6 -
CHANGES/12173.contrib.rst | 1 -
CHANGES/12195.bugfix.rst | 2 -
CHANGES/12234.bugfix.rst | 2 -
CHANGES/12281.bugfix.rst | 1 -
CHANGES/12312.bugfix.rst | 1 -
CHANGES/12321.misc.rst | 2 -
CHANGES/12349.contrib.rst | 1 -
CHANGES/12358.misc.rst | 1 -
CHANGES/12364.contrib.rst | 1 -
CHANGES/12395.bugfix.rst | 4 -
CHANGES/12406.contrib.rst | 2 -
CHANGES/12436.bugfix.rst | 1 -
CHANGES/12452.feature.rst | 2 -
CHANGES/12493.bugfix | 3 -
CHANGES/12499.deprecation.rst | 7 -
CHANGES/12499.feature.rst | 3 -
CHANGES/12512.misc.rst | 1 -
CHANGES/12540.bugfix.rst | 1 -
CHANGES/12549.doc.rst | 1 -
CHANGES/12561.misc.rst | 2 -
CHANGES/12562.contrib.rst | 1 -
CHANGES/12569.misc.rst | 3 -
CHANGES/12571.contrib.rst | 4 -
CHANGES/12576.packaging.rst | 4 -
CHANGES/12589.contrib.rst | 7 -
CHANGES/12592.contrib.rst | 6 -
CHANGES/12595.contrib.rst | 5 -
CHANGES/12596.contrib.rst | 4 -
CHANGES/12597.breaking.rst | 1 -
CHANGES/12600.contrib.rst | 4 -
CHANGES/12603.contrib.rst | 8 -
CHANGES/12606.contrib.rst | 6 -
CHANGES/12624.contrib.rst | 4 -
CHANGES/12629.contrib.rst | 7 -
CHANGES/12641.contrib.rst | 5 -
CHANGES/12643.contrib.rst | 5 -
CHANGES/12647.contrib.rst | 4 -
CHANGES/12651.contrib.rst | 5 -
CHANGES/12655.contrib.rst | 4 -
CHANGES/12678.packaging.rst | 1 -
CHANGES/12681.packaging.rst | 1 -
CHANGES/12689.breaking.rst | 7 -
CHANGES/12703.bugfix.rst | 1 -
CHANGES/12706.bugfix.rst | 1 -
CHANGES/12722.bugfix.rst | 1 -
CHANGES/12726.doc.rst | 1 -
CHANGES/12727.bugfix.rst | 1 -
CHANGES/12727.doc.rst | 1 -
CHANGES/12753.misc.rst | 2 -
CHANGES/3951.feature.rst | 1 -
CHANGES/7157.bugfix.rst | 6 -
CHANGES/9308.breaking.rst | 12 -
CHANGES/9705.contrib.rst | 2 -
aiohttp/__init__.py | 2 +-
77 files changed, 693 insertions(+), 228 deletions(-)
delete mode 100644 CHANGES/10142.bugfix.rst
delete mode 100644 CHANGES/10587.bugfix.rst
delete mode 100644 CHANGES/10600.bugfix.rst
delete mode 100644 CHANGES/10665.feature.rst
delete mode 100644 CHANGES/10683.bugfix.rst
delete mode 100644 CHANGES/10753.bugfix.rst
delete mode 100644 CHANGES/10785.misc.rst
delete mode 100644 CHANGES/10795.doc.rst
delete mode 100644 CHANGES/11601.breaking.rst
delete mode 100644 CHANGES/11681.feature.rst
delete mode 100644 CHANGES/11750.packaging.rst
delete mode 100644 CHANGES/11761.bugfix.rst
delete mode 100644 CHANGES/11763.feature.rst
delete mode 120000 CHANGES/11764.feature.rst
delete mode 100644 CHANGES/11766.feature.rst
delete mode 100644 CHANGES/11826.contrib.rst
delete mode 100644 CHANGES/11937.misc.rst
delete mode 100644 CHANGES/11966.feature.rst
delete mode 100644 CHANGES/11989.feature.rst
delete mode 100644 CHANGES/12011.bugfix.rst
delete mode 100644 CHANGES/12030.bugfix.rst
delete mode 100644 CHANGES/12091.bugfix.rst
delete mode 100644 CHANGES/12173.contrib.rst
delete mode 100644 CHANGES/12195.bugfix.rst
delete mode 100644 CHANGES/12234.bugfix.rst
delete mode 100644 CHANGES/12281.bugfix.rst
delete mode 100644 CHANGES/12312.bugfix.rst
delete mode 100644 CHANGES/12321.misc.rst
delete mode 100644 CHANGES/12349.contrib.rst
delete mode 100644 CHANGES/12358.misc.rst
delete mode 100644 CHANGES/12364.contrib.rst
delete mode 100644 CHANGES/12395.bugfix.rst
delete mode 100644 CHANGES/12406.contrib.rst
delete mode 100644 CHANGES/12436.bugfix.rst
delete mode 100644 CHANGES/12452.feature.rst
delete mode 100644 CHANGES/12493.bugfix
delete mode 100644 CHANGES/12499.deprecation.rst
delete mode 100644 CHANGES/12499.feature.rst
delete mode 100644 CHANGES/12512.misc.rst
delete mode 100644 CHANGES/12540.bugfix.rst
delete mode 100644 CHANGES/12549.doc.rst
delete mode 100644 CHANGES/12561.misc.rst
delete mode 100644 CHANGES/12562.contrib.rst
delete mode 100644 CHANGES/12569.misc.rst
delete mode 100644 CHANGES/12571.contrib.rst
delete mode 100644 CHANGES/12576.packaging.rst
delete mode 100644 CHANGES/12589.contrib.rst
delete mode 100644 CHANGES/12592.contrib.rst
delete mode 100644 CHANGES/12595.contrib.rst
delete mode 100644 CHANGES/12596.contrib.rst
delete mode 120000 CHANGES/12597.breaking.rst
delete mode 100644 CHANGES/12600.contrib.rst
delete mode 100644 CHANGES/12603.contrib.rst
delete mode 100644 CHANGES/12606.contrib.rst
delete mode 100644 CHANGES/12624.contrib.rst
delete mode 100644 CHANGES/12629.contrib.rst
delete mode 100644 CHANGES/12641.contrib.rst
delete mode 100644 CHANGES/12643.contrib.rst
delete mode 100644 CHANGES/12647.contrib.rst
delete mode 100644 CHANGES/12651.contrib.rst
delete mode 100644 CHANGES/12655.contrib.rst
delete mode 100644 CHANGES/12678.packaging.rst
delete mode 100644 CHANGES/12681.packaging.rst
delete mode 100644 CHANGES/12689.breaking.rst
delete mode 100644 CHANGES/12703.bugfix.rst
delete mode 100644 CHANGES/12706.bugfix.rst
delete mode 100644 CHANGES/12722.bugfix.rst
delete mode 100644 CHANGES/12726.doc.rst
delete mode 100644 CHANGES/12727.bugfix.rst
delete mode 100644 CHANGES/12727.doc.rst
delete mode 100644 CHANGES/12753.misc.rst
delete mode 100644 CHANGES/3951.feature.rst
delete mode 100644 CHANGES/7157.bugfix.rst
delete mode 100644 CHANGES/9308.breaking.rst
delete mode 100644 CHANGES/9705.contrib.rst
diff --git a/CHANGES.rst b/CHANGES.rst
index 2a7638f50a3..3f569fd5b45 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -10,6 +10,698 @@
.. towncrier release notes start
+3.14.0 (2026-06-01)
+===================
+
+We have a new website! https://aio-libs.org
+Subscribe to the news feed to find out more about what we're working on in future.
+
+Features
+--------
+
+- Added ``RequestKey`` and ``ResponseKey`` classes,
+ which enable static type checking for request & response
+ context storages in the same way that ``AppKey`` does for ``Application``
+ -- by :user:`gsoldatov`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`11766`.
+
+
+
+- Added :func:`~aiohttp.encode_basic_auth` for encoding HTTP Basic
+ Authentication credentials. Replaces the now-deprecated
+ :class:`~aiohttp.BasicAuth` -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12499`.
+
+
+
+- Started accepting :term:`asynchronous context managers ` for cleanup contexts.
+ Legacy single-yield :term:`asynchronous generator` cleanup contexts continue to be
+ supported; async context managers are adapted internally so they are
+ entered at startup and exited during cleanup.
+
+ -- by :user:`MannXo`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`11681`.
+
+
+
+- Added :py:attr:`~aiohttp.CookieJar.cookies` and :py:attr:`~aiohttp.CookieJar.host_only_cookies` read-only properties to :py:class:`~aiohttp.CookieJar` exposing the stored cookies with their full attributes -- by :user:`Br1an67`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`3951`.
+
+
+
+- Added :py:attr:`~aiohttp.web.TCPSite.port` accessor for dynamic port allocations in :class:`~aiohttp.web.TCPSite` -- by :user:`twhittock-disguise` and :user:`rodrigobnogueira`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`10665`.
+
+
+
+- Added ``decode_text`` parameter to :meth:`~aiohttp.ClientSession.ws_connect` and :class:`~aiohttp.web.WebSocketResponse` to receive WebSocket TEXT messages as raw bytes instead of decoded strings, enabling direct use with high-performance JSON parsers like ``orjson`` -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`11763`, :issue:`11764`.
+
+
+
+- Large overhaul of parser/decompression code.
+
+ The zip bomb security fix in 3.13 stopped highly compressed payloads
+ from being decompressed, regardless of validity. Now aiohttp will
+ decompress such payloads in chunks of 256+ KiB, allowing safe decompression
+ of such payloads.
+
+ -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`11966`.
+
+
+
+- Added explicit APIs for bytes-returning JSON serializer:
+ ``JSONBytesEncoder`` type, ``JsonBytesPayload``,
+ :func:`~aiohttp.web.json_bytes_response`,
+ :meth:`~aiohttp.web.WebSocketResponse.send_json_bytes` and
+ :meth:`~aiohttp.ClientWebSocketResponse.send_json_bytes` methods, and
+ ``json_serialize_bytes`` parameter for :class:`~aiohttp.ClientSession`
+ -- by :user:`kevinpark1217`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`11989`.
+
+
+
+- Added :attr:`~aiohttp.ClientResponse.output_size` and
+ :attr:`~aiohttp.ClientResponse.upload_complete` -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12452`.
+
+
+Bug fixes
+---------
+
+- Fixed ``ZLibDecompressor`` silently dropping data past the first
+ member when decompressing concatenated gzip/deflate streams. Each subsequent
+ member is now handed to a fresh decompressor, matching the behaviour already
+ implemented for ZSTD multi-frame streams.
+
+ -- by :user:`Ashutosh-177`
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`7157`.
+
+
+
+- Improved the parser error message shown when TLS handshake bytes are received on an HTTP port -- by :user:`puneetdixit200`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`10142`.
+
+
+
+- Fixed the C parser failing to reject a response with a body when none was expected -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`10587`.
+
+
+
+- Fixed http parser not rejecting HTTP/1.1 requests that do not have valid Host header.
+ -- by :user:`Cycloctane`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`10600`.
+
+
+
+- Fixed misleading TLS-in-TLS warning being emitted when sending HTTPS requests through an HTTP proxy. The warning now only fires when the proxy itself uses HTTPS, which is the only case where TLS-in-TLS actually applies -- by :user:`wavebyrd`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`10683`.
+
+
+
+- Fixed ``AssertionError`` when the transport is ``None`` during WebSocket
+ preparation or file response sending (e.g. when a client disconnects
+ immediately after connecting). A ``ConnectionResetError`` is now raised
+ instead -- by :user:`agners`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`11761`.
+
+
+
+- Fixed ad-hoc cookies passed to individual requests not being sent when the session's cookie jar has ``unsafe=True`` and the target URL uses an IP address, by copying the ``unsafe`` setting from the session's cookie jar to the temporary cookie jar -- by :user:`Krishnachaitanyakc`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12011`.
+
+
+
+- Reset the WebSocket heartbeat timer on inbound data to avoid false ping/pong timeouts while receiving large frames
+ -- by :user:`hoffmang9`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12030`.
+
+
+
+- Switched :py:meth:`~aiohttp.CookieJar.save` to use JSON format and
+ :py:meth:`~aiohttp.CookieJar.load` to try JSON first with a fallback to
+ a restricted pickle unpickler -- by :user:`YuvalElbar6`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12091`.
+
+
+
+- Fixed redirects with consumed non-rewindable request bodies to raise
+ :class:`aiohttp.ClientPayloadError` instead of silently sending an empty body.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12195`.
+
+
+
+- Fixed zstd decompression failing with ``ClientPayloadError`` when the server
+ sends a response as multiple zstd frames -- by :user:`josu-moreno`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12234`.
+
+
+
+- Fixed spurious ``Future exception was never retrieved`` warning on disconnect during back-pressure -- by :user:`availov`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12281`.
+
+
+
+- ``Cookiejar.save()`` now uses ``0x600`` permissions to better protect them from being read by other users -- by :user:`digiscrypt`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12312`.
+
+
+
+- Fixed a crash (:external+python:exc:`~http.cookies.CookieError`) in the cookie parser when receiving cookies
+ containing ASCII control characters on CPython builds with the :cve:`2026-3644`
+ patch. The parser now gracefully skips cookies whose value contains control
+ characters instead of letting the exception propagate -- by :user:`rodrigobnogueira`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12395`.
+
+
+
+- Fixed digest authentication failing for requests whose path or query string contains percent-encoded reserved characters; the digest signature now uses the encoded request-target that is sent on the wire instead of the decoded form -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12436`.
+
+
+
+- Fixed :func:`aiohttp.web.run_app` losing inner traceback frames when an
+ exception is raised during application startup (e.g. inside
+ ``cleanup_ctx`` or ``on_startup``). Regression since 3.10.6.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12493`.
+
+
+
+- Fixed per-request ``cookies`` not being dropped on cross-origin redirects -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12550`.
+
+
+
+- Fixed invalid bytes being allowed in multipart/payload headers -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12719`.
+
+
+
+- Fixed :py:meth:`~aiohttp.FormData.add_field` accepting invalid bytes in ``name`` and ``filename`` -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12721`.
+
+
+
+- Fixed websocket upgrade occurring when header contained a value like `notupgrade` -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12723`.
+
+
+
+
+Deprecations (removal in next major release)
+--------------------------------------------
+
+- Deprecated :class:`~aiohttp.BasicAuth` and the ``auth`` / ``proxy_auth``
+ parameters. They will be removed in aiohttp 4.0. Use the new
+ :func:`~aiohttp.encode_basic_auth` helper together with
+ ``headers={"Authorization": ...}`` (or
+ ``proxy_headers={"Proxy-Authorization": ...}`` for proxies) instead.
+ Note that ``encode_basic_auth()`` defaults to `utf-8`, not `latin1`
+ -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12499`.
+
+
+
+- Added deprecation warning to ``aiohttp.pytest_plugin``, please switch to ``pytest-aiohttp`` -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`10785`.
+
+
+
+Removals and backward incompatible breaking changes
+---------------------------------------------------
+
+- Stopped calling :func:`socket.getfqdn` as the fallback for
+ :attr:`aiohttp.web.BaseRequest.host`. :func:`socket.getfqdn`
+ performs blocking reverse DNS resolution on the event loop
+ thread and can stall a worker for many seconds when the system
+ resolver is slow, and could be triggered remotely by an HTTP/1.0
+ request that omits the ``Host`` header. The fallback when no
+ ``Host`` header is present is now the local socket address the
+ request arrived on (transport ``sockname``), or an empty string
+ if no transport information is available. Code that relied on
+ the FQDN being returned must now read it from
+ :func:`socket.getfqdn` directly, off the event loop
+ -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`9308`, :issue:`12597`.
+
+
+
+- Dropped support for Python 3.9 -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`11601`.
+
+
+
+- Tightened outbound header serialization to reject all ASCII control
+ characters forbidden by :rfc:`9110#section-5.5` and :rfc:`9112#section-4`
+ (``0x00``\-``0x08``, ``0x0A``\-``0x1F``, ``0x7F``) in status lines,
+ header field-names, and field-values. Previously only CR, LF and NUL were
+ rejected. HTAB (``0x09``) remains permitted in field values. Applications
+ that placed bare control characters in outbound headers will now raise
+ :exc:`ValueError` instead of emitting non-RFC-compliant bytes -- by :user:`rodrigobnogueira`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12689`.
+
+
+
+
+Improved documentation
+----------------------
+
+- Replaced the deprecated ``ujson`` library with ``orjson`` in the
+ client quickstart documentation. ``ujson`` has been put into
+ maintenance-only mode; ``orjson`` is the recommended alternative.
+ -- by :user:`indoor47`
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`10795`.
+
+
+
+- Added the :doc:`threat_model` to the Sphinx documentation -- by :user:`omkar-334`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12549`.
+
+
+
+- Removed archived and deprecated repositories from third party list -- by :user:`Polandia94`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12726`.
+
+
+
+- Added ``aiointercept`` to list of third-party libraries -- by :user:`Polandia94`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12727`.
+
+
+
+
+Packaging updates and notes for downstreams
+-------------------------------------------
+
+- Added wheels for Android and iOS platforms -- by :user:`timrid`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`11750`.
+
+
+
+- Parallelized the Cython extension compilation by defaulting
+ ``build_ext.parallel`` to ``os.cpu_count()``, so each module's
+ ``gcc`` invocation now runs concurrently instead of one at a time
+ -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12576`.
+
+
+
+- Submitted vendored `llhttp` to Github's SBOM -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12678`.
+
+
+
+- Updated ``llhttp`` to v9.4.1 -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12681`.
+
+
+
+
+Contributor-facing changes
+--------------------------
+
+- The coverage tool is now configured using the new native
+ auto-discovered :file:`.coveragerc.toml` file
+ -- by :user:`webknjaz`.
+
+ It is also set up to use the ``ctrace`` core that works
+ around the performance issues in the ``sysmon`` tracer
+ which is default under Python 3.14.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`11826`.
+
+
+
+- Fixed and reworked ``autobahn`` tests -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12173`.
+
+
+
+- Added a CI job to measure Cython coverage -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12349`.
+
+
+
+- Disabled ``coverage`` and ``xdist`` by default to ease local development -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12364`.
+
+
+
+- Avoid installation of backports.zstd on Python 3.14 in linting dependency set
+ -- by :user:`seifertm`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12406`.
+
+
+
+- Added ``--durations=30`` to the benchmark CI run so the slowest tests are reported when the job hits its timeout -- by :user:`aiolibsbot`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12562`.
+
+
+
+- Fixed two flakey ``test_middleware_uses_session_avoids_recursion_with_*`` tests
+ that hard coded ``localhost`` in the inner middleware request; they now target
+ the bound server URL so happy eyeballs cannot pick an unbound address on
+ Windows runners -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12571`.
+
+
+
+- Restricted the ``isal`` test dependency to CPython, since
+ ``isal`` 1.8.0 stopped publishing PyPy wheels and the source
+ build requires ``nasm``, which is not available on the CI
+ runners. The ``parametrize_zlib_backend`` fixture already
+ calls ``pytest.importorskip``, so PyPy continues to exercise
+ the ``zlib`` and ``zlib_ng`` backends with no further
+ changes -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12589`.
+
+
+
+- Fixed a flakey ``test_tcp_connector_fingerprint_ok`` by aborting
+ the SSL shutdown on the test's TCP connector before returning.
+ The graceful TLS close was occasionally outliving the test event
+ loop on one of the CI jobs, and the teardown ``gc.collect()``
+ then surfaced the still-open transport as a
+ ``PytestUnraisableExceptionWarning`` -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12592`.
+
+
+
+- Switched the ``cibuildwheel`` build frontend to ``build[uv]`` so
+ that ``uv`` provisions every build-isolation virtual environment
+ in the wheel matrix, replacing the per-ABI ``pip`` resolve with a
+ roughly sub-second ``uv`` resolve
+ -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12595`.
+
+
+
+- Fixed flaky ``test_handler_returns_not_response`` and
+ ``test_handler_returns_none`` by routing ``loop.set_debug(True)``
+ through a new ``loop_debug_mode`` fixture that disables debug
+ mode before the ``aiohttp_client`` fixture finalizes. Leaving
+ debug on through teardown let PyPy 3.11's asyncio slow-callback
+ logger walk into ``Task.__repr__`` during connector close,
+ surfacing a spurious ``RuntimeWarning: coroutine was never
+ awaited`` -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12603`.
+
+
+
+- Reduced runtime of several of the slowest unit tests
+ (decompress size-limit payloads from 64 MiB to 2 MiB,
+ ``test_chunk_splits_after_pause`` chunk count from 50000
+ to 20000, and ``test_set_cookies_max_age`` sleep from 2
+ seconds to 1.1 seconds) without changing what they
+ exercise -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12606`.
+
+
+
+- Added a default 120-second per-test timeout via ``pytest-timeout`` so a
+ hung test surfaces by name in CI output instead of getting hidden behind
+ the job-level timeout added in :pr:`12619`. The ``autobahn`` and
+ benchmark jobs opt out with ``--timeout=0`` -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12624`.
+
+
+
+- Switched the CI ``test`` and ``autobahn`` jobs from
+ ``actions/setup-python`` to ``astral-sh/setup-uv`` for installing
+ interpreters, cutting the ``Setup Python`` step from 40-58s to a
+ few seconds on ``macos-latest`` and ``windows-latest`` runners for
+ variants not in the hosted tool-cache (notably the free-threaded
+ ``3.14t``)
+ -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12629`.
+
+
+
+- Made the ``pip`` command used by the :file:`Makefile` configurable via a
+ ``PIP`` variable; downstream consumers can now run, for example,
+ ``make .develop PIP="uv pip"`` to install via ``uv`` without us
+ maintaining a parallel target
+ -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12641`.
+
+
+
+- Allowed re-running the ``deploy`` job in ``.github/workflows/ci-cd.yml``
+ after a partial release failure: the ``Make Release`` step now skips
+ when the GitHub Release already exists, and the PyPI publish step uses
+ ``skip-existing`` so dists that were already uploaded on a prior
+ attempt do not break the retry -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12651`.
+
+
+
+- Switched the armv7l wheel builds onto GitHub's hosted ARM runners. The
+ 32-bit ARM build still runs under QEMU, but the host is now aarch64
+ rather than x86_64, so the emulation overhead drops sharply
+ -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12655`.
+
+
+
+
+Miscellaneous internal changes
+------------------------------
+
+- Added win_arm64 to the wheels that gets pushed to PyPI
+ -- by :user:`AraHaan`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`11937`.
+
+
+
+- Added ``cdef`` type declarations and inlined the upgrade check in the HTTP parser
+ -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12321`.
+
+
+
+- Changed ``zlib_executor_size`` default so compressed payloads are async by default -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12358`.
+
+
+
+- Added ``THREAT_MODEL.md`` detailing our security stance -- by :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12512`.
+
+
+
+- Reduced payload sizes and request counts in the slowest client and URL
+ dispatcher benchmarks so they no longer dominate CI runtime
+ -- by :user:`bdraco`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12569`.
+
+
+
+- Improved ``ContentLengthError`` exception messages to include both expected and received byte counts. This enhancement provides better diagnostics when debugging response body size mismatches
+ -- by :user:`bdraco` and :user:`Dreamsorcerer`.
+
+
+ *Related issues and pull requests on GitHub:*
+ :issue:`12753`.
+
+
+
+
+----
+
+
3.13.5 (2026-03-31)
===================
diff --git a/CHANGES/10142.bugfix.rst b/CHANGES/10142.bugfix.rst
deleted file mode 100644
index 821db59818d..00000000000
--- a/CHANGES/10142.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Improved the parser error message shown when TLS handshake bytes are received on an HTTP port -- by :user:`puneetdixit200`.
diff --git a/CHANGES/10587.bugfix.rst b/CHANGES/10587.bugfix.rst
deleted file mode 100644
index f60e1d17c5f..00000000000
--- a/CHANGES/10587.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed the C parser failing to reject a response with a body when none was expected -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/10600.bugfix.rst b/CHANGES/10600.bugfix.rst
deleted file mode 100644
index eba47bf56e6..00000000000
--- a/CHANGES/10600.bugfix.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Fixed http parser not rejecting HTTP/1.1 requests that do not have valid Host header.
--- by :user:`Cycloctane`.
diff --git a/CHANGES/10665.feature.rst b/CHANGES/10665.feature.rst
deleted file mode 100644
index afb4768c7cf..00000000000
--- a/CHANGES/10665.feature.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added :py:attr:`~aiohttp.web.TCPSite.port` accessor for dynamic port allocations in :class:`~aiohttp.web.TCPSite` -- by :user:`twhittock-disguise` and :user:`rodrigobnogueira`.
diff --git a/CHANGES/10683.bugfix.rst b/CHANGES/10683.bugfix.rst
deleted file mode 100644
index 9631cc5fa05..00000000000
--- a/CHANGES/10683.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed misleading TLS-in-TLS warning being emitted when sending HTTPS requests through an HTTP proxy. The warning now only fires when the proxy itself uses HTTPS, which is the only case where TLS-in-TLS actually applies -- by :user:`wavebyrd`.
diff --git a/CHANGES/10753.bugfix.rst b/CHANGES/10753.bugfix.rst
deleted file mode 100644
index e0f4cfd0dd1..00000000000
--- a/CHANGES/10753.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Widened ``trace_request_ctx`` parameter type from ``Mapping[str, Any] | None`` to ``object`` to allow passing instances of user-defined classes as trace context -- by :user:`nightcityblade`.
diff --git a/CHANGES/10785.misc.rst b/CHANGES/10785.misc.rst
deleted file mode 100644
index 5b43f63a42d..00000000000
--- a/CHANGES/10785.misc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added deprecation warning to ``aiohttp.pytest_plugin``, please switch to ``pytest-aiohttp`` -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/10795.doc.rst b/CHANGES/10795.doc.rst
deleted file mode 100644
index 91094f70c13..00000000000
--- a/CHANGES/10795.doc.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Replaced the deprecated ``ujson`` library with ``orjson`` in the
-client quickstart documentation. ``ujson`` has been put into
-maintenance-only mode; ``orjson`` is the recommended alternative.
--- by :user:`indoor47`
diff --git a/CHANGES/11601.breaking.rst b/CHANGES/11601.breaking.rst
deleted file mode 100644
index c2eccbd9e1c..00000000000
--- a/CHANGES/11601.breaking.rst
+++ /dev/null
@@ -1 +0,0 @@
-Dropped support for Python 3.9 -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/11681.feature.rst b/CHANGES/11681.feature.rst
deleted file mode 100644
index 21b0ab1f7c7..00000000000
--- a/CHANGES/11681.feature.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-Started accepting :term:`asynchronous context managers ` for cleanup contexts.
-Legacy single-yield :term:`asynchronous generator` cleanup contexts continue to be
-supported; async context managers are adapted internally so they are
-entered at startup and exited during cleanup.
-
--- by :user:`MannXo`.
diff --git a/CHANGES/11750.packaging.rst b/CHANGES/11750.packaging.rst
deleted file mode 100644
index 2af5e11cf45..00000000000
--- a/CHANGES/11750.packaging.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added wheels for Android and iOS platforms -- by :user:`timrid`.
diff --git a/CHANGES/11761.bugfix.rst b/CHANGES/11761.bugfix.rst
deleted file mode 100644
index d4661c6d4a1..00000000000
--- a/CHANGES/11761.bugfix.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Fixed ``AssertionError`` when the transport is ``None`` during WebSocket
-preparation or file response sending (e.g. when a client disconnects
-immediately after connecting). A ``ConnectionResetError`` is now raised
-instead -- by :user:`agners`.
diff --git a/CHANGES/11763.feature.rst b/CHANGES/11763.feature.rst
deleted file mode 100644
index b34bfafaca8..00000000000
--- a/CHANGES/11763.feature.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added ``decode_text`` parameter to :meth:`~aiohttp.ClientSession.ws_connect` and :class:`~aiohttp.web.WebSocketResponse` to receive WebSocket TEXT messages as raw bytes instead of decoded strings, enabling direct use with high-performance JSON parsers like ``orjson`` -- by :user:`bdraco`.
diff --git a/CHANGES/11764.feature.rst b/CHANGES/11764.feature.rst
deleted file mode 120000
index 0860becd808..00000000000
--- a/CHANGES/11764.feature.rst
+++ /dev/null
@@ -1 +0,0 @@
-11763.feature.rst
\ No newline at end of file
diff --git a/CHANGES/11766.feature.rst b/CHANGES/11766.feature.rst
deleted file mode 100644
index de57ca44543..00000000000
--- a/CHANGES/11766.feature.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Added ``RequestKey`` and ``ResponseKey`` classes,
-which enable static type checking for request & response
-context storages in the same way that ``AppKey`` does for ``Application``
--- by :user:`gsoldatov`.
diff --git a/CHANGES/11826.contrib.rst b/CHANGES/11826.contrib.rst
deleted file mode 100644
index 134eda601c2..00000000000
--- a/CHANGES/11826.contrib.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-The coverage tool is now configured using the new native
-auto-discovered :file:`.coveragerc.toml` file
--- by :user:`webknjaz`.
-
-It is also set up to use the ``ctrace`` core that works
-around the performance issues in the ``sysmon`` tracer
-which is default under Python 3.14.
diff --git a/CHANGES/11937.misc.rst b/CHANGES/11937.misc.rst
deleted file mode 100644
index e8435d14618..00000000000
--- a/CHANGES/11937.misc.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Added win_arm64 to the wheels that gets pushed to PyPI
--- by :user:`AraHaan`.
diff --git a/CHANGES/11966.feature.rst b/CHANGES/11966.feature.rst
deleted file mode 100644
index 9f298f98e12..00000000000
--- a/CHANGES/11966.feature.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-Large overhaul of parser/decompression code.
-
-The zip bomb security fix in 3.13 stopped highly compressed payloads
-from being decompressed, regardless of validity. Now aiohttp will
-decompress such payloads in chunks of 256+ KiB, allowing safe decompression
-of such payloads.
-
--- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/11989.feature.rst b/CHANGES/11989.feature.rst
deleted file mode 100644
index ced05b5e100..00000000000
--- a/CHANGES/11989.feature.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-Added explicit APIs for bytes-returning JSON serializer:
-``JSONBytesEncoder`` type, ``JsonBytesPayload``,
-:func:`~aiohttp.web.json_bytes_response`,
-:meth:`~aiohttp.web.WebSocketResponse.send_json_bytes` and
-:meth:`~aiohttp.ClientWebSocketResponse.send_json_bytes` methods, and
-``json_serialize_bytes`` parameter for :class:`~aiohttp.ClientSession`
--- by :user:`kevinpark1217`.
diff --git a/CHANGES/12011.bugfix.rst b/CHANGES/12011.bugfix.rst
deleted file mode 100644
index 701a2ed01e3..00000000000
--- a/CHANGES/12011.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed ad-hoc cookies passed to individual requests not being sent when the session's cookie jar has ``unsafe=True`` and the target URL uses an IP address, by copying the ``unsafe`` setting from the session's cookie jar to the temporary cookie jar -- by :user:`Krishnachaitanyakc`.
diff --git a/CHANGES/12030.bugfix.rst b/CHANGES/12030.bugfix.rst
deleted file mode 100644
index 5f2f8ba5c3c..00000000000
--- a/CHANGES/12030.bugfix.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Reset the WebSocket heartbeat timer on inbound data to avoid false ping/pong timeouts while receiving large frames
--- by :user:`hoffmang9`.
diff --git a/CHANGES/12091.bugfix.rst b/CHANGES/12091.bugfix.rst
deleted file mode 100644
index 45ffbc3557f..00000000000
--- a/CHANGES/12091.bugfix.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-Switched :py:meth:`~aiohttp.CookieJar.save` to use JSON format and
-:py:meth:`~aiohttp.CookieJar.load` to try JSON first with a fallback to
-a restricted pickle unpickler that only allows cookie-related types
-(``SimpleCookie``, ``Morsel``, ``defaultdict``, etc.), preventing
-arbitrary code execution via malicious pickle payloads
-(CWE-502) -- by :user:`YuvalElbar6`.
diff --git a/CHANGES/12173.contrib.rst b/CHANGES/12173.contrib.rst
deleted file mode 100644
index 7c1bd3d1c1b..00000000000
--- a/CHANGES/12173.contrib.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed and reworked ``autobahn`` tests -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12195.bugfix.rst b/CHANGES/12195.bugfix.rst
deleted file mode 100644
index 34729b52f0d..00000000000
--- a/CHANGES/12195.bugfix.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Fixed redirects with consumed non-rewindable request bodies to raise
-:class:`aiohttp.ClientPayloadError` instead of silently sending an empty body.
diff --git a/CHANGES/12234.bugfix.rst b/CHANGES/12234.bugfix.rst
deleted file mode 100644
index 64bcfa24f69..00000000000
--- a/CHANGES/12234.bugfix.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Fixed zstd decompression failing with ``ClientPayloadError`` when the server
-sends a response as multiple zstd frames -- by :user:`josu-moreno`.
diff --git a/CHANGES/12281.bugfix.rst b/CHANGES/12281.bugfix.rst
deleted file mode 100644
index 63521a73b1c..00000000000
--- a/CHANGES/12281.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed spurious ``Future exception was never retrieved`` warning on disconnect during back-pressure -- by :user:`availov`.
diff --git a/CHANGES/12312.bugfix.rst b/CHANGES/12312.bugfix.rst
deleted file mode 100644
index a7d240ad79c..00000000000
--- a/CHANGES/12312.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-``Cookiejar.save()`` now uses ``0x600`` permissions to better protect them from being read by other users -- by :user:`digiscrypt`.
diff --git a/CHANGES/12321.misc.rst b/CHANGES/12321.misc.rst
deleted file mode 100644
index 71f9db77675..00000000000
--- a/CHANGES/12321.misc.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Added ``cdef`` type declarations and inlined the upgrade check in the HTTP parser
--- by :user:`bdraco`.
diff --git a/CHANGES/12349.contrib.rst b/CHANGES/12349.contrib.rst
deleted file mode 100644
index 12aeb069354..00000000000
--- a/CHANGES/12349.contrib.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added a CI job to measure Cython coverage -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12358.misc.rst b/CHANGES/12358.misc.rst
deleted file mode 100644
index 7b035023500..00000000000
--- a/CHANGES/12358.misc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Changed ``zlib_executor_size`` default so compressed payloads are async by default -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12364.contrib.rst b/CHANGES/12364.contrib.rst
deleted file mode 100644
index 21b9eb1b271..00000000000
--- a/CHANGES/12364.contrib.rst
+++ /dev/null
@@ -1 +0,0 @@
-Disabled ``coverage`` and ``xdist`` by default to ease local development -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12395.bugfix.rst b/CHANGES/12395.bugfix.rst
deleted file mode 100644
index e3c67bfa7aa..00000000000
--- a/CHANGES/12395.bugfix.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Fixed a crash (:external+python:exc:`~http.cookies.CookieError`) in the cookie parser when receiving cookies
-containing ASCII control characters on CPython builds with the :cve:`2026-3644`
-patch. The parser now gracefully skips cookies whose value contains control
-characters instead of letting the exception propagate -- by :user:`rodrigobnogueira`.
diff --git a/CHANGES/12406.contrib.rst b/CHANGES/12406.contrib.rst
deleted file mode 100644
index 9bcbee91e09..00000000000
--- a/CHANGES/12406.contrib.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Avoid installation of backports.zstd on Python 3.14 in linting dependency set
--- by :user:`seifertm`.
diff --git a/CHANGES/12436.bugfix.rst b/CHANGES/12436.bugfix.rst
deleted file mode 100644
index d6f7e160697..00000000000
--- a/CHANGES/12436.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed digest authentication failing for requests whose path or query string contains percent-encoded reserved characters; the digest signature now uses the encoded request-target that is sent on the wire instead of the decoded form -- by :user:`bdraco`.
diff --git a/CHANGES/12452.feature.rst b/CHANGES/12452.feature.rst
deleted file mode 100644
index ad4936323da..00000000000
--- a/CHANGES/12452.feature.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Added :attr:`~aiohttp.ClientResponse.output_size` and
-:attr:`~aiohttp.ClientResponse.upload_complete` -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12493.bugfix b/CHANGES/12493.bugfix
deleted file mode 100644
index 7a68daec0ba..00000000000
--- a/CHANGES/12493.bugfix
+++ /dev/null
@@ -1,3 +0,0 @@
-Fixed :func:`aiohttp.web.run_app` losing inner traceback frames when an
-exception is raised during application startup (e.g. inside
-``cleanup_ctx`` or ``on_startup``). Regression since 3.10.6.
diff --git a/CHANGES/12499.deprecation.rst b/CHANGES/12499.deprecation.rst
deleted file mode 100644
index c9a5b533c77..00000000000
--- a/CHANGES/12499.deprecation.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-Deprecated :class:`~aiohttp.BasicAuth` and the ``auth`` / ``proxy_auth``
-parameters. They will be removed in aiohttp 4.0. Use the new
-:func:`~aiohttp.encode_basic_auth` helper together with
-``headers={"Authorization": ...}`` (or
-``proxy_headers={"Proxy-Authorization": ...}`` for proxies) instead.
-Note that ``encode_basic_auth()`` defaults to `utf-8`, not `latin1`
--- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12499.feature.rst b/CHANGES/12499.feature.rst
deleted file mode 100644
index 8b242432367..00000000000
--- a/CHANGES/12499.feature.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-Added :func:`~aiohttp.encode_basic_auth` for encoding HTTP Basic
-Authentication credentials. Replaces the now-deprecated
-:class:`~aiohttp.BasicAuth` -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12512.misc.rst b/CHANGES/12512.misc.rst
deleted file mode 100644
index c3acbcce786..00000000000
--- a/CHANGES/12512.misc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added ``THREAT_MODEL.md`` detailing our security stance -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12540.bugfix.rst b/CHANGES/12540.bugfix.rst
deleted file mode 100644
index dfd98129e4b..00000000000
--- a/CHANGES/12540.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed per-request ``cookies`` not being dropped on cross-origin redirects -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12549.doc.rst b/CHANGES/12549.doc.rst
deleted file mode 100644
index 0288c33cba5..00000000000
--- a/CHANGES/12549.doc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added the :doc:`threat_model` to the Sphinx documentation -- by :user:`omkar-334`.
diff --git a/CHANGES/12561.misc.rst b/CHANGES/12561.misc.rst
deleted file mode 100644
index 4201bb90b0d..00000000000
--- a/CHANGES/12561.misc.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Bumped the benchmark CI job timeout from 12 to 15 minutes to prevent
-spurious failures on slower runners -- by :user:`aiolibsbot`.
diff --git a/CHANGES/12562.contrib.rst b/CHANGES/12562.contrib.rst
deleted file mode 100644
index 0d89212d365..00000000000
--- a/CHANGES/12562.contrib.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added ``--durations=30`` to the benchmark CI run so the slowest tests are reported when the job hits its timeout -- by :user:`aiolibsbot`.
diff --git a/CHANGES/12569.misc.rst b/CHANGES/12569.misc.rst
deleted file mode 100644
index 32af3f259b4..00000000000
--- a/CHANGES/12569.misc.rst
+++ /dev/null
@@ -1,3 +0,0 @@
-Reduced payload sizes and request counts in the slowest client and URL
-dispatcher benchmarks so they no longer dominate CI runtime
--- by :user:`bdraco`.
diff --git a/CHANGES/12571.contrib.rst b/CHANGES/12571.contrib.rst
deleted file mode 100644
index 8fb1b309375..00000000000
--- a/CHANGES/12571.contrib.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Fixed two flakey ``test_middleware_uses_session_avoids_recursion_with_*`` tests
-that hard coded ``localhost`` in the inner middleware request; they now target
-the bound server URL so happy eyeballs cannot pick an unbound address on
-Windows runners -- by :user:`bdraco`.
diff --git a/CHANGES/12576.packaging.rst b/CHANGES/12576.packaging.rst
deleted file mode 100644
index dc748bfe726..00000000000
--- a/CHANGES/12576.packaging.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Parallelized the Cython extension compilation by defaulting
-``build_ext.parallel`` to ``os.cpu_count()``, so each module's
-``gcc`` invocation now runs concurrently instead of one at a time
--- by :user:`bdraco`.
diff --git a/CHANGES/12589.contrib.rst b/CHANGES/12589.contrib.rst
deleted file mode 100644
index dc7f6400d57..00000000000
--- a/CHANGES/12589.contrib.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-Restricted the ``isal`` test dependency to CPython, since
-``isal`` 1.8.0 stopped publishing PyPy wheels and the source
-build requires ``nasm``, which is not available on the CI
-runners. The ``parametrize_zlib_backend`` fixture already
-calls ``pytest.importorskip``, so PyPy continues to exercise
-the ``zlib`` and ``zlib_ng`` backends with no further
-changes -- by :user:`bdraco`.
diff --git a/CHANGES/12592.contrib.rst b/CHANGES/12592.contrib.rst
deleted file mode 100644
index 76d2f8b7035..00000000000
--- a/CHANGES/12592.contrib.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-Fixed a flakey ``test_tcp_connector_fingerprint_ok`` by aborting
-the SSL shutdown on the test's TCP connector before returning.
-The graceful TLS close was occasionally outliving the test event
-loop on one of the CI jobs, and the teardown ``gc.collect()``
-then surfaced the still-open transport as a
-``PytestUnraisableExceptionWarning`` -- by :user:`bdraco`.
diff --git a/CHANGES/12595.contrib.rst b/CHANGES/12595.contrib.rst
deleted file mode 100644
index 54af2262fb1..00000000000
--- a/CHANGES/12595.contrib.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Switched the ``cibuildwheel`` build frontend to ``build[uv]`` so
-that ``uv`` provisions every build-isolation virtual environment
-in the wheel matrix, replacing the per-ABI ``pip`` resolve with a
-roughly sub-second ``uv`` resolve
--- by :user:`bdraco`.
diff --git a/CHANGES/12596.contrib.rst b/CHANGES/12596.contrib.rst
deleted file mode 100644
index ff96abe5bfc..00000000000
--- a/CHANGES/12596.contrib.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Switched the test-results upload step to ``codecov/codecov-action@v6``
-with ``report_type: test_results``, since
-``codecov/test-results-action`` is being deprecated in favor of
-``codecov-action`` -- by :user:`bdraco`.
diff --git a/CHANGES/12597.breaking.rst b/CHANGES/12597.breaking.rst
deleted file mode 120000
index 651891d5065..00000000000
--- a/CHANGES/12597.breaking.rst
+++ /dev/null
@@ -1 +0,0 @@
-9308.breaking.rst
\ No newline at end of file
diff --git a/CHANGES/12600.contrib.rst b/CHANGES/12600.contrib.rst
deleted file mode 100644
index 4d361389191..00000000000
--- a/CHANGES/12600.contrib.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Silenced the ``DeprecationWarning`` raised by ``uvloop`` 0.22+ when it
-accesses the ``asyncio.AbstractEventLoopPolicy`` alias that Python 3.14
-marks for removal in 3.16, so the test suite passes against the newer
-``uvloop`` release -- by :user:`bdraco`.
diff --git a/CHANGES/12603.contrib.rst b/CHANGES/12603.contrib.rst
deleted file mode 100644
index 51574d02868..00000000000
--- a/CHANGES/12603.contrib.rst
+++ /dev/null
@@ -1,8 +0,0 @@
-Fixed flaky ``test_handler_returns_not_response`` and
-``test_handler_returns_none`` by routing ``loop.set_debug(True)``
-through a new ``loop_debug_mode`` fixture that disables debug
-mode before the ``aiohttp_client`` fixture finalizes. Leaving
-debug on through teardown let PyPy 3.11's asyncio slow-callback
-logger walk into ``Task.__repr__`` during connector close,
-surfacing a spurious ``RuntimeWarning: coroutine was never
-awaited`` -- by :user:`bdraco`.
diff --git a/CHANGES/12606.contrib.rst b/CHANGES/12606.contrib.rst
deleted file mode 100644
index 72925114e8c..00000000000
--- a/CHANGES/12606.contrib.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-Reduced runtime of several of the slowest unit tests
-(decompress size-limit payloads from 64 MiB to 2 MiB,
-``test_chunk_splits_after_pause`` chunk count from 50000
-to 20000, and ``test_set_cookies_max_age`` sleep from 2
-seconds to 1.1 seconds) without changing what they
-exercise -- by :user:`bdraco`.
diff --git a/CHANGES/12624.contrib.rst b/CHANGES/12624.contrib.rst
deleted file mode 100644
index 0c7e2a0a548..00000000000
--- a/CHANGES/12624.contrib.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Added a default 120-second per-test timeout via ``pytest-timeout`` so a
-hung test surfaces by name in CI output instead of getting hidden behind
-the job-level timeout added in :pr:`12619`. The ``autobahn`` and
-benchmark jobs opt out with ``--timeout=0`` -- by :user:`bdraco`.
diff --git a/CHANGES/12629.contrib.rst b/CHANGES/12629.contrib.rst
deleted file mode 100644
index 8c10b198425..00000000000
--- a/CHANGES/12629.contrib.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-Switched the CI ``test`` and ``autobahn`` jobs from
-``actions/setup-python`` to ``astral-sh/setup-uv`` for installing
-interpreters, cutting the ``Setup Python`` step from 40-58s to a
-few seconds on ``macos-latest`` and ``windows-latest`` runners for
-variants not in the hosted tool-cache (notably the free-threaded
-``3.14t``)
--- by :user:`bdraco`.
diff --git a/CHANGES/12641.contrib.rst b/CHANGES/12641.contrib.rst
deleted file mode 100644
index f431e0fd1e3..00000000000
--- a/CHANGES/12641.contrib.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Made the ``pip`` command used by the :file:`Makefile` configurable via a
-``PIP`` variable; downstream consumers can now run, for example,
-``make .develop PIP="uv pip"`` to install via ``uv`` without us
-maintaining a parallel target
--- by :user:`bdraco`.
diff --git a/CHANGES/12643.contrib.rst b/CHANGES/12643.contrib.rst
deleted file mode 100644
index 8477457ce16..00000000000
--- a/CHANGES/12643.contrib.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Merged the "Update pip, wheel, setuptools, build, twine" step into
-the following ``Install dependencies`` (or ``Install cython``) step
-in every job in :file:`.github/workflows/ci-cd.yml`, so each job now
-runs a single ``pip``/``uv pip install`` invocation instead of two
--- by :user:`bdraco`.
diff --git a/CHANGES/12647.contrib.rst b/CHANGES/12647.contrib.rst
deleted file mode 100644
index 5203ba0a395..00000000000
--- a/CHANGES/12647.contrib.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Override ``CIBW_BUILD_FRONTEND=build`` on the QEMU-emulated odd-arch wheel
-jobs so cibuildwheel falls back to plain pip, because the pypa
-``manylinux``/``musllinux`` containers for those arches do not ship ``uv``
-preinstalled -- by :user:`bdraco`.
diff --git a/CHANGES/12651.contrib.rst b/CHANGES/12651.contrib.rst
deleted file mode 100644
index 0318ed0d084..00000000000
--- a/CHANGES/12651.contrib.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Allowed re-running the ``deploy`` job in ``.github/workflows/ci-cd.yml``
-after a partial release failure: the ``Make Release`` step now skips
-when the GitHub Release already exists, and the PyPI publish step uses
-``skip-existing`` so dists that were already uploaded on a prior
-attempt do not break the retry -- by :user:`bdraco`.
diff --git a/CHANGES/12655.contrib.rst b/CHANGES/12655.contrib.rst
deleted file mode 100644
index f5c76cd4105..00000000000
--- a/CHANGES/12655.contrib.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-Switched the armv7l wheel builds onto GitHub's hosted ARM runners. The
-32-bit ARM build still runs under QEMU, but the host is now aarch64
-rather than x86_64, so the emulation overhead drops sharply
--- by :user:`bdraco`.
diff --git a/CHANGES/12678.packaging.rst b/CHANGES/12678.packaging.rst
deleted file mode 100644
index 7e459cf9ff5..00000000000
--- a/CHANGES/12678.packaging.rst
+++ /dev/null
@@ -1 +0,0 @@
-Submitted vendored `llhttp` to Github's SBOM -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12681.packaging.rst b/CHANGES/12681.packaging.rst
deleted file mode 100644
index 1cd12837ecb..00000000000
--- a/CHANGES/12681.packaging.rst
+++ /dev/null
@@ -1 +0,0 @@
-Updated ``llhttp`` to v9.4.1 -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12689.breaking.rst b/CHANGES/12689.breaking.rst
deleted file mode 100644
index 9d807e00769..00000000000
--- a/CHANGES/12689.breaking.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-Tightened outbound header serialization to reject all ASCII control
-characters forbidden by :rfc:`9110#section-5.5` and :rfc:`9112#section-4`
-(``0x00``\-``0x08``, ``0x0A``\-``0x1F``, ``0x7F``) in status lines,
-header field-names, and field-values. Previously only CR, LF and NUL were
-rejected. HTAB (``0x09``) remains permitted in field values. Applications
-that placed bare control characters in outbound headers will now raise
-:exc:`ValueError` instead of emitting non-RFC-compliant bytes -- by :user:`rodrigobnogueira`.
diff --git a/CHANGES/12703.bugfix.rst b/CHANGES/12703.bugfix.rst
deleted file mode 100644
index 7cfca2415c3..00000000000
--- a/CHANGES/12703.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed irrelevant URLs pointing to the corresponding sections in the documentation of Gunicorn -- by :user:`serhiiur`.
diff --git a/CHANGES/12706.bugfix.rst b/CHANGES/12706.bugfix.rst
deleted file mode 100644
index 9248585f99f..00000000000
--- a/CHANGES/12706.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed invalid bytes being allowed in multipart/payload headers -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12722.bugfix.rst b/CHANGES/12722.bugfix.rst
deleted file mode 100644
index af8b103f566..00000000000
--- a/CHANGES/12722.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed :py:meth:`~aiohttp.FormData.add_field` accepting invalid bytes in ``name`` and ``filename`` -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12726.doc.rst b/CHANGES/12726.doc.rst
deleted file mode 100644
index 45e1212acd1..00000000000
--- a/CHANGES/12726.doc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Removed archived and deprecated repositories from third party list -- by :user:`Polandia94`.
diff --git a/CHANGES/12727.bugfix.rst b/CHANGES/12727.bugfix.rst
deleted file mode 100644
index d74b5eae2d3..00000000000
--- a/CHANGES/12727.bugfix.rst
+++ /dev/null
@@ -1 +0,0 @@
-Fixed websocket upgrade occurring when header contained a value like `notupgrade` -- by :user:`Dreamsorcerer`.
diff --git a/CHANGES/12727.doc.rst b/CHANGES/12727.doc.rst
deleted file mode 100644
index 61ffeb82274..00000000000
--- a/CHANGES/12727.doc.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added ``aiointercept`` to list of third-party libraries -- by :user:`Polandia94`.
diff --git a/CHANGES/12753.misc.rst b/CHANGES/12753.misc.rst
deleted file mode 100644
index 0b73044f46a..00000000000
--- a/CHANGES/12753.misc.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Improved ``ContentLengthError`` exception messages to include both expected and received byte counts. This enhancement provides better diagnostics when debugging response body size mismatches
--- by :user:`bdraco` and :user:`Dreamsorcerer`.
diff --git a/CHANGES/3951.feature.rst b/CHANGES/3951.feature.rst
deleted file mode 100644
index ba127c20a83..00000000000
--- a/CHANGES/3951.feature.rst
+++ /dev/null
@@ -1 +0,0 @@
-Added :py:attr:`~aiohttp.CookieJar.cookies` and :py:attr:`~aiohttp.CookieJar.host_only_cookies` read-only properties to :py:class:`~aiohttp.CookieJar` exposing the stored cookies with their full attributes -- by :user:`Br1an67`.
diff --git a/CHANGES/7157.bugfix.rst b/CHANGES/7157.bugfix.rst
deleted file mode 100644
index 60f06d8da9f..00000000000
--- a/CHANGES/7157.bugfix.rst
+++ /dev/null
@@ -1,6 +0,0 @@
-Fixed ``ZLibDecompressor`` silently dropping data past the first
-member when decompressing concatenated gzip/deflate streams. Each subsequent
-member is now handed to a fresh decompressor, matching the behaviour already
-implemented for ZSTD multi-frame streams.
-
--- by :user:`Ashutosh-177`
diff --git a/CHANGES/9308.breaking.rst b/CHANGES/9308.breaking.rst
deleted file mode 100644
index afb3965ab8f..00000000000
--- a/CHANGES/9308.breaking.rst
+++ /dev/null
@@ -1,12 +0,0 @@
-Stopped calling :func:`socket.getfqdn` as the fallback for
-:attr:`aiohttp.web.BaseRequest.host`. :func:`socket.getfqdn`
-performs blocking reverse DNS resolution on the event loop
-thread and can stall a worker for many seconds when the system
-resolver is slow, and could be triggered remotely by an HTTP/1.0
-request that omits the ``Host`` header. The fallback when no
-``Host`` header is present is now the local socket address the
-request arrived on (transport ``sockname``), or an empty string
-if no transport information is available. Code that relied on
-the FQDN being returned must now read it from
-:func:`socket.getfqdn` directly, off the event loop
--- by :user:`bdraco`.
diff --git a/CHANGES/9705.contrib.rst b/CHANGES/9705.contrib.rst
deleted file mode 100644
index 5eaef0c4398..00000000000
--- a/CHANGES/9705.contrib.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Reduced the runtime of a client timeout regression test by shortening its artificial response delay.
--- by :user:`nightcityblade`.
diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py
index 4bbf8d308ed..b6afca45c60 100644
--- a/aiohttp/__init__.py
+++ b/aiohttp/__init__.py
@@ -1,4 +1,4 @@
-__version__ = "3.14.0.dev0"
+__version__ = "3.14.0"
from typing import TYPE_CHECKING
From a1fc2691f695517186a987dc9960a929a0efd978 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 08:13:25 +0000
Subject: [PATCH 179/191] Bump pip from 26.1.1 to 26.1.2 (#12764)
Bumps [pip](https://github.com/pypa/pip) from 26.1.1 to 26.1.2.
Changelog
Sourced from pip's
changelog.
26.1.2 (2026-05-31)
Bug Fixes
- Reject
console_scripts and gui_scripts
entry points whose name would
install a script outside the scripts directory.
([#14000](https://github.com/pypa/pip/issues/14000)
<https://github.com/pypa/pip/issues/14000>_)
- Fix installation incorrectly failing when the target path contains a
doubled
slash, such as with
pip install --root //....
([#14001](https://github.com/pypa/pip/issues/14001)
<https://github.com/pypa/pip/issues/14001>_)
- Send a consistent
Accept-Encoding header to avoid a
spurious Cache entry deserialization failed warning.
([#14012](https://github.com/pypa/pip/issues/14012)
<https://github.com/pypa/pip/issues/14012>_)
Commits
31d7d16
Bump for release
79f348c
Update AUTHORS.txt
237a925
Merge pull request #14001 from
notatallshaw/fix-is-within-directory
34d0285
Merge pull request #14006 from
laymonage/fix-requirements_from_scripts-space-...
09d3e07
Merge pull request #14012 from
notatallshaw/stable-accept-encoding
fa7854f
Use is_within_directory for entry point check
d01b46c
NEWS ENTRY
7ff8bdd
Fix is_within_directory for doubled-slash roots
7ea3466
NEWS ENTRY
85673ea
Fix Accept-Encoding to gzip, deflate
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 10 ++++------
requirements/dev.txt | 10 ++++------
2 files changed, 8 insertions(+), 12 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 89b95c66ca4..f0d3f1efc64 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -148,6 +148,8 @@ packaging==26.2
# wheel
pathspec==1.1.1
# via mypy
+pip==26.1.2
+ # via pip-tools
pip-tools==7.5.3
# via -r requirements/dev.in
pkgconfig==1.6.0
@@ -229,6 +231,8 @@ requests==2.34.2
# sphinxcontrib-spelling
rich==15.0.0
# via pytest-codspeed
+setuptools==82.0.1
+ # via pip-tools
setuptools-git==1.2
# via -r requirements/test-common-base.in
six==1.17.0
@@ -315,9 +319,3 @@ zlib-ng==1.0.0
# via
# -r requirements/lint.in
# -r requirements/test-common.in
-
-# The following packages are considered to be unsafe in a requirements file:
-pip==26.1.1
- # via pip-tools
-setuptools==82.0.1
- # via pip-tools
diff --git a/requirements/dev.txt b/requirements/dev.txt
index a1525e6247d..ab52c26ee9a 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -145,6 +145,8 @@ packaging==26.2
# wheel
pathspec==1.1.1
# via mypy
+pip==26.1.2
+ # via pip-tools
pip-tools==7.5.3
# via -r requirements/dev.in
pkgconfig==1.6.0
@@ -222,6 +224,8 @@ requests==2.34.2
# via sphinx
rich==15.0.0
# via pytest-codspeed
+setuptools==82.0.1
+ # via pip-tools
setuptools-git==1.2
# via -r requirements/test-common-base.in
six==1.17.0
@@ -305,9 +309,3 @@ zlib-ng==1.0.0
# via
# -r requirements/lint.in
# -r requirements/test-common.in
-
-# The following packages are considered to be unsafe in a requirements file:
-pip==26.1.1
- # via pip-tools
-setuptools==82.0.1
- # via pip-tools
From 3848cc95fd6b7c10ca65eddc161a98ca2f413252 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 09:11:07 +0000
Subject: [PATCH 180/191] Bump pytest from 9.0.2 to 9.0.3 (#12766)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.2 to
9.0.3.
Release notes
Sourced from pytest's
releases.
9.0.3
pytest 9.0.3 (2026-04-07)
Bug fixes
-
#12444:
Fixed pytest.approx which now correctly takes into account
~collections.abc.Mapping keys order to compare them.
-
#13634:
Blocking a conftest.py file using the -p no:
option is now explicitly disallowed.
Previously this resulted in an internal assertion failure during
plugin loading.
Pytest now raises a clear UsageError explaining that
conftest files are not plugins and cannot be disabled via
-p.
-
#13734:
Fixed crash when a test raises an exceptiongroup with
__tracebackhide__ = True.
-
#14195:
Fixed an issue where non-string messages passed to unittest.TestCase.subTest() were not
printed.
-
#14343:
Fixed use of insecure temporary directory (CVE-2025-71176).
Improved documentation
- #13388:
Clarified documentation for
-p vs
PYTEST_PLUGINS plugin loading and fixed an incorrect
-p example.
- #13731:
Clarified that capture fixtures (e.g.
capsys and
capfd) take precedence over the -s /
--capture=no command-line options in Accessing
captured output from a test function
<accessing-captured-output>.
- #14088:
Clarified that the default
pytest_collection hook sets
session.items before it calls
pytest_collection_finish, not after.
- #14255:
TOML integer log levels must be quoted: Updating reference
documentation.
Contributor-facing changes
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/test-common-base.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt
index 9065c1c2340..cf525f96844 100644
--- a/requirements/test-common-base.txt
+++ b/requirements/test-common-base.txt
@@ -26,7 +26,7 @@ proxy-py==2.4.10
# via -r requirements/test-common-base.in
pygments==2.19.2
# via pytest
-pytest==9.0.2
+pytest==9.0.3
# via
# -r requirements/test-common-base.in
# pytest-cov
From 2daf6a6db995cffec9fee935d321a3a083d753d3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 09:32:51 +0000
Subject: [PATCH 181/191] Bump tomli from 2.4.0 to 2.4.1 (#12769)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [tomli](https://github.com/hukkin/tomli) from 2.4.0 to 2.4.1.
Changelog
Sourced from tomli's
changelog.
2.4.1
- Fixed
- Limit number of parts of a TOML key to address quadratic time
complexity
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/test-common-base.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt
index cf525f96844..f3bf694c5d2 100644
--- a/requirements/test-common-base.txt
+++ b/requirements/test-common-base.txt
@@ -44,7 +44,7 @@ setuptools-git==1.2
# via -r requirements/test-common-base.in
six==1.17.0
# via python-dateutil
-tomli==2.4.0
+tomli==2.4.1
# via
# coverage
# pytest
From 7f4ed944cad5392c31f39cb355e253be9d696c13 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 09:50:21 +0000
Subject: [PATCH 182/191] Bump packaging from 26.0 to 26.2 (#12772)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [packaging](https://github.com/pypa/packaging) from 26.0 to 26.2.
Release notes
Sourced from packaging's
releases.
26.2
What's Changed
Fixes:
Documentation:
Internal:
New Contributors
Full Changelog: https://github.com/pypa/packaging/compare/26.1...26.2
26.1
Features:
Behavior adaptations:
... (truncated)
Changelog
Sourced from packaging's
changelog.
26.2 - 2026-04-24
Fixes:
- Fix incorrect sysconfig var name for pyemscripten in
(:pull:
1160)
- Make
Version, Specifier,
SpecifierSet, Tag, Marker, and
Requirement pickle-safe
and backward-compatible with pickles created in 25.0-26.1 (including
references to the removed
packaging._structures module) (:pull:1163,
:pull:1168, :pull:1170,
:pull:1171)
- Re-export
ExceptionGroup in metadata for now in
(:pull:1164)
Documentation:
- Add errors section and fix missing details in
(:pull:
1159)
- Document our property-based test suite in
(:pull:
1167)
- Fix a
DirectUrl typo in (:pull:1169)
- Add example of
is_unsatisfiable in
(:pull:1166)
Internal:
- Enable the auditor persona on zizmor in
(:pull:
1158)
- Test new pickle guarantees in (:pull:
1174)
- Use new native ReadTheDocs uv integration in
(:pull:
1175)
26.1 - 2026-04-14
Features:
- PEP 783: add handling for Emscripten wheel tags in
(:pull:
804) (old name used in implementation, fixed in next
release)
- PEP 803: add handling for the
abi3.abi3t free-threading
tag in (:pull:1099)
- PEP 723: add
packaging.dependency_groups module, based
on the dependency-groups package in
(:pull:1065)
- Add the
packaging.direct_url module in
(:pull:944)
- Add the
packaging.errors module in
(:pull:1071)
- Add
SpecifierSet.is_unsatisfiable using ranges (new
internals that will be expanded in future versions) in
(:pull:1119)
- Add
create_compatible_tags_selector to select
compatible tags in (:pull:1110)
- Add a
key argument to
SpecifierSet.filter() in (:pull:1068)
- Support
& and | for
Marker's in (:pull:1146)
- Normalize
Version.__replace__ and add
Version.from_parts in (:pull:1078)
- Add an option to validate compressed tag set sort order in
parse_wheel_filename in (:pull:1150)
Behavior adaptations:
- Narrow exclusion of pre-releases for
<V.postN to
match spec in (:pull:1140)
- Narrow exclusion of post-releases for
>V to match
spec in (:pull:1141)
- Rename
format_full_version to
_format_full_version to make it visibly private in
(:pull:1125)
- Restrict local version to ASCII in (:pull:
1102)
Pylock (PEP 751) updates:
... (truncated)
Commits
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/test-common-base.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt
index f3bf694c5d2..acaa4aa9347 100644
--- a/requirements/test-common-base.txt
+++ b/requirements/test-common-base.txt
@@ -14,7 +14,7 @@ freezegun==1.5.5
# via -r requirements/test-common-base.in
iniconfig==2.3.0
# via pytest
-packaging==26.0
+packaging==26.2
# via pytest
pkgconfig==1.6.0
# via -r requirements/test-common-base.in
From 0cbc93e95289060cf5fa216c7706d7e3017b99ff Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 09:58:32 +0000
Subject: [PATCH 183/191] Bump pytest-cov from 7.0.0 to 7.1.0 (#12768)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 7.0.0
to 7.1.0.
Changelog
Sourced from pytest-cov's
changelog.
7.1.0 (2026-03-21)
-
Fixed total coverage computation to always be consistent, regardless
of reporting settings.
Previously some reports could produce different total counts, and
consequently can make --cov-fail-under behave different depending on
reporting options.
See [#641](https://github.com/pytest-dev/pytest-cov/issues/641)
<https://github.com/pytest-dev/pytest-cov/issues/641>_.
-
Improve handling of ResourceWarning from sqlite3.
The plugin adds warning filter for sqlite3
ResourceWarning unclosed database (since 6.2.0).
It checks if there is already existing plugin for this message by
comparing filter regular expression.
When filter is specified on command line the message is escaped and does
not match an expected message.
A check for an escaped regular expression is added to handle this
case.
With this fix one can suppress ResourceWarning from
sqlite3 from command line::
pytest -W "ignore:unclosed database in <sqlite3.Connection
object at:ResourceWarning" ...
-
Various improvements to documentation.
Contributed by Art Pelling in
[#718](https://github.com/pytest-dev/pytest-cov/issues/718)
<https://github.com/pytest-dev/pytest-cov/pull/718>_ and
"vivodi" in
[#738](https://github.com/pytest-dev/pytest-cov/issues/738)
<https://github.com/pytest-dev/pytest-cov/pull/738>.
Also closed
[#736](https://github.com/pytest-dev/pytest-cov/issues/736)
<https://github.com/pytest-dev/pytest-cov/issues/736>.
-
Fixed some assertions in tests.
Contributed by in Markéta Machová in
[#722](https://github.com/pytest-dev/pytest-cov/issues/722)
<https://github.com/pytest-dev/pytest-cov/pull/722>_.
-
Removed unnecessary coverage configuration copying (meant as a backup
because reporting commands had configuration side-effects before
coverage 5.0).
Commits
66c8a52
Bump version: 7.0.0 → 7.1.0
f707662
Make the examples use pypy 3.11.
6049a78
Make context test use the old ctracer (seems the new sysmon tracer
behaves di...
8ebf20b
Update changelog.
861d30e
Remove the backup context manager - shouldn't be needed since coverage
5.0, ...
fd4c956
Pass the precision on the nulled total (seems that there's some caching
goion...
78c9c4e
Only run the 3.9 on older deps.
4849a92
Punctuation.
197c35e
Update changelog and hopefully I don't forget to publish release again
:))
14dc1c9
Update examples to use 3.11 and make the adhoc layout example look a bit
more...
- Additional commits viewable in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/test-common-base.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt
index acaa4aa9347..fedf2066a58 100644
--- a/requirements/test-common-base.txt
+++ b/requirements/test-common-base.txt
@@ -32,7 +32,7 @@ pytest==9.0.3
# pytest-cov
# pytest-mock
# pytest-timeout
-pytest-cov==7.0.0
+pytest-cov==7.1.0
# via -r requirements/test-common-base.in
pytest-mock==3.15.1
# via -r requirements/test-common-base.in
From c84f414acbb03f02ee2ece07a0c963b96ada3e81 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 09:58:48 +0000
Subject: [PATCH 184/191] Bump pygments from 2.19.2 to 2.20.0 (#12767)
Bumps [pygments](https://github.com/pygments/pygments) from 2.19.2 to
2.20.0.
Release notes
Sourced from pygments's
releases.
2.20.0
-
New lexers:
-
Updated lexers:
- archetype: Fix catastrophic backtracking in GUID and ID patterns (#3064)
- ASN.1: Recognize minus sign and fix range operator (#3014,
#3060)
- C++: Add C++26 keywords (#2955),
add integer literal suffixes (#2966)
- ComponentPascal: Fix
analyse_text (#3028,
#3032)
- Coq renamed to Rocq (#2883,
#2908)
- Cython: Various improvements (#2932,
#2933)
- Debian control: Improve architecture parsing (#3052)
- Devicetree: Add support for overlay/fragments (#3021),
add bytestring support (#3022),
fix catastrophic backtracking (#3057)
- Fennel: Various improvements (#2911)
- Haskell: Handle escape sequences in character literals (#3069,
#1795)
- Java: Add module keywords (#2955)
- Lean4: Add operators
]', ]?,
]! (#2946)
- LESS: Support single-line comments (#3005)
- LilyPond: Update to 2.25.29 (#2974)
- LLVM: Support C-style comments (#3023,
#2978)
- Lua(u): Fix catastrophic backtracking (#3047)
- Macaulay2: Update to 1.25.05 (#2893),
1.25.11 (#2988)
- Mathematica: Various improvements (#2957)
- meson: Add additional operators (#2919)
- MySQL: Update keywords (#2970)
- org-Mode: Support both schedule and deadline (#2899)
- PHP: Add
__PROPERTY__ magic constant (#2924),
add reserved keywords (#3002)
- PostgreSQL: Add more keywords (#2985)
- protobuf: Fix namespace tokenization (#2929)
- Python: Add
t-string support (#2973,
#3009,
#3010)
- Tablegen: Fix infinite loop (#2972,
#2940)
- Tera Term macro: Add commands introduced in v5.3 through v5.6 (#2951)
- TOML: Support TOML 1.1.0 (#3026,
#3027)
- Turtle: Allow empty comment lines (#2980)
- XML: Added
.xbrl as file ending (#2890,
#2891)
-
Drop Python 3.8, and add Python 3.14 as a supported version (#2987,
#3012)
-
Various improvements to autopygmentize (#2894)
-
Update onedark style to support more token types (#2977)
-
Update rtt style to support more token types (#2895)
-
Cache entry points to improve performance (#2979)
-
Fix xterm-256 color table (#3043)
-
Fix kwargs dictionary getting mutated on each call (#3044)
Changelog
Sourced from pygments's
changelog.
Version 2.20.0
(released March 29th, 2026)
-
New lexers:
-
Updated lexers:
- archetype: Fix catastrophic backtracking in GUID and ID patterns (#3064)
- ASN.1: Recognize minus sign and fix range operator (#3014,
#3060)
- C++: Add C++26 keywords (#2955),
add integer literal suffixes (#2966)
- ComponentPascal: Fix
analyse_text (#3028,
#3032)
- Coq renamed to Rocq (#2883,
#2908)
- Cython: Various improvements (#2932,
#2933)
- Debian control: Improve architecture parsing (#3052)
- Devicetree: Add support for overlay/fragments (#3021),
add bytestring support (#3022),
fix catastrophic backtracking (#3057)
- Fennel: Various improvements (#2911)
- Haskell: Handle escape sequences in character literals (#3069,
#1795)
- Java: Add module keywords (#2955)
- Lean4: Add operators
]', ]?,
]! (#2946)
- LESS: Support single-line comments (#3005)
- LilyPond: Update to 2.25.29 (#2974)
- LLVM: Support C-style comments (#3023,
#2978)
- Lua(u): Fix catastrophic backtracking (#3047)
- Macaulay2: Update to 1.25.05 (#2893),
1.25.11 (#2988)
- Mathematica: Various improvements (#2957)
- meson: Add additional operators (#2919)
- MySQL: Update keywords (#2970)
- org-Mode: Support both schedule and deadline (#2899)
- PHP: Add
__PROPERTY__ magic constant (#2924),
add reserved keywords (#3002)
- PostgreSQL: Add more keywords (#2985)
- protobuf: Fix namespace tokenization (#2929)
- Python: Add
t-string support (#2973,
#3009,
#3010)
- Tablegen: Fix infinite loop (#2972,
#2940)
- Tera Term macro: Add commands introduced in v5.3 through v5.6 (#2951)
- TOML: Support TOML 1.1.0 (#3026,
#3027)
- Turtle: Allow empty comment lines (#2980)
- XML: Added
.xbrl as file ending (#2890,
#2891)
-
Drop Python 3.8, and add Python 3.14 as a supported version (#2987,
#3012)
-
Various improvements to autopygmentize (#2894)
-
Update onedark style to support more token types (#2977)
-
Update rtt style to support more token types (#2895)
-
Cache entry points to improve performance (#2979)
-
Fix xterm-256 color table (#3043)
-
Fix kwargs dictionary getting mutated on each call (#3044)
Commits
708197d
Fix underline length.
1d4538a
Prepare 2.20 release.
2ceaee4
Update CHANGES.
e3a3c54
Fix Haskell lexer: handle escape sequences in character literals (#3069)
d7c3453
Merge pull request #3071
from pygments/harden-html-formatter
0f97e7c
Harden the HTML formatter against CSS.
9f981b2
Update CHANGES.
1d88915
Update CHANGES.
c3d93ad
Fix ASN.1 lexer: recognize minus sign and fix range operator (#3060)
4f06bcf
fix bad behaving backtracking regex in CommonLispLexer
- Additional commits viewable in compare
view
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/test-common-base.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt
index fedf2066a58..fc94dc894e0 100644
--- a/requirements/test-common-base.txt
+++ b/requirements/test-common-base.txt
@@ -24,7 +24,7 @@ pluggy==1.6.0
# pytest-cov
proxy-py==2.4.10
# via -r requirements/test-common-base.in
-pygments==2.19.2
+pygments==2.20.0
# via pytest
pytest==9.0.3
# via
From 635266b287b59b38b0e9009e0780a9667ebea599 Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Tue, 2 Jun 2026 15:11:23 +0100
Subject: [PATCH 185/191] Fix CHANGES.rst (#12775)
---
CHANGES.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/CHANGES.rst b/CHANGES.rst
index f876266d68c..97689f035a0 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -32,7 +32,7 @@ Features
- Added :func:`~aiohttp.encode_basic_auth` for encoding HTTP Basic
Authentication credentials. Replaces the now-deprecated
- :class:`~aiohttp.BasicAuth` -- by :user:`Dreamsorcerer`.
+ ``aiohttp.BasicAuth`` -- by :user:`Dreamsorcerer`.
*Related issues and pull requests on GitHub:*
@@ -300,7 +300,7 @@ Bug fixes
Deprecations (removal in next major release)
--------------------------------------------
-- Deprecated :class:`~aiohttp.BasicAuth` and the ``auth`` / ``proxy_auth``
+- Deprecated ``aiohttp.BasicAuth`` and the ``auth`` / ``proxy_auth``
parameters. They will be removed in aiohttp 4.0. Use the new
:func:`~aiohttp.encode_basic_auth` helper together with
``headers={"Authorization": ...}`` (or
From c66e5cb4702b7e3a7373efe2848e35a61fe95e6e Mon Sep 17 00:00:00 2001
From: Sam Bull
Date: Tue, 2 Jun 2026 15:44:13 +0100
Subject: [PATCH 186/191] Point Dependabot to 3.15 branch (#12774)
---
.github/dependabot.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 6ac64362454..fddd531da59 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -25,7 +25,7 @@ updates:
directory: "/"
labels:
- dependencies
- target-branch: "3.14"
+ target-branch: "3.15"
schedule:
interval: "daily"
open-pull-requests-limit: 10
@@ -37,7 +37,7 @@ updates:
- dependency-type: "all"
labels:
- dependencies
- target-branch: "3.14"
+ target-branch: "3.15"
schedule:
interval: "daily"
open-pull-requests-limit: 10
@@ -53,6 +53,6 @@ updates:
directory: "/tests/autobahn/"
labels:
- dependencies
- target-branch: "3.14"
+ target-branch: "3.15"
schedule:
interval: "monthly"
From a3c548728f6b62c4e699a590cc3dc8a705a4e203 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 15:49:45 +0000
Subject: [PATCH 187/191] Bump idna from 3.17 to 3.18 (#12778)
Bumps [idna](https://github.com/kjd/idna) from 3.17 to 3.18.
Changelog
Sourced from idna's
changelog.
3.18 (2026-06-02)
- When decoding a domain, add a
display argument that
will pass
through invalid labels rather than raising an exception.
Commits
f39ea90
Release 3.18
40f4e40
Pre-release 3.18rc0
1a5bf80
Merge pull request #253 from
kjd/lenient-decode
5bbb26f
Merge branch 'master' into lenient-decode
c532bae
Rename decode() lenient= option to display= (issue #248)
0b1758b
Merge pull request #252 from
kjd/release-3.17
47b5cde
Add lenient option to decode() for best-effort label recovery (issue #248)
- See full diff in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/base-ft.txt | 2 +-
requirements/base.txt | 2 +-
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/doc-spelling.txt | 2 +-
requirements/doc.txt | 2 +-
requirements/lint.txt | 2 +-
requirements/runtime-deps.txt | 2 +-
requirements/test-common-base.txt | 2 +-
requirements/test-common.txt | 2 +-
requirements/test-ft.txt | 2 +-
requirements/test-mobile.txt | 2 +-
requirements/test.txt | 2 +-
13 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt
index 1eeed867279..eddfe0ba674 100644
--- a/requirements/base-ft.txt
+++ b/requirements/base-ft.txt
@@ -24,7 +24,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base-ft.in
-idna==3.17
+idna==3.18
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/base.txt b/requirements/base.txt
index 159c65a0ce2..863268a60c8 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -24,7 +24,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base.in
-idna==3.17
+idna==3.18
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 01204e1ff22..2d0f6d47207 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -100,7 +100,7 @@ gunicorn==26.0.0
# via -r requirements/base.in
identify==2.6.19
# via pre-commit
-idna==3.17
+idna==3.18
# via
# requests
# trustme
diff --git a/requirements/dev.txt b/requirements/dev.txt
index bb5cab60d3f..afa68f03c45 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -98,7 +98,7 @@ gunicorn==26.0.0
# via -r requirements/base.in
identify==2.6.19
# via pre-commit
-idna==3.17
+idna==3.18
# via
# requests
# trustme
diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt
index 2878edd94f6..e5c3306697f 100644
--- a/requirements/doc-spelling.txt
+++ b/requirements/doc-spelling.txt
@@ -20,7 +20,7 @@ docutils==0.21.2
# via
# myst-parser
# sphinx
-idna==3.17
+idna==3.18
# via requests
imagesize==2.0.0
# via sphinx
diff --git a/requirements/doc.txt b/requirements/doc.txt
index 55d30f206cb..1a68ba65c49 100644
--- a/requirements/doc.txt
+++ b/requirements/doc.txt
@@ -20,7 +20,7 @@ docutils==0.21.2
# via
# myst-parser
# sphinx
-idna==3.17
+idna==3.18
# via requests
imagesize==2.0.0
# via sphinx
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 198f532bfa7..7d88216875f 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -54,7 +54,7 @@ frozenlist==1.8.0
# aiosignal
identify==2.6.19
# via pre-commit
-idna==3.17
+idna==3.18
# via
# trustme
# yarl
diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt
index f7080a0e341..859820baf29 100644
--- a/requirements/runtime-deps.txt
+++ b/requirements/runtime-deps.txt
@@ -22,7 +22,7 @@ frozenlist==1.8.0
# via
# -r requirements/runtime-deps.in
# aiosignal
-idna==3.17
+idna==3.18
# via yarl
multidict==6.7.1
# via
diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt
index ae3ed0da85e..c10df074bf5 100644
--- a/requirements/test-common-base.txt
+++ b/requirements/test-common-base.txt
@@ -26,7 +26,7 @@ frozenlist==1.8.0
# via
# aiohttp
# aiosignal
-idna==3.17
+idna==3.18
# via yarl
iniconfig==2.3.0
# via pytest
diff --git a/requirements/test-common.txt b/requirements/test-common.txt
index 205bf161ec0..cd305172d11 100644
--- a/requirements/test-common.txt
+++ b/requirements/test-common.txt
@@ -42,7 +42,7 @@ frozenlist==1.8.0
# via
# aiohttp
# aiosignal
-idna==3.17
+idna==3.18
# via
# trustme
# yarl
diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt
index 97aff3e0cd5..77c73738dee 100644
--- a/requirements/test-ft.txt
+++ b/requirements/test-ft.txt
@@ -59,7 +59,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base-ft.in
-idna==3.17
+idna==3.18
# via
# trustme
# yarl
diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt
index 2cfbd3ec357..459d80d34ac 100644
--- a/requirements/test-mobile.txt
+++ b/requirements/test-mobile.txt
@@ -47,7 +47,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base-ft.in
-idna==3.17
+idna==3.18
# via yarl
iniconfig==2.3.0
# via pytest
diff --git a/requirements/test.txt b/requirements/test.txt
index 3f524555daa..66deab1b0e6 100644
--- a/requirements/test.txt
+++ b/requirements/test.txt
@@ -59,7 +59,7 @@ frozenlist==1.8.0
# aiosignal
gunicorn==26.0.0
# via -r requirements/base.in
-idna==3.17
+idna==3.18
# via
# trustme
# yarl
From ec8591eee72dab4374c7a6a892d084495f3012b0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 16:10:20 +0000
Subject: [PATCH 188/191] Bump pytest-cov from 7.0.0 to 7.1.0 (#12781)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 7.0.0
to 7.1.0.
Changelog
Sourced from pytest-cov's
changelog.
7.1.0 (2026-03-21)
-
Fixed total coverage computation to always be consistent, regardless
of reporting settings.
Previously some reports could produce different total counts, and
consequently can make --cov-fail-under behave different depending on
reporting options.
See [#641](https://github.com/pytest-dev/pytest-cov/issues/641)
<https://github.com/pytest-dev/pytest-cov/issues/641>_.
-
Improve handling of ResourceWarning from sqlite3.
The plugin adds warning filter for sqlite3
ResourceWarning unclosed database (since 6.2.0).
It checks if there is already existing plugin for this message by
comparing filter regular expression.
When filter is specified on command line the message is escaped and does
not match an expected message.
A check for an escaped regular expression is added to handle this
case.
With this fix one can suppress ResourceWarning from
sqlite3 from command line::
pytest -W "ignore:unclosed database in <sqlite3.Connection
object at:ResourceWarning" ...
-
Various improvements to documentation.
Contributed by Art Pelling in
[#718](https://github.com/pytest-dev/pytest-cov/issues/718)
<https://github.com/pytest-dev/pytest-cov/pull/718>_ and
"vivodi" in
[#738](https://github.com/pytest-dev/pytest-cov/issues/738)
<https://github.com/pytest-dev/pytest-cov/pull/738>.
Also closed
[#736](https://github.com/pytest-dev/pytest-cov/issues/736)
<https://github.com/pytest-dev/pytest-cov/issues/736>.
-
Fixed some assertions in tests.
Contributed by in Markéta Machová in
[#722](https://github.com/pytest-dev/pytest-cov/issues/722)
<https://github.com/pytest-dev/pytest-cov/pull/722>_.
-
Removed unnecessary coverage configuration copying (meant as a backup
because reporting commands had configuration side-effects before
coverage 5.0).
Commits
66c8a52
Bump version: 7.0.0 → 7.1.0
f707662
Make the examples use pypy 3.11.
6049a78
Make context test use the old ctracer (seems the new sysmon tracer
behaves di...
8ebf20b
Update changelog.
861d30e
Remove the backup context manager - shouldn't be needed since coverage
5.0, ...
fd4c956
Pass the precision on the nulled total (seems that there's some caching
goion...
78c9c4e
Only run the 3.9 on older deps.
4849a92
Punctuation.
197c35e
Update changelog and hopefully I don't forget to publish release again
:))
14dc1c9
Update examples to use 3.11 and make the adhoc layout example look a bit
more...
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/test-common-base.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt
index c10df074bf5..86acaec4339 100644
--- a/requirements/test-common-base.txt
+++ b/requirements/test-common-base.txt
@@ -62,7 +62,7 @@ pytest-aiohttp==1.1.0
# via -r requirements/test-common-base.in
pytest-asyncio==1.3.0
# via pytest-aiohttp
-pytest-cov==7.0.0
+pytest-cov==7.1.0
# via -r requirements/test-common-base.in
pytest-mock==3.15.1
# via -r requirements/test-common-base.in
From f8efc27f2fa2df66e50e55ef329638a9b5ffb794 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 16:11:50 +0000
Subject: [PATCH 189/191] Bump pytest-asyncio from 1.3.0 to 1.4.0 (#12783)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
[//]: # (dependabot-start)
⚠️ **Dependabot is rebasing this PR** ⚠️
Rebasing might not happen immediately, so don't worry if this takes some
time.
Note: if you make any changes to this PR yourself, they will take
precedence over the rebase.
---
[//]: # (dependabot-end)
Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio)
from 1.3.0 to 1.4.0.
Release notes
Sourced from pytest-asyncio's
releases.
pytest-asyncio v1.4.0
1.4.0
- 2026-05-26
Deprecated
- Overriding the event_loop_policy fixture is deprecated. Use
the
pytest_asyncio_loop_factories hook instead. (#1419)
Added
-
Added the pytest_asyncio_loop_factories hook to
parametrize asyncio tests with custom event loop factories.
The hook returns a mapping of factory names to loop factories, and
pytest.mark.asyncio(loop_factories=[...]) selects a subset
of configured factories per test. When a single factory is configured,
test names are unchanged.
Synchronous @pytest_asyncio.fixture functions now see
the correct event loop when custom loop factories are configured, even
when test code disrupts the current event loop (e.g., via
asyncio.run() or
asyncio.set_event_loop(None)). (#1164)
Changed
- Improved the readability of the warning message that is displayed
when
asyncio_default_fixture_loop_scope is unset (#1298)
- Only import
asyncio.AbstractEventLoopPolicy for type
checking to avoid raising
a DeprecationWarning. (#1394)
- Updated minimum supported pytest version to v8.4.0. (#1397)
Fixed
- Fixed a
ResourceWarning: unclosed event loop warning
that could occur when a synchronous test called
asyncio.run() or otherwise unset the current event loop
after pytest-asyncio had run an async test or fixture. (#724)
Notes for Downstream Packagers
- Added dependency on
sphinx-tabs >= 3.5 to organize
documentation examples into tabs. (#1395)
pytest-asyncio v1.4.0a2
Deprecated
- Overriding the event_loop_policy fixture is deprecated. Use
the
pytest_asyncio_loop_factories hook instead. (#1419)
Added
-
Added the pytest_asyncio_loop_factories hook to
parametrize asyncio tests with custom event loop factories.
The hook returns a mapping of factory names to loop factories, and
pytest.mark.asyncio(loop_factories=[...]) selects a subset
of configured factories per test. When a single factory is configured,
test names are unchanged on pytest 8.4+.
Synchronous @pytest_asyncio.fixture functions now see
the correct event loop when custom loop factories are configured, even
when test code disrupts the current event loop (e.g., via
asyncio.run() or
asyncio.set_event_loop(None)). (#1164)
Changed
- Improved the readability of the warning message that is displayed
when
asyncio_default_fixture_loop_scope is unset (#1298)
- Only import
asyncio.AbstractEventLoopPolicy for type
checking to avoid raising
a DeprecationWarning. (#1394)
... (truncated)
Commits
6e14cd2
chore: Prepare release of v1.4.0.
4b900fb
Build(deps): Bump codecov/codecov-action from 6.0.0 to 6.0.1
ab9f632
Build(deps): Bump zipp from 3.23.1 to 4.1.0
a56fc77
Build(deps): Bump hypothesis from 6.152.6 to 6.152.8
e8bae9b
Build(deps): Bump requests from 2.34.0 to 2.34.2
fc43340
Build(deps): Bump idna from 3.14 to 3.15
762eaf5
Build(deps): Bump jaraco-functools from 4.4.0 to 4.5.0
b62e222
Build(deps): Bump click from 8.3.3 to 8.4.0
9190447
Build(deps): Bump pydantic from 2.13.3 to 2.13.4
82a393c
ci: Remove unnecessary debug output.
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/test-common-base.txt | 2 +-
requirements/test-mobile.txt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt
index 86acaec4339..a9645132192 100644
--- a/requirements/test-common-base.txt
+++ b/requirements/test-common-base.txt
@@ -60,7 +60,7 @@ pytest==9.0.3
# pytest-timeout
pytest-aiohttp==1.1.0
# via -r requirements/test-common-base.in
-pytest-asyncio==1.3.0
+pytest-asyncio==1.4.0
# via pytest-aiohttp
pytest-cov==7.1.0
# via -r requirements/test-common-base.in
diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt
index 459d80d34ac..7e874b4c008 100644
--- a/requirements/test-mobile.txt
+++ b/requirements/test-mobile.txt
@@ -91,7 +91,7 @@ pytest==9.0.3
# pytest-timeout
pytest-aiohttp==1.1.0
# via -r requirements/test-common-base.in
-pytest-asyncio==1.4.0a2
+pytest-asyncio==1.4.0
# via pytest-aiohttp
pytest-cov==7.1.0
# via -r requirements/test-common-base.in
From 3fae5596c620d4a2b29847d65691387097bdeacd Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 16:13:42 +0000
Subject: [PATCH 190/191] Bump distlib from 0.4.0 to 0.4.1 (#12785)
Bumps [distlib](https://github.com/pypa/distlib) from 0.4.0 to 0.4.1.
Changelog
Sourced from distlib's
changelog.
0.4.1
Released: 2026-06-02
-
scripts
- Fix path traversal bug in handling entry points which allowed
escaping the scripts directory.
Thanks to tonghuaroot for the comprehensive report.
-
tests
- Fix #251:
Change test function following a reorganization which happened in the
Python stdlib.
Commits
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/constraints.txt | 2 +-
requirements/dev.txt | 2 +-
requirements/lint.txt | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/requirements/constraints.txt b/requirements/constraints.txt
index 2d0f6d47207..0e1600af659 100644
--- a/requirements/constraints.txt
+++ b/requirements/constraints.txt
@@ -71,7 +71,7 @@ cryptography==48.0.0
# via trustme
cython==3.2.5
# via -r requirements/cython.in
-distlib==0.4.0
+distlib==0.4.1
# via virtualenv
docutils==0.21.2
# via
diff --git a/requirements/dev.txt b/requirements/dev.txt
index afa68f03c45..8a34e18868e 100644
--- a/requirements/dev.txt
+++ b/requirements/dev.txt
@@ -69,7 +69,7 @@ coverage==7.14.1
# pytest-cov
cryptography==48.0.0
# via trustme
-distlib==0.4.0
+distlib==0.4.1
# via virtualenv
docutils==0.21.2
# via
diff --git a/requirements/lint.txt b/requirements/lint.txt
index 7d88216875f..b76fb26d65f 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -36,7 +36,7 @@ click==8.4.1
# via slotscheck
cryptography==48.0.0
# via trustme
-distlib==0.4.0
+distlib==0.4.1
# via virtualenv
exceptiongroup==1.3.1
# via pytest
From f387f620459cf5b8e0e1df2f18dc70e9c3d29909 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 2 Jun 2026 16:14:38 +0000
Subject: [PATCH 191/191] Bump click from 8.3.1 to 8.4.1 (#12784)
Bumps [click](https://github.com/pallets/click) from 8.3.1 to 8.4.1.
Release notes
Sourced from click's
releases.
8.4.1
This is the Click 8.4.1 fix release, which fixes bugs but does not
otherwise change behavior and should not result in breaking changes
compared to the latest feature release.
PyPI: https://pypi.org/project/click/8.4.1/
Changes: https://click.palletsprojects.com/page/changes/#version-8-4-1
Milestone: https://github.com/pallets/click/milestone/32?closed=1
get_parameter_source() is available during eager
callbacks and type conversion again. #3458
#3484
- Zsh completion scripts parse correctly on Windows. #3277 #
3466
- Shell completion of
Choice Enum values
produces a valid completion result. #3015
- Fix empty byte-string handling in echo. #3487
- Fix closed file error with
echo_via_pager. #3449
8.4.0
This is the Click 8.4.0 feature release. A feature release may
include new features, remove previously deprecated code, add new
deprecation, or introduce potentially breaking changes.
We encourage everyone to upgrade. You can read more about our Version Support Policy
on our website.
PyPI: https://pypi.org/project/click/8.4.0/
Changes: https://click.palletsprojects.com/page/changes/#version-8-4-0
Milestone https://github.com/pallets/click/milestone/30
-
ParamType typing improvements. #3371
- :class:
ParamType is now a generic abstract base class,
parameterized by its converted value type.
- :meth:
~ParamType.convert return types are narrowed on
all
concrete types (str for :class:STRING,
int for
:class:INT, etc.).
- :meth:
~ParamType.to_info_dict returns specific
:class:~typing.TypedDict subclasses instead of
dict[str, Any].
- :class:
CompositeParamType and the number-range base are
now
generic with abstract methods.
-
Refactor convert_type to extract type inference into a
private
_guess_type helper, and add
:func:typing.overload signatures.
#3372
-
Parameter typing improvements. #2805
- :class:
Parameter is now an abstract base class, making
explicit
that it cannot be instantiated directly.
- :attr:
Parameter.name is now str instead of
str | None.
When expose_value=False, the name is set to
"" instead
of None.
- The
ctx parameter of
:meth:Parameter.get_error_hint is now
typed as Context | None, matching the runtime
behavior.
-
Split string values from default_map for parameters with
nargs > 1
or :class:Tuple type, matching environment variable
behavior.
... (truncated)
Changelog
Sourced from click's
changelog.
Version 8.4.1
Released 2026-05-21
get_parameter_source() is available during eager
callbacks and type
conversion again. :issue:3458 :issue:3484
- Zsh completion scripts parse correctly on Windows.
:issue:
3277 :pr:3466
- Shell completion of
Choice Enum values
produces a valid completion
result. :issue:3015
- Fix empty byte-string handling in echo.
:issue:
3487
- Fix closed file error with
echo_via_pager.
:issue:3449
Version 8.4.0
Released 2026-05-17
-
:class:ParamType typing improvements.
:pr:3371
- :class:
ParamType is now a generic abstract base class,
parameterized by its converted value type.
- :meth:
~ParamType.convert return types are narrowed on
all
concrete types (str for :class:STRING,
int for
:class:INT, etc.).
- :meth:
~ParamType.to_info_dict returns specific
:class:~typing.TypedDict subclasses instead of
dict[str, Any].
- :class:
CompositeParamType and the number-range base are
now
generic with abstract methods.
-
Refactor convert_type to extract type inference into a
private
_guess_type helper, and add
:func:typing.overload signatures.
:pr:3372
-
:class:Parameter typing improvements.
:pr:2805
- :class:
Parameter is now an abstract base class, making
explicit
that it cannot be instantiated directly.
- :attr:
Parameter.name is now str instead of
str | None.
When expose_value=False, the name is set to
"" instead
of None.
- The
ctx parameter of
:meth:Parameter.get_error_hint is now
typed as Context | None, matching the runtime
behavior.
-
Split string values from default_map for parameters with
nargs > 1
or :class:Tuple type, matching environment variable
behavior.
:issue:2745 :pr:3364
-
Auto-detect type=UNPROCESSED for flag_value
of non-basic types
(not str, int, float, or
bool), so programmer-provided
Python objects like classes and enum members are passed through
unchanged
instead of being stringified. Previously
type=click.UNPROCESSED had
to be set explicitly. :issue:2012 :pr:3363
... (truncated)
Commits
6eeb50e
release version 8.4.1
67921d5
change log and doc fixes (#3495)
9c41f46
Fix changelog and version admonitions
6cb3477
fix skip condition
5ee8e31
fix I/O operation on closed file error with CliRunner and echo_via_pager
(#3482)
becbde5
pager doesn't close std streams
a5f5aa6
Handle empty bytes in echo (#3493)
4d3db84
handle empty bytes in echo
d42f15b
Fix get_parameter_source() during type conversion and eager
callbacks (#3484)
0baa8db
Document ctx.params bypass with test and doc
- Additional commits viewable in compare
view
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
requirements/test-common-base.txt | 2 +-
requirements/test-mobile.txt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt
index a9645132192..8624b473f95 100644
--- a/requirements/test-common-base.txt
+++ b/requirements/test-common-base.txt
@@ -14,7 +14,7 @@ attrs==26.1.0
# via aiohttp
backports-asyncio-runner==1.2.0
# via pytest-asyncio
-click==8.3.1
+click==8.4.1
# via wait-for-it
coverage==7.14.1
# via pytest-cov
diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt
index 7e874b4c008..f5c66fce657 100644
--- a/requirements/test-mobile.txt
+++ b/requirements/test-mobile.txt
@@ -32,7 +32,7 @@ cffi==2.0.0 ; sys_platform != "android" and sys_platform != "ios"
# via
# -r requirements/test-mobile.in
# pycares
-click==8.4.0
+click==8.4.1
# via wait-for-it
coverage==7.14.1
# via pytest-cov