From 25740ee0334d7e7ca9f56d01f82464f0e3022dc9 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 7 Jun 2026 23:25:19 +0100 Subject: [PATCH 001/137] Bump version --- aiohttp/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index 1b35d23de57..5dfcd3841b9 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.14.1" +__version__ = "3.14.1.dev0" from typing import TYPE_CHECKING From e53c64addf50599d736724237855cf90c59e8e3f Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 02:01:20 +0100 Subject: [PATCH 002/137] [PR #12814/68923cfb backport][3.15] Optimize CI output for iOS/Android (#12866) **This is a backport of PR #12814 as merged into master (68923cfb9e5f7a9e87e156bb497ceec1703d3bc4).** Co-authored-by: timrid <6593626+timrid@users.noreply.github.com> --- .github/workflows/ci-cd.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index a6ae26a7134..05d37a3bad8 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -298,15 +298,29 @@ jobs: - name: Cythonize run: | make cythonize + - name: Enable KVM group perms for Android emulator + if: ${{ matrix.config.platform == 'android' }} + # This is normally done by cibuildwheel automatically, when it detects Github Actions. But by unsetting GITHUB_ACTIONS + # in the test step, we also disable that automatic setup. So we need to do it manually here. + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Install cibuildwheel + run: uv pip install cibuildwheel==3.4.1 - name: Build wheels and test - uses: pypa/cibuildwheel@v3.4.1 + # cibuildwheel normally uses grouping in its outputs for its build/test steps. But the loading time when + # expanding large groups in GitHub Actions is very high. So by unsetting GITHUB_ACTIONS, cibuildwheel does + # not know that it is running in a GitHub Action and thus does not use groups. + run: env -u GITHUB_ACTIONS cibuildwheel 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 + # Currently only Android supports colored output. See https://github.com/python/cpython/issues/150932 for iOS. + CIBW_TEST_COMMAND: python -m pytest ${{ matrix.config.platform == 'android' && '--color=yes' || '' }} autobahn: permissions: From 5e302dfcbd884c93f27a2108587965819767fb39 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 02:01:34 +0100 Subject: [PATCH 003/137] [PR #12814/68923cfb backport][3.14] Optimize CI output for iOS/Android (#12865) **This is a backport of PR #12814 as merged into master (68923cfb9e5f7a9e87e156bb497ceec1703d3bc4).** Co-authored-by: timrid <6593626+timrid@users.noreply.github.com> --- .github/workflows/ci-cd.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 9bb01cb551c..8e435a56bbe 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -298,15 +298,29 @@ jobs: - name: Cythonize run: | make cythonize + - name: Enable KVM group perms for Android emulator + if: ${{ matrix.config.platform == 'android' }} + # This is normally done by cibuildwheel automatically, when it detects Github Actions. But by unsetting GITHUB_ACTIONS + # in the test step, we also disable that automatic setup. So we need to do it manually here. + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Install cibuildwheel + run: uv pip install cibuildwheel==3.4.1 - name: Build wheels and test - uses: pypa/cibuildwheel@v3.4.1 + # cibuildwheel normally uses grouping in its outputs for its build/test steps. But the loading time when + # expanding large groups in GitHub Actions is very high. So by unsetting GITHUB_ACTIONS, cibuildwheel does + # not know that it is running in a GitHub Action and thus does not use groups. + run: env -u GITHUB_ACTIONS cibuildwheel 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 + # Currently only Android supports colored output. See https://github.com/python/cpython/issues/150932 for iOS. + CIBW_TEST_COMMAND: python -m pytest ${{ matrix.config.platform == 'android' && '--color=yes' || '' }} autobahn: permissions: From f497531aa10d169a9953708119f37e7b59ba98ed Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 02:44:41 +0100 Subject: [PATCH 004/137] [PR #12797/86c33baa backport][3.15] docs: remove stale response_factory param from add_static docs (#12868) **This is a backport of PR #12797 as merged into master (86c33baaa748bcb240a085e2d2e89cf2055a160a).** Co-authored-by: Wahaj Ahmed --- docs/web_reference.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 6fcb1a8f42f..5932d016536 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -2006,7 +2006,6 @@ Application and Router .. method:: add_static(prefix, path, *, name=None, expect_handler=None, \ chunk_size=256*1024, \ - response_factory=StreamResponse, \ show_index=False, \ follow_symlinks=False, \ append_version=False) From 2e2b936bd9f12c25f42d399f7bb4dd6cff54a777 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 02:47:30 +0100 Subject: [PATCH 005/137] [PR #12797/86c33baa backport][3.14] docs: remove stale response_factory param from add_static docs (#12867) **This is a backport of PR #12797 as merged into master (86c33baaa748bcb240a085e2d2e89cf2055a160a).** Co-authored-by: Wahaj Ahmed --- docs/web_reference.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 6fcb1a8f42f..5932d016536 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -2006,7 +2006,6 @@ Application and Router .. method:: add_static(prefix, path, *, name=None, expect_handler=None, \ chunk_size=256*1024, \ - response_factory=StreamResponse, \ show_index=False, \ follow_symlinks=False, \ append_version=False) From baa7f8adf7efa190f22e54d661d0aca1812391cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:14:52 +0000 Subject: [PATCH 006/137] Bump pypa/cibuildwheel from 3.4.1 to 4.0.0 (#12875) 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.1 to 4.0.0.
Release notes

Sourced from pypa/cibuildwheel's releases.

v4.0.0

See @​henryiii's release post for more info on new features!

  • 🌟 Adds wheel auditing with abi3audit as a default after the repair step, with new audit-requires and audit-command options (#2805)

  • 🌟 Adds pyemscripten platform tag support (PEP 783), updates Pyodide to 314.0.0a2, and adds a pyodide-eol enable flag for building end-of-life Pyodide versions (#2812, #2848)

  • 🌟 Sets up delvewheel as the default repair-wheel-command for Windows, so extension module DLLs are now bundled automatically. Skip by setting it to empty if not needed. (#2831)

  • ✨ Adds CPython 3.15 support, under the enable option cpython-prerelease. This version of cibuildwheel uses 3.15.0b2. (#2833, #2850)

    While CPython is in beta, the ABI can change, so your wheels might not be compatible with the final release. For this reason, we don't recommend distributing wheels until RC1, at which point 3.15 will be available in cibuildwheel without the flag.

  • ✨ Adds CPython 3.15 support for iOS and Android (#2857, #2858)

  • ✨ Adds Android improvements for building NumPy and related packages, including auditwheel support, pkg-config and Fortran configuration, and the xbuild-files option (#2695)

  • ✨ Adds CIBUILDWHEEL_BUILD_IDENTIFIER environment variable set to the current build identifier (e.g. cp311-manylinux_x86_64) during per-build steps (#2872)

  • ✨ Adds {project} and {package} placeholders to config-settings (#2827)

  • ⚠️ Drops support for Python 3.8 (#2686)

  • ⚠️ Removes the experimental CPython 3.13 free-threading builds and the cpython-freethreading enable option. CPython 3.14+ free-threading support remains available without the enable flag. (#2684)

  • ⚠️ Drops support for Cirrus CI, which is shutting down June 1, 2026 (#2817)

  • ⚠️ Drops GraalPy 3.11 (gp311) support, as agreed in #2741, and removes GraalPy 24-only workarounds (#2895)

  • πŸ” Adds SHA256 verification for direct downloads of Python interpreters, virtualenv, and python-build-standalone assets (#2873)

  • πŸ” Adds tarfile extraction filter for safe archive extraction (#2856)

  • πŸ› Fixes UV_PYTHON not being set for before-build on Linux when using uv as the build-frontend (#2830)

  • πŸ› Fixes detection of musl libc when downloading python-build-standalone, which previously always selected the gnu asset on musl hosts like Alpine (#2889)

  • πŸ› Fixes config-settings expansion when {project} or {package} contains spaces or backslashes (#2886)

  • πŸ› Prevents deadlock when linux32 fails and forwards platform args to the sanity check (#2880, #2888)

  • πŸ› Fixes container resource leaks on start failure and during teardown (#2879, #2887)

  • πŸ› Removes potential partial cache-population in case of error (#2892)

  • πŸ› Raises a clear error when ANDROID_API_LEVEL is not an integer (#2891)

  • πŸ› Replaces assert with proper exception in python-build-standalone (#2859)

  • πŸ› Uses ConfigurationError when package_dir is outside cwd instead of a generic Exception (#2898)

  • πŸ›  Updates dependencies and container pins (#2893, #2882, #2874, #2868, #2862, #2884, #2845, #2837, #2818, #2810, #2838, #2813)

  • πŸ›  Updates Android to Python 3.13.13 and 3.14.4 (#2821)

  • πŸ›  Applies Pyodide-specific patches to the Emscripten toolchain installation (#2800)

  • πŸ›  Uses python -V -V for Windows build diagnostics (#2832)

  • πŸ›  Simplifies pinned container image lookup (#2897)

  • πŸ›  Minor fixups across error messages, OCI container, and options (#2860)

  • πŸ’Ό Adds PEP 723 metadata for bin/ scripts and drops the bin dependency group (#2819)

  • πŸ’Ό Improves Azure test reliability with retries and caching (#2890)

  • πŸ’Ό Fixes Windows GitLab CI test running (#2870)

  • πŸ’Ό Updates CI action pins and dev dependencies (#2902, #2867, #2851, #2843, #2826, #2823, #2820, #2807)

  • πŸ’Ό Adds agent and copilot setup files (#2861)

  • πŸ’Ό Uses if TYPE_CHECKING: blocks (#2866, #2864)

  • πŸ§ͺ Fixes Android tests using the uv frontend (#2809)

  • πŸ§ͺ Fixes the update-dependencies workflow to use uv to run nox (#2808)

  • πŸ§ͺ Adds unit tests for OCIContainer._get_platform_args (#2878)

  • πŸ“š Updates documentation for delvewheel as the default Windows repair-wheel-command, including the build diagram, schema defaults, and legal note (#2877, #2853, #2891)

  • πŸ“š Documents platform-specific before-build configuration (#2834)

  • πŸ“š Updates the "How it works" diagram with details of Android, iOS, and Pyodide builds (#2816)

  • πŸ“š Adds Pyodide icon and regenerates working examples data for Android, iOS, and Pyodide (#2815, #2811)

  • πŸ“š Adds intersphinx support for external documentation linking (#2871)

  • πŸ“š Adds instructions for building CUDA wheels and fixes manylinux container references in FAQ (#2896, #2900)

... (truncated)

Changelog

Sourced from pypa/cibuildwheel's changelog.

v4.0.0

7 June 2026

See @​henryiii's release post for more info on new features!

  • 🌟 Adds wheel auditing with abi3audit as a default after the repair step, with new audit-requires and audit-command options (#2805)

  • 🌟 Adds pyemscripten platform tag support (PEP 783), updates Pyodide to 314.0.0a2, and adds a pyodide-eol enable flag for building end-of-life Pyodide versions (#2812, #2848)

  • 🌟 Sets up delvewheel as the default repair-wheel-command for Windows, so extension module DLLs are now bundled automatically. Skip by setting it to empty if not needed. (#2831)

  • ✨ Adds CPython 3.15 support, under the enable option cpython-prerelease. This version of cibuildwheel uses 3.15.0b2. (#2833, #2850)

    While CPython is in beta, the ABI can change, so your wheels might not be compatible with the final release. For this reason, we don't recommend distributing wheels until RC1, at which point 3.15 will be available in cibuildwheel without the flag.

  • ✨ Adds CPython 3.15 support for iOS and Android (#2857, #2858)

  • ✨ Adds Android improvements for building NumPy and related packages, including auditwheel support, pkg-config and Fortran configuration, and the xbuild-files option (#2695)

  • ✨ Adds CIBUILDWHEEL_BUILD_IDENTIFIER environment variable set to the current build identifier (e.g. cp311-manylinux_x86_64) during per-build steps (#2872)

  • ✨ Adds {project} and {package} placeholders to config-settings (#2827)

  • ⚠️ Drops support for Python 3.8 (#2686)

  • ⚠️ Removes the experimental CPython 3.13 free-threading builds and the cpython-freethreading enable option. CPython 3.14+ free-threading support remains available without the enable flag. (#2684)

  • ⚠️ Drops support for Cirrus CI, which is shutting down June 1, 2026 (#2817)

  • ⚠️ Drops GraalPy 3.11 (gp311) support, as agreed in #2741, and removes GraalPy 24-only workarounds (#2895)

  • πŸ” Adds SHA256 verification for direct downloads of Python interpreters, virtualenv, and python-build-standalone assets (#2873)

  • πŸ” Adds tarfile extraction filter for safe archive extraction (#2856)

  • πŸ› Fixes UV_PYTHON not being set for before-build on Linux when using uv as the build-frontend (#2830)

  • πŸ› Fixes detection of musl libc when downloading python-build-standalone, which previously always selected the gnu asset on musl hosts like Alpine (#2889)

  • πŸ› Fixes config-settings expansion when {project} or {package} contains spaces or backslashes (#2886)

  • πŸ› Prevents deadlock when linux32 fails and forwards platform args to the sanity check (#2880, #2888)

  • πŸ› Fixes container resource leaks on start failure and during teardown (#2879, #2887)

  • πŸ› Removes potential partial cache-population in case of error (#2892)

  • πŸ› Raises a clear error when ANDROID_API_LEVEL is not an integer (#2891)

  • πŸ› Replaces assert with proper exception in python-build-standalone (#2859)

  • πŸ› Uses ConfigurationError when package_dir is outside cwd instead of a generic Exception (#2898)

  • πŸ›  Updates dependencies and container pins (#2893, #2882, #2874, #2868, #2862, #2884, #2845, #2837, #2818, #2810, #2838, #2813)

  • πŸ›  Updates Android to Python 3.13.13 and 3.14.4 (#2821)

  • πŸ›  Applies Pyodide-specific patches to the Emscripten toolchain installation (#2800)

  • πŸ›  Uses python -V -V for Windows build diagnostics (#2832)

  • πŸ›  Simplifies pinned container image lookup (#2897)

  • πŸ›  Minor fixups across error messages, OCI container, and options (#2860)

  • πŸ’Ό Adds PEP 723 metadata for bin/ scripts and drops the bin dependency group (#2819)

  • πŸ’Ό Improves Azure test reliability with retries and caching (#2890)

  • πŸ’Ό Fixes Windows GitLab CI test running (#2870)

  • πŸ’Ό Updates CI action pins and dev dependencies (#2902, #2867, #2851, #2843, #2826, #2823, #2820, #2807)

  • πŸ’Ό Adds agent and copilot setup files (#2861)

  • πŸ’Ό Uses if TYPE_CHECKING: blocks (#2866, #2864)

  • πŸ§ͺ Fixes Android tests using the uv frontend (#2809)

  • πŸ§ͺ Fixes the update-dependencies workflow to use uv to run nox (#2808)

  • πŸ§ͺ Adds unit tests for OCIContainer._get_platform_args (#2878)

  • πŸ“š Updates documentation for delvewheel as the default Windows repair-wheel-command, including the build diagram, schema defaults, and legal note (#2877, #2853, #2891)

  • πŸ“š Documents platform-specific before-build configuration (#2834)

  • πŸ“š Updates the "How it works" diagram with details of Android, iOS, and Pyodide builds (#2816)

... (truncated)

Commits
  • f03ac76 Bump version: v4.0.0
  • 557c5f6 feat: remove GraalPy 3.11 (gp311) support (#2895)
  • 70975c2 chore: use ConfigurationError when package_dir is outside cwd (#2898)
  • e2f143c chore(deps): bump docker/setup-qemu-action from 4.0.0 to 4.1.0 in the actions...
  • 866ae74 docs: fix CUDA manylinux container references in FAQ (#2900)
  • 84b518a chore: simplify pinned image lookup (#2897)
  • 785d812 docs: add instructions for building CUDA wheels (#2896)
  • f6bd047 Bump version: v4.0.0rc2
  • 6cd2d19 fix: remove potential partial cache-population in case of error (#2892)
  • cdb170b [Bot] Update dependencies (#2893)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pypa/cibuildwheel&package-manager=github_actions&previous-version=3.4.1&new-version=4.0.0)](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 05d37a3bad8..da1a4497162 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -665,7 +665,7 @@ jobs: run: | make cythonize - name: Build wheels - uses: pypa/cibuildwheel@v3.4.1 + uses: pypa/cibuildwheel@v4.0.0 with: # `build-frontend = "build[uv]"` (pyproject.toml) requires uv to be # available on the runner for Windows and macOS. Installing From 644138b34473e682d8bbc56de5e46d36ef203bb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:33:00 +0100 Subject: [PATCH 007/137] Bump codecov/codecov-action from 6 to 7 (#12876) --- .github/workflows/ci-cd.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index da1a4497162..92a167c1c33 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -235,7 +235,7 @@ jobs: run: | python -m coverage xml - name: Upload coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 with: files: ./coverage.xml flags: >- @@ -249,7 +249,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 with: files: ./junit.xml report_type: test_results @@ -386,7 +386,7 @@ jobs: run: | python -m coverage xml - name: Upload coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 with: files: ./coverage.xml flags: >- @@ -400,7 +400,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Upload test results to Codecov if: ${{ !cancelled() }} - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 with: files: ./junit.xml report_type: test_results @@ -498,7 +498,7 @@ jobs: run: | python -m coverage xml -o cython-coverage.xml --rcfile=.coveragerc-cython.toml - name: Upload coverage - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 with: files: ./cython-coverage.xml disable_search: true @@ -523,7 +523,7 @@ jobs: with: jobs: ${{ toJSON(needs) }} - name: Trigger codecov notification - uses: codecov/codecov-action@v6 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true From fa73cf161e17ab1da02b1a156e240c59702a60fd Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 8 Jun 2026 23:47:41 +0100 Subject: [PATCH 008/137] Fix gunicorn endless restarting (#12879) (#12880) (cherry picked from commit e7d755d9a7a6220114df1ed4f6a879d597b823dc) --- CHANGES/12879.bugfix.rst | 1 + aiohttp/worker.py | 24 ++++++++++++++++++------ tests/test_worker.py | 28 ++++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 CHANGES/12879.bugfix.rst diff --git a/CHANGES/12879.bugfix.rst b/CHANGES/12879.bugfix.rst new file mode 100644 index 00000000000..9dc8057a64d --- /dev/null +++ b/CHANGES/12879.bugfix.rst @@ -0,0 +1 @@ +Fixed ``GunicornWebWorker`` endlessly reloading when app fails during startup -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/worker.py b/aiohttp/worker.py index a86573b1d45..6bbdbdcfc13 100644 --- a/aiohttp/worker.py +++ b/aiohttp/worker.py @@ -60,14 +60,20 @@ def init_process(self) -> None: super().init_process() def run(self) -> None: - self._task = self.loop.create_task(self._run()) + # base.Worker.init_process() sets self.booted = True before + # invoking run(), but for the aiohttp worker the real boot work + # (factory call, runner setup, binding sockets) happens here. + # Reset until _run() reaches the serve loop so that the arbiter + # can tell a startup failure from a normal worker exit and + # halt instead of endlessly respawning workers. + self.booted = False - try: # ignore all finalization problems + self._task = self.loop.create_task(self._run()) + try: self.loop.run_until_complete(self._task) - except Exception: - self.log.exception("Exception in gunicorn worker") - self.loop.run_until_complete(self.loop.shutdown_asyncgens()) - self.loop.close() + finally: + self.loop.run_until_complete(self.loop.shutdown_asyncgens()) + self.loop.close() sys.exit(self.exit_code) @@ -118,6 +124,12 @@ async def _run(self) -> None: ) await site.start() + # Sockets are bound; tell the arbiter the worker is ready to + # accept requests. Any failure before this point propagates out + # of run() with self.booted=False so the arbiter exits with + # WORKER_BOOT_ERROR instead of treating this as a clean exit. + self.booted = True + # If our parent changed then we shut down. pid = os.getpid() try: diff --git a/tests/test_worker.py b/tests/test_worker.py index 9be9e41c20c..9609d7b9d68 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -143,9 +143,33 @@ def test_run_not_app( worker.loop = loop worker.wsgi = "not-app" worker.alive = False - with pytest.raises(SystemExit): + with pytest.raises(RuntimeError, match="wsgi app should be"): + worker.run() + assert not worker.booted + assert loop.is_closed() + + +def test_run_on_startup_raises( + worker: base_worker.GunicornWebWorker, loop: asyncio.AbstractEventLoop +) -> None: + worker.log = mock.Mock() + worker.cfg = mock.Mock() + worker.cfg.access_log_format = ACCEPTABLE_LOG_FORMAT + worker.cfg.is_ssl = False + worker.cfg.graceful_timeout = 100 + worker.sockets = [] + + app = web.Application() + + async def boom(app: web.Application) -> None: + raise RuntimeError("boom during startup") + + app.on_startup.append(boom) + worker.wsgi = app + worker.loop = loop + with pytest.raises(RuntimeError, match="boom during startup"): worker.run() - worker.log.exception.assert_called_with("Exception in gunicorn worker") + assert not worker.booted assert loop.is_closed() From 4160fb1c2b09a077d1fc1eb37b840ec333e110eb Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 9 Jun 2026 00:12:10 +0100 Subject: [PATCH 009/137] Fix gunicorn endless restarting (#12879) (#12881) (cherry picked from commit e7d755d9a7a6220114df1ed4f6a879d597b823dc) --- CHANGES/12879.bugfix.rst | 1 + aiohttp/worker.py | 24 ++++++++++++++++++------ tests/test_worker.py | 28 ++++++++++++++++++++++++++-- 3 files changed, 45 insertions(+), 8 deletions(-) create mode 100644 CHANGES/12879.bugfix.rst diff --git a/CHANGES/12879.bugfix.rst b/CHANGES/12879.bugfix.rst new file mode 100644 index 00000000000..9dc8057a64d --- /dev/null +++ b/CHANGES/12879.bugfix.rst @@ -0,0 +1 @@ +Fixed ``GunicornWebWorker`` endlessly reloading when app fails during startup -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/worker.py b/aiohttp/worker.py index a86573b1d45..6bbdbdcfc13 100644 --- a/aiohttp/worker.py +++ b/aiohttp/worker.py @@ -60,14 +60,20 @@ def init_process(self) -> None: super().init_process() def run(self) -> None: - self._task = self.loop.create_task(self._run()) + # base.Worker.init_process() sets self.booted = True before + # invoking run(), but for the aiohttp worker the real boot work + # (factory call, runner setup, binding sockets) happens here. + # Reset until _run() reaches the serve loop so that the arbiter + # can tell a startup failure from a normal worker exit and + # halt instead of endlessly respawning workers. + self.booted = False - try: # ignore all finalization problems + self._task = self.loop.create_task(self._run()) + try: self.loop.run_until_complete(self._task) - except Exception: - self.log.exception("Exception in gunicorn worker") - self.loop.run_until_complete(self.loop.shutdown_asyncgens()) - self.loop.close() + finally: + self.loop.run_until_complete(self.loop.shutdown_asyncgens()) + self.loop.close() sys.exit(self.exit_code) @@ -118,6 +124,12 @@ async def _run(self) -> None: ) await site.start() + # Sockets are bound; tell the arbiter the worker is ready to + # accept requests. Any failure before this point propagates out + # of run() with self.booted=False so the arbiter exits with + # WORKER_BOOT_ERROR instead of treating this as a clean exit. + self.booted = True + # If our parent changed then we shut down. pid = os.getpid() try: diff --git a/tests/test_worker.py b/tests/test_worker.py index 9be9e41c20c..9609d7b9d68 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -143,9 +143,33 @@ def test_run_not_app( worker.loop = loop worker.wsgi = "not-app" worker.alive = False - with pytest.raises(SystemExit): + with pytest.raises(RuntimeError, match="wsgi app should be"): + worker.run() + assert not worker.booted + assert loop.is_closed() + + +def test_run_on_startup_raises( + worker: base_worker.GunicornWebWorker, loop: asyncio.AbstractEventLoop +) -> None: + worker.log = mock.Mock() + worker.cfg = mock.Mock() + worker.cfg.access_log_format = ACCEPTABLE_LOG_FORMAT + worker.cfg.is_ssl = False + worker.cfg.graceful_timeout = 100 + worker.sockets = [] + + app = web.Application() + + async def boom(app: web.Application) -> None: + raise RuntimeError("boom during startup") + + app.on_startup.append(boom) + worker.wsgi = app + worker.loop = loop + with pytest.raises(RuntimeError, match="boom during startup"): worker.run() - worker.log.exception.assert_called_with("Exception in gunicorn worker") + assert not worker.booted assert loop.is_closed() From 02dd35e6f7f389bd464a5d364862941d73dc8b42 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:39:35 +0100 Subject: [PATCH 010/137] [PR #12794/c67c9e46 backport][3.15] reject non-digit Content-Length in multipart body parts (#12883) **This is a backport of PR #12794 as merged into master (c67c9e4622d5a30a76e1b7b2e2d255eff6ec6640).** --------- Co-authored-by: Javid Khan Co-authored-by: Sam Bull --- CHANGES/12794.bugfix.rst | 4 ++++ aiohttp/multipart.py | 5 +++++ tests/test_multipart.py | 7 +++++++ 3 files changed, 16 insertions(+) create mode 100644 CHANGES/12794.bugfix.rst diff --git a/CHANGES/12794.bugfix.rst b/CHANGES/12794.bugfix.rst new file mode 100644 index 00000000000..ec72efc1f3c --- /dev/null +++ b/CHANGES/12794.bugfix.rst @@ -0,0 +1,4 @@ +Rejected multipart body parts whose ``Content-Length`` header is not a +plain sequence of digits (e.g. ``+5``, ``-1``, ``1_0``), matching the +strictness of the main request parser per :rfc:`9110#section-8.6` +-- by :user:`dxbjavid`. diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 7cc3d4db0e8..b3d434a87eb 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -281,6 +281,11 @@ def __init__( self._is_form_data = subtype == "form-data" # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 length = None if self._is_form_data else self.headers.get(CONTENT_LENGTH, None) + if length is not None and not (length.isascii() and length.isdigit()): + # Reject sign prefixes, underscores, whitespace and non-ASCII + # digits that int() would otherwise accept. + # https://www.rfc-editor.org/rfc/rfc9110#section-8.6 + raise ValueError(f"invalid Content-Length: {length!r}") self._length = int(length) if length is not None else None self._read_bytes = 0 self._unread: deque[bytes] = deque() diff --git a/tests/test_multipart.py b/tests/test_multipart.py index fb5bbe562d5..86ca20b177f 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -273,6 +273,13 @@ async def test_multi_read_chunk(self) -> None: assert b"" == result assert obj.at_eof() + @pytest.mark.parametrize("value", ["-1", "+5", "1_0", " 5", "0x5", "οΌ•"]) + async def test_rejects_malformed_content_length(self, value: str) -> None: + h = CIMultiDict({"CONTENT-LENGTH": value}) + with Stream(b"Hello, world!\r\n--:--") as stream: + with pytest.raises(ValueError, match="Content-Length"): + aiohttp.BodyPartReader(BOUNDARY, h, stream) + async def test_read_chunk_properly_counts_read_bytes(self) -> None: expected = b"." * 10 tail = b"%s--:--" % newline From b0778e50d14aeede10549b019afd62bf3f9b1fff Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 00:39:51 +0100 Subject: [PATCH 011/137] [PR #12794/c67c9e46 backport][3.14] reject non-digit Content-Length in multipart body parts (#12882) **This is a backport of PR #12794 as merged into master (c67c9e4622d5a30a76e1b7b2e2d255eff6ec6640).** --------- Co-authored-by: Javid Khan Co-authored-by: Sam Bull --- CHANGES/12794.bugfix.rst | 4 ++++ aiohttp/multipart.py | 5 +++++ tests/test_multipart.py | 7 +++++++ 3 files changed, 16 insertions(+) create mode 100644 CHANGES/12794.bugfix.rst diff --git a/CHANGES/12794.bugfix.rst b/CHANGES/12794.bugfix.rst new file mode 100644 index 00000000000..ec72efc1f3c --- /dev/null +++ b/CHANGES/12794.bugfix.rst @@ -0,0 +1,4 @@ +Rejected multipart body parts whose ``Content-Length`` header is not a +plain sequence of digits (e.g. ``+5``, ``-1``, ``1_0``), matching the +strictness of the main request parser per :rfc:`9110#section-8.6` +-- by :user:`dxbjavid`. diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 7cc3d4db0e8..b3d434a87eb 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -281,6 +281,11 @@ def __init__( self._is_form_data = subtype == "form-data" # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 length = None if self._is_form_data else self.headers.get(CONTENT_LENGTH, None) + if length is not None and not (length.isascii() and length.isdigit()): + # Reject sign prefixes, underscores, whitespace and non-ASCII + # digits that int() would otherwise accept. + # https://www.rfc-editor.org/rfc/rfc9110#section-8.6 + raise ValueError(f"invalid Content-Length: {length!r}") self._length = int(length) if length is not None else None self._read_bytes = 0 self._unread: deque[bytes] = deque() diff --git a/tests/test_multipart.py b/tests/test_multipart.py index fb5bbe562d5..86ca20b177f 100644 --- a/tests/test_multipart.py +++ b/tests/test_multipart.py @@ -273,6 +273,13 @@ async def test_multi_read_chunk(self) -> None: assert b"" == result assert obj.at_eof() + @pytest.mark.parametrize("value", ["-1", "+5", "1_0", " 5", "0x5", "οΌ•"]) + async def test_rejects_malformed_content_length(self, value: str) -> None: + h = CIMultiDict({"CONTENT-LENGTH": value}) + with Stream(b"Hello, world!\r\n--:--") as stream: + with pytest.raises(ValueError, match="Content-Length"): + aiohttp.BodyPartReader(BOUNDARY, h, stream) + async def test_read_chunk_properly_counts_read_bytes(self) -> None: expected = b"." * 10 tail = b"%s--:--" % newline From 5f680e755aa0b9c78be1795f20f5b5686e1c371a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:00:33 +0000 Subject: [PATCH 012/137] Bump distlib from 0.4.1 to 0.4.2 (#12885) Bumps [distlib](https://github.com/pypa/distlib) from 0.4.1 to 0.4.2.
Changelog

Sourced from distlib's changelog.

0.4.2


Released: 2026-06-08
  • locators

    • Fix URL percent-encoding using space-padding instead of zero-padding. Thanks to Kadir Can Ozden for the patch.

    • Harden decompression against malicious input. Thanks to tonghuaroot for the patch, which was adapted slightly.

  • manifest

    • Use os.lstat in findall to correctly detect symlinked directories. Thanks to Kadir Can Ozden for the patch.
  • metadata

    • Improve logic to incorporate newer metadata versions.
  • resources

    • Ensure that constructed resource paths don't escape the package. Thanks to tonghuaroot for the patch.
  • util

    • Fix #255: Update cache_from_source() for Python 3.15. Thanks to Victor Stinner for the patch.

    • Check during unarchiving that the destination directory isn't escaped via symlinks. Thanks to tonghuaroot for the patch.

    • Improved performance of normalize_name using dual replace. Thanks to Hugo van Kemenade for the patch.

  • wheel

    • Add checks that installed files don't escape the installation directory. Thanks to tonghuaroot for the patch.

    • Add checks when mounting extensions to ensure path containment. Thanks to tonghuaroot for the patch.

Commits
  • 8183ea6 Update change log.
  • 55ca016 Changes for 0.4.2.
  • 7e282ab Update metadata logic to incorporate newer versions.
  • fa4ea50 Remove non-portable portion of test.
  • e06a1d5 Further refine tests.
  • ccd2bb0 Update unit tests.
  • ab58056 Formatting tidy-up.
  • 4d4c926 Add checks when mounting extensions to ensure path containment.
  • 9efc6fd Check during unarchiving that the destination directory isn't escaped via sym...
  • 63fd4f1 Ensure that constructed resource paths don't escape the package.
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=distlib&package-manager=pip&previous-version=0.4.1&new-version=0.4.2)](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 c294cb16fe3..969f0879c22 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -64,7 +64,7 @@ cryptography==48.0.0 # via trustme cython==3.2.5 # via -r requirements/cython.in -distlib==0.4.1 +distlib==0.4.2 # via virtualenv docutils==0.21.2 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 793a4b9c1ed..f50b6be5cf6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -62,7 +62,7 @@ coverage==7.14.1 # pytest-cov cryptography==48.0.0 # via trustme -distlib==0.4.1 +distlib==0.4.2 # via virtualenv docutils==0.21.2 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index 9caef56aff0..5b8535145dd 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -26,7 +26,7 @@ click==8.4.1 # via slotscheck cryptography==48.0.0 # via trustme -distlib==0.4.1 +distlib==0.4.2 # via virtualenv exceptiongroup==1.3.1 # via pytest From 5179394bf8285d765ea8e9e102c891a821e7a123 Mon Sep 17 00:00:00 2001 From: Taras Kozlov Date: Wed, 10 Jun 2026 00:29:50 +0200 Subject: [PATCH 013/137] =?UTF-8?q?[PR=20#12823/c67c9e46=20backport][3.15]?= =?UTF-8?q?=20Parameterize=20some=20codspeed=20benchmarks=20by=20connectio?= =?UTF-8?q?n=20type=20(tcp=20vs=20ssl)=E2=80=A6=20(#12884)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGES/12823.misc.rst | 2 ++ aiohttp/pytest_plugin.py | 32 ++++++++++++++++++++--- aiohttp/test_utils.py | 4 +-- docs/spelling_wordlist.txt | 1 + tests/test_benchmarks_client.py | 17 +++++++----- tests/test_benchmarks_client_ws.py | 7 ++--- tests/test_benchmarks_web_fileresponse.py | 12 +++++---- 7 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 CHANGES/12823.misc.rst diff --git a/CHANGES/12823.misc.rst b/CHANGES/12823.misc.rst new file mode 100644 index 00000000000..747f5e1fbee --- /dev/null +++ b/CHANGES/12823.misc.rst @@ -0,0 +1,2 @@ +Parameterized some codspeed benchmarks by connection type (SSL + TCP). +Previously, benchmarks only ran for TCP connection type. -- by :user:`tarasko`. diff --git a/aiohttp/pytest_plugin.py b/aiohttp/pytest_plugin.py index a2c7bdca03e..6034a70c30c 100644 --- a/aiohttp/pytest_plugin.py +++ b/aiohttp/pytest_plugin.py @@ -1,9 +1,11 @@ import asyncio import contextlib import inspect +import ssl import warnings from collections.abc import Awaitable, Callable, Iterator -from typing import Any, Protocol, overload +from dataclasses import dataclass +from typing import Any, Protocol, TypedDict, overload import pytest @@ -407,8 +409,8 @@ async def go( else: assert not args, "args should be empty" + server_kwargs = server_kwargs or {} if isinstance(__param, Application): - server_kwargs = server_kwargs or {} server = TestServer(__param, loop=loop, **server_kwargs) client = TestClient(server, loop=loop, **kwargs) elif isinstance(__param, BaseTestServer): @@ -416,7 +418,7 @@ async def go( else: raise ValueError("Unknown argument type: %r" % type(__param)) - await client.start_server() + await client.start_server(**server_kwargs) clients.append(client) return client @@ -437,3 +439,27 @@ def test_client(aiohttp_client): # type: ignore[no-untyped-def] # pragma: no c stacklevel=2, ) return aiohttp_client + + +class _ConnArgs(TypedDict, total=False): + ssl: ssl.SSLContext + + +@dataclass(frozen=True) +class ConnectionType: + s_kwargs: _ConnArgs + c_kwargs: _ConnArgs + + +@pytest.fixture(params=("tcp", "ssl"), ids=("tcp", "ssl")) +def conn_type( + request: pytest.FixtureRequest, + ssl_ctx: ssl.SSLContext, + client_ssl_ctx: ssl.SSLContext, +) -> ConnectionType: + if request.param == "ssl": + return ConnectionType( + s_kwargs={"ssl": ssl_ctx}, + c_kwargs={"ssl": client_ssl_ctx}, + ) + return ConnectionType(s_kwargs={}, c_kwargs={}) diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py index 6b50e23b810..2b417ef8b2d 100644 --- a/aiohttp/test_utils.py +++ b/aiohttp/test_utils.py @@ -308,8 +308,8 @@ def __init__( self._responses: list[ClientResponse] = [] self._websockets: list[ClientWebSocketResponse[bool]] = [] - async def start_server(self) -> None: - await self._server.start_server(loop=self._loop) + async def start_server(self, **server_kwargs: Any) -> None: + await self._server.start_server(loop=self._loop, **server_kwargs) @property def host(self) -> str: diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index a62ffed4ae9..920fc7f17fa 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -76,6 +76,7 @@ cls cmd codebase codec +codspeed Codings committer committers diff --git a/tests/test_benchmarks_client.py b/tests/test_benchmarks_client.py index d33936b5c8b..a4557c3c38c 100644 --- a/tests/test_benchmarks_client.py +++ b/tests/test_benchmarks_client.py @@ -7,7 +7,7 @@ from yarl import URL from aiohttp import hdrs, request, web -from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer +from aiohttp.pytest_plugin import AiohttpClient, AiohttpServer, ConnectionType if TYPE_CHECKING: from pytest_codspeed import BenchmarkFixture @@ -20,6 +20,7 @@ def test_one_hundred_simple_get_requests( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, + conn_type: ConnectionType, ) -> None: """Benchmark 100 simple GET requests.""" message_count = 100 @@ -31,9 +32,9 @@ async def handler(request: web.Request) -> web.Response: app.router.add_route("GET", "/", handler) async def run_client_benchmark() -> None: - client = await aiohttp_client(app) + client = await aiohttp_client(app, server_kwargs=conn_type.s_kwargs) for _ in range(message_count): - await client.get("/") + await client.get("/", **conn_type.c_kwargs) await client.close() @benchmark @@ -129,6 +130,7 @@ def test_one_hundred_get_requests_with_1024_chunked_payload( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, + conn_type: ConnectionType, ) -> None: """Benchmark 100 GET requests with a small payload of 1024 bytes.""" message_count = 100 @@ -143,9 +145,9 @@ async def handler(request: web.Request) -> web.Response: app.router.add_route("GET", "/", handler) async def run_client_benchmark() -> None: - client = await aiohttp_client(app) + client = await aiohttp_client(app, server_kwargs=conn_type.s_kwargs) for _ in range(message_count): - resp = await client.get("/") + resp = await client.get("/", **conn_type.c_kwargs) await resp.read() await client.close() @@ -187,6 +189,7 @@ def test_one_hundred_get_requests_with_1mb_chunked_payload( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, + conn_type: ConnectionType, ) -> None: """Benchmark 100 GET requests with a 1 MiB chunked payload using read.""" message_count = 100 @@ -201,9 +204,9 @@ async def handler(request: web.Request) -> web.Response: app.router.add_route("GET", "/", handler) async def run_client_benchmark() -> None: - client = await aiohttp_client(app) + client = await aiohttp_client(app, server_kwargs=conn_type.s_kwargs) for _ in range(message_count): - resp = await client.get("/") + resp = await client.get("/", **conn_type.c_kwargs) await resp.read() await client.close() diff --git a/tests/test_benchmarks_client_ws.py b/tests/test_benchmarks_client_ws.py index 60b0a027133..721d2c5ffdc 100644 --- a/tests/test_benchmarks_client_ws.py +++ b/tests/test_benchmarks_client_ws.py @@ -7,7 +7,7 @@ from aiohttp import web from aiohttp._websocket.helpers import MSG_SIZE -from aiohttp.pytest_plugin import AiohttpClient +from aiohttp.pytest_plugin import AiohttpClient, ConnectionType if TYPE_CHECKING: from pytest_codspeed import BenchmarkFixture @@ -52,6 +52,7 @@ def test_one_thousand_round_trip_websocket_binary_messages( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, + conn_type: ConnectionType, msg_size: int, ) -> None: """Benchmark round trip of 1000 WebSocket binary messages.""" @@ -70,8 +71,8 @@ async def handler(request: web.Request) -> web.WebSocketResponse: app.router.add_route("GET", "/", handler) async def run_websocket_benchmark() -> None: - client = await aiohttp_client(app) - resp = await client.ws_connect("/") + client = await aiohttp_client(app, server_kwargs=conn_type.s_kwargs) + resp = await client.ws_connect("/", **conn_type.c_kwargs) for _ in range(message_count): await resp.receive() await resp.close() diff --git a/tests/test_benchmarks_web_fileresponse.py b/tests/test_benchmarks_web_fileresponse.py index ea7ef6c5ab0..5f06bb71caa 100644 --- a/tests/test_benchmarks_web_fileresponse.py +++ b/tests/test_benchmarks_web_fileresponse.py @@ -8,7 +8,7 @@ from multidict import CIMultiDict from aiohttp import ClientResponse, web -from aiohttp.pytest_plugin import AiohttpClient +from aiohttp.pytest_plugin import AiohttpClient, ConnectionType if TYPE_CHECKING: from pytest_codspeed import BenchmarkFixture @@ -21,6 +21,7 @@ def test_simple_web_file_response( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, + conn_type: ConnectionType, ) -> None: """Benchmark creating 100 simple web.FileResponse.""" response_count = 100 @@ -33,9 +34,9 @@ async def handler(request: web.Request) -> web.FileResponse: app.router.add_route("GET", "/", handler) async def run_file_response_benchmark() -> None: - client = await aiohttp_client(app) + client = await aiohttp_client(app, server_kwargs=conn_type.s_kwargs) for _ in range(response_count): - await client.get("/") + await client.get("/", **conn_type.c_kwargs) await client.close() @benchmark @@ -47,6 +48,7 @@ def test_simple_web_file_sendfile_fallback_response( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, + conn_type: ConnectionType, ) -> None: """Benchmark creating 100 simple web.FileResponse without sendfile.""" response_count = 100 @@ -62,9 +64,9 @@ async def handler(request: web.Request) -> web.FileResponse: app.router.add_route("GET", "/", handler) async def run_file_response_benchmark() -> None: - client = await aiohttp_client(app) + client = await aiohttp_client(app, server_kwargs=conn_type.s_kwargs) for _ in range(response_count): - await client.get("/") + await client.get("/", **conn_type.c_kwargs) await client.close() @benchmark From 56badc9122e26a014d1776727c79d162a35a4061 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:03:11 +0000 Subject: [PATCH 014/137] Bump cryptography from 48.0.0 to 48.0.1 (#12892) Bumps [cryptography](https://github.com/pyca/cryptography) from 48.0.0 to 48.0.1.
Changelog

Sourced from cryptography's changelog.

48.0.1 - 2026-06-09


* Updated Windows, macOS, and Linux wheels to be compiled with OpenSSL
4.0.1.

.. _v48-0-0:

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cryptography&package-manager=pip&previous-version=48.0.0&new-version=48.0.1)](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 969f0879c22..9f9b4694425 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -60,7 +60,7 @@ coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov -cryptography==48.0.0 +cryptography==48.0.1 # via trustme cython==3.2.5 # via -r requirements/cython.in diff --git a/requirements/dev.txt b/requirements/dev.txt index f50b6be5cf6..e2c7ac7c746 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -60,7 +60,7 @@ coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov -cryptography==48.0.0 +cryptography==48.0.1 # via trustme distlib==0.4.2 # via virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index 5b8535145dd..85adb37c1f3 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -24,7 +24,7 @@ cfgv==3.5.0 # via pre-commit click==8.4.1 # via slotscheck -cryptography==48.0.0 +cryptography==48.0.1 # via trustme distlib==0.4.2 # via virtualenv diff --git a/requirements/test-common.txt b/requirements/test-common.txt index d3e2f267c3e..3bafeed46fe 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -18,7 +18,7 @@ coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov -cryptography==48.0.0 +cryptography==48.0.1 # via trustme exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index d17865edc97..f7d399bce14 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -34,7 +34,7 @@ coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov -cryptography==48.0.0 +cryptography==48.0.1 # via trustme exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test.txt b/requirements/test.txt index aa15eeadcfb..40ccbfadd71 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -34,7 +34,7 @@ coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov -cryptography==48.0.0 +cryptography==48.0.1 # via trustme exceptiongroup==1.3.1 # via pytest From 3990bbcdca061abe72db0b39538b7978adf1784a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:08:54 +0000 Subject: [PATCH 015/137] Bump filelock from 3.29.1 to 3.29.3 (#12900) 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.29.1 to 3.29.3.
Release notes

Sourced from filelock's releases.

3.29.3

What's Changed

Full Changelog: https://github.com/tox-dev/filelock/compare/3.29.2...3.29.3

3.29.2

What's Changed

Full Changelog: https://github.com/tox-dev/filelock/compare/3.29.1...3.29.2

Changelog

Sourced from filelock's changelog.

########### Changelog ###########


3.29.3 (2026-06-10)


  • πŸ› fix(ci): restore release environment on tag job :pr:559
  • validate pid range in _parse_lock_holder :pr:556 - by :user:dxbjavid
  • πŸ”§ ci(release): publish to PyPI on tag push :pr:557
  • build(deps): bump astral-sh/setup-uv from 8.1.0 to 8.2.0 :pr:558 - by :user:dependabot[bot]

3.29.2 (2026-06-10)


  • build(deps): bump actions/checkout from 6.0.2 to 6.0.3 :pr:555 - by :user:dependabot[bot]
  • [pre-commit.ci] pre-commit autoupdate :pr:554 - by :user:pre-commit-ci[bot]
  • check hostname in is_lock_held_by_us :pr:553 - by :user:dxbjavid
  • πŸ”’ fix(soft): harden stale-lock breaking and self-heal malformed locks :pr:551
  • open marker reads non-blocking to refuse attacker-placed fifo :pr:549 - by :user:dxbjavid

3.29.1 (2026-06-03)


  • πŸ› fix(soft): refuse to follow symlinks when reading the lock file :pr:548 - by :user:dxbjavid
  • [pre-commit.ci] pre-commit autoupdate :pr:547 - by :user:pre-commit-ci[bot]
  • [pre-commit.ci] pre-commit autoupdate :pr:546 - by :user:pre-commit-ci[bot]
  • chore: improve filelock maintenance path :pr:545 - by :user:lphuc2250gma
  • chore: improve filelock maintenance path :pr:544 - by :user:lphuc2250gma
  • chore: improve filelock maintenance path :pr:542 - by :user:lphuc2250gma
  • docs: clarify per-thread scope of FileLock configuration :pr:543 - by :user:Gares95
  • [pre-commit.ci] pre-commit autoupdate :pr:541 - by :user:pre-commit-ci[bot]
  • docs: fix API docs of release() :pr:540 - by :user:MrAnno
  • [pre-commit.ci] pre-commit autoupdate :pr:539 - by :user:pre-commit-ci[bot]
  • [pre-commit.ci] pre-commit autoupdate :pr:538 - by :user:pre-commit-ci[bot]
  • [pre-commit.ci] pre-commit autoupdate :pr:537 - by :user:pre-commit-ci[bot]
  • build(deps): bump astral-sh/setup-uv from 8.0.0 to 8.1.0 :pr:536 - by :user:dependabot[bot]
  • [pre-commit.ci] pre-commit autoupdate :pr:535 - by :user:pre-commit-ci[bot]

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]

... (truncated)

Commits
  • 85e73d7 πŸ› fix(ci): publish from release.yaml on tag push (#560)
  • f86dcb1 Release 3.29.3
  • 643bdbe πŸ› fix(ci): restore release environment on tag job (#559)
  • 7a8f74a validate pid range in _parse_lock_holder (#556)
  • d1d49a0 πŸ”§ ci(release): publish to PyPI on tag push (#557)
  • b37e162 build(deps): bump astral-sh/setup-uv from 8.1.0 to 8.2.0 (#558)
  • d9216de Release 3.29.2
  • ab6844d build(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#555)
  • b862ead [pre-commit.ci] pre-commit autoupdate (#554)
  • 2ff7c3c check hostname in is_lock_held_by_us (#553)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=filelock&package-manager=pip&previous-version=3.29.1&new-version=3.29.3)](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 9f9b4694425..301beb819fd 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -74,7 +74,7 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.29.1 +filelock==3.29.3 # via # python-discovery # virtualenv diff --git a/requirements/dev.txt b/requirements/dev.txt index e2c7ac7c746..726ca309bb0 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -72,7 +72,7 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.29.1 +filelock==3.29.3 # via # python-discovery # virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index 85adb37c1f3..4a6b9bcd3d4 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -30,7 +30,7 @@ distlib==0.4.2 # via virtualenv exceptiongroup==1.3.1 # via pytest -filelock==3.29.1 +filelock==3.29.3 # via # python-discovery # virtualenv From 6995b10e2efe009a4e18493a913b37f4a8e8f52c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 11:12:24 +0000 Subject: [PATCH 016/137] Bump sigstore/gh-action-sigstore-python from 3.3.0 to 3.4.0 (#12899) Bumps [sigstore/gh-action-sigstore-python](https://github.com/sigstore/gh-action-sigstore-python) from 3.3.0 to 3.4.0.
Release notes

Sourced from sigstore/gh-action-sigstore-python's releases.

v3.4.0

What's Changed

  • Used sigstore package version is now 4.3.0
  • Other dependency updates

Full Changelog: https://github.com/sigstore/gh-action-sigstore-python/compare/v3.3.0...v3.4.0

Commits
  • 5b79a39 build(deps): bump sigstore in the python-dependencies group (#408)
  • 3d7f17b build(deps): bump idna in the python-dependencies group (#407)
  • 2d128b7 build(deps): bump github/codeql-action in the actions group (#405)
  • 06882d3 build(deps): bump the python-dependencies group with 3 updates (#406)
  • bab6f77 build(deps): bump the actions group across 1 directory with 3 updates (#403)
  • f98c429 build(deps): bump securesystemslib in the python-dependencies group (#404)
  • db9196e build(deps): bump the python-dependencies group across 1 directory with 3 upd...
  • 839093d build(deps): bump certifi from 2026.4.22 to 2026.5.20 in the python-dependenc...
  • d9df9da build(deps): bump ast-serialize in the python-dependencies group (#397)
  • 2f7a761 build(deps): bump github/codeql-action in the actions group (#395)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=sigstore/gh-action-sigstore-python&package-manager=github_actions&previous-version=3.3.0&new-version=3.4.0)](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 92a167c1c33..5478fe3171a 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -773,7 +773,7 @@ jobs: skip-existing: true - name: Sign the dists with Sigstore - uses: sigstore/gh-action-sigstore-python@v3.3.0 + uses: sigstore/gh-action-sigstore-python@v3.4.0 with: inputs: >- ./dist/*.tar.gz From dece773ea22d8efae02711fe9a3eaf59c173e8dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:27:44 +0000 Subject: [PATCH 017/137] Bump distlib from 0.4.2 to 0.4.3 (#12908) Bumps [distlib](https://github.com/pypa/distlib) from 0.4.2 to 0.4.3.
Changelog

Sourced from distlib's changelog.

0.4.3


Released: 2026-06-12
  • resources

    • Removed too-restrictive check for escaping resources.
Commits
  • 3f4a377 Changes for 0.4.3.
  • 5ee19e6 Relax too-strict escaping check for resources.
  • f0e3d1a Bump version.
  • 06e010d Add upper version limit for setuptools for build, to keep metadata version to...
  • 8b5aa55 Added tag 0.4.2 for changeset 5295c0ceb419
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=distlib&package-manager=pip&previous-version=0.4.2&new-version=0.4.3)](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 301beb819fd..fd03e057ae9 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -64,7 +64,7 @@ cryptography==48.0.1 # via trustme cython==3.2.5 # via -r requirements/cython.in -distlib==0.4.2 +distlib==0.4.3 # via virtualenv docutils==0.21.2 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 726ca309bb0..d83fed4fe56 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -62,7 +62,7 @@ coverage==7.14.1 # pytest-cov cryptography==48.0.1 # via trustme -distlib==0.4.2 +distlib==0.4.3 # via virtualenv docutils==0.21.2 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index 4a6b9bcd3d4..a57c568d1c6 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -26,7 +26,7 @@ click==8.4.1 # via slotscheck cryptography==48.0.1 # via trustme -distlib==0.4.2 +distlib==0.4.3 # via virtualenv exceptiongroup==1.3.1 # via pytest From 061bc47c6786062b54127a42d8e860193c06ba7e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:38:18 +0000 Subject: [PATCH 018/137] Bump python-discovery from 1.4.0 to 1.4.2 (#12910) 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.4.0 to 1.4.2.
Release notes

Sourced from python-discovery's releases.

v1.4.2

What's Changed

Full Changelog: https://github.com/tox-dev/python-discovery/compare/1.4.1...1.4.2

v1.4.1

What's Changed

Full Changelog: https://github.com/tox-dev/python-discovery/compare/1.4.0...1.4.1

Changelog

Sourced from python-discovery's changelog.

Bug fixes - 1.4.2

  • Stop executable symlink resolution once the stdlib landmark is reachable and keep macOS framework builds untouched, matching getpath - Homebrew interpreters no longer get version-pinned Cellar paths recorded and stable aliases such as Debian's /usr/bin/python3 are preserved - by :user:gaborbernat. (:issue:86)

v1.4.1 (2026-06-11)


Bug fixes - 1.4.1

  • Resolve executable-only symlinks when computing system_executable, mirroring CPython's getpath.realpath python/cpython#115237 symlinked interpreter tree is kept as-is - by :user:gaborbernat. (:issue:84)

v1.4.0 (2026-05-28)


Commits
  • ca8d938 release 1.4.2
  • 4f6132b πŸ› fix: stop symlink resolution at stdlib landmark and framework builds (#87)
  • 95e6470 release 1.4.1
  • 011f953 πŸ› fix: resolve executable-only symlinks in system_executable (#85)
  • 3a761d5 build(deps): bump astral-sh/setup-uv from 8.1.0 to 8.2.0 (#83)
  • 5d3efc2 [pre-commit.ci] pre-commit autoupdate (#81)
  • 31365ac build(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#82)
  • 834a327 [pre-commit.ci] pre-commit autoupdate (#80)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=python-discovery&package-manager=pip&previous-version=1.4.0&new-version=1.4.2)](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 fd03e057ae9..9704fe564bb 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -214,7 +214,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.4.0 +python-discovery==1.4.2 # via virtualenv python-on-whales==0.81.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index d83fed4fe56..031a117a15b 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -209,7 +209,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.4.0 +python-discovery==1.4.2 # via virtualenv python-on-whales==0.81.0 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index a57c568d1c6..0d816702b88 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.4.0 +python-discovery==1.4.2 # via virtualenv python-on-whales==0.81.0 # via -r requirements/lint.in From 3242ec2909ecfcb3f7e8b2093338dc23bc2dc094 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 12 Jun 2026 12:11:18 +0000 Subject: [PATCH 019/137] Bump virtualenv from 21.4.2 to 21.4.3 (#12912) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.4.2 to 21.4.3.
Release notes

Sourced from virtualenv's releases.

21.4.3

What's Changed

New Contributors

Full Changelog: https://github.com/pypa/virtualenv/compare/21.4.2...21.4.3

Changelog

Sourced from virtualenv's changelog.

Bugfixes - 21.4.3

  • Upgrade embedded wheels:

    • pip to 26.1.2 from 26.1.1 (:issue:u)
  • Resolve executable-only symlinks when recording home and base-executable in pyvenv.cfg, mirroring CPython's getpath.realpathpython/cpython#115237 binary locate the base stdlib (for example python-build-standalone); a fully symlinked interpreter tree is kept as-is

    • by :user:gaborbernat. (:issue:3157)
  • Stop exporting PS1 from the bash activator so child processes do not inherit shell prompt state. (:issue:3158)

  • Handle CYGWIN/MSYS/MINGW path conversions in fish activation script - by user::LuNoX. (:issue:3160)


v21.4.2 (2026-05-31)


Commits
  • 134b080 release 21.4.3
  • 2a36128 πŸ› fix(discovery): resolve base interpreter executable-only symlinks (#3166)
  • 5389c25 Add wheel-0.47.0 to seed packages as mitigation of CVE-2026-24049 (#3167)
  • 0134fee chore(deps): bump astral-sh/setup-uv from 8.1.0 to 8.2.0 (#3165)
  • af1ed9f chore(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#3164)
  • 1b00ec8 [pre-commit.ci] pre-commit autoupdate (#3163)
  • ce9729e Stop exporting PS1 in bash activator (#3162)
  • 9e6f59f Fish activator passes VIRTUAL_ENV through cygpath (#3161)
  • a423028 [pre-commit.ci] pre-commit autoupdate (#3156)
  • f838c21 Upgrade embedded pip/setuptools/wheel (#3155)
  • 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 9704fe564bb..8532cc81ea9 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.4.2 +virtualenv==21.4.3 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common-base.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 031a117a15b..7f03cf37afa 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.4.2 +virtualenv==21.4.3 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common-base.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 0d816702b88..f6cea4f0bc7 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.4.2 +virtualenv==21.4.3 # via pre-commit zlib-ng==1.0.0 # via -r requirements/lint.in From 51d6ea5e483ce418f60f7b757d0c01d5a60d7fca Mon Sep 17 00:00:00 2001 From: Rodrigo Nogueira Date: Fri, 12 Jun 2026 22:15:42 -0300 Subject: [PATCH 020/137] [PR #12746/ff38c236 backport][3.15] Tokenize Connection header in WebSocket client handshake (#12904) **This is a backport of PR #12746 as merged into master (ff38c236ce9cda3a05ae01e7a967012e05074fe6).** --- aiohttp/client.py | 2 +- aiohttp/client_reqrep.py | 2 ++ tests/test_client_proto.py | 39 ++++++++++++++++++++++++++++++++++++++ tests/test_client_ws.py | 4 ++++ tests/test_http_parser.py | 28 +++++++++++++++++++++++++++ 5 files changed, 74 insertions(+), 1 deletion(-) diff --git a/aiohttp/client.py b/aiohttp/client.py index d9d8dfd5db7..c0a616edd6b 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -1275,7 +1275,7 @@ async def _ws_connect( headers=resp.headers, ) - if resp.headers.get(hdrs.CONNECTION, "").lower() != "upgrade": + if not resp._upgraded: raise WSServerHandshakeError( resp.request_info, resp.history, diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index c77213e3ed2..8d99cc9818f 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -289,6 +289,7 @@ class ClientResponse(HeadersMixin): _headers: CIMultiDictProxy[str] = None # type: ignore[assignment] _history: tuple["ClientResponse", ...] = () _raw_headers: RawHeaders = None # type: ignore[assignment] + _upgraded: bool = False # parser saw a Connection: upgrade token _connection: Optional["Connection"] = None # current connection _cookies: SimpleCookie | None = None @@ -583,6 +584,7 @@ async def start(self, connection: "Connection") -> "ClientResponse": # headers self._headers = message.headers # type is CIMultiDictProxy self._raw_headers = message.raw_headers # type is Tuple[bytes, bytes] + self._upgraded = message.upgrade # payload self.content = payload diff --git a/tests/test_client_proto.py b/tests/test_client_proto.py index b4cd15b00f8..54ba0685a7d 100644 --- a/tests/test_client_proto.py +++ b/tests/test_client_proto.py @@ -385,3 +385,42 @@ async def test_abort_without_transport(loop: asyncio.AbstractEventLoop) -> None: # Should not raise and should still clean up assert proto._exception is None mock_drop_timeout.assert_not_called() + + +@pytest.mark.parametrize( + ("connection", "expected"), + [(b"upgrade, keep-alive", True), (b"keep-alive", False)], +) +async def test_response_start_records_upgrade( + connection: bytes, expected: bool +) -> None: + """ClientResponse.start() preserves the parser's Connection upgrade flag.""" + loop = asyncio.get_running_loop() + proto = ResponseHandler(loop=loop) + proto.connection_made(mock.Mock()) + conn = mock.Mock(protocol=proto) + proto.set_response_params(read_until_eof=True) + proto.data_received( + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: " + connection + b"\r\n\r\n" + ) + + url = URL("http://ws-upgrade.org") + response = ClientResponse( + "get", + url, + writer=mock.Mock(), + continue100=None, + timer=TimerNoop(), + request_info=mock.Mock(), + traces=[], + loop=loop, + session=mock.Mock(), + stream_writer=mock.create_autospec( + AbstractStreamWriter, spec_set=True, instance=True + ), + ) + await response.start(conn) + assert response._upgraded is expected + response.close() diff --git a/tests/test_client_ws.py b/tests/test_client_ws.py index 169330b7d1a..b4953833bd4 100644 --- a/tests/test_client_ws.py +++ b/tests/test_client_ws.py @@ -22,6 +22,7 @@ async def test_ws_connect(ws_key: Any, loop: Any, key_data: Any) -> None: hdrs.SEC_WEBSOCKET_ACCEPT: ws_key, hdrs.SEC_WEBSOCKET_PROTOCOL: "chat", } + resp._upgraded = True resp.connection.protocol.read_timeout = None with mock.patch("aiohttp.client.os") as m_os: with mock.patch("aiohttp.client.ClientSession.request") as m_req: @@ -247,6 +248,8 @@ async def test_ws_connect_err_upgrade(loop, ws_key, key_data) -> None: async def test_ws_connect_err_conn(loop, ws_key, key_data) -> None: + # The parser did not see a Connection: upgrade token (resp._upgraded is + # False), so the handshake must be rejected. resp = mock.Mock() resp.status = 101 resp.headers = { @@ -254,6 +257,7 @@ async def test_ws_connect_err_conn(loop, ws_key, key_data) -> None: hdrs.CONNECTION: "close", hdrs.SEC_WEBSOCKET_ACCEPT: ws_key, } + resp._upgraded = False with mock.patch("aiohttp.client.os") as m_os: with mock.patch("aiohttp.client.ClientSession.request") as m_req: m_os.urandom.return_value = key_data diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index f2e0e9ae3f6..97643fd64ba 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -789,6 +789,34 @@ def test_upgrade_header_non_ascii(parser: HttpRequestParser) -> None: assert not upgrade +@pytest.mark.parametrize( + ("connection", "expected"), + [ + ("upgrade", True), + ("upgrade, keep-alive", True), # other tokens alongside upgrade + ("keep-alive, upgrade", True), # upgrade not first + ("Upgrade, Keep-Alive", True), # case-insensitive + ("keep-alive", False), # no upgrade token + ("keep-alive, notupgrade", False), # substring is not a token + ], +) +def test_response_upgrade_token_in_connection_list( + response: HttpResponseParser, connection: str, expected: bool +) -> None: + # RFC 9110 Β§7.6.1: Connection is a comma-separated token list, so the parser + # must set msg.upgrade for a 101 response whenever "upgrade" appears as a + # token, regardless of position, case, or neighbouring tokens. + text = ( + b"HTTP/1.1 101 Switching Protocols\r\n" + b"Upgrade: websocket\r\n" + b"Connection: " + connection.encode() + b"\r\n\r\n" + ) + messages, upgrade, tail = response.feed_data(text) + msg = messages[0][0] + assert msg.upgrade == expected + assert upgrade == expected + + def test_request_te_chunked_with_content_length(parser: HttpRequestParser) -> None: text = ( b"GET /test HTTP/1.1\r\n" From 83638e03d5eefb67a80644d6ded2b820566d364c Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 13 Jun 2026 16:43:33 +0100 Subject: [PATCH 021/137] Narrow Exception.args types (#12920) (#12921) (cherry picked from commit 83b06e48159c697b13741983d9c1f24d6a179fc5) --- CHANGES/12920.feature.rst | 1 + aiohttp/client_exceptions.py | 11 +++++++++++ aiohttp/http_exceptions.py | 6 ++++++ tests/test_client_exceptions.py | 20 ++++++++++++++++++++ tests/test_http_exceptions.py | 10 ++++++++++ 5 files changed, 48 insertions(+) create mode 100644 CHANGES/12920.feature.rst diff --git a/CHANGES/12920.feature.rst b/CHANGES/12920.feature.rst new file mode 100644 index 00000000000..d5802ab27ad --- /dev/null +++ b/CHANGES/12920.feature.rst @@ -0,0 +1 @@ +Narrowed the ``args`` type on aiohttp exception classes -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/client_exceptions.py b/aiohttp/client_exceptions.py index e3e503b7ca2..0a6ac27ee97 100644 --- a/aiohttp/client_exceptions.py +++ b/aiohttp/client_exceptions.py @@ -72,6 +72,8 @@ class ClientResponseError(ClientError): headers: Response headers. """ + args: tuple[RequestInfo, tuple[ClientResponse, ...]] + def __init__( self, request_info: RequestInfo, @@ -177,6 +179,8 @@ class ClientConnectorError(ClientOSError): a connection can not be established. """ + args: tuple[ConnectionKey, OSError] + def __init__(self, connection_key: ConnectionKey, os_error: OSError) -> None: self._conn_key = connection_key self._os_error = os_error @@ -254,6 +258,8 @@ class ServerConnectionError(ClientConnectionError): class ServerDisconnectedError(ServerConnectionError): """Server disconnected.""" + args: tuple[RawResponseMessage | str] + def __init__(self, message: RawResponseMessage | str | None = None) -> None: if message is None: message = "Server disconnected" @@ -277,6 +283,8 @@ class SocketTimeoutError(ServerTimeoutError): class ServerFingerprintMismatch(ServerConnectionError): """SSL certificate does not match expected fingerprint.""" + args: tuple[bytes, bytes, str, int] + def __init__(self, expected: bytes, got: bytes, host: str, port: int) -> None: self.expected = expected self.got = got @@ -301,6 +309,8 @@ class InvalidURL(ClientError, ValueError): # Derive from ValueError for backward compatibility + args: tuple[StrOrURL] | tuple[StrOrURL, str] + def __init__(self, url: StrOrURL, description: str | None = None) -> None: # The type of url is not yarl.URL because the exception can be raised # on URL(url) call @@ -381,6 +391,7 @@ class ClientConnectorCertificateError(*cert_errors_bases): # type: ignore[misc] """Response certificate error.""" _conn_key: ConnectionKey + args: tuple[ConnectionKey, Exception] def __init__( # TODO: If we require ssl in future, this can become ssl.CertificateError diff --git a/aiohttp/http_exceptions.py b/aiohttp/http_exceptions.py index 54cc4af36be..9404790e0bc 100644 --- a/aiohttp/http_exceptions.py +++ b/aiohttp/http_exceptions.py @@ -78,6 +78,8 @@ class DecompressSizeError(PayloadEncodingError): class LineTooLong(BadHttpMessage): + args: tuple[str | bytes, str | int, str] + def __init__( self, line: str | bytes, @@ -89,6 +91,8 @@ def __init__( class InvalidHeader(BadHttpMessage): + args: tuple[bytes | str] + def __init__(self, hdr: bytes | str) -> None: hdr_s = hdr.decode(errors="backslashreplace") if isinstance(hdr, bytes) else hdr super().__init__(f"Invalid HTTP header: {hdr!r}") @@ -97,6 +101,8 @@ def __init__(self, hdr: bytes | str) -> None: class BadStatusLine(BadHttpMessage): + args: tuple[str] + def __init__(self, line: str = "", error: str | None = None) -> None: if not isinstance(line, str): line = repr(line) diff --git a/tests/test_client_exceptions.py b/tests/test_client_exceptions.py index e5a8d73ca10..67e64d1c5de 100644 --- a/tests/test_client_exceptions.py +++ b/tests/test_client_exceptions.py @@ -2,6 +2,7 @@ import errno import pickle +import sys from unittest import mock import pytest @@ -9,6 +10,11 @@ from yarl import URL from aiohttp import client, client_reqrep +from aiohttp.http_parser import RawResponseMessage +from aiohttp.typedefs import StrOrURL + +if sys.version_info >= (3, 11): + from typing import assert_type class TestClientResponseError: @@ -22,6 +28,10 @@ class TestClientResponseError: def test_default_status(self) -> None: err = client.ClientResponseError(history=(), request_info=self.request_info) assert err.status == 0 + if sys.version_info >= (3, 11): + assert_type( + err.args, tuple[client.RequestInfo, tuple[client.ClientResponse, ...]] + ) def test_status(self) -> None: err = client.ClientResponseError( @@ -138,6 +148,8 @@ def test_ctor(self) -> None: assert err.host == "example.com" assert err.port == 8080 assert err.ssl is True + if sys.version_info >= (3, 11): + assert_type(err.args, tuple[client_reqrep.ConnectionKey, OSError]) def test_pickle(self) -> None: err = client.ClientConnectorError( @@ -196,6 +208,8 @@ def test_ctor(self) -> None: assert err.host == "example.com" assert err.port == 8080 assert err.ssl is False + if sys.version_info >= (3, 11): + assert_type(err.args, tuple[client_reqrep.ConnectionKey, Exception]) def test_pickle(self) -> None: certificate_error = Exception("Bad certificate") @@ -249,6 +263,8 @@ def test_ctor(self) -> None: err = client.ServerDisconnectedError(message="No connection") assert err.message == "No connection" + if sys.version_info >= (3, 11): + assert_type(err.args, tuple[RawResponseMessage | str]) def test_pickle(self) -> None: err = client.ServerDisconnectedError(message="No connection") @@ -283,6 +299,8 @@ def test_ctor(self) -> None: assert err.got == b"got" assert err.host == "example.com" assert err.port == 8080 + if sys.version_info >= (3, 11): + assert_type(err.args, tuple[bytes, bytes, str, int]) def test_pickle(self) -> None: err = client.ServerFingerprintMismatch( @@ -311,6 +329,8 @@ def test_ctor(self) -> None: err = client.InvalidURL(url=":wrong:url:", description=":description:") assert err.url == ":wrong:url:" assert err.description == ":description:" + if sys.version_info >= (3, 11): + assert_type(err.args, tuple[StrOrURL] | tuple[StrOrURL, str]) def test_pickle(self) -> None: err = client.InvalidURL(url=":wrong:url:") diff --git a/tests/test_http_exceptions.py b/tests/test_http_exceptions.py index 6186a71c681..38d388add8b 100644 --- a/tests/test_http_exceptions.py +++ b/tests/test_http_exceptions.py @@ -1,9 +1,13 @@ # Tests for http_exceptions.py import pickle +import sys from aiohttp import http_exceptions +if sys.version_info >= (3, 11): + from typing import assert_type + class TestHttpProcessingError: def test_ctor(self) -> None: @@ -73,6 +77,8 @@ def test_ctor(self) -> None: assert err.code == 400 assert err.message == "Got more than 10 bytes when reading: b'spam'." assert err.headers is None + if sys.version_info >= (3, 11): + assert_type(err.args, tuple[bytes, int]) def test_pickle(self) -> None: err = http_exceptions.LineTooLong(line=b"spam", limit=10, actual_size="12") @@ -104,6 +110,8 @@ def test_ctor(self) -> None: assert err.code == 400 assert err.message == "Invalid HTTP header: 'X-Spam'" assert err.headers is None + if sys.version_info >= (3, 11): + assert_type(err.args, tuple[bytes | str]) def test_pickle(self) -> None: err = http_exceptions.InvalidHeader(hdr="X-Spam") @@ -131,6 +139,8 @@ def test_ctor(self) -> None: err = http_exceptions.BadStatusLine("Test") assert err.line == "Test" assert str(err) == "400, message:\n Bad status line 'Test'" + if sys.version_info >= (3, 11): + assert_type(err.args, tuple[str]) def test_ctor2(self) -> None: err = http_exceptions.BadStatusLine(b"") From 5c6ac4c937f347742d72c5c330318646bcd60cb5 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sat, 13 Jun 2026 16:44:35 +0100 Subject: [PATCH 022/137] =?UTF-8?q?explicitly=20throw=20error=20for=20sock?= =?UTF-8?q?s5=20proxy=20since=20is=20not=20supported=20suppor=E2=80=A6=20(?= =?UTF-8?q?#12919)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …ted currently and will be implied disconnected if connect to (#10147) (cherry picked from commit 31702b2532c74e3dec4c9ffd9465343f7a2fd37a) --------- Co-authored-by: NewUserHa <32261870+NewUserHa@users.noreply.github.com> --- aiohttp/client.py | 9 ++------- aiohttp/client_reqrep.py | 7 +++++++ aiohttp/connector.py | 8 +------- aiohttp/helpers.py | 7 +++++++ tests/test_client_request.py | 8 ++++++++ 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/aiohttp/client.py b/aiohttp/client.py index c0a616edd6b..0bec36e072f 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -80,19 +80,14 @@ ClientWebSocketResponse as ClientWebSocketResponse, ClientWSTimeout as ClientWSTimeout, ) -from .connector import ( - HTTP_AND_EMPTY_SCHEMA_SET, - BaseConnector as BaseConnector, - NamedPipeConnector as NamedPipeConnector, - TCPConnector as TCPConnector, - UnixConnector as UnixConnector, -) +from .connector import BaseConnector, NamedPipeConnector, TCPConnector, UnixConnector from .cookiejar import CookieJar from .helpers import ( _SENTINEL, DEBUG, DEFAULT_CHUNK_SIZE, EMPTY_BODY_METHODS, + HTTP_AND_EMPTY_SCHEMA_SET, BasicAuth, TimeoutHandle, basicauth_from_netrc, diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 8d99cc9818f..684b1e77ae7 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -36,6 +36,7 @@ from .formdata import FormData from .helpers import ( _SENTINEL, + HTTP_AND_EMPTY_SCHEMA_SET, BaseTimerContext, BasicAuth, HeadersMixin, @@ -1337,6 +1338,12 @@ def update_proxy( self.proxy_headers = None return + if proxy.scheme not in HTTP_AND_EMPTY_SCHEMA_SET: + raise ValueError( + f"aiohttp only supports http(s) proxies (got: {proxy.scheme!r}).\n" + "See third-party libraries for other proxy schemes." + ) + if proxy_auth and not isinstance(proxy_auth, helpers.BasicAuth): raise ValueError("proxy_auth must be None or BasicAuth() tuple") self.proxy_auth = proxy_auth diff --git a/aiohttp/connector.py b/aiohttp/connector.py index b7aa7b7647a..438c336da7f 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -37,6 +37,7 @@ from .client_reqrep import ClientRequest, Fingerprint, _merge_ssl_params from .helpers import ( _SENTINEL, + HIGH_LEVEL_SCHEMA_SET, ceil_timeout, is_canonical_ipv4_address, is_ip_address, @@ -66,13 +67,6 @@ ssl = None # type: ignore[assignment] SSLContext = object # type: ignore[misc,assignment] -EMPTY_SCHEMA_SET = frozenset({""}) -HTTP_SCHEMA_SET = frozenset({"http", "https"}) -WS_SCHEMA_SET = frozenset({"ws", "wss"}) - -HTTP_AND_EMPTY_SCHEMA_SET = HTTP_SCHEMA_SET | EMPTY_SCHEMA_SET -HIGH_LEVEL_SCHEMA_SET = HTTP_AND_EMPTY_SCHEMA_SET | WS_SCHEMA_SET - NEEDS_CLEANUP_CLOSED = (3, 13, 0) <= sys.version_info < ( 3, 13, diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 469c99dd63c..f86a7387af0 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -83,6 +83,13 @@ ) +EMPTY_SCHEMA_SET = frozenset({""}) +HTTP_SCHEMA_SET = frozenset({"http", "https"}) +WS_SCHEMA_SET = frozenset({"ws", "wss"}) +HTTP_AND_EMPTY_SCHEMA_SET = HTTP_SCHEMA_SET | EMPTY_SCHEMA_SET +HIGH_LEVEL_SCHEMA_SET = HTTP_AND_EMPTY_SCHEMA_SET | WS_SCHEMA_SET + + CHAR = {chr(i) for i in range(0, 128)} CTL = {chr(i) for i in range(0, 32)} | { chr(127), diff --git a/tests/test_client_request.py b/tests/test_client_request.py index 90ffcecb5d8..93920055c4d 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -220,6 +220,14 @@ def test_hostname_err(make_request) -> None: make_request("get", "http://:8080/") +@pytest.mark.parametrize("scheme", ("socks5", "socks5h")) +async def test_proxy_scheme_err(make_request: _RequestMaker, scheme: str) -> None: + with pytest.raises(ValueError, match=f"'{scheme}'"): + make_request( + "get", URL("http://py.org/"), proxy=URL(f"{scheme}://127.0.0.1:80") + ) + + def test_host_header_host_first(make_request) -> None: req = make_request("get", "http://python.org/") assert list(req.headers)[0] == hdrs.HOST From 197fdbb975fa1079e673b3c733c9ec105859d39f Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 18:51:08 +0100 Subject: [PATCH 023/137] [PR #12914/de1aded8 backport][3.15] Add incident response plan (#12924) **This is a backport of PR #12914 as merged into master (de1aded81301aba0f96471904df1df97683f9e18).** Co-authored-by: Sam Bull --- CHANGES/12914.contrib.rst | 3 + docs/contributing-admins.rst | 297 +++++++++++++++++++++++++++++++++++ docs/spelling_wordlist.txt | 3 + 3 files changed, 303 insertions(+) create mode 100644 CHANGES/12914.contrib.rst diff --git a/CHANGES/12914.contrib.rst b/CHANGES/12914.contrib.rst new file mode 100644 index 00000000000..5ef030c2786 --- /dev/null +++ b/CHANGES/12914.contrib.rst @@ -0,0 +1,3 @@ +Added admin documentation on incident response and on running reproducer code +safely, covering security vulnerability handling and supply-chain, account, and +CI/infrastructure compromise -- by :user:`Dreamsorcerer`. diff --git a/docs/contributing-admins.rst b/docs/contributing-admins.rst index 749e1f87da1..2fffc965fce 100644 --- a/docs/contributing-admins.rst +++ b/docs/contributing-admins.rst @@ -8,6 +8,19 @@ For regular contributors, return to :doc:`contributing`. .. contents:: :local: + :depth: 1 + +.. highlight:: none + +Running reproducer code +----------------------- + +.. warning:: + + When evaluating a bug report or vulnerability report, treat reproducer code as + untrusted. If you don't understand what it does or are unfamiliar with a library + it imports **do not run it** (and ask the reporter to provide a simpler reproducer). + We also recommend that any reproducers you do run are executed in a container. Creating a new release ---------------------- @@ -60,3 +73,287 @@ If doing a minor release: #. Update both ``target-branch`` backports for Dependabot to reference the new branch name in ``.github/dependabot.yml``. #. Delete the older backport label (e.g. backport-3.8): https://github.com/aio-libs/aiohttp/labels #. Add a new backport label (e.g. backport-3.10). + +Incident response +----------------- + +This section covers responding to a reported security vulnerability and to +three classes of compromise -- of the supply chain, of a maintainer account, +and of the project infrastructure. It picks up *after* a report or a problem +is in hand. + +.. note:: + + Vulnerability reports arrive through the organization's ``SECURITY.md`` + -- GitHub private vulnerability reporting, or email to the security coordinators. + Never triage a suspected vulnerability in a public issue or pull request. + +The security coordinator who first triages a report is its *incident lead*, +and may explicitly hand off to another coordinator. The lead owns the GitHub +Security Advisory (GHSA) draft, fix coordination, the release, and notification. + +Severity tiers +~~~~~~~~~~~~~~ + +aiohttp is volunteer-maintained, so there is no response-time commitment. The +tier guides prioritization, the release decision, and how widely the fix is +announced. All security fixes -- regardless of tier -- land on ``master`` and +are backported to the currently supported ``x.y`` branch (and the next +``x.y`` branch, if one is in development). + +High + A concrete remote impact an attacker can actually achieve against a + default deployment. Examples: a single request that stops the server from + handling further requests (a server-wide denial of service), reading files + on the server outside the project root, remote code execution, + authentication bypass. Cut a dedicated security release once the fix lands. + +Medium + Bounded impact, or impact that needs a non-default option, an unusual + configuration, or a local position. Examples: request smuggling demonstrated + to cause a real issue against a common proxy, or a DoS that use significant server + resources with a low effort sustained attack. + Cut a dedicated security release. + +Low + Limited or hard-to-exploit impact, or impact confined to debug or + non-default paths. Examples: minor version or error-message disclosure, + parser leniency with no demonstrated security impact, or request smuggling + that has not been demonstrated to cause a real issue against a common + proxy. May ride the next routine release rather than cutting a dedicated + security release. + +Severity is rated by demonstrated attacker impact. A denial of service that +disables further request handling for the whole server from a single request +leans High; one that requires sustained low-effort traffic to consume +significant server resources leans Medium. Request smuggling and parser bugs +are rated by what an attacker can actually achieve against a common proxy or +in a realistic deployment, not by the shape of the bug. + +Responding to a reported vulnerability +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#. **Acknowledge and confirm.** Reply to the reporter in the GitHub private + report or the email thread. Reproduce the issue + (`but don't run untrusted code `_) against + ``master`` and the maintained ``x.y`` branches. If it is not reproducible + or is out of scope, record the decision and close the report. +#. **Open the GHSA draft.** For an email report, if the reporter can't use Github + for some reason, then open a new advisory to track the issue. +#. **Develop the fix privately** in the GHSA's temporary private fork ("Start a + temporary private fork" in the draft) -- never on a public branch or in a + public pull request. Include a regression test unless it would provide too + much information for an attacker to replicate. +#. **Title the advisory** with the vulnerability class and affected component. +#. **Write the description** using the GHSA description template:: + + ### Summary + + + + ### Impact + + + + ### Workaround + + + + --- + + Patch: + + An important detail that Github will use to decide the CVE severity is exploit + maturity. Try to clarify the likelihood of a vulnerability being attacked + (e.g. if exploit code is available, or active attacks in-the-wild already exist). +#. **Set the affected and fixed versions**. +#. **Assign a severity tier** (see `Severity tiers`_). +#. **Credit the reporter** (if report came by email) in the GHSA Credits field. +#. **Credit the developer** using "Remediation developer" if different from Reporter. +#. **Request a CVE** by clicking the button in the advisory. +#. **Get the fix reviewed** in the private fork by at least one other maintainer. +#. **Consider early notify** for high severity issues + (see `Notifying about a disclosed vulnerability`_). +#. **Coordinate a release.** + + #. **Agree the timing.** For an embargoed high-severity incident, set the + release date to align with the lift of any private-list embargo. + #. **Merge all the private forks** and create and merge the backports for + each. + #. **Create the release.** Follow `Creating a new release`_. +#. **Update patch link** in GHSA description. +#. **Publish the GHSA** usually around 1 day after release. +#. **Notify** according to the severity tier + (see `Notifying about a disclosed vulnerability`_). +#. **Run the post-incident steps** (see `Post-incident`_). + +Notifying about a disclosed vulnerability +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Notification is cumulative -- each tier adds to the ones below it. Mind the +timing: the ``linux-distros`` pre-notification happens under embargo, *before* +the release, while everything else happens at or after the release. + +Baseline (all severities) + The published GHSA (which feeds the CVE and the GitHub Advisory Database). + +Higher (Medium-High) + For most high severity issues, additionally post to the public ``oss-security`` + mailing list once the advisory is public. + +Highest (High-Critical) + For the most serious issues, additionally *before* the public release, send + an embargoed pre-notification to the private distribution-security lists -- primarily + ``linux-distros`` -- and attempt to notify affected downstreams. Use the + `vuln_search.py dependents-enumeration script + `_ + to search aiohttp's GitHub dependents; it needs a GitHub token and may need to be + run over a few days, so start it early. + +Keep the embargo as short as practical. Typically an embargo of 1 or 2 days is expected. + +Template for the embargoed ``linux-distros`` pre-notification:: + + To: linux-distros@vs.openwall.org + Subject: aiohttp: + + We are coordinating disclosure of a security vulnerability in aiohttp + (https://github.com/aio-libs/aiohttp), the asyncio HTTP client/server + library. + + Summary + <2-3 sentences: the flaw and its impact> + + Affected versions + + + CVE + + + Fix + + + Proposed public disclosure date + -- as short as practical. On that date we will publish the + GitHub Security Advisory, ship a patched PyPI release, and post to + oss-security. + + This issue is not yet public; please observe the embargo until that date. + + Contact: + +Template for the public ``oss-security`` disclosure:: + + To: oss-security@lists.openwall.com + Subject: CVE-XXXX-XXXXX: aiohttp + + A security vulnerability has been fixed in aiohttp, the asyncio HTTP + client/server library (https://github.com/aio-libs/aiohttp). + + CVE: CVE-XXXX-XXXXX + Advisory: + + Affected versions: <...> + Fixed versions: <...> + + Description + + + Impact + + + Mitigation + "> + +Post-incident +~~~~~~~~~~~~~ + +#. **Update** ``THREAT_MODEL.md``. Have an AI coding agent fetch the new advisories + and update the document. Review the changes. +#. **File follow-up hardening** as ordinary public issues or pull requests. + +Supply-chain or release compromise +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A malicious or tampered release on PyPI, or a compromised publish pipeline. + +#. **Pull the bad artifact.** On PyPI, *yank* the affected release -- this + hides it from new resolves while keeping existing pins working. *Delete* it + only if it is actively malicious and removal is the safer choice. This needs + a PyPI project owner or maintainer account. +#. **Verify the legitimate artifacts with Sigstore.** Every genuine release is + signed by the release pipeline, with the ``.sigstore`` bundles attached to + the GitHub Release. Verify the ``sdist`` and wheels; a divergence from what is on + PyPI localizes the tampering. +#. **Lock the publish path.** Publishing uses a PyPI OIDC trusted publisher, so + there is no long-lived token to rotate. Instead, on PyPI temporarily remove + the trusted-publisher binding, and on GitHub restrict or pause the ``pypi`` + deployment environment used by the release job. +#. **Audit the release inputs.** Review recent changes to the release workflow, + its tag trigger, and the third-party action versions it uses. +#. **Audit the committed Cython sources.** The generated ``.c`` files ship in + the ``sdist`` and are not checked against their ``.pyx`` sources in CI (see + ``THREAT_MODEL.md`` section 5.19). Regenerate them with ``make cythonize`` + and ``git diff`` against the released revision. Re-verify the + ``vendor/llhttp`` pin and ``package-lock.json``. +#. **Reissue a clean release** via `Creating a new release`_ once the cause is + fixed. Never re-publish over a yanked version number. +#. **Notify** per severity -- a malicious published release is High. + +Maintainer account compromise +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A GitHub or PyPI account takeover, or leaked publishing or signing material. + +#. **Suspend the account.** Another GitHub organization owner can suspend it, + revoke its repository access, and start the audit-log review. If the + compromised account is the sole organization owner, or no other owner is + reachable, contact GitHub Support at https://support.github.com/contact + for emergency account lock down and organization recovery. +#. **Kill sessions and credentials** on the affected account. On GitHub: change + the password, sign out all sessions, revoke every personal access token, + OAuth app, and SSH or GPG key, then re-enroll two-factor authentication. + On PyPI: change the password, re-enroll two-factor authentication, + and revoke API tokens. +#. **Audit PyPI project ownership.** OIDC publishing means there is usually no + long-lived PyPI token to leak, but a compromised project owner can add a + trusted publisher or upload directly. Review the project collaborators and + trusted-publisher bindings, and remove anything unrecognized. +#. **Audit what the account could have done** from the GitHub audit log: + pushes, tag creation, branch-protection or settings changes, new secrets or + deploy keys, new collaborators, and workflow edits. +#. **Revert and re-verify.** Force-revert any unauthorized commits or tags, and + re-verify recent releases against their ``Sigstore`` bundles. If a malicious + release shipped, escalate to `Supply-chain or release compromise`_. +#. **Handle signing material.** ``Sigstore`` signing is key-less, so there is no + static signing key to rotate. Any separate GPG key used for signed tags must + be treated as compromised, then revoked and rotated. + +CI or infrastructure compromise +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A compromise of GitHub Actions, repository settings, or branch protections. + +#. **Freeze the release path.** Pause the ``pypi`` deployment environment and + stop tag-triggered deploys until the compromise is scoped. +#. **Review branch protection and required reviews** on ``master`` and the + ``x.y`` branches. These are enforced on GitHub and are not visible in the + repository; confirm nothing was relaxed and restore the known-good + configuration. +#. **Audit Actions secrets and environments** -- the repository and + organization secrets, the ``pypi`` environment's protection rules and + reviewers, and any new deploy keys. Confirm the workflow keeps empty + top-level permissions and that ``id-token: write`` is scoped to the deploy + job only. +#. **Diff recently changed workflows** with ``git log -- .github/workflows/``, + especially the release workflow, the auto-merge workflow (which runs in the + privileged ``pull_request_target`` context), and the CodeQL workflow. Look + for added steps, changed action references, or new triggers. +#. **Check the committed Cython sources** as in `Supply-chain or release + compromise`_ -- regenerate them and ``git diff``. +#. **Re-pin and rebuild.** If a third-party action was compromised, pin the + affected actions by full commit hash, re-run CI from a clean known-good + revision, and re-verify with ``Sigstore`` any release made during the suspected + window. +#. **Escalate** to `Supply-chain or release compromise`_ if a release shipped + through the compromised pipeline. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 920fc7f17fa..9ff16a5df2a 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -35,6 +35,7 @@ backoff backend backends backport +backported Backport Backporting backports @@ -319,6 +320,7 @@ seekable sendfile serializable serializer +severities shourtcuts skipuntil Skyscanner @@ -360,6 +362,7 @@ toolbar toplevel towncrier tp +triages tuples ue UI From f0b55124fcef4519960748af1933daac4c5e174a Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 18:51:21 +0100 Subject: [PATCH 024/137] [PR #12914/de1aded8 backport][3.14] Add incident response plan (#12923) **This is a backport of PR #12914 as merged into master (de1aded81301aba0f96471904df1df97683f9e18).** Co-authored-by: Sam Bull --- CHANGES/12914.contrib.rst | 3 + docs/contributing-admins.rst | 297 +++++++++++++++++++++++++++++++++++ docs/spelling_wordlist.txt | 3 + 3 files changed, 303 insertions(+) create mode 100644 CHANGES/12914.contrib.rst diff --git a/CHANGES/12914.contrib.rst b/CHANGES/12914.contrib.rst new file mode 100644 index 00000000000..5ef030c2786 --- /dev/null +++ b/CHANGES/12914.contrib.rst @@ -0,0 +1,3 @@ +Added admin documentation on incident response and on running reproducer code +safely, covering security vulnerability handling and supply-chain, account, and +CI/infrastructure compromise -- by :user:`Dreamsorcerer`. diff --git a/docs/contributing-admins.rst b/docs/contributing-admins.rst index 749e1f87da1..2fffc965fce 100644 --- a/docs/contributing-admins.rst +++ b/docs/contributing-admins.rst @@ -8,6 +8,19 @@ For regular contributors, return to :doc:`contributing`. .. contents:: :local: + :depth: 1 + +.. highlight:: none + +Running reproducer code +----------------------- + +.. warning:: + + When evaluating a bug report or vulnerability report, treat reproducer code as + untrusted. If you don't understand what it does or are unfamiliar with a library + it imports **do not run it** (and ask the reporter to provide a simpler reproducer). + We also recommend that any reproducers you do run are executed in a container. Creating a new release ---------------------- @@ -60,3 +73,287 @@ If doing a minor release: #. Update both ``target-branch`` backports for Dependabot to reference the new branch name in ``.github/dependabot.yml``. #. Delete the older backport label (e.g. backport-3.8): https://github.com/aio-libs/aiohttp/labels #. Add a new backport label (e.g. backport-3.10). + +Incident response +----------------- + +This section covers responding to a reported security vulnerability and to +three classes of compromise -- of the supply chain, of a maintainer account, +and of the project infrastructure. It picks up *after* a report or a problem +is in hand. + +.. note:: + + Vulnerability reports arrive through the organization's ``SECURITY.md`` + -- GitHub private vulnerability reporting, or email to the security coordinators. + Never triage a suspected vulnerability in a public issue or pull request. + +The security coordinator who first triages a report is its *incident lead*, +and may explicitly hand off to another coordinator. The lead owns the GitHub +Security Advisory (GHSA) draft, fix coordination, the release, and notification. + +Severity tiers +~~~~~~~~~~~~~~ + +aiohttp is volunteer-maintained, so there is no response-time commitment. The +tier guides prioritization, the release decision, and how widely the fix is +announced. All security fixes -- regardless of tier -- land on ``master`` and +are backported to the currently supported ``x.y`` branch (and the next +``x.y`` branch, if one is in development). + +High + A concrete remote impact an attacker can actually achieve against a + default deployment. Examples: a single request that stops the server from + handling further requests (a server-wide denial of service), reading files + on the server outside the project root, remote code execution, + authentication bypass. Cut a dedicated security release once the fix lands. + +Medium + Bounded impact, or impact that needs a non-default option, an unusual + configuration, or a local position. Examples: request smuggling demonstrated + to cause a real issue against a common proxy, or a DoS that use significant server + resources with a low effort sustained attack. + Cut a dedicated security release. + +Low + Limited or hard-to-exploit impact, or impact confined to debug or + non-default paths. Examples: minor version or error-message disclosure, + parser leniency with no demonstrated security impact, or request smuggling + that has not been demonstrated to cause a real issue against a common + proxy. May ride the next routine release rather than cutting a dedicated + security release. + +Severity is rated by demonstrated attacker impact. A denial of service that +disables further request handling for the whole server from a single request +leans High; one that requires sustained low-effort traffic to consume +significant server resources leans Medium. Request smuggling and parser bugs +are rated by what an attacker can actually achieve against a common proxy or +in a realistic deployment, not by the shape of the bug. + +Responding to a reported vulnerability +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +#. **Acknowledge and confirm.** Reply to the reporter in the GitHub private + report or the email thread. Reproduce the issue + (`but don't run untrusted code `_) against + ``master`` and the maintained ``x.y`` branches. If it is not reproducible + or is out of scope, record the decision and close the report. +#. **Open the GHSA draft.** For an email report, if the reporter can't use Github + for some reason, then open a new advisory to track the issue. +#. **Develop the fix privately** in the GHSA's temporary private fork ("Start a + temporary private fork" in the draft) -- never on a public branch or in a + public pull request. Include a regression test unless it would provide too + much information for an attacker to replicate. +#. **Title the advisory** with the vulnerability class and affected component. +#. **Write the description** using the GHSA description template:: + + ### Summary + + + + ### Impact + + + + ### Workaround + + + + --- + + Patch: + + An important detail that Github will use to decide the CVE severity is exploit + maturity. Try to clarify the likelihood of a vulnerability being attacked + (e.g. if exploit code is available, or active attacks in-the-wild already exist). +#. **Set the affected and fixed versions**. +#. **Assign a severity tier** (see `Severity tiers`_). +#. **Credit the reporter** (if report came by email) in the GHSA Credits field. +#. **Credit the developer** using "Remediation developer" if different from Reporter. +#. **Request a CVE** by clicking the button in the advisory. +#. **Get the fix reviewed** in the private fork by at least one other maintainer. +#. **Consider early notify** for high severity issues + (see `Notifying about a disclosed vulnerability`_). +#. **Coordinate a release.** + + #. **Agree the timing.** For an embargoed high-severity incident, set the + release date to align with the lift of any private-list embargo. + #. **Merge all the private forks** and create and merge the backports for + each. + #. **Create the release.** Follow `Creating a new release`_. +#. **Update patch link** in GHSA description. +#. **Publish the GHSA** usually around 1 day after release. +#. **Notify** according to the severity tier + (see `Notifying about a disclosed vulnerability`_). +#. **Run the post-incident steps** (see `Post-incident`_). + +Notifying about a disclosed vulnerability +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Notification is cumulative -- each tier adds to the ones below it. Mind the +timing: the ``linux-distros`` pre-notification happens under embargo, *before* +the release, while everything else happens at or after the release. + +Baseline (all severities) + The published GHSA (which feeds the CVE and the GitHub Advisory Database). + +Higher (Medium-High) + For most high severity issues, additionally post to the public ``oss-security`` + mailing list once the advisory is public. + +Highest (High-Critical) + For the most serious issues, additionally *before* the public release, send + an embargoed pre-notification to the private distribution-security lists -- primarily + ``linux-distros`` -- and attempt to notify affected downstreams. Use the + `vuln_search.py dependents-enumeration script + `_ + to search aiohttp's GitHub dependents; it needs a GitHub token and may need to be + run over a few days, so start it early. + +Keep the embargo as short as practical. Typically an embargo of 1 or 2 days is expected. + +Template for the embargoed ``linux-distros`` pre-notification:: + + To: linux-distros@vs.openwall.org + Subject: aiohttp: + + We are coordinating disclosure of a security vulnerability in aiohttp + (https://github.com/aio-libs/aiohttp), the asyncio HTTP client/server + library. + + Summary + <2-3 sentences: the flaw and its impact> + + Affected versions + + + CVE + + + Fix + + + Proposed public disclosure date + -- as short as practical. On that date we will publish the + GitHub Security Advisory, ship a patched PyPI release, and post to + oss-security. + + This issue is not yet public; please observe the embargo until that date. + + Contact: + +Template for the public ``oss-security`` disclosure:: + + To: oss-security@lists.openwall.com + Subject: CVE-XXXX-XXXXX: aiohttp + + A security vulnerability has been fixed in aiohttp, the asyncio HTTP + client/server library (https://github.com/aio-libs/aiohttp). + + CVE: CVE-XXXX-XXXXX + Advisory: + + Affected versions: <...> + Fixed versions: <...> + + Description + + + Impact + + + Mitigation + "> + +Post-incident +~~~~~~~~~~~~~ + +#. **Update** ``THREAT_MODEL.md``. Have an AI coding agent fetch the new advisories + and update the document. Review the changes. +#. **File follow-up hardening** as ordinary public issues or pull requests. + +Supply-chain or release compromise +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A malicious or tampered release on PyPI, or a compromised publish pipeline. + +#. **Pull the bad artifact.** On PyPI, *yank* the affected release -- this + hides it from new resolves while keeping existing pins working. *Delete* it + only if it is actively malicious and removal is the safer choice. This needs + a PyPI project owner or maintainer account. +#. **Verify the legitimate artifacts with Sigstore.** Every genuine release is + signed by the release pipeline, with the ``.sigstore`` bundles attached to + the GitHub Release. Verify the ``sdist`` and wheels; a divergence from what is on + PyPI localizes the tampering. +#. **Lock the publish path.** Publishing uses a PyPI OIDC trusted publisher, so + there is no long-lived token to rotate. Instead, on PyPI temporarily remove + the trusted-publisher binding, and on GitHub restrict or pause the ``pypi`` + deployment environment used by the release job. +#. **Audit the release inputs.** Review recent changes to the release workflow, + its tag trigger, and the third-party action versions it uses. +#. **Audit the committed Cython sources.** The generated ``.c`` files ship in + the ``sdist`` and are not checked against their ``.pyx`` sources in CI (see + ``THREAT_MODEL.md`` section 5.19). Regenerate them with ``make cythonize`` + and ``git diff`` against the released revision. Re-verify the + ``vendor/llhttp`` pin and ``package-lock.json``. +#. **Reissue a clean release** via `Creating a new release`_ once the cause is + fixed. Never re-publish over a yanked version number. +#. **Notify** per severity -- a malicious published release is High. + +Maintainer account compromise +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A GitHub or PyPI account takeover, or leaked publishing or signing material. + +#. **Suspend the account.** Another GitHub organization owner can suspend it, + revoke its repository access, and start the audit-log review. If the + compromised account is the sole organization owner, or no other owner is + reachable, contact GitHub Support at https://support.github.com/contact + for emergency account lock down and organization recovery. +#. **Kill sessions and credentials** on the affected account. On GitHub: change + the password, sign out all sessions, revoke every personal access token, + OAuth app, and SSH or GPG key, then re-enroll two-factor authentication. + On PyPI: change the password, re-enroll two-factor authentication, + and revoke API tokens. +#. **Audit PyPI project ownership.** OIDC publishing means there is usually no + long-lived PyPI token to leak, but a compromised project owner can add a + trusted publisher or upload directly. Review the project collaborators and + trusted-publisher bindings, and remove anything unrecognized. +#. **Audit what the account could have done** from the GitHub audit log: + pushes, tag creation, branch-protection or settings changes, new secrets or + deploy keys, new collaborators, and workflow edits. +#. **Revert and re-verify.** Force-revert any unauthorized commits or tags, and + re-verify recent releases against their ``Sigstore`` bundles. If a malicious + release shipped, escalate to `Supply-chain or release compromise`_. +#. **Handle signing material.** ``Sigstore`` signing is key-less, so there is no + static signing key to rotate. Any separate GPG key used for signed tags must + be treated as compromised, then revoked and rotated. + +CI or infrastructure compromise +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A compromise of GitHub Actions, repository settings, or branch protections. + +#. **Freeze the release path.** Pause the ``pypi`` deployment environment and + stop tag-triggered deploys until the compromise is scoped. +#. **Review branch protection and required reviews** on ``master`` and the + ``x.y`` branches. These are enforced on GitHub and are not visible in the + repository; confirm nothing was relaxed and restore the known-good + configuration. +#. **Audit Actions secrets and environments** -- the repository and + organization secrets, the ``pypi`` environment's protection rules and + reviewers, and any new deploy keys. Confirm the workflow keeps empty + top-level permissions and that ``id-token: write`` is scoped to the deploy + job only. +#. **Diff recently changed workflows** with ``git log -- .github/workflows/``, + especially the release workflow, the auto-merge workflow (which runs in the + privileged ``pull_request_target`` context), and the CodeQL workflow. Look + for added steps, changed action references, or new triggers. +#. **Check the committed Cython sources** as in `Supply-chain or release + compromise`_ -- regenerate them and ``git diff``. +#. **Re-pin and rebuild.** If a third-party action was compromised, pin the + affected actions by full commit hash, re-run CI from a clean known-good + revision, and re-verify with ``Sigstore`` any release made during the suspected + window. +#. **Escalate** to `Supply-chain or release compromise`_ if a release shipped + through the compromised pipeline. diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index a62ffed4ae9..24561f52d67 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -35,6 +35,7 @@ backoff backend backends backport +backported Backport Backporting backports @@ -318,6 +319,7 @@ seekable sendfile serializable serializer +severities shourtcuts skipuntil Skyscanner @@ -359,6 +361,7 @@ toolbar toplevel towncrier tp +triages tuples ue UI From f5c10beff9e5165dc53ec9d140c37b058c3a22bf Mon Sep 17 00:00:00 2001 From: Taras Kozlov Date: Sun, 14 Jun 2026 20:28:25 +0200 Subject: [PATCH 025/137] [PR #12913/c2f4ac403 backport][3.15] Parameterize test_simple_web_file_response benchmarks by file size (#12926) --- CHANGES/12913.misc.rst | 2 + tests/test_benchmarks_web_fileresponse.py | 49 +++++++++++++++++------ 2 files changed, 39 insertions(+), 12 deletions(-) create mode 100644 CHANGES/12913.misc.rst diff --git a/CHANGES/12913.misc.rst b/CHANGES/12913.misc.rst new file mode 100644 index 00000000000..e2c2673bcaa --- /dev/null +++ b/CHANGES/12913.misc.rst @@ -0,0 +1,2 @@ +Parameterized test_simple_web_file_response and test_simple_web_file_response_fallback benchmarks by file size (small, large) +Previously, benchmarks only ran for a small (around 11 kb) file. -- by :user:`tarasko`. diff --git a/tests/test_benchmarks_web_fileresponse.py b/tests/test_benchmarks_web_fileresponse.py index 5f06bb71caa..09df89daa01 100644 --- a/tests/test_benchmarks_web_fileresponse.py +++ b/tests/test_benchmarks_web_fileresponse.py @@ -1,7 +1,9 @@ """codspeed benchmarks for the web file responses.""" import asyncio +import os import pathlib +from dataclasses import dataclass from typing import TYPE_CHECKING import pytest @@ -17,26 +19,48 @@ BenchmarkFixture = pytest_codspeed.BenchmarkFixture +@dataclass(frozen=True) +class BenchmarkFile: + path: pathlib.Path + response_count: int + + +@pytest.fixture( + params=((10 * 1024, 100), (1024 * 1024, 10)), + ids=("small", "large"), +) +def benchmark_file( + request: pytest.FixtureRequest, tmp_path: pathlib.Path +) -> BenchmarkFile: + size, response_count = request.param + filepath = tmp_path / "sample.txt" + filepath.touch() + os.truncate(filepath, size) + return BenchmarkFile(filepath, response_count) + + def test_simple_web_file_response( loop: asyncio.AbstractEventLoop, aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, conn_type: ConnectionType, + benchmark_file: BenchmarkFile, ) -> None: - """Benchmark creating 100 simple web.FileResponse.""" - response_count = 100 - filepath = pathlib.Path(__file__).parent / "sample.txt" + """Benchmark simple web.FileResponse.""" async def handler(request: web.Request) -> web.FileResponse: - return web.FileResponse(path=filepath) + return web.FileResponse(path=benchmark_file.path) app = web.Application() app.router.add_route("GET", "/", handler) async def run_file_response_benchmark() -> None: client = await aiohttp_client(app, server_kwargs=conn_type.s_kwargs) - for _ in range(response_count): - await client.get("/", **conn_type.c_kwargs) + for _ in range(benchmark_file.response_count): + response = await client.get("/", **conn_type.c_kwargs) + # Consume response. + # Large responses may leave transport unclosed on at least python 3.10. + await response.read() await client.close() @benchmark @@ -49,24 +73,25 @@ def test_simple_web_file_sendfile_fallback_response( aiohttp_client: AiohttpClient, benchmark: BenchmarkFixture, conn_type: ConnectionType, + benchmark_file: BenchmarkFile, ) -> None: - """Benchmark creating 100 simple web.FileResponse without sendfile.""" - response_count = 100 - filepath = pathlib.Path(__file__).parent / "sample.txt" + """Benchmark simple web.FileResponse without sendfile.""" async def handler(request: web.Request) -> web.FileResponse: transport = request.transport assert transport is not None transport._sendfile_compatible = False # type: ignore[attr-defined] - return web.FileResponse(path=filepath) + return web.FileResponse(path=benchmark_file.path) app = web.Application() app.router.add_route("GET", "/", handler) async def run_file_response_benchmark() -> None: client = await aiohttp_client(app, server_kwargs=conn_type.s_kwargs) - for _ in range(response_count): - await client.get("/", **conn_type.c_kwargs) + + for _ in range(benchmark_file.response_count): + response = await client.get("/", **conn_type.c_kwargs) + await response.read() await client.close() @benchmark From 8a0f750569cebe2bb1e5d0132587daa15f9325b1 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 14 Jun 2026 19:59:38 +0100 Subject: [PATCH 026/137] [PR #12928/23725105 backport][3.15] Create SECURITY_EXTRA.md (#12929) **This is a backport of PR #12928 as merged into master (2372510576b3bfeecb8438056288681a95536bb8).** Co-authored-by: Sam Bull --- SECURITY_EXTRA.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 SECURITY_EXTRA.md diff --git a/SECURITY_EXTRA.md b/SECURITY_EXTRA.md new file mode 100644 index 00000000000..f9b9da7c80a --- /dev/null +++ b/SECURITY_EXTRA.md @@ -0,0 +1,10 @@ +# Vulnerability reporting guidelines for aiohttp + +- Most reports need a reproducer that makes an HTTP request. Attackers do not + have direct access to aiohttp internals; a report must demonstrate how an + attacker can actually exploit it. +- Any report about excessive memory use must generally use a payload of + atlest 1 MiB and show that it completely bypasses any size restrictions + (asyncio reads upto 256 KiB from the socket at a time, so many parts of + aiohttp assume that 256 KiB are being loaded into memory all the time for + every request). From 7b899aa54de455b912fc7874d17c54bc8bad28d4 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 14 Jun 2026 20:07:31 +0100 Subject: [PATCH 027/137] Return ResourceRoute from .set_options_route() (#12917) (#12930) (cherry picked from commit 9ad62d093fafb2e64b589b1f23620bfedc799732) --- CHANGES/12917.feature.rst | 2 ++ aiohttp/web_urldispatcher.py | 6 ++++-- docs/web_reference.rst | 15 +++++++++++++++ tests/test_urldispatch.py | 6 +++++- 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 CHANGES/12917.feature.rst diff --git a/CHANGES/12917.feature.rst b/CHANGES/12917.feature.rst new file mode 100644 index 00000000000..773c2ff5d90 --- /dev/null +++ b/CHANGES/12917.feature.rst @@ -0,0 +1,2 @@ +Changed ``StaticResource.set_options_route()`` to return the +created ``ResourceRoute``, matching ``Resource.add_route`` -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/web_urldispatcher.py b/aiohttp/web_urldispatcher.py index 74351eecb06..b49307948e0 100644 --- a/aiohttp/web_urldispatcher.py +++ b/aiohttp/web_urldispatcher.py @@ -620,13 +620,15 @@ def get_info(self) -> _InfoDict: "routes": self._routes, } - def set_options_route(self, handler: Handler) -> None: + def set_options_route(self, handler: Handler) -> "ResourceRoute": if "OPTIONS" in self._routes: raise RuntimeError("OPTIONS route was set already") - self._routes["OPTIONS"] = ResourceRoute( + route = ResourceRoute( "OPTIONS", handler, self, expect_handler=self._expect_handler ) + self._routes["OPTIONS"] = route self._allowed_methods.add("OPTIONS") + return route async def resolve(self, request: Request) -> _Resolve: path = request.rel_url.path_safe diff --git a/docs/web_reference.rst b/docs/web_reference.rst index 5932d016536..811ed9fc97d 100644 --- a/docs/web_reference.rst +++ b/docs/web_reference.rst @@ -2368,6 +2368,21 @@ Resource classes hierarchy:: if file not found has no impact + .. method:: set_options_route(handler) + + Register *handler* as the ``OPTIONS`` route for this resource. + + Raises :exc:`RuntimeError` if an ``OPTIONS`` route was already set. + + :param handler: a :ref:`web-handler` for + ``OPTIONS`` requests. + + :return: the newly created :class:`ResourceRoute`. + + .. versionchanged:: 3.15 + + Now returns the created :class:`ResourceRoute` instead of ``None``. + .. class:: PrefixedSubAppResource diff --git a/tests/test_urldispatch.py b/tests/test_urldispatch.py index 8d1134ed8d1..49fbf2951c7 100644 --- a/tests/test_urldispatch.py +++ b/tests/test_urldispatch.py @@ -520,7 +520,11 @@ async def test_add_static_set_options_route(router: web.UrlDispatcher) -> None: async def handler(request: web.Request) -> NoReturn: assert False - resource.set_options_route(handler) + route = resource.set_options_route(handler) + assert isinstance(route, web.ResourceRoute) + assert route.method == hdrs.METH_OPTIONS + assert route.handler is handler + assert route.resource is resource mapping, allowed_methods = await resource.resolve( make_mocked_request("OPTIONS", "/st/path") ) From ec86af4b0dac66b692a23594f78c938ddcc2e2ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 10:55:03 +0000 Subject: [PATCH 028/137] Bump pypa/cibuildwheel from 4.0.0 to 4.1.0 (#12933) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 4.0.0 to 4.1.0.
Release notes

Sourced from pypa/cibuildwheel's releases.

v4.1.0

  • ✨ Updates Pyodide to the final 314.0.0 release, so Pyodide 3.14 wheels now build by default without the pyodide-prerelease enable flag. (#2906)
  • πŸ› Raises clear errors when a build produces no wheel, instead of failing later with a confusing message (#2909)
  • πŸ›  Speeds up CLI startup through lazy imports on Python 3.15 (#2797)
  • πŸ“š Adds an FAQ section on caching cibuildwheel's downloaded tools with CIBW_CACHE_PATH (#2842)
  • πŸ“š Documentation improvements: clarifies which shell is used for command options, clarifies environment variable precedence, and fixes a dead Pyodide env info link (#2904, #2905, #2911)
Changelog

Sourced from pypa/cibuildwheel's changelog.

v4.1.0

12 June 2026

  • ✨ Updates Pyodide to the final 314.0.0 release, so Pyodide 3.14 wheels now build by default without the pyodide-prerelease enable flag. (#2906)
  • πŸ› Raises clear errors when a build produces no wheel, instead of failing later with a confusing message (#2909)
  • πŸ›  Speeds up CLI startup through lazy imports on Python 3.15 (#2797)
  • πŸ“š Adds an FAQ section on caching cibuildwheel's downloaded tools with CIBW_CACHE_PATH (#2842)
  • πŸ“š Documentation improvements: clarifies which shell is used for command options, clarifies environment variable precedence, and fixes a dead Pyodide env info link (#2904, #2905, #2911)
Commits
  • 2947353 Bump version: v4.1.0
  • 14a3c3a Remove Travis pre-commit check
  • 42aa134 chore: minor cleanups and perf tweaks from code review (#2910)
  • 01265e5 Clarify shell used for command options (#2904)
  • f4afd95 Add FAQ section on caching cibuildwheel's downloaded tools (#2842)
  • 6c08562 fix: faster CLI on Python 3.15 (#2797)
  • 4f42ee3 fix: raise clear errors when no wheel is produced (#2909)
  • f3aa1be Fix dead Pyodide env info link, remove mention of alpha ABI (#2911)
  • d60fc2b Support new graalpy asset names that include Python version. (#2863)
  • 55c8985 docs: clarify environment precedence (#2905)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pypa/cibuildwheel&package-manager=github_actions&previous-version=4.0.0&new-version=4.1.0)](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 5478fe3171a..5884fa7d81b 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -665,7 +665,7 @@ jobs: run: | make cythonize - name: Build wheels - uses: pypa/cibuildwheel@v4.0.0 + uses: pypa/cibuildwheel@v4.1.0 with: # `build-frontend = "build[uv]"` (pyproject.toml) requires uv to be # available on the runner for Windows and macOS. Installing From b88de9278a02d3ab202eaf6c439bfb9e18d83f0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:27:59 +0000 Subject: [PATCH 029/137] Bump pytest from 9.0.3 to 9.1.0 (#12935) Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.0.3 to 9.1.0.
Release notes

Sourced from pytest's releases.

9.1.0

pytest 9.1.0 (2026-06-13)

Removals and backward incompatible breaking changes

  • #14533: When using --doctest-modules, autouse fixtures with module, package or session scope that are defined inline in Python test modules (not plugins or conftests) will now possibly execute twice.

    If this is undesirable, move the fixture definition to a conftest.py file if possible.

    Technical explanation for those interested: When using --doctest-modules, pytest possibly collects Python modules twice, once as pytest.Module and once as a DoctestModule (depending on the configuration). Due to improvements in pytest's fixture implementation, if e.g. the DoctestModule collects a fixture, it is now visible to it only, and not to the Module. This means that both need to register the fixtures independently.

Deprecations (removal in next major release)

  • #10819: Added a deprecation warning for class-scoped fixtures defined as instance methods (without @classmethod). Such fixtures set attributes on a different instance than the test methods use, leading to unexpected behavior. Use @classmethod decorator instead -- by yastcher.

    See 10819 and 14011.

  • #12882: Calling request.getfixturevalue() <pytest.FixtureRequest.getfixturevalue> during teardown to request a fixture that was not already requested is now deprecated and will become an error in pytest 10.

    See dynamic-fixture-request-during-teardown for details.

  • #13409: Using non-~collections.abc.Collection iterables (such as generators, iterators, or custom iterable objects) for the argvalues parameter in @pytest.mark.parametrize <pytest.mark.parametrize ref> and metafunc.parametrize <pytest.Metafunc.parametrize> is now deprecated.

    These iterables get exhausted after the first iteration, leading to tests getting unexpectedly skipped in cases such as running pytest.main() multiple times, using class-level parametrize decorators, or collecting tests multiple times.

    See parametrize-iterators for details and suggestions.

  • #13946: The private config.inicfg attribute is now deprecated. Use config.getini() <pytest.Config.getini> to access configuration values instead.

    See config-inicfg for more details.

  • #14004: Passing baseid to ~pytest.FixtureDef or nodeid strings to fixture registration APIs is now deprecated. These are internal pytest APIs that are used by some plugins.

    Use the node parameter instead for fixture scoping. This enables more robust node-based matching instead of string prefix matching. If you've used nodeid=None, pass node=session instead.

    This will be removed in pytest 10.

  • #14335: The method of configuring hooks using markers, deprecated since pytest 7.2, is now scheduled to be removed in pytest 10. See hook-markers for more details.

  • #14434: The --pastebin option is now deprecated.

... (truncated)

Commits
  • b2522cf Prepare release version 9.1.0
  • 368d2fc [refactor] Tighten SetComparisonFunction to Iterator[str] (#14587)
  • ff77cd8 [refactor] Make base assertion comparisons return an iterator instead of a li...
  • 0d8491a build(deps): Bump actions/stale from 10.2.0 to 10.3.0
  • 4a809d9 Merge pull request #14568 from pytest-dev/register-fixture
  • 5dfa385 Fix recursion traceback test to cover all styles (#14582)
  • f52ff0c Add pytest.register_fixture
  • a8ac094 Merge pull request #14567 from pytest-dev/more-visibility-deprecate
  • e5620cd [pre-commit.ci] pre-commit autoupdate (#14577)
  • 2ce9c6d Merge pull request #14540 from minbang930/fix-14533-doctest-module-fixtures
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pytest&package-manager=pip&previous-version=9.0.3&new-version=9.1.0)](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-base.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test-mobile.txt | 2 +- requirements/test.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 8532cc81ea9..bf55a4737e4 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -189,7 +189,7 @@ pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==9.0.3 +pytest==9.1.0 # via # -r requirements/lint.in # -r requirements/test-common-base.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 7f03cf37afa..a7359a2657d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -184,7 +184,7 @@ pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==9.0.3 +pytest==9.1.0 # via # -r requirements/lint.in # -r requirements/test-common-base.in diff --git a/requirements/lint.txt b/requirements/lint.txt index f6cea4f0bc7..2de3a877182 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -82,7 +82,7 @@ pygments==2.20.0 # via # pytest # rich -pytest==9.0.3 +pytest==9.1.0 # via # -r requirements/lint.in # pytest-codspeed diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index 1a8e58dcfce..26d7d72738d 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.20.0 # via pytest -pytest==9.0.3 +pytest==9.1.0 # via # -r requirements/test-common-base.in # pytest-cov diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 3bafeed46fe..ca0a16320e4 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.3 +pytest==9.1.0 # via # -r requirements/test-common-base.in # pytest-codspeed diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index f7d399bce14..88b3bb3e53a 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -102,7 +102,7 @@ pygments==2.20.0 # via # pytest # rich -pytest==9.0.3 +pytest==9.1.0 # via # -r requirements/test-common-base.in # pytest-codspeed diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index 907bc041b1d..6cb95848459 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -70,7 +70,7 @@ pycparser==3.0 # via cffi pygments==2.20.0 # via pytest -pytest==9.0.3 +pytest==9.1.0 # via # -r requirements/test-common-base.in # pytest-cov diff --git a/requirements/test.txt b/requirements/test.txt index 40ccbfadd71..64b6826bf67 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -102,7 +102,7 @@ pygments==2.20.0 # via # pytest # rich -pytest==9.0.3 +pytest==9.1.0 # via # -r requirements/test-common-base.in # pytest-codspeed From 693235b11a74899c4c4b3a257921f3c69beadcbe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:47:06 +0000 Subject: [PATCH 030/137] Bump virtualenv from 21.4.3 to 21.5.0 (#12939) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.4.3 to 21.5.0.
Release notes

Sourced from virtualenv's releases.

21.5.0

What's Changed

Full Changelog: https://github.com/pypa/virtualenv/compare/21.4.3...21.5.0

Changelog

Sourced from virtualenv's changelog.

Features - 21.5.0

  • Drop support for Python 3.8; virtualenv now requires Python 3.9 or later to run and to create environments. Remove the embedded wheel seed package, which virtualenv bundled only for Python 3.8. The --wheel and --no-wheel options stay as no-ops, but now warn that virtualenv will remove them in a release after 2026-12 - by :user:gaborbernat. (:issue:3170)

Bugfixes - 21.5.0

  • Upgrade embedded wheels:

    Removed wheel of 0.47.0 (:issue:u)


v21.4.3 (2026-06-11)


Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=virtualenv&package-manager=pip&previous-version=21.4.3&new-version=21.5.0)](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 bf55a4737e4..326a8b5189d 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.4.3 +virtualenv==21.5.0 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common-base.in diff --git a/requirements/dev.txt b/requirements/dev.txt index a7359a2657d..23bbe416a64 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.4.3 +virtualenv==21.5.0 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common-base.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 2de3a877182..cbd02a39e0f 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.4.3 +virtualenv==21.5.0 # via pre-commit zlib-ng==1.0.0 # via -r requirements/lint.in From cf74b0fec1c0e63adc6659010cdca9e35dfbf66b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:49:30 +0000 Subject: [PATCH 031/137] Bump cryptography from 48.0.1 to 49.0.0 (#12937) Bumps [cryptography](https://github.com/pyca/cryptography) from 48.0.1 to 49.0.0.
Changelog

Sourced from cryptography's changelog.

49.0.0 - 2026-06-12


* **BACKWARDS INCOMPATIBLE:** Support for ``x86_64`` macOS has been
removed.
  We now only publish ``arm64`` wheels for macOS.
* **BACKWARDS INCOMPATIBLE:** Support for 32-bit Windows has been
removed.
  Users should move to a 64-bit Python installation.
* **BACKWARDS INCOMPATIBLE:** Removed the deprecated
  ``PUBLIC_KEY_TYPES``, ``PRIVATE_KEY_TYPES``,
``CERTIFICATE_PRIVATE_KEY_TYPES``,
``CERTIFICATE_ISSUER_PUBLIC_KEY_TYPES``,
  and ``CERTIFICATE_PUBLIC_KEY_TYPES`` type aliases. Use
``PublicKeyTypes``, ``PrivateKeyTypes``,
``CertificateIssuerPrivateKeyTypes``,
  ``CertificateIssuerPublicKeyTypes``, and ``CertificatePublicKeyTypes``
  instead. These were deprecated in version 40.0.
* **BACKWARDS INCOMPATIBLE:**
:class:`~cryptography.hazmat.primitives.ciphers.algorithms.ChaCha20`
now treats the first 4 bytes of the ``nonce`` as a 32-bit little-endian
block
counter (as defined in :rfc:`7539`) and tracks the number of bytes
processed.
Attempting to encrypt or decrypt more data than the counter allows
before it
would overflow now raises a :class:`ValueError` rather than silently
diverging
from RFC 7539. Setting the counter portion of the ``nonce`` to zero
allows
  encrypting up to 256 GiB with a given nonce.
* **BACKWARDS INCOMPATIBLE:** Loading an X.509 certificate whose ECDSA
or DSA
signature ``AlgorithmIdentifier`` contains encoded NULL parameters now
raises
a :class:`ValueError`. Such certificates are invalid, but older versions
of
  Java emitted them; previously they loaded with a deprecation warning.
* Fixed cross-compilation of the CFFI bindings when
``PYO3_CROSS_LIB_DIR``
  is set. The build now derives the Python include directory from
  ``PYO3_CROSS_LIB_DIR`` instead of querying the host interpreter, which
previously caused the build to fail during cross-compilations for
embedded
  systems, on hosts which have same-version Python development headers
  installed as the target Python.
* Added support for signing and verifying X.509 certificates,
certificate
  signing requests, and certificate revocation lists with
  :doc:`/hazmat/primitives/asymmetric/mldsa` keys, as well as loading
  certificates that contain ML-DSA public keys.
* Added :meth:`~cryptography.hazmat.primitives.hpke.KEM.enc_length` to
:class:`~cryptography.hazmat.primitives.hpke.KEM` so callers can split
the
  encapsulated key from the ciphertext returned by
  :meth:`~cryptography.hazmat.primitives.hpke.Suite.encrypt`.
*
:meth:`~cryptography.x509.verification.ExtensionPolicy.require_present`,
:meth:`~cryptography.x509.verification.ExtensionPolicy.may_be_present`,
and

:meth:`~cryptography.x509.verification.ExtensionPolicy.require_not_present`
now accept any extension type. Previously only a fixed set of extension
  types was supported, which made it impossible to account for otherwise
  unrecognized critical extensions during path validation.
* Added support for using :class:`~cryptography.x509.Certificate`,
  :class:`~cryptography.x509.CertificateSigningRequest`, and
:class:`~cryptography.x509.CertificateRevocationList` as field types in
  :doc:`/hazmat/asn1/index` structures.
* Added :func:`~cryptography.hazmat.asn1.value_set`, a class decorator
that
</tr></table>

... (truncated)

Commits
  • e300bbe bump version and changelog for 49.0.0 (#15030)
  • fa74cd8 Add external mu (message representative) support for ML-DSA (#14979)
  • f594db3 chore(deps): bump openssl from 0.10.80 to 0.10.81 (#15029)
  • 608e011 chore(deps): bump openssl-sys from 0.9.116 to 0.9.117 (#15028)
  • a322bc4 chore(deps): bump cc from 1.2.63 to 1.2.64 (#15027)
  • 33181a7 Reject critical nameConstraints extensions containing directoryName constrain...
  • 6080dc7 Bump dependencies that dependabot isn't (#15026)
  • 121faa3 chore(deps): bump virtualenv from 21.4.2 to 21.4.3 (#15023)
  • 829520b Add more robust processing for DH parameters. (#15016)
  • 0f05001 Bump downstream dependencies in CI (#15025)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cryptography&package-manager=pip&previous-version=48.0.1&new-version=49.0.0)](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 326a8b5189d..a550bd8941a 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -60,7 +60,7 @@ coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov -cryptography==48.0.1 +cryptography==49.0.0 # via trustme cython==3.2.5 # via -r requirements/cython.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 23bbe416a64..0f2cd51094a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -60,7 +60,7 @@ coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov -cryptography==48.0.1 +cryptography==49.0.0 # via trustme distlib==0.4.3 # via virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index cbd02a39e0f..5f7798f896a 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -24,7 +24,7 @@ cfgv==3.5.0 # via pre-commit click==8.4.1 # via slotscheck -cryptography==48.0.1 +cryptography==49.0.0 # via trustme distlib==0.4.3 # via virtualenv diff --git a/requirements/test-common.txt b/requirements/test-common.txt index ca0a16320e4..7d2ed8fa4fc 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -18,7 +18,7 @@ coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov -cryptography==48.0.1 +cryptography==49.0.0 # via trustme exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 88b3bb3e53a..0496da82e69 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -34,7 +34,7 @@ coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov -cryptography==48.0.1 +cryptography==49.0.0 # via trustme exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test.txt b/requirements/test.txt index 64b6826bf67..4930af9466e 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -34,7 +34,7 @@ coverage==7.14.1 # via # -r requirements/test-common.in # pytest-cov -cryptography==48.0.1 +cryptography==49.0.0 # via trustme exceptiongroup==1.3.1 # via pytest From 9c8ae422da361771e6a4a78181e6c82c3d98a056 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2026 11:57:30 +0000 Subject: [PATCH 032/137] Bump filelock from 3.29.3 to 3.29.4 (#12941) 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.29.3 to 3.29.4.
Release notes

Sourced from filelock's releases.

3.29.4

What's Changed

Full Changelog: https://github.com/tox-dev/filelock/compare/3.29.3...3.29.4

Changelog

Sourced from filelock's changelog.

########### Changelog ###########


3.29.4 (2026-06-13)


  • keep the read/write heartbeat alive on a transient touch error :pr:562 - by :user:dxbjavid
  • verify inode in break_lock_file before unlinking a stale lock :pr:561 - by :user:dxbjavid

3.29.3 (2026-06-10)


  • πŸ› fix(ci): restore release environment on tag job :pr:559
  • validate pid range in _parse_lock_holder :pr:556 - by :user:dxbjavid
  • πŸ”§ ci(release): publish to PyPI on tag push :pr:557
  • build(deps): bump astral-sh/setup-uv from 8.1.0 to 8.2.0 :pr:558 - by :user:dependabot[bot]

3.29.2 (2026-06-10)


  • build(deps): bump actions/checkout from 6.0.2 to 6.0.3 :pr:555 - by :user:dependabot[bot]
  • [pre-commit.ci] pre-commit autoupdate :pr:554 - by :user:pre-commit-ci[bot]
  • check hostname in is_lock_held_by_us :pr:553 - by :user:dxbjavid
  • πŸ”’ fix(soft): harden stale-lock breaking and self-heal malformed locks :pr:551
  • open marker reads non-blocking to refuse attacker-placed fifo :pr:549 - by :user:dxbjavid

3.29.1 (2026-06-03)


  • πŸ› fix(soft): refuse to follow symlinks when reading the lock file :pr:548 - by :user:dxbjavid
  • [pre-commit.ci] pre-commit autoupdate :pr:547 - by :user:pre-commit-ci[bot]
  • [pre-commit.ci] pre-commit autoupdate :pr:546 - by :user:pre-commit-ci[bot]
  • chore: improve filelock maintenance path :pr:545 - by :user:lphuc2250gma
  • chore: improve filelock maintenance path :pr:544 - by :user:lphuc2250gma
  • chore: improve filelock maintenance path :pr:542 - by :user:lphuc2250gma
  • docs: clarify per-thread scope of FileLock configuration :pr:543 - by :user:Gares95
  • [pre-commit.ci] pre-commit autoupdate :pr:541 - by :user:pre-commit-ci[bot]
  • docs: fix API docs of release() :pr:540 - by :user:MrAnno
  • [pre-commit.ci] pre-commit autoupdate :pr:539 - by :user:pre-commit-ci[bot]
  • [pre-commit.ci] pre-commit autoupdate :pr:538 - by :user:pre-commit-ci[bot]
  • [pre-commit.ci] pre-commit autoupdate :pr:537 - by :user:pre-commit-ci[bot]
  • build(deps): bump astral-sh/setup-uv from 8.0.0 to 8.1.0 :pr:536 - by :user:dependabot[bot]
  • [pre-commit.ci] pre-commit autoupdate :pr:535 - by :user:pre-commit-ci[bot]

... (truncated)

Commits
  • f3c11c0 Release 3.29.4
  • 5d663ee keep the read/write heartbeat alive on a transient touch error (#562)
  • 406d0a2 verify inode in break_lock_file before unlinking a stale lock (#561)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=filelock&package-manager=pip&previous-version=3.29.3&new-version=3.29.4)](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 a550bd8941a..88ad90a18bf 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -74,7 +74,7 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.29.3 +filelock==3.29.4 # via # python-discovery # virtualenv diff --git a/requirements/dev.txt b/requirements/dev.txt index 0f2cd51094a..e4171c5e7c4 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -72,7 +72,7 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.29.3 +filelock==3.29.4 # via # python-discovery # virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index 5f7798f896a..026bfbacb3d 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -30,7 +30,7 @@ distlib==0.4.3 # via virtualenv exceptiongroup==1.3.1 # via pytest -filelock==3.29.3 +filelock==3.29.4 # via # python-discovery # virtualenv From afd8e342bac43bf7f180740231637c6678040294 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:08:08 +0000 Subject: [PATCH 033/137] Bump virtualenv from 21.5.0 to 21.5.1 (#12949) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.5.0 to 21.5.1.
Release notes

Sourced from virtualenv's releases.

21.5.1

What's Changed

Full Changelog: https://github.com/pypa/virtualenv/compare/21.5.0...21.5.1

Changelog

Sourced from virtualenv's changelog.

Bugfixes - 21.5.1

  • Refuse to create environments whose Python the bundled wheels no longer cover (currently below 3.9). virtualenv used to substitute the newest bundled pip, which cannot run on such a target, leaving a broken environment; seeder selection now rejects it up front with a clear error. --no-seed and third-party seeders that ship compatible wheels still work - by :user:gaborbernat. (:issue:3171)

v21.5.0 (2026-06-13)


Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=virtualenv&package-manager=pip&previous-version=21.5.0&new-version=21.5.1)](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 88ad90a18bf..cad5dc730e2 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.5.0 +virtualenv==21.5.1 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common-base.in diff --git a/requirements/dev.txt b/requirements/dev.txt index e4171c5e7c4..3b123b7667b 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.5.0 +virtualenv==21.5.1 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common-base.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 026bfbacb3d..7368a3c6163 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.5.0 +virtualenv==21.5.1 # via pre-commit zlib-ng==1.0.0 # via -r requirements/lint.in From d16f3683af05f71de04f6fa47636ed4a5d5e8c9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jun 2026 11:13:04 +0000 Subject: [PATCH 034/137] Bump certifi from 2026.5.20 to 2026.6.17 (#12951) Bumps [certifi](https://github.com/certifi/python-certifi) from 2026.5.20 to 2026.6.17.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=certifi&package-manager=pip&previous-version=2026.5.20&new-version=2026.6.17)](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 cad5dc730e2..e245a276c4e 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -40,7 +40,7 @@ brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != # via -r requirements/runtime-deps.in build==1.5.0 # via pip-tools -certifi==2026.5.20 +certifi==2026.6.17 # via requests cffi==2.0.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 3b123b7667b..c1212e57885 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -40,7 +40,7 @@ brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != # via -r requirements/runtime-deps.in build==1.5.0 # via pip-tools -certifi==2026.5.20 +certifi==2026.6.17 # via requests cffi==2.0.0 # via diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 3380097a3d9..c745d520c8d 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.5.20 +certifi==2026.6.17 # via requests charset-normalizer==3.4.7 # via requests diff --git a/requirements/doc.txt b/requirements/doc.txt index 32758208e3d..c43abb3b32b 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.5.20 +certifi==2026.6.17 # via requests charset-normalizer==3.4.7 # via requests From d4d201be6579263223f1d259cd7b01fc313222fc Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 23:59:20 +0100 Subject: [PATCH 035/137] [PR #12956/7b0b0135 backport][3.15] Llhttp 9.4.2 (#12958) **This is a backport of PR #12956 as merged into master (7b0b01350ce8fd7ddb63b69c51ff9db6451c507a).** Co-authored-by: Sam Bull --- CHANGES/12956.packaging.rst | 1 + vendor/llhttp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12956.packaging.rst diff --git a/CHANGES/12956.packaging.rst b/CHANGES/12956.packaging.rst new file mode 100644 index 00000000000..7ea0cd11ac6 --- /dev/null +++ b/CHANGES/12956.packaging.rst @@ -0,0 +1 @@ +Upgraded ``llhttp`` to v9.4.2 -- by :user:`Dreamsorcerer`. diff --git a/vendor/llhttp b/vendor/llhttp index 96b15fb3bc0..01e105a30fd 160000 --- a/vendor/llhttp +++ b/vendor/llhttp @@ -1 +1 @@ -Subproject commit 96b15fb3bc00117d2db3df8e87fa6d3e9bcff328 +Subproject commit 01e105a30fd06e248bc8ac73c4adb34a63d4114a From 1c4e0e705269c312103f3b5cee960ce8af9c9911 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 00:03:39 +0100 Subject: [PATCH 036/137] [PR #12956/7b0b0135 backport][3.14] Llhttp 9.4.2 (#12957) **This is a backport of PR #12956 as merged into master (7b0b01350ce8fd7ddb63b69c51ff9db6451c507a).** Co-authored-by: Sam Bull --- CHANGES/12956.packaging.rst | 1 + vendor/llhttp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12956.packaging.rst diff --git a/CHANGES/12956.packaging.rst b/CHANGES/12956.packaging.rst new file mode 100644 index 00000000000..7ea0cd11ac6 --- /dev/null +++ b/CHANGES/12956.packaging.rst @@ -0,0 +1 @@ +Upgraded ``llhttp`` to v9.4.2 -- by :user:`Dreamsorcerer`. diff --git a/vendor/llhttp b/vendor/llhttp index 96b15fb3bc0..01e105a30fd 160000 --- a/vendor/llhttp +++ b/vendor/llhttp @@ -1 +1 @@ -Subproject commit 96b15fb3bc00117d2db3df8e87fa6d3e9bcff328 +Subproject commit 01e105a30fd06e248bc8ac73c4adb34a63d4114a From 948728d5b7dc9a02b17f5c509bf08d80208b51f9 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:03:03 +0100 Subject: [PATCH 037/137] [PR #12959/ea0c52e9 backport][3.15] Exclude .hash from builds (#12961) **This is a backport of PR #12959 as merged into master (ea0c52e9982cce5da4e2e0ba90daba33b4a4189e).** Co-authored-by: Sam Bull --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index ea5d39d4722..c9d517222a1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -17,5 +17,6 @@ global-exclude *.lib global-exclude *.dll global-exclude *.a global-exclude *.obj +global-exclude *.hash exclude aiohttp/*.html prune docs/_build From 0d4ea143c4ff25637ec7c4aa85cdd4631aca5a1f Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 02:14:16 +0100 Subject: [PATCH 038/137] [PR #12948/f5e63e85 backport][3.15] Fix IndexError in parse_content_disposition when param value is empty (#12963) **This is a backport of PR #12948 as merged into master (f5e63e856708076a5a5d0500cb19b798cb0a94c2).** Co-authored-by: JSap0914 <116227558+JSap0914@users.noreply.github.com> --- CHANGES/12948.bugfix.rst | 3 +++ aiohttp/multipart.py | 2 +- tests/test_multipart_helpers.py | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12948.bugfix.rst diff --git a/CHANGES/12948.bugfix.rst b/CHANGES/12948.bugfix.rst new file mode 100644 index 00000000000..8cd00cfe0a6 --- /dev/null +++ b/CHANGES/12948.bugfix.rst @@ -0,0 +1,3 @@ +Fixed ``IndexError: string index out of range`` in ``parse_content_disposition`` +when a header parameter has an empty value (e.g. ``filename=``). +-- by :user:`JSap0914`. diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index b3d434a87eb..aaed5178eb2 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -79,7 +79,7 @@ def is_token(string: str) -> bool: return bool(string) and TOKEN >= set(string) def is_quoted(string: str) -> bool: - return string[0] == string[-1] == '"' + return len(string) >= 2 and string[0] == string[-1] == '"' def is_rfc5987(string: str) -> bool: return is_token(string) and string.count("'") == 2 diff --git a/tests/test_multipart_helpers.py b/tests/test_multipart_helpers.py index d4fb610a22c..3548feacd40 100644 --- a/tests/test_multipart_helpers.py +++ b/tests/test_multipart_helpers.py @@ -643,6 +643,22 @@ def test_bad_continuous_param(self) -> None: assert "attachment" == disptype assert {} == params + def test_empty_param_value_no_crash(self) -> None: + """Empty param value (e.g. filename=) must not raise IndexError.""" + with pytest.warns(aiohttp.BadContentDispositionHeader): + disptype, params = parse_content_disposition("attachment; filename=") + assert disptype is None + assert {} == params + + def test_empty_param_value_multiple(self) -> None: + """Multiple params where one has empty value must not raise IndexError.""" + with pytest.warns(aiohttp.BadContentDispositionHeader): + disptype, params = parse_content_disposition( + "attachment; name=foo; filename=" + ) + assert disptype is None + assert {} == params + class TestContentDispositionFilename: # http://greenbytes.de/tech/tc2231/ From e67a2e841c498047b234a18f9f8c0f029f00fdc0 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Fri, 19 Jun 2026 02:50:30 +0100 Subject: [PATCH 039/137] Fix flaky tests using DNS (#12789) (#12964) (cherry picked from commit 972e68c6563a0c8942dad0be0573fbd8d625b1aa) --- tests/test_connector.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_connector.py b/tests/test_connector.py index 75bd1461ba1..4d0817f55f9 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -2137,7 +2137,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_passed_to_create_connection( ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() - req = ClientRequest("GET", URL("https://example.com"), loop=loop) + req = ClientRequest("GET", URL("https://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): assert create_connection.call_args.kwargs["ssl_shutdown_timeout"] == 2.5 @@ -2155,7 +2155,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_passed_to_create_connection( ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() - req = ClientRequest("GET", URL("https://example.com"), loop=loop) + req = ClientRequest("GET", URL("https://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): # When ssl_shutdown_timeout is None, it should not be in kwargs @@ -2174,7 +2174,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_passed_to_create_connection( ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() - req = ClientRequest("GET", URL("http://example.com"), loop=loop) + req = ClientRequest("GET", URL("http://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): # For non-SSL connections, ssl_shutdown_timeout should not be passed @@ -2202,12 +2202,12 @@ async def test_tcp_connector_ssl_shutdown_timeout_not_passed_pre_311( create_connection.return_value = mock.Mock(), mock.Mock() # Test with HTTPS - req = ClientRequest("GET", URL("https://example.com"), loop=loop) + req = ClientRequest("GET", URL("https://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): assert "ssl_shutdown_timeout" not in create_connection.call_args.kwargs # Test with HTTP - req = ClientRequest("GET", URL("http://example.com"), loop=loop) + req = ClientRequest("GET", URL("http://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): assert "ssl_shutdown_timeout" not in create_connection.call_args.kwargs @@ -2367,13 +2367,13 @@ async def test_tcp_connector_ssl_shutdown_timeout_zero_not_passed( create_connection.return_value = mock.Mock(), mock.Mock() # Test with HTTPS - req = ClientRequest("GET", URL("https://example.com"), loop=loop) + req = ClientRequest("GET", URL("https://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): # Verify ssl_shutdown_timeout was NOT passed assert "ssl_shutdown_timeout" not in create_connection.call_args.kwargs # Test with HTTP (should not have ssl_shutdown_timeout anyway) - req = ClientRequest("GET", URL("http://example.com"), loop=loop) + req = ClientRequest("GET", URL("http://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): assert "ssl_shutdown_timeout" not in create_connection.call_args.kwargs @@ -2398,13 +2398,13 @@ async def test_tcp_connector_ssl_shutdown_timeout_nonzero_passed( create_connection.return_value = mock.Mock(), mock.Mock() # Test with HTTPS - req = ClientRequest("GET", URL("https://example.com"), loop=loop) + req = ClientRequest("GET", URL("https://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): # Verify ssl_shutdown_timeout WAS passed assert create_connection.call_args.kwargs["ssl_shutdown_timeout"] == 5.0 # Test with HTTP (should not have ssl_shutdown_timeout) - req = ClientRequest("GET", URL("http://example.com"), loop=loop) + req = ClientRequest("GET", URL("http://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): assert "ssl_shutdown_timeout" not in create_connection.call_args.kwargs From b144b889f49dc6216ea6e479353c3fbd4688c232 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 03:06:03 +0100 Subject: [PATCH 040/137] [PR #12959/ea0c52e9 backport][3.14] Exclude .hash from builds (#12960) **This is a backport of PR #12959 as merged into master (ea0c52e9982cce5da4e2e0ba90daba33b4a4189e).** Co-authored-by: Sam Bull --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index ea5d39d4722..c9d517222a1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -17,5 +17,6 @@ global-exclude *.lib global-exclude *.dll global-exclude *.a global-exclude *.obj +global-exclude *.hash exclude aiohttp/*.html prune docs/_build From 97cc2a9568949da6f938e3634a030b35f8a91cab Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 03:16:29 +0100 Subject: [PATCH 041/137] [PR #12948/f5e63e85 backport][3.14] Fix IndexError in parse_content_disposition when param value is empty (#12962) **This is a backport of PR #12948 as merged into master (f5e63e856708076a5a5d0500cb19b798cb0a94c2).** Co-authored-by: JSap0914 <116227558+JSap0914@users.noreply.github.com> --- CHANGES/12948.bugfix.rst | 3 +++ aiohttp/multipart.py | 2 +- tests/test_multipart_helpers.py | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12948.bugfix.rst diff --git a/CHANGES/12948.bugfix.rst b/CHANGES/12948.bugfix.rst new file mode 100644 index 00000000000..8cd00cfe0a6 --- /dev/null +++ b/CHANGES/12948.bugfix.rst @@ -0,0 +1,3 @@ +Fixed ``IndexError: string index out of range`` in ``parse_content_disposition`` +when a header parameter has an empty value (e.g. ``filename=``). +-- by :user:`JSap0914`. diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index b3d434a87eb..aaed5178eb2 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -79,7 +79,7 @@ def is_token(string: str) -> bool: return bool(string) and TOKEN >= set(string) def is_quoted(string: str) -> bool: - return string[0] == string[-1] == '"' + return len(string) >= 2 and string[0] == string[-1] == '"' def is_rfc5987(string: str) -> bool: return is_token(string) and string.count("'") == 2 diff --git a/tests/test_multipart_helpers.py b/tests/test_multipart_helpers.py index d4fb610a22c..3548feacd40 100644 --- a/tests/test_multipart_helpers.py +++ b/tests/test_multipart_helpers.py @@ -643,6 +643,22 @@ def test_bad_continuous_param(self) -> None: assert "attachment" == disptype assert {} == params + def test_empty_param_value_no_crash(self) -> None: + """Empty param value (e.g. filename=) must not raise IndexError.""" + with pytest.warns(aiohttp.BadContentDispositionHeader): + disptype, params = parse_content_disposition("attachment; filename=") + assert disptype is None + assert {} == params + + def test_empty_param_value_multiple(self) -> None: + """Multiple params where one has empty value must not raise IndexError.""" + with pytest.warns(aiohttp.BadContentDispositionHeader): + disptype, params = parse_content_disposition( + "attachment; name=foo; filename=" + ) + assert disptype is None + assert {} == params + class TestContentDispositionFilename: # http://greenbytes.de/tech/tc2231/ From 248cc2cc0362b15f0b0839cb9db2a142e956c3ec Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 03:16:45 +0100 Subject: [PATCH 042/137] [PR #12964/e67a2e84 backport][3.14] Fix flaky tests using DNS (#12789) (#12965) **This is a backport of PR #12964 as merged into 3.15 (e67a2e841c498047b234a18f9f8c0f029f00fdc0).** (cherry picked from commit 972e68c6563a0c8942dad0be0573fbd8d625b1aa) Co-authored-by: Sam Bull --- tests/test_connector.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_connector.py b/tests/test_connector.py index 75bd1461ba1..4d0817f55f9 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -2137,7 +2137,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_passed_to_create_connection( ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() - req = ClientRequest("GET", URL("https://example.com"), loop=loop) + req = ClientRequest("GET", URL("https://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): assert create_connection.call_args.kwargs["ssl_shutdown_timeout"] == 2.5 @@ -2155,7 +2155,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_passed_to_create_connection( ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() - req = ClientRequest("GET", URL("https://example.com"), loop=loop) + req = ClientRequest("GET", URL("https://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): # When ssl_shutdown_timeout is None, it should not be in kwargs @@ -2174,7 +2174,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_passed_to_create_connection( ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() - req = ClientRequest("GET", URL("http://example.com"), loop=loop) + req = ClientRequest("GET", URL("http://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): # For non-SSL connections, ssl_shutdown_timeout should not be passed @@ -2202,12 +2202,12 @@ async def test_tcp_connector_ssl_shutdown_timeout_not_passed_pre_311( create_connection.return_value = mock.Mock(), mock.Mock() # Test with HTTPS - req = ClientRequest("GET", URL("https://example.com"), loop=loop) + req = ClientRequest("GET", URL("https://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): assert "ssl_shutdown_timeout" not in create_connection.call_args.kwargs # Test with HTTP - req = ClientRequest("GET", URL("http://example.com"), loop=loop) + req = ClientRequest("GET", URL("http://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): assert "ssl_shutdown_timeout" not in create_connection.call_args.kwargs @@ -2367,13 +2367,13 @@ async def test_tcp_connector_ssl_shutdown_timeout_zero_not_passed( create_connection.return_value = mock.Mock(), mock.Mock() # Test with HTTPS - req = ClientRequest("GET", URL("https://example.com"), loop=loop) + req = ClientRequest("GET", URL("https://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): # Verify ssl_shutdown_timeout was NOT passed assert "ssl_shutdown_timeout" not in create_connection.call_args.kwargs # Test with HTTP (should not have ssl_shutdown_timeout anyway) - req = ClientRequest("GET", URL("http://example.com"), loop=loop) + req = ClientRequest("GET", URL("http://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): assert "ssl_shutdown_timeout" not in create_connection.call_args.kwargs @@ -2398,13 +2398,13 @@ async def test_tcp_connector_ssl_shutdown_timeout_nonzero_passed( create_connection.return_value = mock.Mock(), mock.Mock() # Test with HTTPS - req = ClientRequest("GET", URL("https://example.com"), loop=loop) + req = ClientRequest("GET", URL("https://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): # Verify ssl_shutdown_timeout WAS passed assert create_connection.call_args.kwargs["ssl_shutdown_timeout"] == 5.0 # Test with HTTP (should not have ssl_shutdown_timeout) - req = ClientRequest("GET", URL("http://example.com"), loop=loop) + req = ClientRequest("GET", URL("http://127.0.0.1"), loop=loop) with closing(await conn.connect(req, [], ClientTimeout())): assert "ssl_shutdown_timeout" not in create_connection.call_args.kwargs From e4e318b5abe5ad39dd8386174c0f76e5ea438acf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jun 2026 10:49:56 +0000 Subject: [PATCH 043/137] Bump actions/checkout from 6 to 7 (#12969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
Release notes

Sourced from actions/checkout's releases.

v7.0.0

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v6.0.3...v7.0.0

v6.0.3

What's Changed

New Contributors

Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.3

v6.0.2

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v6.0.1...v6.0.2

v6.0.1

What's Changed

Full Changelog: https://github.com/actions/checkout/compare/v6...v6.0.1

Changelog

Sourced from actions/checkout's changelog.

Changelog

v7.0.0

v6.0.3

v6.0.2

v6.0.1

v6.0.0

v5.0.1

v5.0.0

v4.3.1

v4.3.0

v4.2.2

v4.2.1

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/checkout&package-manager=github_actions&previous-version=6&new-version=7)](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 | 20 ++++++++++---------- .github/workflows/codeql.yml | 2 +- .github/workflows/dependency-submission.yml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 5884fa7d81b..b83ba79af52 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -56,7 +56,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: >- @@ -113,7 +113,7 @@ jobs: timeout-minutes: 5 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Cache llhttp generated files @@ -167,7 +167,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Setup Python ${{ matrix.pyver }} @@ -274,7 +274,7 @@ jobs: archs: arm64_iphonesimulator steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Setup Python ${{ matrix.pyver }} @@ -338,7 +338,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Setup Python ${{ matrix.pyver }} @@ -417,7 +417,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout project - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Setup Python 3.13.2 @@ -460,7 +460,7 @@ jobs: timeout-minutes: 15 steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Setup Python @@ -547,7 +547,7 @@ jobs: needs: pre-deploy steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Setup Python @@ -624,7 +624,7 @@ jobs: platform: ios steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Set up QEMU @@ -714,7 +714,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Login diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 797b7591cd8..445a2efd906 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Initialize CodeQL uses: github/codeql-action/init@v4 diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml index 9a499f22629..75e265936dc 100644 --- a/.github/workflows/dependency-submission.yml +++ b/.github/workflows/dependency-submission.yml @@ -23,7 +23,7 @@ jobs: contents: write # Required by the Dependency submission API steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Read vendored llhttp version From eae0621fa1f6b56e1107c161d347a2bbf16341d3 Mon Sep 17 00:00:00 2001 From: Sergei Grachev Date: Fri, 19 Jun 2026 15:18:10 +0200 Subject: [PATCH 044/137] [PR #12954/5c293f4f backport][3.15] Don't re-arm read timeout on a connection returned to the pool (#12967) **This is a backport of PR #12954 as merged into master (5c293f4f71f6188b446afd331afa47262a874f4f).** --- CHANGES/12953.bugfix.rst | 6 ++++++ CHANGES/12954.bugfix.rst | 1 + CONTRIBUTORS.txt | 1 + aiohttp/client_proto.py | 4 +++- tests/test_client_functional.py | 37 +++++++++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12953.bugfix.rst create mode 120000 CHANGES/12954.bugfix.rst diff --git a/CHANGES/12953.bugfix.rst b/CHANGES/12953.bugfix.rst new file mode 100644 index 00000000000..20edc2dda87 --- /dev/null +++ b/CHANGES/12953.bugfix.rst @@ -0,0 +1,6 @@ +Fixed the ``sock_read`` timeout being re-armed on a keep-alive connection after +it had been returned to the pool. An idle pooled connection could be left with a +pending read timeout that fired and poisoned it, so the next request reusing the +connection failed immediately with :exc:`aiohttp.SocketTimeoutError`. The read +timeout is now only rescheduled when resuming a transport that was actually +paused -- by :user:`daragok`. diff --git a/CHANGES/12954.bugfix.rst b/CHANGES/12954.bugfix.rst new file mode 120000 index 00000000000..baccfb94cc8 --- /dev/null +++ b/CHANGES/12954.bugfix.rst @@ -0,0 +1 @@ +12953.bugfix.rst \ No newline at end of file diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 830d86cb3a6..6ce89d092ae 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -334,6 +334,7 @@ Sebastian Hanula Sebastian HΓΌther Sebastien Geffroy SeongSoo Cho +Sergei Grachev Sergey Ninua Sergey Skripnick Serhii Charykov diff --git a/aiohttp/client_proto.py b/aiohttp/client_proto.py index a0b8512af9b..2bac26b24d4 100644 --- a/aiohttp/client_proto.py +++ b/aiohttp/client_proto.py @@ -190,8 +190,10 @@ def pause_reading(self) -> None: self._drop_timeout() def resume_reading(self, resume_parser: bool = True) -> None: + was_paused = self._reading_paused super().resume_reading(resume_parser) - self._reschedule_timeout() + if was_paused: + self._reschedule_timeout() def set_exception( self, diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 6bd57bee612..524c4a2efcd 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -1251,6 +1251,43 @@ async def handler(request: web.Request) -> web.Response: assert result == b"foo" +async def test_sock_read_timeout_not_rearmed_on_pooled_connection( + aiohttp_client: AiohttpClient, +) -> None: + # Reading the buffered body of a completed response must not re-arm the + # sock_read timeout on a connection that has already been released to the + # keep-alive pool. Otherwise the timer fires while the connection sits idle + # in the pool, stamps SocketTimeoutError on it, and the next request that + # reuses it fails immediately (with no real read having stalled). + async def handler(request: web.Request) -> web.Response: + return web.json_response({"ok": True}) + + app = web.Application() + app.router.add_get("/", handler) + + timeout = aiohttp.ClientTimeout(total=30, sock_read=0.1) + client = await aiohttp_client(app, timeout=timeout) + + async with client.get("/") as resp: + assert resp.status == 200 + await resp.read() + + assert client.session.connector is not None + pooled = next(iter(client.session.connector._conns.values())) + proto = pooled[0][0] + # The pooled connection must carry no read-timeout handle, otherwise + # it could trigger an exception on the next request. + assert proto._read_timeout_handle is None + assert proto.exception() is None + + # The connection is still reusable. + async with client.get("/") as resp: + assert resp.status == 200 + assert await resp.json() == {"ok": True} + + assert next(iter(client.session.connector._conns.values()))[0][0] is proto + + async def test_timeout_on_reading_data(aiohttp_client, mocker) -> None: loop = asyncio.get_event_loop() From 4cce8a5f551227633f6afb6bb694086029080030 Mon Sep 17 00:00:00 2001 From: Sergei Grachev Date: Fri, 19 Jun 2026 15:18:32 +0200 Subject: [PATCH 045/137] [PR #12954/5c293f4f backport][3.14] Don't re-arm read timeout on a connection returned to the pool (#12966) **This is a backport of PR #12954 as merged into master (5c293f4f71f6188b446afd331afa47262a874f4f).** --- CHANGES/12953.bugfix.rst | 6 ++++++ CHANGES/12954.bugfix.rst | 1 + CONTRIBUTORS.txt | 1 + aiohttp/client_proto.py | 4 +++- tests/test_client_functional.py | 37 +++++++++++++++++++++++++++++++++ 5 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12953.bugfix.rst create mode 120000 CHANGES/12954.bugfix.rst diff --git a/CHANGES/12953.bugfix.rst b/CHANGES/12953.bugfix.rst new file mode 100644 index 00000000000..20edc2dda87 --- /dev/null +++ b/CHANGES/12953.bugfix.rst @@ -0,0 +1,6 @@ +Fixed the ``sock_read`` timeout being re-armed on a keep-alive connection after +it had been returned to the pool. An idle pooled connection could be left with a +pending read timeout that fired and poisoned it, so the next request reusing the +connection failed immediately with :exc:`aiohttp.SocketTimeoutError`. The read +timeout is now only rescheduled when resuming a transport that was actually +paused -- by :user:`daragok`. diff --git a/CHANGES/12954.bugfix.rst b/CHANGES/12954.bugfix.rst new file mode 120000 index 00000000000..baccfb94cc8 --- /dev/null +++ b/CHANGES/12954.bugfix.rst @@ -0,0 +1 @@ +12953.bugfix.rst \ No newline at end of file diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 830d86cb3a6..6ce89d092ae 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -334,6 +334,7 @@ Sebastian Hanula Sebastian HΓΌther Sebastien Geffroy SeongSoo Cho +Sergei Grachev Sergey Ninua Sergey Skripnick Serhii Charykov diff --git a/aiohttp/client_proto.py b/aiohttp/client_proto.py index a0b8512af9b..2bac26b24d4 100644 --- a/aiohttp/client_proto.py +++ b/aiohttp/client_proto.py @@ -190,8 +190,10 @@ def pause_reading(self) -> None: self._drop_timeout() def resume_reading(self, resume_parser: bool = True) -> None: + was_paused = self._reading_paused super().resume_reading(resume_parser) - self._reschedule_timeout() + if was_paused: + self._reschedule_timeout() def set_exception( self, diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 6bd57bee612..524c4a2efcd 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -1251,6 +1251,43 @@ async def handler(request: web.Request) -> web.Response: assert result == b"foo" +async def test_sock_read_timeout_not_rearmed_on_pooled_connection( + aiohttp_client: AiohttpClient, +) -> None: + # Reading the buffered body of a completed response must not re-arm the + # sock_read timeout on a connection that has already been released to the + # keep-alive pool. Otherwise the timer fires while the connection sits idle + # in the pool, stamps SocketTimeoutError on it, and the next request that + # reuses it fails immediately (with no real read having stalled). + async def handler(request: web.Request) -> web.Response: + return web.json_response({"ok": True}) + + app = web.Application() + app.router.add_get("/", handler) + + timeout = aiohttp.ClientTimeout(total=30, sock_read=0.1) + client = await aiohttp_client(app, timeout=timeout) + + async with client.get("/") as resp: + assert resp.status == 200 + await resp.read() + + assert client.session.connector is not None + pooled = next(iter(client.session.connector._conns.values())) + proto = pooled[0][0] + # The pooled connection must carry no read-timeout handle, otherwise + # it could trigger an exception on the next request. + assert proto._read_timeout_handle is None + assert proto.exception() is None + + # The connection is still reusable. + async with client.get("/") as resp: + assert resp.status == 200 + assert await resp.json() == {"ok": True} + + assert next(iter(client.session.connector._conns.values()))[0][0] is proto + + async def test_timeout_on_reading_data(aiohttp_client, mocker) -> None: loop = asyncio.get_event_loop() From ced30b9d8c68139ba2a14f616def1ee92194247a Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 01:57:10 +0100 Subject: [PATCH 046/137] [PR #12946/b9ef540d backport][3.15] Free up disk space for Android emulator in CI (#12971) **This is a backport of PR #12946 as merged into master (b9ef540d67accd5df0f0368fca42c8fc2304fced).** --- .github/workflows/ci-cd.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index b83ba79af52..2d5f15510f8 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -298,6 +298,14 @@ jobs: - name: Cythonize run: | make cythonize + - name: Free up disk space for Android emulator + if: ${{ matrix.config.platform == 'android' }} + uses: BRAINSia/free-disk-space@v2.1.3 + with: + android: false + docker-images: false + mandb: false + large-packages: false - name: Enable KVM group perms for Android emulator if: ${{ matrix.config.platform == 'android' }} # This is normally done by cibuildwheel automatically, when it detects Github Actions. But by unsetting GITHUB_ACTIONS From c5293938af13b3efbcd77619d34501cbaa255c2a Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 21 Jun 2026 01:57:31 +0100 Subject: [PATCH 047/137] [PR #12946/b9ef540d backport][3.14] Free up disk space for Android emulator in CI (#12970) **This is a backport of PR #12946 as merged into master (b9ef540d67accd5df0f0368fca42c8fc2304fced).** --- .github/workflows/ci-cd.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 8e435a56bbe..b13477ce446 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -298,6 +298,14 @@ jobs: - name: Cythonize run: | make cythonize + - name: Free up disk space for Android emulator + if: ${{ matrix.config.platform == 'android' }} + uses: BRAINSia/free-disk-space@v2.1.3 + with: + android: false + docker-images: false + mandb: false + large-packages: false - name: Enable KVM group perms for Android emulator if: ${{ matrix.config.platform == 'android' }} # This is normally done by cibuildwheel automatically, when it detects Github Actions. But by unsetting GITHUB_ACTIONS From c2883123dea10ed7394d64b916c0853cb35ad199 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:13:22 +0000 Subject: [PATCH 048/137] Bump pytest from 9.1.0 to 9.1.1 (#12974) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [pytest](https://github.com/pytest-dev/pytest) from 9.1.0 to 9.1.1.
Release notes

Sourced from pytest's releases.

9.1.1

pytest 9.1.1 (2026-06-19)

Bug fixes

  • #14220: Fixed a logic bug in pytest.RaisesGroup which would might cause it to display incorrect "It matches FooError() which was paired with BarError" messages.
  • #14591: Fixed a regression in pytest 9.1.0 which caused overriding a parametrized fixture with an indirect @​pytest.mark.parametrize to fail with "duplicate parametrization of '<fixture name>'".
  • #14606: Fixed list-item typing errors from mypy in @pytest.mark.parametrize <pytest.mark.parametrize ref> argvalues parameter.
  • #14608: Fixed a regression in pytest 9.1.0 where conftest.py files located in <invocation dir>/test* were no longer loaded as initial conftests when invoked without arguments. This could cause certain hooks (like pytest_addoption) in these files to not fire.
Commits
  • cf470ec Prepare release version 9.1.1
  • e0c8ce6 Merge pull request #14625 from pytest-dev/patchback/backports/9.1.x/a07c31a97...
  • 1b82d16 Merge pull request #14624 from pytest-dev/patchback/backports/9.1.x/b375b79ec...
  • 501c4bc Merge pull request #14596 from bluetech/doc-classmethod
  • b61f588 Merge pull request #14622 from chrisburr/fix-14608-initial-conftest-test-subdir
  • 9a567e0 [automated] Update plugin list (#14617) (#14618)
  • ef8b299 Merge pull request #14620 from pytest-dev/patchback/backports/9.1.x/680f9f3ed...
  • 66abd07 Merge pull request #14220 from bysiber/fix-stale-iexp-raisesgroup
  • 79fbf93 Merge pull request #14612 from pytest-dev/patchback/backports/9.1.x/974ed48b6...
  • 0d312eb Merge pull request #14611 from bluetech/parametrize-argvalues-typing
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=pytest&package-manager=pip&previous-version=9.1.0&new-version=9.1.1)](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-base.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test-mobile.txt | 2 +- requirements/test.txt | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index e245a276c4e..1ecda530527 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -189,7 +189,7 @@ pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==9.1.0 +pytest==9.1.1 # via # -r requirements/lint.in # -r requirements/test-common-base.in diff --git a/requirements/dev.txt b/requirements/dev.txt index c1212e57885..75c6840db77 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -184,7 +184,7 @@ pyproject-hooks==1.2.0 # via # build # pip-tools -pytest==9.1.0 +pytest==9.1.1 # via # -r requirements/lint.in # -r requirements/test-common-base.in diff --git a/requirements/lint.txt b/requirements/lint.txt index 7368a3c6163..51f167d4840 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -82,7 +82,7 @@ pygments==2.20.0 # via # pytest # rich -pytest==9.1.0 +pytest==9.1.1 # via # -r requirements/lint.in # pytest-codspeed diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index 26d7d72738d..6ae41a8d067 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.20.0 # via pytest -pytest==9.1.0 +pytest==9.1.1 # via # -r requirements/test-common-base.in # pytest-cov diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 7d2ed8fa4fc..d7a299a442b 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -66,7 +66,7 @@ pygments==2.20.0 # via # pytest # rich -pytest==9.1.0 +pytest==9.1.1 # via # -r requirements/test-common-base.in # pytest-codspeed diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 0496da82e69..3f0752d0c33 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -102,7 +102,7 @@ pygments==2.20.0 # via # pytest # rich -pytest==9.1.0 +pytest==9.1.1 # via # -r requirements/test-common-base.in # pytest-codspeed diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index 6cb95848459..1740cb6dcbb 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -70,7 +70,7 @@ pycparser==3.0 # via cffi pygments==2.20.0 # via pytest -pytest==9.1.0 +pytest==9.1.1 # via # -r requirements/test-common-base.in # pytest-cov diff --git a/requirements/test.txt b/requirements/test.txt index 4930af9466e..4fd68d6a9ed 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -102,7 +102,7 @@ pygments==2.20.0 # via # pytest # rich -pytest==9.1.0 +pytest==9.1.1 # via # -r requirements/test-common-base.in # pytest-codspeed From dcee65d34ff3437461fe0a39b511983a33be9c6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:17:55 +0000 Subject: [PATCH 049/137] Bump coverage from 7.14.1 to 7.14.2 (#12975) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.14.1 to 7.14.2.
Changelog

Sourced from coverage's changelog.

Version 7.14.2 β€” 2026-06-20

  • Fix: some messages were being written to stdout, making coverage json -o - useless for capturing JSON output. Now messages are written to stderr, fixing issue 2197_.

  • Fix: CoverageData kept one SQLite connection per thread that recorded coverage, but never closed them when those threads terminated. On long runs with many short-lived threads this leaked one file descriptor per dead thread, eventually failing with OSError: [Errno 24] Too many open files. Connections belonging to terminated threads are now closed and dropped. Fixes issue 2192. Thanks, Matthew Lloyd <pull 2193_>.

  • Fix: when using sys.monitoring, we were assuming we could use the COVERAGE_ID tool id. But other tools might also assume they could use that id. Pre-allocated ids don't really make sense, so now we search for a usable one instead. Fixes issue 2187_.

  • Following the advice of cibuildwheel <no-13t_>_, we no longer distribute wheels for Python 3.13 free-threaded.

.. _issue 2187: coveragepy/coveragepy#2187 .. _issue 2192: coveragepy/coveragepy#2192 .. _pull 2193: coveragepy/coveragepy#2193 .. _issue 2197: coveragepy/coveragepy#2197 .. _no-13t: https://py-free-threading.github.io/ci/#building-free-threaded-wheels-with-cibuildwheel

.. _changes_7-14-1:

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coverage&package-manager=pip&previous-version=7.14.1&new-version=7.14.2)](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-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 1ecda530527..4d11e29c33b 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.1 +coverage==7.14.2 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/dev.txt b/requirements/dev.txt index 75c6840db77..73f36742cf7 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.1 +coverage==7.14.2 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index 6ae41a8d067..da32fe6c19e 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -6,7 +6,7 @@ # click==8.4.1 # via wait-for-it -coverage==7.14.1 +coverage==7.14.2 # via pytest-cov exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test-common.txt b/requirements/test-common.txt index d7a299a442b..26033e340f5 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.1 +coverage==7.14.2 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 3f0752d0c33..52d6182a86d 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.1 +coverage==7.14.2 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index 1740cb6dcbb..b6d9bcb1f44 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.1 # via wait-for-it -coverage==7.14.1 +coverage==7.14.2 # via pytest-cov exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test.txt b/requirements/test.txt index 4fd68d6a9ed..adedfe7f74a 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.1 +coverage==7.14.2 # via # -r requirements/test-common.in # pytest-cov From 14ca990100210f85c63637c27ff9e11799fb30ab Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 23 Jun 2026 01:08:38 +0100 Subject: [PATCH 050/137] Fix decompressing frames without negotiation (#12976) (#12977) (cherry picked from commit dcac82ea3323605097d77ae2dde60d389546a9ba) --- CHANGES/12976.bugfix.rst | 1 + THREAT_MODEL.md | 8 +++- aiohttp/_websocket/reader_py.py | 4 +- aiohttp/client.py | 7 +++- tests/autobahn/test_autobahn.py | 1 - tests/test_benchmarks_http_websocket.py | 8 +++- tests/test_client_ws_functional.py | 52 ++++++++++++++++++++++++- tests/test_websocket_parser.py | 31 +++++++++------ tests/test_websocket_writer.py | 6 +-- 9 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 CHANGES/12976.bugfix.rst diff --git a/CHANGES/12976.bugfix.rst b/CHANGES/12976.bugfix.rst new file mode 100644 index 00000000000..008095a3351 --- /dev/null +++ b/CHANGES/12976.bugfix.rst @@ -0,0 +1 @@ +Fixed the client decompressing frames when ``permessage-deflate`` was not negotiated -- by :user:`Dreamsorcerer`. diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 2834202427f..4b5af95cbd4 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -527,7 +527,7 @@ client-side, the writer adds masks to outgoing frames. | :--- | :--- | :--- | :--- | | 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.3 | RSV bits | `reader_py.py:WebSocketReader._feed_data` gates RSV1 on 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. | @@ -546,6 +546,12 @@ client-side, the writer adds masks to outgoing frames. `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. +- **PR #12976** β€” the client created its `WebSocketReader` + without passing `compress`, so the reader defaulted to `compress=True` and + decompressed RSV1 frames even when PMCE was never negotiated (threat 3.3; + RFC 6455 Β§5.2 requires failing such frames). Fixed by passing + `compress=bool(compress)` in `client.py:_ws_connect` and removing the + `compress` / `decode_text` defaults on `WebSocketReader.__init__`. - No formal CVE has been published against the WebSocket framing layer to date. diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index 784fb08b0ee..8c6a1e1148a 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -138,8 +138,8 @@ def __init__( self, queue: WebSocketDataQueue, max_msg_size: int, - compress: bool = True, - decode_text: bool = True, + compress: bool, + decode_text: bool, ) -> None: self.queue = queue self._max_msg_size = max_msg_size diff --git a/aiohttp/client.py b/aiohttp/client.py index 0bec36e072f..a5241426ea5 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -1365,7 +1365,12 @@ async def _ws_connect( compress=compress, client_notakeover=notakeover, ) - parser = WebSocketReader(reader, max_msg_size, decode_text=decode_text) + parser = WebSocketReader( + reader, + max_msg_size, + compress=bool(compress), + decode_text=decode_text, + ) cb = None if heartbeat is None else ws_resp._on_data_received conn_proto.set_parser(parser, reader, data_received_cb=cb) return ws_resp diff --git a/tests/autobahn/test_autobahn.py b/tests/autobahn/test_autobahn.py index a72cf8dbbb0..cb8d44e0192 100644 --- a/tests/autobahn/test_autobahn.py +++ b/tests/autobahn/test_autobahn.py @@ -103,7 +103,6 @@ def test_client(report_dir: Path, request: pytest.FixtureRequest) -> None: results = get_test_results(report_dir / "clients", "aiohttp") xfail = { - "3.4": "Actual events match at least one expected.", "7.9.5": "The close code should have been 1002 or empty", "9.1.4": "Did not receive message within 100 seconds.", "9.1.5": "Did not receive message within 100 seconds.", diff --git a/tests/test_benchmarks_http_websocket.py b/tests/test_benchmarks_http_websocket.py index a5e506f8e9a..8a64d5585ca 100644 --- a/tests/test_benchmarks_http_websocket.py +++ b/tests/test_benchmarks_http_websocket.py @@ -23,7 +23,9 @@ def test_read_large_binary_websocket_messages( ) -> None: """Read one hundred large binary websocket messages.""" queue = WebSocketDataQueue(BaseProtocol(loop), DEFAULT_CHUNK_SIZE, loop=loop) - reader = WebSocketReader(queue, max_msg_size=DEFAULT_CHUNK_SIZE) + reader = WebSocketReader( + queue, max_msg_size=DEFAULT_CHUNK_SIZE, compress=True, decode_text=True + ) # PACK3 has a minimum message length of 2**16 bytes. message = b"x" * ((2**16) + 1) @@ -44,7 +46,9 @@ def test_read_one_hundred_websocket_text_messages( ) -> None: """Benchmark reading 100 WebSocket text messages.""" queue = WebSocketDataQueue(BaseProtocol(loop), DEFAULT_CHUNK_SIZE, loop=loop) - reader = WebSocketReader(queue, max_msg_size=DEFAULT_CHUNK_SIZE) + reader = WebSocketReader( + queue, max_msg_size=DEFAULT_CHUNK_SIZE, compress=True, decode_text=True + ) 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_ws_functional.py b/tests/test_client_ws_functional.py index 2e50d636f9e..f098b33b5b7 100644 --- a/tests/test_client_ws_functional.py +++ b/tests/test_client_ws_functional.py @@ -2,6 +2,7 @@ import json import struct import sys +import zlib from contextlib import suppress from typing import Any, Literal, NoReturn from unittest import mock @@ -17,9 +18,10 @@ hdrs, web, ) +from aiohttp._websocket.models import WS_DEFLATE_TRAILING from aiohttp._websocket.reader import WebSocketDataQueue from aiohttp.client_ws import ClientWSTimeout -from aiohttp.http import WSCloseCode +from aiohttp.http import WebSocketError, WSCloseCode from aiohttp.pytest_plugin import AiohttpClient if sys.version_info >= (3, 11): @@ -1614,3 +1616,51 @@ async def handler(request: web.Request) -> web.WebSocketResponse: # receive_json() with orjson-style loads should work with bytes data = await ws.receive_json(loads=orjson_style_loads) assert data == {"value": 42} + + +async def test_client_rejects_compressed_frame_without_negotiation( + aiohttp_client: AiohttpClient, +) -> None: + """A client that never negotiated permessage-deflate must reject RSV1 frames. + + Per RFC 6455 section 5.2, a non-zero reserved bit with no negotiated + extension defining it MUST fail the connection. The client used to build its + WebSocketReader without passing ``compress``, so the reader defaulted to + ``compress=True`` and silently decompressed server frames with compression + off. + """ + payload = b"this frame should never be decompressed by the client" + + async def handler(request: web.Request) -> web.WebSocketResponse: + ws = web.WebSocketResponse() + await ws.prepare(request) + transport = request.transport + assert transport is not None + # Compress the payload the permessage-deflate way (raw DEFLATE minus the + # trailing 00 00 ff ff) and frame it with FIN + RSV1 set, even though the + # handshake never negotiated permessage-deflate. + compressor = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS + ) + compressed = compressor.compress(payload) + compressed += compressor.flush(zlib.Z_SYNC_FLUSH) + compressed = compressed.removesuffix(WS_DEFLATE_TRAILING) + first_byte = 0x80 | 0x40 | WSMsgType.TEXT.value # FIN + RSV1 + TEXT + frame = struct.pack("!BB", first_byte, len(compressed)) + compressed + transport.write(frame) + # Keep the connection open until the client tears it down. + await ws.receive() + return ws + + app = web.Application() + app.router.add_get("/", handler) + client = await aiohttp_client(app) + + async with client.ws_connect("/") as ws: + # Default compress=0: no permessage-deflate offered or negotiated. + assert ws.compress == 0 + msg = await ws.receive() + + assert msg.type is WSMsgType.ERROR, msg + assert isinstance(msg.data, WebSocketError) + assert msg.data.code == WSCloseCode.PROTOCOL_ERROR diff --git a/tests/test_websocket_parser.py b/tests/test_websocket_parser.py index 0e5890c72b4..16580002568 100644 --- a/tests/test_websocket_parser.py +++ b/tests/test_websocket_parser.py @@ -129,12 +129,16 @@ def out_low_limit( def parser_low_limit( out_low_limit: WebSocketDataQueue, ) -> PatchableWebSocketReader: - return PatchableWebSocketReader(out_low_limit, 4 * 1024 * 1024) + return PatchableWebSocketReader( + out_low_limit, 4 * 1024 * 1024, compress=True, decode_text=True + ) @pytest.fixture() def parser(out: WebSocketDataQueue) -> PatchableWebSocketReader: - return PatchableWebSocketReader(out, 4 * 1024 * 1024) + return PatchableWebSocketReader( + out, 4 * 1024 * 1024, compress=True, decode_text=True + ) def test_feed_data_remembers_exception(parser: WebSocketReader) -> None: @@ -611,7 +615,9 @@ def test_parse_compress_error_frame(parser: PatchableWebSocketReader) -> None: async def test_parse_no_compress_frame_single( loop: asyncio.AbstractEventLoop, out: WebSocketDataQueue ) -> None: - parser_no_compress = PatchableWebSocketReader(out, 0, compress=False) + parser_no_compress = PatchableWebSocketReader( + out, 0, compress=False, decode_text=True + ) with pytest.raises(WebSocketError) as ctx: parser_no_compress.parse_frame(struct.pack("!BB", 0b11000001, 0b00000001)) parser_no_compress.parse_frame(b"1") @@ -620,23 +626,24 @@ async def test_parse_no_compress_frame_single( def test_msg_too_large(out) -> None: - parser = WebSocketReader(out, 256, compress=False) + parser = WebSocketReader(out, 256, compress=False, decode_text=True) data = build_frame(b"text" * 256, WSMsgType.TEXT) with pytest.raises(WebSocketError) as ctx: parser._feed_data(data) assert ctx.value.code == WSCloseCode.MESSAGE_TOO_BIG -def test_msg_too_large_not_fin(out) -> None: - parser = WebSocketReader(out, 256, compress=False) +def test_msg_too_large_not_fin(out: WebSocketDataQueue) -> None: + parser = WebSocketReader(out, 256, compress=False, decode_text=True) data = build_frame(b"text" * 256, WSMsgType.TEXT, is_fin=False) with pytest.raises(WebSocketError) as ctx: parser._feed_data(data) assert ctx.value.code == WSCloseCode.MESSAGE_TOO_BIG -def test_compressed_msg_too_large(out) -> None: - parser = WebSocketReader(out, 256, compress=True) +@pytest.mark.usefixtures("parametrize_zlib_backend") +def test_compressed_msg_too_large(out: WebSocketDataQueue) -> None: + parser = WebSocketReader(out, 256, compress=True, decode_text=True) data = build_frame(b"aaa" * 256, WSMsgType.TEXT, ZLibBackend=ZLibBackend) with pytest.raises(WebSocketError) as ctx: parser._feed_data(data) @@ -646,7 +653,7 @@ def test_compressed_msg_too_large(out) -> None: @pytest.mark.parametrize("fin", (0x80, 0x00), ids=("fin", "non-fin")) def test_msg_too_large_at_header(out: WebSocketDataQueue, fin: int) -> None: max_msg_size = 256 - parser = WebSocketReader(out, max_msg_size, compress=False) + parser = WebSocketReader(out, max_msg_size, compress=False, decode_text=True) # Header alone: TEXT, 64-bit length, declares 1 MiB of payload. header = PACK_LEN3(fin | WSMsgType.TEXT, 127, 1024 * 1024) @@ -660,7 +667,7 @@ def test_msg_too_large_at_header(out: WebSocketDataQueue, fin: int) -> None: def test_msg_too_large_across_fragments(out: WebSocketDataQueue) -> None: # Individual fragments fit under max_msg_size but accumulate past it. max_msg_size = 256 - parser = WebSocketReader(out, max_msg_size, compress=False) + parser = WebSocketReader(out, max_msg_size, compress=False, decode_text=True) first = build_frame(b"a" * 100, WSMsgType.TEXT, is_fin=False) parser._feed_data(first) @@ -680,7 +687,7 @@ def test_msg_too_large_text_after_non_fin_text(out: WebSocketDataQueue) -> None: # Protocol-violating sequence: a fresh TEXT arrives while a fragmented # message is still open. max_msg_size = 256 - parser = WebSocketReader(out, max_msg_size, compress=False) + parser = WebSocketReader(out, max_msg_size, compress=False, decode_text=True) first = build_frame(b"a" * 200, WSMsgType.TEXT, is_fin=False) parser._feed_data(first) @@ -703,7 +710,7 @@ def test_reserved_opcode_rejected_at_header( out: WebSocketDataQueue, opcode: int ) -> None: # RFC 6455 reserves opcodes 0x3-0x7 (non-control) and 0xB-0xF (control). - parser = WebSocketReader(out, max_msg_size=256, compress=False) + parser = WebSocketReader(out, max_msg_size=256, compress=False, decode_text=True) header = PACK_LEN3(0x80 | opcode, 127, 1024 * 1024) with pytest.raises(WebSocketError, match=rf"^Unexpected opcode={opcode}$") as ctx: diff --git a/tests/test_websocket_writer.py b/tests/test_websocket_writer.py index 646f199d720..0dab67a6001 100644 --- a/tests/test_websocket_writer.py +++ b/tests/test_websocket_writer.py @@ -156,7 +156,7 @@ async def test_send_compress_cancelled( queue = WebSocketDataQueue( mock.Mock(_reading_paused=False), DEFAULT_CHUNK_SIZE, loop=loop ) - reader = WebSocketReader(queue, 50000) + reader = WebSocketReader(queue, 50000, compress=True, decode_text=True) # Replace executor with slow one to make race condition reproducible writer._compressobj = writer._get_compressor(None) @@ -211,7 +211,7 @@ async def test_send_compress_multiple_cancelled( writer = WebSocketWriter(protocol, transport, compress=15) loop = asyncio.get_running_loop() queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**16, loop=loop) - reader = WebSocketReader(queue, 50000) + reader = WebSocketReader(queue, 50000, compress=True, decode_text=True) # Replace executor with slow one writer._compressobj = writer._get_compressor(None) @@ -305,7 +305,7 @@ async def test_concurrent_messages( queue = WebSocketDataQueue( mock.Mock(_reading_paused=False), DEFAULT_CHUNK_SIZE, loop=loop ) - reader = WebSocketReader(queue, 50000) + reader = WebSocketReader(queue, 50000, compress=True, decode_text=True) writers = [] payloads = [] for count in range(1, 64 + 1): From 47fb6ae354d4fa22048f4dbe7dbf82b625f0a2f6 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 23 Jun 2026 01:08:55 +0100 Subject: [PATCH 051/137] Fix decompressing frames without negotiation (#12976) (#12978) (cherry picked from commit dcac82ea3323605097d77ae2dde60d389546a9ba) --- CHANGES/12976.bugfix.rst | 1 + THREAT_MODEL.md | 8 +++- aiohttp/_websocket/reader_py.py | 4 +- aiohttp/client.py | 7 +++- tests/autobahn/test_autobahn.py | 1 - tests/test_benchmarks_http_websocket.py | 10 +++-- tests/test_client_ws_functional.py | 52 ++++++++++++++++++++++++- tests/test_websocket_parser.py | 26 ++++++++----- tests/test_websocket_writer.py | 6 +-- 9 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 CHANGES/12976.bugfix.rst diff --git a/CHANGES/12976.bugfix.rst b/CHANGES/12976.bugfix.rst new file mode 100644 index 00000000000..008095a3351 --- /dev/null +++ b/CHANGES/12976.bugfix.rst @@ -0,0 +1 @@ +Fixed the client decompressing frames when ``permessage-deflate`` was not negotiated -- by :user:`Dreamsorcerer`. diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 2834202427f..4b5af95cbd4 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -527,7 +527,7 @@ client-side, the writer adds masks to outgoing frames. | :--- | :--- | :--- | :--- | | 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.3 | RSV bits | `reader_py.py:WebSocketReader._feed_data` gates RSV1 on 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. | @@ -546,6 +546,12 @@ client-side, the writer adds masks to outgoing frames. `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. +- **PR #12976** β€” the client created its `WebSocketReader` + without passing `compress`, so the reader defaulted to `compress=True` and + decompressed RSV1 frames even when PMCE was never negotiated (threat 3.3; + RFC 6455 Β§5.2 requires failing such frames). Fixed by passing + `compress=bool(compress)` in `client.py:_ws_connect` and removing the + `compress` / `decode_text` defaults on `WebSocketReader.__init__`. - No formal CVE has been published against the WebSocket framing layer to date. diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index 784fb08b0ee..8c6a1e1148a 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -138,8 +138,8 @@ def __init__( self, queue: WebSocketDataQueue, max_msg_size: int, - compress: bool = True, - decode_text: bool = True, + compress: bool, + decode_text: bool, ) -> None: self.queue = queue self._max_msg_size = max_msg_size diff --git a/aiohttp/client.py b/aiohttp/client.py index d9d8dfd5db7..72f4ee0c04a 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -1370,7 +1370,12 @@ async def _ws_connect( compress=compress, client_notakeover=notakeover, ) - parser = WebSocketReader(reader, max_msg_size, decode_text=decode_text) + parser = WebSocketReader( + reader, + max_msg_size, + compress=bool(compress), + decode_text=decode_text, + ) cb = None if heartbeat is None else ws_resp._on_data_received conn_proto.set_parser(parser, reader, data_received_cb=cb) return ws_resp diff --git a/tests/autobahn/test_autobahn.py b/tests/autobahn/test_autobahn.py index a72cf8dbbb0..cb8d44e0192 100644 --- a/tests/autobahn/test_autobahn.py +++ b/tests/autobahn/test_autobahn.py @@ -103,7 +103,6 @@ def test_client(report_dir: Path, request: pytest.FixtureRequest) -> None: results = get_test_results(report_dir / "clients", "aiohttp") xfail = { - "3.4": "Actual events match at least one expected.", "7.9.5": "The close code should have been 1002 or empty", "9.1.4": "Did not receive message within 100 seconds.", "9.1.5": "Did not receive message within 100 seconds.", diff --git a/tests/test_benchmarks_http_websocket.py b/tests/test_benchmarks_http_websocket.py index a5e506f8e9a..2ce73aa0678 100644 --- a/tests/test_benchmarks_http_websocket.py +++ b/tests/test_benchmarks_http_websocket.py @@ -23,7 +23,9 @@ def test_read_large_binary_websocket_messages( ) -> None: """Read one hundred large binary websocket messages.""" queue = WebSocketDataQueue(BaseProtocol(loop), DEFAULT_CHUNK_SIZE, loop=loop) - reader = WebSocketReader(queue, max_msg_size=DEFAULT_CHUNK_SIZE) + reader = WebSocketReader( + queue, max_msg_size=DEFAULT_CHUNK_SIZE, compress=True, decode_text=True + ) # PACK3 has a minimum message length of 2**16 bytes. message = b"x" * ((2**16) + 1) @@ -43,8 +45,10 @@ def test_read_one_hundred_websocket_text_messages( loop: asyncio.AbstractEventLoop, benchmark: BenchmarkFixture ) -> None: """Benchmark reading 100 WebSocket text messages.""" - queue = WebSocketDataQueue(BaseProtocol(loop), DEFAULT_CHUNK_SIZE, loop=loop) - reader = WebSocketReader(queue, max_msg_size=DEFAULT_CHUNK_SIZE) + queue = WebSocketDataQueue(BaseProtocol(loop), DEFAULT_CHUNK_SIZE, loop=loop) # + reader = WebSocketReader( + queue, max_msg_size=DEFAULT_CHUNK_SIZE, compress=True, decode_text=True + ) # 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_ws_functional.py b/tests/test_client_ws_functional.py index 2e50d636f9e..f098b33b5b7 100644 --- a/tests/test_client_ws_functional.py +++ b/tests/test_client_ws_functional.py @@ -2,6 +2,7 @@ import json import struct import sys +import zlib from contextlib import suppress from typing import Any, Literal, NoReturn from unittest import mock @@ -17,9 +18,10 @@ hdrs, web, ) +from aiohttp._websocket.models import WS_DEFLATE_TRAILING from aiohttp._websocket.reader import WebSocketDataQueue from aiohttp.client_ws import ClientWSTimeout -from aiohttp.http import WSCloseCode +from aiohttp.http import WebSocketError, WSCloseCode from aiohttp.pytest_plugin import AiohttpClient if sys.version_info >= (3, 11): @@ -1614,3 +1616,51 @@ async def handler(request: web.Request) -> web.WebSocketResponse: # receive_json() with orjson-style loads should work with bytes data = await ws.receive_json(loads=orjson_style_loads) assert data == {"value": 42} + + +async def test_client_rejects_compressed_frame_without_negotiation( + aiohttp_client: AiohttpClient, +) -> None: + """A client that never negotiated permessage-deflate must reject RSV1 frames. + + Per RFC 6455 section 5.2, a non-zero reserved bit with no negotiated + extension defining it MUST fail the connection. The client used to build its + WebSocketReader without passing ``compress``, so the reader defaulted to + ``compress=True`` and silently decompressed server frames with compression + off. + """ + payload = b"this frame should never be decompressed by the client" + + async def handler(request: web.Request) -> web.WebSocketResponse: + ws = web.WebSocketResponse() + await ws.prepare(request) + transport = request.transport + assert transport is not None + # Compress the payload the permessage-deflate way (raw DEFLATE minus the + # trailing 00 00 ff ff) and frame it with FIN + RSV1 set, even though the + # handshake never negotiated permessage-deflate. + compressor = zlib.compressobj( + zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -zlib.MAX_WBITS + ) + compressed = compressor.compress(payload) + compressed += compressor.flush(zlib.Z_SYNC_FLUSH) + compressed = compressed.removesuffix(WS_DEFLATE_TRAILING) + first_byte = 0x80 | 0x40 | WSMsgType.TEXT.value # FIN + RSV1 + TEXT + frame = struct.pack("!BB", first_byte, len(compressed)) + compressed + transport.write(frame) + # Keep the connection open until the client tears it down. + await ws.receive() + return ws + + app = web.Application() + app.router.add_get("/", handler) + client = await aiohttp_client(app) + + async with client.ws_connect("/") as ws: + # Default compress=0: no permessage-deflate offered or negotiated. + assert ws.compress == 0 + msg = await ws.receive() + + assert msg.type is WSMsgType.ERROR, msg + assert isinstance(msg.data, WebSocketError) + assert msg.data.code == WSCloseCode.PROTOCOL_ERROR diff --git a/tests/test_websocket_parser.py b/tests/test_websocket_parser.py index 0e5890c72b4..99f57dd5aed 100644 --- a/tests/test_websocket_parser.py +++ b/tests/test_websocket_parser.py @@ -129,12 +129,16 @@ def out_low_limit( def parser_low_limit( out_low_limit: WebSocketDataQueue, ) -> PatchableWebSocketReader: - return PatchableWebSocketReader(out_low_limit, 4 * 1024 * 1024) + return PatchableWebSocketReader( + out_low_limit, 4 * 1024 * 1024, compress=True, decode_text=True + ) @pytest.fixture() def parser(out: WebSocketDataQueue) -> PatchableWebSocketReader: - return PatchableWebSocketReader(out, 4 * 1024 * 1024) + return PatchableWebSocketReader( + out, 4 * 1024 * 1024, compress=True, decode_text=True + ) def test_feed_data_remembers_exception(parser: WebSocketReader) -> None: @@ -611,7 +615,9 @@ def test_parse_compress_error_frame(parser: PatchableWebSocketReader) -> None: async def test_parse_no_compress_frame_single( loop: asyncio.AbstractEventLoop, out: WebSocketDataQueue ) -> None: - parser_no_compress = PatchableWebSocketReader(out, 0, compress=False) + parser_no_compress = PatchableWebSocketReader( + out, 0, compress=False, decode_text=True + ) with pytest.raises(WebSocketError) as ctx: parser_no_compress.parse_frame(struct.pack("!BB", 0b11000001, 0b00000001)) parser_no_compress.parse_frame(b"1") @@ -620,7 +626,7 @@ async def test_parse_no_compress_frame_single( def test_msg_too_large(out) -> None: - parser = WebSocketReader(out, 256, compress=False) + parser = WebSocketReader(out, 256, compress=False, decode_text=True) data = build_frame(b"text" * 256, WSMsgType.TEXT) with pytest.raises(WebSocketError) as ctx: parser._feed_data(data) @@ -628,7 +634,7 @@ def test_msg_too_large(out) -> None: def test_msg_too_large_not_fin(out) -> None: - parser = WebSocketReader(out, 256, compress=False) + parser = WebSocketReader(out, 256, compress=False, decode_text=True) data = build_frame(b"text" * 256, WSMsgType.TEXT, is_fin=False) with pytest.raises(WebSocketError) as ctx: parser._feed_data(data) @@ -636,7 +642,7 @@ def test_msg_too_large_not_fin(out) -> None: def test_compressed_msg_too_large(out) -> None: - parser = WebSocketReader(out, 256, compress=True) + parser = WebSocketReader(out, 256, compress=True, decode_text=True) data = build_frame(b"aaa" * 256, WSMsgType.TEXT, ZLibBackend=ZLibBackend) with pytest.raises(WebSocketError) as ctx: parser._feed_data(data) @@ -646,7 +652,7 @@ def test_compressed_msg_too_large(out) -> None: @pytest.mark.parametrize("fin", (0x80, 0x00), ids=("fin", "non-fin")) def test_msg_too_large_at_header(out: WebSocketDataQueue, fin: int) -> None: max_msg_size = 256 - parser = WebSocketReader(out, max_msg_size, compress=False) + parser = WebSocketReader(out, max_msg_size, compress=False, decode_text=True) # Header alone: TEXT, 64-bit length, declares 1 MiB of payload. header = PACK_LEN3(fin | WSMsgType.TEXT, 127, 1024 * 1024) @@ -660,7 +666,7 @@ def test_msg_too_large_at_header(out: WebSocketDataQueue, fin: int) -> None: def test_msg_too_large_across_fragments(out: WebSocketDataQueue) -> None: # Individual fragments fit under max_msg_size but accumulate past it. max_msg_size = 256 - parser = WebSocketReader(out, max_msg_size, compress=False) + parser = WebSocketReader(out, max_msg_size, compress=False, decode_text=True) first = build_frame(b"a" * 100, WSMsgType.TEXT, is_fin=False) parser._feed_data(first) @@ -680,7 +686,7 @@ def test_msg_too_large_text_after_non_fin_text(out: WebSocketDataQueue) -> None: # Protocol-violating sequence: a fresh TEXT arrives while a fragmented # message is still open. max_msg_size = 256 - parser = WebSocketReader(out, max_msg_size, compress=False) + parser = WebSocketReader(out, max_msg_size, compress=False, decode_text=True) first = build_frame(b"a" * 200, WSMsgType.TEXT, is_fin=False) parser._feed_data(first) @@ -703,7 +709,7 @@ def test_reserved_opcode_rejected_at_header( out: WebSocketDataQueue, opcode: int ) -> None: # RFC 6455 reserves opcodes 0x3-0x7 (non-control) and 0xB-0xF (control). - parser = WebSocketReader(out, max_msg_size=256, compress=False) + parser = WebSocketReader(out, max_msg_size=256, compress=False, decode_text=True) header = PACK_LEN3(0x80 | opcode, 127, 1024 * 1024) with pytest.raises(WebSocketError, match=rf"^Unexpected opcode={opcode}$") as ctx: diff --git a/tests/test_websocket_writer.py b/tests/test_websocket_writer.py index 646f199d720..0dab67a6001 100644 --- a/tests/test_websocket_writer.py +++ b/tests/test_websocket_writer.py @@ -156,7 +156,7 @@ async def test_send_compress_cancelled( queue = WebSocketDataQueue( mock.Mock(_reading_paused=False), DEFAULT_CHUNK_SIZE, loop=loop ) - reader = WebSocketReader(queue, 50000) + reader = WebSocketReader(queue, 50000, compress=True, decode_text=True) # Replace executor with slow one to make race condition reproducible writer._compressobj = writer._get_compressor(None) @@ -211,7 +211,7 @@ async def test_send_compress_multiple_cancelled( writer = WebSocketWriter(protocol, transport, compress=15) loop = asyncio.get_running_loop() queue = WebSocketDataQueue(mock.Mock(_reading_paused=False), 2**16, loop=loop) - reader = WebSocketReader(queue, 50000) + reader = WebSocketReader(queue, 50000, compress=True, decode_text=True) # Replace executor with slow one writer._compressobj = writer._get_compressor(None) @@ -305,7 +305,7 @@ async def test_concurrent_messages( queue = WebSocketDataQueue( mock.Mock(_reading_paused=False), DEFAULT_CHUNK_SIZE, loop=loop ) - reader = WebSocketReader(queue, 50000) + reader = WebSocketReader(queue, 50000, compress=True, decode_text=True) writers = [] payloads = [] for count in range(1, 64 + 1): From 36cae390cd759a0addedab5bf72eeb5a4dd6b7e3 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 23 Jun 2026 02:53:28 +0100 Subject: [PATCH 052/137] Fix case sensitivity (#12931) (#12979) (cherry picked from commit c4de98382fb641cf3ffb26739920cc9d58ff0295) --- CHANGES/12931.bugfix.rst | 1 + aiohttp/client.py | 1 + aiohttp/hdrs.py | 10 ++-------- aiohttp/helpers.py | 6 +++--- aiohttp/http_parser.py | 1 + aiohttp/test_utils.py | 2 +- aiohttp/web_response.py | 2 +- tests/test_helpers.py | 2 ++ tests/test_http_parser.py | 27 +++++++++++++++++++++++++-- tests/test_test_utils.py | 23 +++++++++++++++++++++++ tests/test_urldispatch.py | 13 ++++++++++++- 11 files changed, 72 insertions(+), 16 deletions(-) create mode 100644 CHANGES/12931.bugfix.rst diff --git a/CHANGES/12931.bugfix.rst b/CHANGES/12931.bugfix.rst new file mode 100644 index 00000000000..6b62e87dac7 --- /dev/null +++ b/CHANGES/12931.bugfix.rst @@ -0,0 +1 @@ +Fixed some inconsistent case sensitivity on request methods -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/client.py b/aiohttp/client.py index a5241426ea5..b4c09d05e12 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -575,6 +575,7 @@ async def _request( if self.closed: raise RuntimeError("Session is closed") + method = method.upper() ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint) if auth is not None: diff --git a/aiohttp/hdrs.py b/aiohttp/hdrs.py index b64b62ee7f2..1082da3f4cc 100644 --- a/aiohttp/hdrs.py +++ b/aiohttp/hdrs.py @@ -108,14 +108,8 @@ X_FORWARDED_HOST: Final[istr] = istr("X-Forwarded-Host") X_FORWARDED_PROTO: Final[istr] = istr("X-Forwarded-Proto") -# These are the upper/lower case variants of the headers/methods -# Example: {'hOst', 'host', 'HoST', 'HOSt', 'hOsT', 'HosT', 'hoSt', ...} -METH_HEAD_ALL: Final = frozenset( - map("".join, itertools.product(*zip(METH_HEAD.upper(), METH_HEAD.lower()))) -) -METH_CONNECT_ALL: Final = frozenset( - map("".join, itertools.product(*zip(METH_CONNECT.upper(), METH_CONNECT.lower()))) -) +# Case permutations of the Host header β€” for callers that match against +# raw header tokens before istr/CIMultiDict folding. HOST_ALL: Final = frozenset( map("".join, itertools.product(*zip(HOST.upper(), HOST.lower()))) ) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index f86a7387af0..486b4976cac 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -76,7 +76,7 @@ EMPTY_BODY_STATUS_CODES = frozenset((204, 304, *range(100, 200))) # https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.1 # https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.2 -EMPTY_BODY_METHODS = hdrs.METH_HEAD_ALL +EMPTY_BODY_METHODS = frozenset({hdrs.METH_HEAD}) DEBUG = sys.flags.dev_mode or ( not sys.flags.ignore_environment and bool(os.environ.get("PYTHONASYNCIODEBUG")) @@ -1037,7 +1037,7 @@ def must_be_empty_body(method: str, code: int) -> bool: return ( code in EMPTY_BODY_STATUS_CODES or method in EMPTY_BODY_METHODS - or (200 <= code < 300 and method in hdrs.METH_CONNECT_ALL) + or (200 <= code < 300 and method == hdrs.METH_CONNECT) ) @@ -1049,5 +1049,5 @@ def should_remove_content_length(method: str, code: int) -> bool: # https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-8 # https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.5-4 return code in EMPTY_BODY_STATUS_CODES or ( - 200 <= code < 300 and method in hdrs.METH_CONNECT_ALL + 200 <= code < 300 and method == hdrs.METH_CONNECT ) diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index 5b46101ddaa..ae0993d2880 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -657,6 +657,7 @@ def parse_message(self, lines: list[bytes]) -> RawRequestMessage: # method if not TOKENRE.fullmatch(method): raise BadHttpMethod(method) + method = method.upper() # version match = VERSRE.fullmatch(version) diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py index 2b417ef8b2d..7994b9ef75c 100644 --- a/aiohttp/test_utils.py +++ b/aiohttp/test_utils.py @@ -738,7 +738,7 @@ def make_mocked_request( ) message = RawRequestMessage( - method, + method.upper(), path, version, headers, diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py index cbe4985cfb9..cea31be1733 100644 --- a/aiohttp/web_response.py +++ b/aiohttp/web_response.py @@ -828,7 +828,7 @@ async def _start(self, request: "BaseRequest") -> AbstractStreamWriter: body_len = len(self._body) if self._body else "0" # https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-7 if body_len != "0" or ( - self.status != 304 and request.method not in hdrs.METH_HEAD_ALL + self.status != 304 and request.method != hdrs.METH_HEAD ): self._headers[hdrs.CONTENT_LENGTH] = str(body_len) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 081b807cca4..9c1193a7a87 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1046,6 +1046,8 @@ def test_method_must_be_empty_body(): assert "HEAD" in EMPTY_BODY_METHODS # CONNECT is only empty on a successful response assert "CONNECT" not in EMPTY_BODY_METHODS + # Callers are expected to pass already-normalised (uppercase) methods. + assert "head" not in EMPTY_BODY_METHODS def test_should_remove_content_length_is_subset_of_must_be_empty_body(): diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 97643fd64ba..342530a2df1 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -571,12 +571,35 @@ def test_parse_unusual_request_line(parser) -> None: msg, _ = messages[0] assert msg.compression is None assert not msg.upgrade - assert msg.method == "#smol" + assert msg.method == "#SMOL" assert msg.path == "//a" assert msg.version == (1, 3) -def test_parse(parser) -> None: +def test_py_parser_normalises_method_to_uppercase( + loop: asyncio.AbstractEventLoop, server: Server +) -> None: + """Test Python parser canonicalises method tokens. + + llhttp rejects lowercase upstream, so this only applies to the Python parser. + """ + protocol = RequestHandler(server, loop=loop) + parser = HttpRequestParserPy( + protocol, + loop, + 2**16, + max_line_size=8190, + max_field_size=8190, + ) + protocol._parser = parser + 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] + assert msg.method == "GET" + + +def test_parse(parser: HttpRequestParser) -> None: 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 diff --git a/tests/test_test_utils.py b/tests/test_test_utils.py index 9056ba79e00..972d9fd8da2 100644 --- a/tests/test_test_utils.py +++ b/tests/test_test_utils.py @@ -357,6 +357,29 @@ async def handler(request: web.Request) -> web.Response: assert num_requests == 1 +async def test_retry_persistent_connection_lowercase_method( + aiohttp_client: AiohttpClient, +) -> None: + """Lowercase idempotent methods must still trigger retry.""" + num_requests = 0 + + async def handler(request: web.Request) -> web.Response: + nonlocal num_requests + num_requests += 1 + if num_requests == 1: + request.protocol.force_close() + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + client = await aiohttp_client(app) + client.session._retry_connection = True + async with client.request("get", "/") as resp: + assert resp.status == 200 + + assert num_requests == 2 + + async def test_server_context_manager(app, loop) -> None: async with TestServer(app, loop=loop) as server: async with aiohttp.ClientSession(loop=loop) as client: diff --git a/tests/test_urldispatch.py b/tests/test_urldispatch.py index 49fbf2951c7..9589b14c618 100644 --- a/tests/test_urldispatch.py +++ b/tests/test_urldispatch.py @@ -182,7 +182,18 @@ async def test_add_route_with_add_head_shortcut(router) -> None: assert info.route.name is None -async def test_add_with_name(router) -> None: +async def test_dispatch_lowercase_method(router: web.UrlDispatcher) -> None: + """Lowercase wire methods normalise on the request and dispatch correctly.""" + handler = make_handler() + router.add_get("/", handler) + req = make_mocked_request("get", "/") + assert req.method == "GET" + info = await router.resolve(req) + assert info is not None + assert handler is info.handler + + +async def test_add_with_name(router: web.UrlDispatcher) -> None: handler = make_handler() router.add_route("GET", "/handler/to/path", handler, name="name") req = make_mocked_request("GET", "/handler/to/path") From 7dde53eda27089940c2f72d74913b65d7c847822 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 23 Jun 2026 02:53:37 +0100 Subject: [PATCH 053/137] Fix case sensitivity (#12931) (#12980) (cherry picked from commit c4de98382fb641cf3ffb26739920cc9d58ff0295) --- CHANGES/12931.bugfix.rst | 1 + aiohttp/client.py | 1 + aiohttp/hdrs.py | 10 ++-------- aiohttp/helpers.py | 6 +++--- aiohttp/http_parser.py | 1 + aiohttp/test_utils.py | 2 +- aiohttp/web_response.py | 2 +- tests/test_helpers.py | 2 ++ tests/test_http_parser.py | 27 +++++++++++++++++++++++++-- tests/test_test_utils.py | 23 +++++++++++++++++++++++ tests/test_urldispatch.py | 13 ++++++++++++- 11 files changed, 72 insertions(+), 16 deletions(-) create mode 100644 CHANGES/12931.bugfix.rst diff --git a/CHANGES/12931.bugfix.rst b/CHANGES/12931.bugfix.rst new file mode 100644 index 00000000000..6b62e87dac7 --- /dev/null +++ b/CHANGES/12931.bugfix.rst @@ -0,0 +1 @@ +Fixed some inconsistent case sensitivity on request methods -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/client.py b/aiohttp/client.py index 72f4ee0c04a..62bcd0ab585 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -580,6 +580,7 @@ async def _request( if self.closed: raise RuntimeError("Session is closed") + method = method.upper() ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint) if auth is not None: diff --git a/aiohttp/hdrs.py b/aiohttp/hdrs.py index b64b62ee7f2..1082da3f4cc 100644 --- a/aiohttp/hdrs.py +++ b/aiohttp/hdrs.py @@ -108,14 +108,8 @@ X_FORWARDED_HOST: Final[istr] = istr("X-Forwarded-Host") X_FORWARDED_PROTO: Final[istr] = istr("X-Forwarded-Proto") -# These are the upper/lower case variants of the headers/methods -# Example: {'hOst', 'host', 'HoST', 'HOSt', 'hOsT', 'HosT', 'hoSt', ...} -METH_HEAD_ALL: Final = frozenset( - map("".join, itertools.product(*zip(METH_HEAD.upper(), METH_HEAD.lower()))) -) -METH_CONNECT_ALL: Final = frozenset( - map("".join, itertools.product(*zip(METH_CONNECT.upper(), METH_CONNECT.lower()))) -) +# Case permutations of the Host header β€” for callers that match against +# raw header tokens before istr/CIMultiDict folding. HOST_ALL: Final = frozenset( map("".join, itertools.product(*zip(HOST.upper(), HOST.lower()))) ) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 469c99dd63c..0fa0e8fbf92 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -76,7 +76,7 @@ EMPTY_BODY_STATUS_CODES = frozenset((204, 304, *range(100, 200))) # https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.1 # https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.2 -EMPTY_BODY_METHODS = hdrs.METH_HEAD_ALL +EMPTY_BODY_METHODS = frozenset({hdrs.METH_HEAD}) DEBUG = sys.flags.dev_mode or ( not sys.flags.ignore_environment and bool(os.environ.get("PYTHONASYNCIODEBUG")) @@ -1030,7 +1030,7 @@ def must_be_empty_body(method: str, code: int) -> bool: return ( code in EMPTY_BODY_STATUS_CODES or method in EMPTY_BODY_METHODS - or (200 <= code < 300 and method in hdrs.METH_CONNECT_ALL) + or (200 <= code < 300 and method == hdrs.METH_CONNECT) ) @@ -1042,5 +1042,5 @@ def should_remove_content_length(method: str, code: int) -> bool: # https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-8 # https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.5-4 return code in EMPTY_BODY_STATUS_CODES or ( - 200 <= code < 300 and method in hdrs.METH_CONNECT_ALL + 200 <= code < 300 and method == hdrs.METH_CONNECT ) diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index 5b46101ddaa..ae0993d2880 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -657,6 +657,7 @@ def parse_message(self, lines: list[bytes]) -> RawRequestMessage: # method if not TOKENRE.fullmatch(method): raise BadHttpMethod(method) + method = method.upper() # version match = VERSRE.fullmatch(version) diff --git a/aiohttp/test_utils.py b/aiohttp/test_utils.py index 6b50e23b810..1d4f59a87aa 100644 --- a/aiohttp/test_utils.py +++ b/aiohttp/test_utils.py @@ -738,7 +738,7 @@ def make_mocked_request( ) message = RawRequestMessage( - method, + method.upper(), path, version, headers, diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py index cbe4985cfb9..cea31be1733 100644 --- a/aiohttp/web_response.py +++ b/aiohttp/web_response.py @@ -828,7 +828,7 @@ async def _start(self, request: "BaseRequest") -> AbstractStreamWriter: body_len = len(self._body) if self._body else "0" # https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-7 if body_len != "0" or ( - self.status != 304 and request.method not in hdrs.METH_HEAD_ALL + self.status != 304 and request.method != hdrs.METH_HEAD ): self._headers[hdrs.CONTENT_LENGTH] = str(body_len) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 081b807cca4..9c1193a7a87 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1046,6 +1046,8 @@ def test_method_must_be_empty_body(): assert "HEAD" in EMPTY_BODY_METHODS # CONNECT is only empty on a successful response assert "CONNECT" not in EMPTY_BODY_METHODS + # Callers are expected to pass already-normalised (uppercase) methods. + assert "head" not in EMPTY_BODY_METHODS def test_should_remove_content_length_is_subset_of_must_be_empty_body(): diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index f2e0e9ae3f6..95c0ac573fb 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -571,12 +571,35 @@ def test_parse_unusual_request_line(parser) -> None: msg, _ = messages[0] assert msg.compression is None assert not msg.upgrade - assert msg.method == "#smol" + assert msg.method == "#SMOL" assert msg.path == "//a" assert msg.version == (1, 3) -def test_parse(parser) -> None: +def test_py_parser_normalises_method_to_uppercase( + loop: asyncio.AbstractEventLoop, server: Server +) -> None: + """Test Python parser canonicalises method tokens. + + llhttp rejects lowercase upstream, so this only applies to the Python parser. + """ + protocol = RequestHandler(server, loop=loop) + parser = HttpRequestParserPy( + protocol, + loop, + 2**16, + max_line_size=8190, + max_field_size=8190, + ) + protocol._parser = parser + 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] + assert msg.method == "GET" + + +def test_parse(parser: HttpRequestParser) -> None: 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 diff --git a/tests/test_test_utils.py b/tests/test_test_utils.py index 9056ba79e00..972d9fd8da2 100644 --- a/tests/test_test_utils.py +++ b/tests/test_test_utils.py @@ -357,6 +357,29 @@ async def handler(request: web.Request) -> web.Response: assert num_requests == 1 +async def test_retry_persistent_connection_lowercase_method( + aiohttp_client: AiohttpClient, +) -> None: + """Lowercase idempotent methods must still trigger retry.""" + num_requests = 0 + + async def handler(request: web.Request) -> web.Response: + nonlocal num_requests + num_requests += 1 + if num_requests == 1: + request.protocol.force_close() + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + client = await aiohttp_client(app) + client.session._retry_connection = True + async with client.request("get", "/") as resp: + assert resp.status == 200 + + assert num_requests == 2 + + async def test_server_context_manager(app, loop) -> None: async with TestServer(app, loop=loop) as server: async with aiohttp.ClientSession(loop=loop) as client: diff --git a/tests/test_urldispatch.py b/tests/test_urldispatch.py index 8d1134ed8d1..409238f8d47 100644 --- a/tests/test_urldispatch.py +++ b/tests/test_urldispatch.py @@ -182,7 +182,18 @@ async def test_add_route_with_add_head_shortcut(router) -> None: assert info.route.name is None -async def test_add_with_name(router) -> None: +async def test_dispatch_lowercase_method(router: web.UrlDispatcher) -> None: + """Lowercase wire methods normalise on the request and dispatch correctly.""" + handler = make_handler() + router.add_get("/", handler) + req = make_mocked_request("get", "/") + assert req.method == "GET" + info = await router.resolve(req) + assert info is not None + assert handler is info.handler + + +async def test_add_with_name(router: web.UrlDispatcher) -> None: handler = make_handler() router.add_route("GET", "/handler/to/path", handler, name="name") req = make_mocked_request("GET", "/handler/to/path") From d8d3c60e7ab583c1aae3dcd94868e470f73c750c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jun 2026 10:56:50 +0000 Subject: [PATCH 054/137] Bump coverage from 7.14.2 to 7.14.3 (#12981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.14.2 to 7.14.3.
Changelog

Sourced from coverage's changelog.

Version 7.14.3 β€” 2026-06-22

  • Fix: the default ... exclusion rule now also matches function bodies whose closing return-type bracket is on its own line (for example, after a long -> dict[ ... ] annotation that a formatter has split over multiple lines). Closes issue 2185, thanks Mengjia Shang <pull 2196_>.

  • Fix: On 3.13t, we incorrectly issued Couldn't import C tracer errors. We can't import the C tracer because in 7.14.2 we stopped shipping compiled wheels for 3.13t. Thanks, Hugo van Kemenade <pull 2203_>_.

.. _issue 2185: coveragepy/coveragepy#2185 .. _pull 2196: coveragepy/coveragepy#2196 .. _pull 2203: coveragepy/coveragepy#2203

.. _changes_7-14-2:

Commits
  • 22f13ea docs: sample HTML for 7.14.3
  • 2ca4e5f docs: prep for 7.14.3
  • 01d714e docs: add changelog entry for #2203
  • f36248d fix: don't emit 'Couldn't import C tracer' warning for 3.13t (#2203)
  • 86d73d1 docs: thanks, Mengjia Shang
  • 3d4ae3c docs: add the #2196 pr link to CHANGES
  • f4b2b4d fix: exclude ... bodies after multi-line return-type annotations (#2185) (#...
  • 1980ed0 chore: bump sigstore/gh-action-sigstore-python (#2201)
  • bca3217 build: since we don't ship 3.13t, don't test it
  • 77550d8 docs: oops, mismatched pull requests
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coverage&package-manager=pip&previous-version=7.14.2&new-version=7.14.3)](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-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 4d11e29c33b..e1324708e2e 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.2 +coverage==7.14.3 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/dev.txt b/requirements/dev.txt index 73f36742cf7..49c44ae7c66 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.2 +coverage==7.14.3 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index da32fe6c19e..eb946d58e35 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -6,7 +6,7 @@ # click==8.4.1 # via wait-for-it -coverage==7.14.2 +coverage==7.14.3 # via pytest-cov exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 26033e340f5..84c0953fe13 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.2 +coverage==7.14.3 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 52d6182a86d..97303d3d1d7 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.2 +coverage==7.14.3 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index b6d9bcb1f44..3ea9f97308d 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.1 # via wait-for-it -coverage==7.14.2 +coverage==7.14.3 # via pytest-cov exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test.txt b/requirements/test.txt index adedfe7f74a..c74de0ec72a 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.2 +coverage==7.14.3 # via # -r requirements/test-common.in # pytest-cov From 4279455d05e135ed46d5dd56ae1de472c8b9c09f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Jun 2026 10:53:31 +0000 Subject: [PATCH 055/137] Bump actions/cache from 5.0.5 to 6.0.0 (#12987) 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.5 to 6.0.0.
Release notes

Sourced from actions/cache's releases.

v6.0.0

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v5...v6.0.0

Changelog

Sourced from actions/cache's changelog.

6.0.0

  • Updated @actions/cache to ^6.0.1, @actions/core to ^3.0.1, @actions/exec to ^3.0.0, @actions/io to ^3.0.2
  • Migrated to ESM module system
  • Upgraded Jest to v30 and test infrastructure to be ESM compatible

5.0.4

  • Bump minimatch to v3.1.5 (fixes ReDoS via globstar patterns)
  • Bump undici to v6.24.1 (WebSocket decompression bomb protection, header validation fixes)
  • Bump fast-xml-parser to v5.5.6

5.0.3

5.0.2

  • Bump @actions/cache to v5.0.3 #1692

5.0.1

  • Update @azure/storage-blob to ^12.29.1 via @actions/cache@5.0.1 #1685

5.0.0

[!IMPORTANT] actions/cache@v5 runs on the Node.js 24 runtime and requires a minimum Actions Runner version of 2.327.1. If you are using self-hosted runners, ensure they are updated before upgrading.

4.3.0

  • Bump @actions/cache to v4.1.0

4.2.4

  • Bump @actions/cache to v4.0.5

4.2.3

  • Bump @actions/cache to v4.0.3 (obfuscates SAS token in debug logs for cache entries)

4.2.2

  • Bump @actions/cache to v4.0.2

4.2.1

  • Bump @actions/cache to v4.0.1

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/cache&package-manager=github_actions&previous-version=5.0.5&new-version=6.0.0)](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 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 2d5f15510f8..7f4d2d83d5e 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.5 + uses: actions/cache@v6.0.0 with: key: pip-lint-${{ hashFiles('requirements/*.txt') }} path: ~/.cache/pip @@ -117,7 +117,7 @@ jobs: with: submodules: true - name: Cache llhttp generated files - uses: actions/cache@v5.0.5 + uses: actions/cache@v6.0.0 id: cache with: key: llhttp-${{ hashFiles('vendor/llhttp/package*.json', 'vendor/llhttp/src/**/*') }} From 947a0e9b73f2c1175232094e6f65e14d0ee1f5eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:02:21 +0000 Subject: [PATCH 056/137] Bump cython from 3.2.5 to 3.2.6 (#12990) Bumps [cython](https://github.com/cython/cython) from 3.2.5 to 3.2.6.
Changelog

Sourced from cython's changelog.

3.2.6 (2026-06-24)

Bugs fixed

  • @functools.wraps() was broken in Py3.14+ for Cython compiled functions. (Github issue :issue:7675)

  • A double-free in the t-string code was fixed. (Github issue :issue:7712)

  • The - operator declarations for iterators in libcpp.vector we corrected. Patch by Vadim Markovtsev. (Github issue :issue:7717)

  • The shared utility code module no longer uses a temporary file path that changed the C code on each generation. (Github issue :issue:7723)

  • On 32 bit platforms, cached constants are no longer made immortal during module import. (Github issue :issue:7744)

Other changes

  • Binary wheels are now built with -DNDEBUG to discard runtime assertions from CPython's inline functions.
Commits
  • f49dd24 Disable failing test in Py3.8.
  • e377190 Update changelog.
  • 98635e5 Fix functools.wraps on cyfunctions on 3.14+ by providing a basic `_annotate...
  • 1d797c1 Update changelog.
  • c57a7c7 pypy fails test_nocopy_star_args (#7711)
  • c94acfe Build: Compile with -DNDEBUG to discard asserts in CPython's inline functions.
  • 3a75be0 Fix a deprecation warning in regex usage.
  • 56b4db9 Prepare release of 3.2.6.
  • 7ea9737 CI: Remove PyPy pin now that there is a bug fix release.
  • fa7ef68 Update changelog.
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cython&package-manager=pip&previous-version=3.2.5&new-version=3.2.6)](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 e1324708e2e..6d59698ede9 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -62,7 +62,7 @@ coverage==7.14.3 # pytest-cov cryptography==49.0.0 # via trustme -cython==3.2.5 +cython==3.2.6 # via -r requirements/cython.in distlib==0.4.3 # via virtualenv diff --git a/requirements/cython.in b/requirements/cython.in index c8fcfa99220..9468fa1cd3d 100644 --- a/requirements/cython.in +++ b/requirements/cython.in @@ -1,3 +1,3 @@ -r multidict.in -Cython >= 3.2.5 +Cython >= 3.2.6 diff --git a/requirements/cython.txt b/requirements/cython.txt index d6e2a89bf4d..a39c83cf8f5 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.5 +cython==3.2.6 # via -r requirements/cython.in multidict==6.7.1 # via -r requirements/multidict.in From 854f1c6bce1386ccd2509c1b0c45edd318c3d5ff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 11:10:11 +0000 Subject: [PATCH 057/137] Bump click from 8.4.1 to 8.4.2 (#12992) Bumps [click](https://github.com/pallets/click) from 8.4.1 to 8.4.2.
Changelog

Sourced from click's changelog.

Version 8.4.2

Unreleased

  • Fix Fish shell completion broken in 8.4.0 by {pr}3126. Newlines and tabs in option help text are now escaped, keeping the original completion format while still supporting multi-line help. {issue}3502 {issue}3043 {pr}3504 {pr}3508
  • Deprecated commands and options with empty or missing help text no longer render a stray leading space before the (DEPRECATED) label. {pr}3509
  • A {class}Group with invoke_without_command=True marks its subcommand as optional in the usage help, showing [COMMAND] instead of COMMAND. {issue}3059 {pr}3507
  • echo_via_pager flushes after each write, so passing a generator streams output to the pager incrementally instead of staying hidden until the pipe buffer fills. {issue}3242 {issue}2542 {pr}3534
  • echo_via_pager and get_pager_file no longer close a borrowed stdout stream when no external pager runs, completing the partial I/O operation on closed file fix from {pr}3482. {issue}3449 {pr}3533
Commits
  • b2e30a1 Release version 8.4.2
  • 7a16b20 Fix package_name resolution when module differs from distribution name (#3582)
  • bec5928 Fix package_name resolution when top-level module differs from distribution...
  • 916883a Fix tests to not rely on -Wdefault option (#3591)
  • 09195f6 Fix double-bracketing of choices in synopsis (#3578)
  • 1557e26 Check for warning exception with idiomatic context manager
  • d9ff133 Static typing improvements in click.shell_completion (#3460)
  • 762c97e Fix double-bracketing of choices in synopsis
  • 8929d39 Convert changes to markdown. (#3559)
  • 237be50 Move changes headings down a level.
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=click&package-manager=pip&previous-version=8.4.1&new-version=8.4.2)](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-base.txt | 2 +- requirements/test-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test-mobile.txt | 2 +- requirements/test.txt | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 6d59698ede9..1d2d4c8c211 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.1 +click==8.4.2 # via # pip-tools # slotscheck diff --git a/requirements/dev.txt b/requirements/dev.txt index 49c44ae7c66..a8acfd23078 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.1 +click==8.4.2 # via # pip-tools # slotscheck diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index c745d520c8d..4c35bc3fc56 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -14,7 +14,7 @@ certifi==2026.6.17 # via requests charset-normalizer==3.4.7 # via requests -click==8.4.1 +click==8.4.2 # via towncrier docutils==0.21.2 # via diff --git a/requirements/doc.txt b/requirements/doc.txt index c43abb3b32b..c374c83ef83 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -14,7 +14,7 @@ certifi==2026.6.17 # via requests charset-normalizer==3.4.7 # via requests -click==8.4.1 +click==8.4.2 # via towncrier docutils==0.21.2 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index 51f167d4840..b7396ae3366 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.1 +click==8.4.2 # via slotscheck cryptography==49.0.0 # via trustme diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index eb946d58e35..580621a8584 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -4,7 +4,7 @@ # # pip-compile --allow-unsafe --output-file=requirements/test-common-base.txt --strip-extras requirements/test-common-base.in # -click==8.4.1 +click==8.4.2 # via wait-for-it coverage==7.14.3 # via pytest-cov diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 84c0953fe13..99879898575 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.1 +click==8.4.2 # via wait-for-it coverage==7.14.3 # via diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 97303d3d1d7..6fc30cbea10 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.1 +click==8.4.2 # via wait-for-it coverage==7.14.3 # via diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index 3ea9f97308d..4dc437fd8f3 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -24,7 +24,7 @@ cffi==2.0.0 ; sys_platform != "android" and sys_platform != "ios" # via # -r requirements/test-mobile.in # pycares -click==8.4.1 +click==8.4.2 # via wait-for-it coverage==7.14.3 # via pytest-cov diff --git a/requirements/test.txt b/requirements/test.txt index c74de0ec72a..b69428243b2 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -28,7 +28,7 @@ cffi==2.0.0 # via # cryptography # pycares -click==8.4.1 +click==8.4.2 # via wait-for-it coverage==7.14.3 # via From 24a730a76370cfb2cb33a64c43475eff43c55f06 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:12:31 +0100 Subject: [PATCH 058/137] [PR #12997/5ff087ab backport][3.15] Fix parse_content_disposition rejecting headers with OWS around disposition type (#12999) **This is a backport of PR #12997 as merged into master (5ff087abcde85d0bef9b65bbde67d1131f5331b5).** Co-authored-by: JSap0914 <116227558+JSap0914@users.noreply.github.com> --- CHANGES/12996.bugfix.rst | 5 +++++ aiohttp/multipart.py | 2 ++ tests/test_multipart_helpers.py | 10 ++++++++++ 3 files changed, 17 insertions(+) create mode 100644 CHANGES/12996.bugfix.rst diff --git a/CHANGES/12996.bugfix.rst b/CHANGES/12996.bugfix.rst new file mode 100644 index 00000000000..c43d973fb77 --- /dev/null +++ b/CHANGES/12996.bugfix.rst @@ -0,0 +1,5 @@ +Fixed ``parse_content_disposition`` rejecting otherwise-valid +``Content-Disposition`` header values that contain optional whitespace (OWS) +around the disposition type (e.g. ``"form-data ; name=\"field\""``). +The disposition type is now stripped before token validation, consistent with +how parameter keys are already handled -- by :user:`JSap0914`. diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index aaed5178eb2..9116171e8cb 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -100,7 +100,9 @@ def unescape(text: str, *, chars: str = "".join(map(re.escape, CHAR))) -> str: if not header: return None, {} + # https://www.rfc-editor.org/info/rfc9110/#section-5.6.6-2 disptype, *parts = header.split(";") + disptype = disptype.strip() if not is_token(disptype): warnings.warn(BadContentDispositionHeader(header)) return None, {} diff --git a/tests/test_multipart_helpers.py b/tests/test_multipart_helpers.py index 3548feacd40..67bbc86c223 100644 --- a/tests/test_multipart_helpers.py +++ b/tests/test_multipart_helpers.py @@ -659,6 +659,16 @@ def test_empty_param_value_multiple(self) -> None: assert disptype is None assert {} == params + def test_disptype_with_trailing_space_before_semicolon(self) -> None: + disptype, params = parse_content_disposition('form-data ; name="field"') + assert disptype == "form-data" + assert params == {"name": "field"} + + def test_disptype_with_trailing_space_no_params(self) -> None: + disptype, params = parse_content_disposition("inline ") + assert disptype == "inline" + assert params == {} + class TestContentDispositionFilename: # http://greenbytes.de/tech/tc2231/ From c852173947406e2eca65957535ff17d3a4035a03 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:12:44 +0100 Subject: [PATCH 059/137] [PR #12997/5ff087ab backport][3.14] Fix parse_content_disposition rejecting headers with OWS around disposition type (#12998) **This is a backport of PR #12997 as merged into master (5ff087abcde85d0bef9b65bbde67d1131f5331b5).** Co-authored-by: JSap0914 <116227558+JSap0914@users.noreply.github.com> --- CHANGES/12996.bugfix.rst | 5 +++++ aiohttp/multipart.py | 2 ++ tests/test_multipart_helpers.py | 10 ++++++++++ 3 files changed, 17 insertions(+) create mode 100644 CHANGES/12996.bugfix.rst diff --git a/CHANGES/12996.bugfix.rst b/CHANGES/12996.bugfix.rst new file mode 100644 index 00000000000..c43d973fb77 --- /dev/null +++ b/CHANGES/12996.bugfix.rst @@ -0,0 +1,5 @@ +Fixed ``parse_content_disposition`` rejecting otherwise-valid +``Content-Disposition`` header values that contain optional whitespace (OWS) +around the disposition type (e.g. ``"form-data ; name=\"field\""``). +The disposition type is now stripped before token validation, consistent with +how parameter keys are already handled -- by :user:`JSap0914`. diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index aaed5178eb2..9116171e8cb 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -100,7 +100,9 @@ def unescape(text: str, *, chars: str = "".join(map(re.escape, CHAR))) -> str: if not header: return None, {} + # https://www.rfc-editor.org/info/rfc9110/#section-5.6.6-2 disptype, *parts = header.split(";") + disptype = disptype.strip() if not is_token(disptype): warnings.warn(BadContentDispositionHeader(header)) return None, {} diff --git a/tests/test_multipart_helpers.py b/tests/test_multipart_helpers.py index 3548feacd40..67bbc86c223 100644 --- a/tests/test_multipart_helpers.py +++ b/tests/test_multipart_helpers.py @@ -659,6 +659,16 @@ def test_empty_param_value_multiple(self) -> None: assert disptype is None assert {} == params + def test_disptype_with_trailing_space_before_semicolon(self) -> None: + disptype, params = parse_content_disposition('form-data ; name="field"') + assert disptype == "form-data" + assert params == {"name": "field"} + + def test_disptype_with_trailing_space_no_params(self) -> None: + disptype, params = parse_content_disposition("inline ") + assert disptype == "inline" + assert params == {} + class TestContentDispositionFilename: # http://greenbytes.de/tech/tc2231/ From 8ed4ee37de9927f188bf2a6350b9019b58102153 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2026 10:53:31 +0000 Subject: [PATCH 060/137] Bump actions/cache from 6.0.0 to 6.1.0 (#13005) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/cache](https://github.com/actions/cache) from 6.0.0 to 6.1.0.
Release notes

Sourced from actions/cache's releases.

v6.1.0

What's Changed

Full Changelog: https://github.com/actions/cache/compare/v6...v6.1.0

Changelog

Sourced from actions/cache's changelog.

6.1.0

Commits
  • 55cc834 Merge pull request #1768 from jasongin/readonly-cache
  • d8cd72f Bump @​actions/cache to v6.1.0 - handle cache write error due to RO token
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/cache&package-manager=github_actions&previous-version=6.0.0&new-version=6.1.0)](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 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 7f4d2d83d5e..28d776da237 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@v6.0.0 + uses: actions/cache@v6.1.0 with: key: pip-lint-${{ hashFiles('requirements/*.txt') }} path: ~/.cache/pip @@ -117,7 +117,7 @@ jobs: with: submodules: true - name: Cache llhttp generated files - uses: actions/cache@v6.0.0 + uses: actions/cache@v6.1.0 id: cache with: key: llhttp-${{ hashFiles('vendor/llhttp/package*.json', 'vendor/llhttp/src/**/*') }} From 2183f8e9187f9718737cd720eddeecc2f3f9b7ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 11:02:24 +0000 Subject: [PATCH 061/137] Bump cython from 3.2.6 to 3.2.8 (#13012) Bumps [cython](https://github.com/cython/cython) from 3.2.6 to 3.2.8.
Changelog

Sourced from cython's changelog.

3.2.8 (2026-06-30)

Bugs fixed

  • Assigning a Python 3.14+ .__annotate__ function to a Cython compiled function no longer evaluates annotations eagerly. Fixes a regression with @functools.wraps() in Cython 3.2.6. Patch by Jelle Zijlstra. (Github issue :issue:7767)

  • In freethreading Python, the necessary object keep-alive while executing user code in .__dealloc__() uses a safer scheme. Patch by Yaxing Cai. (Github issue :issue:7769)

  • The local function state used by sys.monitoring read from uninitialised memory. (Github issue :issue:7774)

  • The .ag_running flag attribute of async generators was always false on big-endian systems.

Commits
  • fee6fd6 Prepare release of 3.2.8.
  • 2feda19 Fix macro usage in Py3.8.
  • 21daa4e Prepare release of 3.2.7.
  • 6a69c3c Update changelog.
  • 60f2bdc Fix dealloc refcount guard resurrecting dying objects on free-threading (...
  • 4f47fd3 Update changelog.
  • 1e7ea1e Update changelog.
  • 410d60c Fix use of uninitialized monitoring fields in generator/coroutine objects (GH...
  • e70ca99 Fix cyfunction annotate setter on Python 3.14 (GH-7768)
  • 86ba584 Fix access to the ".ag_running" flag of async generators on big-endian systems.
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cython&package-manager=pip&previous-version=3.2.6&new-version=3.2.8)](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 1d2d4c8c211..6207c100954 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -62,7 +62,7 @@ coverage==7.14.3 # pytest-cov cryptography==49.0.0 # via trustme -cython==3.2.6 +cython==3.2.8 # via -r requirements/cython.in distlib==0.4.3 # via virtualenv diff --git a/requirements/cython.in b/requirements/cython.in index 9468fa1cd3d..35c3cb717f9 100644 --- a/requirements/cython.in +++ b/requirements/cython.in @@ -1,3 +1,3 @@ -r multidict.in -Cython >= 3.2.6 +Cython >= 3.2.8 diff --git a/requirements/cython.txt b/requirements/cython.txt index a39c83cf8f5..2cf9f068891 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.6 +cython==3.2.8 # via -r requirements/cython.in multidict==6.7.1 # via -r requirements/multidict.in From 2f885c8451c2cf7982d4e38888fa3e301a9bb4e0 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 19:59:10 +0100 Subject: [PATCH 062/137] [PR #13016/8ef76ab8 backport][3.15] Fix body reads after failed WS upgrade (#13018) **This is a backport of PR #13016 as merged into master (8ef76ab8029d0f2d88449fe115feb42ded646c7c).** Co-authored-by: Sam Bull --- CHANGES/13016.bugfix.rst | 1 + aiohttp/_http_parser.pyx | 17 +++++++++--- aiohttp/http_parser.py | 22 ++++++++++++---- tests/test_http_parser.py | 48 +++++++++++++++++++++++++++++++++ tests/test_web_functional.py | 51 ++++++++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 CHANGES/13016.bugfix.rst diff --git a/CHANGES/13016.bugfix.rst b/CHANGES/13016.bugfix.rst new file mode 100644 index 00000000000..a984f64e333 --- /dev/null +++ b/CHANGES/13016.bugfix.rst @@ -0,0 +1 @@ +Fixed request body not being read on rejected WebSocket upgrades -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 5ca1ccf6b0d..01ade16338e 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -320,6 +320,7 @@ cdef class HttpParser: set _seen_singletons list _raw_headers bint _upgraded + bint _pending_upgrade list _messages bint _more_data_available bint _paused @@ -396,6 +397,7 @@ cdef class HttpParser: self._response_with_body = response_with_body self._read_until_eof = read_until_eof self._upgraded = False + self._pending_upgrade = False self._auto_decompress = auto_decompress self._content_encoding = None self._lax = False @@ -480,10 +482,15 @@ cdef class HttpParser: raise BadHttpMessage("Missing 'Host' header in request.") h_upg = headers.get("upgrade", "") if (upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES) or self._cparser.method == cparser.HTTP_CONNECT: - self._upgraded = True + # https://www.rfc-editor.org/info/rfc9110/#section-7.8-15 + # Defer the protocol switch until the complete request has been + # received. + self._pending_upgrade = True else: if upgrade and self._cparser.status_code == 101: - self._upgraded = True + # llhttp pauses for a 101 on its own; just mark the pending + # switch so feed_data returns the upgraded-protocol tail. + self._pending_upgrade = True # do not support old websocket spec if SEC_WEBSOCKET_KEY1 in headers: @@ -642,6 +649,10 @@ 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 + if self._pending_upgrade: + # A supported upgrade whose request body has now been fully read. + self._upgraded = True + self._pending_upgrade = False elif errno is cparser.HPE_PAUSED: cparser.llhttp_resume(self._cparser) pos = cparser.llhttp_get_error_pos(self._cparser) - base @@ -860,8 +871,6 @@ cdef int cb_on_headers_complete(cparser.llhttp_t* parser) except -1: pyparser._last_error = exc return -1 else: - if pyparser._upgraded or pyparser._cparser.method == cparser.HTTP_CONNECT: - return 2 if not pyparser._response_with_body: return 1 return 0 diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index ae0993d2880..212b82228ea 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -293,6 +293,7 @@ def __init__( self._lines: list[bytes] = [] self._tail = b"" self._upgraded = False + self._pending_upgrade = False self._payload = None self._payload_parser: HttpPayloadParser | None = None self._payload_has_more_data = False @@ -424,9 +425,7 @@ def get_content_length() -> int | None: if SEC_WEBSOCKET_KEY1 in msg.headers: raise InvalidHeader(SEC_WEBSOCKET_KEY1) - self._upgraded = msg.upgrade and _is_supported_upgrade( - msg.headers - ) + upgraded = msg.upgrade and _is_supported_upgrade(msg.headers) method = getattr(msg, "method", self.method) # code is only present on responses @@ -438,8 +437,7 @@ def get_content_length() -> int | None: method and method in EMPTY_BODY_METHODS ) if not empty_body and ( - ((length is not None and length > 0) or msg.chunked) - and not self._upgraded + (length is not None and length > 0) or msg.chunked ): payload = StreamReader( self.protocol, @@ -465,6 +463,10 @@ def get_content_length() -> int | None: ) if not payload_parser.done: self._payload_parser = payload_parser + # https://www.rfc-editor.org/info/rfc9110/#section-7.8-15 + # Defer any requested upgrade until the + # complete request has been read. + self._pending_upgrade = upgraded elif method == METH_CONNECT: assert isinstance(msg, RawRequestMessage) payload = StreamReader( @@ -511,6 +513,11 @@ def get_content_length() -> int | None: ) if not payload_parser.done: self._payload_parser = payload_parser + elif upgraded: + # No body to read, so the connection switches to + # the upgraded protocol immediately. + self._upgraded = True + payload = EMPTY_PAYLOAD else: payload = EMPTY_PAYLOAD @@ -568,6 +575,11 @@ def get_content_length() -> int | None: start_pos = 0 data_len = len(data) self._payload_parser = None + if self._pending_upgrade: + # Body fully read: the deferred upgrade takes effect and + # the rest of the connection is the upgraded protocol. + self._upgraded = True + self._pending_upgrade = False if data and start_pos < data_len: data = data[start_pos:] diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 342530a2df1..0ed254557f5 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1538,6 +1538,54 @@ async def test_http_request_upgrade_unknown(parser: Any) -> None: assert await messages[0][-1].read() == b"{}" +@pytest.mark.parametrize("chunked", (False, True), ids=("content-length", "chunked")) +async def test_http_request_upgrade_with_body_read( + parser: HttpRequestParser, chunked: bool +) -> None: + body_request = b"foobarbaz\r\n\r\n" + if chunked: + framing = b"Transfer-Encoding: chunked\r\n" + body = b"%x\r\n%s\r\n0\r\n\r\n" % (len(body_request), body_request) + else: + framing = b"Content-Length: %d\r\n" % len(body_request) + body = body_request + after = b"GET /after HTTP/1.1\r\nHost: a\r\n\r\n" + text = ( + b"GET /ws HTTP/1.1\r\nHost: a\r\n" + b"Connection: Upgrade\r\nUpgrade: websocket\r\n" + + framing + + b"\r\n" + + body + + after + ) + messages, upgrade, tail = parser.feed_data(text) + assert len(messages) == 1 + msg, payload = messages[0] + assert msg.method == "GET" + assert msg.path == "/ws" + assert msg.upgrade + assert await payload.read() == body_request + # The connection switches protocols only after the body is fully read. + assert upgrade + assert tail == after + + +def test_http_request_upgrade_empty_body_allowed(parser: HttpRequestParser) -> None: + text = ( + b"GET /ws HTTP/1.1\r\n" + b"Host: a\r\n" + b"Connection: Upgrade\r\n" + b"Upgrade: websocket\r\n" + b"Content-Length: 0\r\n\r\n" + b"some raw data" + ) + messages, upgrade, tail = parser.feed_data(text) + msg = messages[0][0] + assert msg.upgrade + assert upgrade + assert tail == b"some raw data" + + @pytest.fixture def xfail_c_parser_url(request) -> None: if isinstance(request.getfixturevalue("parser"), HttpRequestParserPy): diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index 869a53797c5..d126347aaf0 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -1845,6 +1845,57 @@ def raw_get(path: str) -> bytes: assert len(handled) == pipelined_requests + 1 +async def test_declined_websocket_upgrade_reads_body( + aiohttp_server: AiohttpServer, +) -> None: + body_read = b"" + + async def ws_handler(request: web.Request) -> web.Response: + nonlocal body_read + # Decline the upgrade; read the body as a normal handler may. + body_read = await request.read() + return web.Response(text="declined") + + async def after_handler(request: web.Request) -> web.Response: + return web.Response(text="after") + + app = web.Application() + app.router.add_get("/ws", ws_handler) + app.router.add_get("/after", after_handler) + server = await aiohttp_server(app) + + body = b"FooBarBaz\r\n\r\n" + # Use raw connection in order to pipeline requests. + pipeline = ( + ( + b"GET /ws HTTP/1.1\r\n" + b"Host: localhost\r\n" + b"Connection: Upgrade\r\n" + b"Upgrade: websocket\r\n" + b"Content-Length: %d\r\n\r\n" % len(body) + ) + + body + + b"GET /after HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" + ) + + reader, writer = await asyncio.open_connection(server.host, server.port) + try: + writer.write(pipeline) + await writer.drain() + # The trailing request sends Connection: close, so the server closes + # once it has answered it -- reading to EOF gathers both responses. + response = await asyncio.wait_for(reader.read(), 5) + finally: + writer.close() + with suppress(ConnectionResetError, BrokenPipeError): + await writer.wait_closed() + + assert body_read == body + # Both the upgrade request and the pipelined request were served. + assert response.count(b"HTTP/1.") == 2, response + assert response.count(b" 200 ") == 2, response + + @pytest.mark.parametrize("decompressed_size", [4 * 1024 * 1024, 32 * 1024 * 1024]) async def test_unread_compressed_body_drain_is_bounded( aiohttp_server: AiohttpServer, From 6ae358f0983c3f4d6f67692b2f8e65dc8e091c98 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 20:21:57 +0100 Subject: [PATCH 063/137] [PR #13016/8ef76ab8 backport][3.14] Fix body reads after failed WS upgrade (#13017) **This is a backport of PR #13016 as merged into master (8ef76ab8029d0f2d88449fe115feb42ded646c7c).** Co-authored-by: Sam Bull --- CHANGES/13016.bugfix.rst | 1 + aiohttp/_http_parser.pyx | 17 +++++++++--- aiohttp/http_parser.py | 22 ++++++++++++---- tests/test_http_parser.py | 48 +++++++++++++++++++++++++++++++++ tests/test_web_functional.py | 51 ++++++++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 CHANGES/13016.bugfix.rst diff --git a/CHANGES/13016.bugfix.rst b/CHANGES/13016.bugfix.rst new file mode 100644 index 00000000000..a984f64e333 --- /dev/null +++ b/CHANGES/13016.bugfix.rst @@ -0,0 +1 @@ +Fixed request body not being read on rejected WebSocket upgrades -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 5ca1ccf6b0d..01ade16338e 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -320,6 +320,7 @@ cdef class HttpParser: set _seen_singletons list _raw_headers bint _upgraded + bint _pending_upgrade list _messages bint _more_data_available bint _paused @@ -396,6 +397,7 @@ cdef class HttpParser: self._response_with_body = response_with_body self._read_until_eof = read_until_eof self._upgraded = False + self._pending_upgrade = False self._auto_decompress = auto_decompress self._content_encoding = None self._lax = False @@ -480,10 +482,15 @@ cdef class HttpParser: raise BadHttpMessage("Missing 'Host' header in request.") h_upg = headers.get("upgrade", "") if (upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES) or self._cparser.method == cparser.HTTP_CONNECT: - self._upgraded = True + # https://www.rfc-editor.org/info/rfc9110/#section-7.8-15 + # Defer the protocol switch until the complete request has been + # received. + self._pending_upgrade = True else: if upgrade and self._cparser.status_code == 101: - self._upgraded = True + # llhttp pauses for a 101 on its own; just mark the pending + # switch so feed_data returns the upgraded-protocol tail. + self._pending_upgrade = True # do not support old websocket spec if SEC_WEBSOCKET_KEY1 in headers: @@ -642,6 +649,10 @@ 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 + if self._pending_upgrade: + # A supported upgrade whose request body has now been fully read. + self._upgraded = True + self._pending_upgrade = False elif errno is cparser.HPE_PAUSED: cparser.llhttp_resume(self._cparser) pos = cparser.llhttp_get_error_pos(self._cparser) - base @@ -860,8 +871,6 @@ cdef int cb_on_headers_complete(cparser.llhttp_t* parser) except -1: pyparser._last_error = exc return -1 else: - if pyparser._upgraded or pyparser._cparser.method == cparser.HTTP_CONNECT: - return 2 if not pyparser._response_with_body: return 1 return 0 diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index ae0993d2880..212b82228ea 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -293,6 +293,7 @@ def __init__( self._lines: list[bytes] = [] self._tail = b"" self._upgraded = False + self._pending_upgrade = False self._payload = None self._payload_parser: HttpPayloadParser | None = None self._payload_has_more_data = False @@ -424,9 +425,7 @@ def get_content_length() -> int | None: if SEC_WEBSOCKET_KEY1 in msg.headers: raise InvalidHeader(SEC_WEBSOCKET_KEY1) - self._upgraded = msg.upgrade and _is_supported_upgrade( - msg.headers - ) + upgraded = msg.upgrade and _is_supported_upgrade(msg.headers) method = getattr(msg, "method", self.method) # code is only present on responses @@ -438,8 +437,7 @@ def get_content_length() -> int | None: method and method in EMPTY_BODY_METHODS ) if not empty_body and ( - ((length is not None and length > 0) or msg.chunked) - and not self._upgraded + (length is not None and length > 0) or msg.chunked ): payload = StreamReader( self.protocol, @@ -465,6 +463,10 @@ def get_content_length() -> int | None: ) if not payload_parser.done: self._payload_parser = payload_parser + # https://www.rfc-editor.org/info/rfc9110/#section-7.8-15 + # Defer any requested upgrade until the + # complete request has been read. + self._pending_upgrade = upgraded elif method == METH_CONNECT: assert isinstance(msg, RawRequestMessage) payload = StreamReader( @@ -511,6 +513,11 @@ def get_content_length() -> int | None: ) if not payload_parser.done: self._payload_parser = payload_parser + elif upgraded: + # No body to read, so the connection switches to + # the upgraded protocol immediately. + self._upgraded = True + payload = EMPTY_PAYLOAD else: payload = EMPTY_PAYLOAD @@ -568,6 +575,11 @@ def get_content_length() -> int | None: start_pos = 0 data_len = len(data) self._payload_parser = None + if self._pending_upgrade: + # Body fully read: the deferred upgrade takes effect and + # the rest of the connection is the upgraded protocol. + self._upgraded = True + self._pending_upgrade = False if data and start_pos < data_len: data = data[start_pos:] diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 95c0ac573fb..9e115620eba 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1510,6 +1510,54 @@ async def test_http_request_upgrade_unknown(parser: Any) -> None: assert await messages[0][-1].read() == b"{}" +@pytest.mark.parametrize("chunked", (False, True), ids=("content-length", "chunked")) +async def test_http_request_upgrade_with_body_read( + parser: HttpRequestParser, chunked: bool +) -> None: + body_request = b"foobarbaz\r\n\r\n" + if chunked: + framing = b"Transfer-Encoding: chunked\r\n" + body = b"%x\r\n%s\r\n0\r\n\r\n" % (len(body_request), body_request) + else: + framing = b"Content-Length: %d\r\n" % len(body_request) + body = body_request + after = b"GET /after HTTP/1.1\r\nHost: a\r\n\r\n" + text = ( + b"GET /ws HTTP/1.1\r\nHost: a\r\n" + b"Connection: Upgrade\r\nUpgrade: websocket\r\n" + + framing + + b"\r\n" + + body + + after + ) + messages, upgrade, tail = parser.feed_data(text) + assert len(messages) == 1 + msg, payload = messages[0] + assert msg.method == "GET" + assert msg.path == "/ws" + assert msg.upgrade + assert await payload.read() == body_request + # The connection switches protocols only after the body is fully read. + assert upgrade + assert tail == after + + +def test_http_request_upgrade_empty_body_allowed(parser: HttpRequestParser) -> None: + text = ( + b"GET /ws HTTP/1.1\r\n" + b"Host: a\r\n" + b"Connection: Upgrade\r\n" + b"Upgrade: websocket\r\n" + b"Content-Length: 0\r\n\r\n" + b"some raw data" + ) + messages, upgrade, tail = parser.feed_data(text) + msg = messages[0][0] + assert msg.upgrade + assert upgrade + assert tail == b"some raw data" + + @pytest.fixture def xfail_c_parser_url(request) -> None: if isinstance(request.getfixturevalue("parser"), HttpRequestParserPy): diff --git a/tests/test_web_functional.py b/tests/test_web_functional.py index 869a53797c5..d126347aaf0 100644 --- a/tests/test_web_functional.py +++ b/tests/test_web_functional.py @@ -1845,6 +1845,57 @@ def raw_get(path: str) -> bytes: assert len(handled) == pipelined_requests + 1 +async def test_declined_websocket_upgrade_reads_body( + aiohttp_server: AiohttpServer, +) -> None: + body_read = b"" + + async def ws_handler(request: web.Request) -> web.Response: + nonlocal body_read + # Decline the upgrade; read the body as a normal handler may. + body_read = await request.read() + return web.Response(text="declined") + + async def after_handler(request: web.Request) -> web.Response: + return web.Response(text="after") + + app = web.Application() + app.router.add_get("/ws", ws_handler) + app.router.add_get("/after", after_handler) + server = await aiohttp_server(app) + + body = b"FooBarBaz\r\n\r\n" + # Use raw connection in order to pipeline requests. + pipeline = ( + ( + b"GET /ws HTTP/1.1\r\n" + b"Host: localhost\r\n" + b"Connection: Upgrade\r\n" + b"Upgrade: websocket\r\n" + b"Content-Length: %d\r\n\r\n" % len(body) + ) + + body + + b"GET /after HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n" + ) + + reader, writer = await asyncio.open_connection(server.host, server.port) + try: + writer.write(pipeline) + await writer.drain() + # The trailing request sends Connection: close, so the server closes + # once it has answered it -- reading to EOF gathers both responses. + response = await asyncio.wait_for(reader.read(), 5) + finally: + writer.close() + with suppress(ConnectionResetError, BrokenPipeError): + await writer.wait_closed() + + assert body_read == body + # Both the upgrade request and the pipelined request were served. + assert response.count(b"HTTP/1.") == 2, response + assert response.count(b" 200 ") == 2, response + + @pytest.mark.parametrize("decompressed_size", [4 * 1024 * 1024, 32 * 1024 * 1024]) async def test_unread_compressed_body_drain_is_bounded( aiohttp_server: AiohttpServer, From d21648f77bb00864db2f046b7e3a4c14d403fb5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:09:24 +0000 Subject: [PATCH 064/137] Bump ast-serialize from 0.5.0 to 0.6.0 (#13027) Bumps [ast-serialize](https://github.com/mypyc/ast_serialize) from 0.5.0 to 0.6.0.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ast-serialize&package-manager=pip&previous-version=0.5.0&new-version=0.6.0)](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 6207c100954..22c1a209a30 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.5.0 +ast-serialize==0.6.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via diff --git a/requirements/dev.txt b/requirements/dev.txt index a8acfd23078..ab980060088 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.5.0 +ast-serialize==0.6.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via diff --git a/requirements/lint.txt b/requirements/lint.txt index b7396ae3366..c4b3d2d839b 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -8,7 +8,7 @@ aiodns==4.0.4 # via -r requirements/lint.in annotated-types==0.7.0 # via pydantic -ast-serialize==0.5.0 +ast-serialize==0.6.0 # via mypy async-timeout==5.0.1 # via valkey diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 99879898575..7bd09d5f507 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.5.0 +ast-serialize==0.6.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 6fc30cbea10..010bfd75ae8 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.5.0 +ast-serialize==0.6.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 b69428243b2..5d36af33250 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.5.0 +ast-serialize==0.6.0 # via mypy async-timeout==5.0.1 ; python_version < "3.11" # via -r requirements/runtime-deps.in From 091ce7b72762c1664c6a344c6a9c525b2e374f59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 11:18:50 +0000 Subject: [PATCH 065/137] Bump librt from 0.11.0 to 0.12.0 (#13028) Bumps [librt](https://github.com/mypyc/librt) from 0.11.0 to 0.12.0.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=librt&package-manager=pip&previous-version=0.11.0&new-version=0.12.0)](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 22c1a209a30..140a21e8ea8 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -111,7 +111,7 @@ jinja2==3.1.6 # sphinx # sphinxcontrib-mermaid # towncrier -librt==0.11.0 +librt==0.12.0 # via mypy markdown-it-py==3.0.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index ab980060088..1aaac6debde 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -109,7 +109,7 @@ jinja2==3.1.6 # sphinx # sphinxcontrib-mermaid # towncrier -librt==0.11.0 +librt==0.12.0 # via mypy markdown-it-py==3.0.0 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index c4b3d2d839b..198f27a91f8 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -46,7 +46,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 # via -r requirements/lint.in -librt==0.11.0 +librt==0.12.0 # via mypy markdown-it-py==4.2.0 # via rich diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 7bd09d5f507..a6e85edad5e 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" and implementation_name == "cpython" # via -r requirements/test-common.in -librt==0.11.0 +librt==0.12.0 # via mypy markdown-it-py==4.2.0 # via rich diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 010bfd75ae8..f8fb4d41bc4 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -58,7 +58,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in -librt==0.11.0 +librt==0.12.0 # via mypy markdown-it-py==4.2.0 # via rich diff --git a/requirements/test.txt b/requirements/test.txt index 5d36af33250..a2f8623fd85 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -58,7 +58,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in -librt==0.11.0 +librt==0.12.0 # via mypy markdown-it-py==4.2.0 # via rich From 732d74a25644604f623154ab479b4dc2e4a02b26 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jul 2026 11:11:21 +0000 Subject: [PATCH 066/137] Bump aiohappyeyeballs from 2.6.2 to 2.7.1 (#13036) Bumps [aiohappyeyeballs](https://github.com/aio-libs/aiohappyeyeballs) from 2.6.2 to 2.7.1.
Release notes

Sourced from aiohappyeyeballs's releases.

v2.7.1 (2026-07-01)

Performance Improvements

  • Use defaultdict to help with performance (#239, e10f335)

Refactoring

  • Collapse collect-then-remove pattern in utils helpers (#227, 0dd3fd1)

  • Merge identical except blocks in _connect_sock cleanup (#229, b7c186e)

  • Simplify _interleave_addrinfos family grouping (#226, fdf6548)


Detailed Changes: v2.7.0...v2.7.1

v2.7.0 (2026-05-20)

Features


Detailed Changes: v2.6.2...v2.7.0

Changelog

Sourced from aiohappyeyeballs's changelog.

v2.7.1 (2026-07-01)

Performance improvements

  • Use defaultdict to help with performance (e10f335)

Refactoring

  • Simplify _interleave_addrinfos family grouping (fdf6548)
  • Collapse collect-then-remove pattern in utils helpers (0dd3fd1)
  • Merge identical except blocks in _connect_sock cleanup (b7c186e)

v2.7.0 (2026-05-20)

Features

  • Add python 3.14 support (10e7d43)
Commits
  • 4dfc926 2.7.1
  • e10f335 perf: use defaultdict to help with performance (#239)
  • 338277b chore: stop tracking .DS_Store files (#251)
  • 1697869 ci: add codspeed benchmark suite for addrinfo reordering hot paths (#250)
  • d40a848 chore(deps-dev): bump starlette from 1.0.1 to 1.3.1 (#242)
  • 51d4266 chore(deps-dev): bump cryptography from 46.0.7 to 48.0.1 (#243)
  • 2df44b3 chore(pre-commit.ci): pre-commit autoupdate (#241)
  • 3e3cb61 chore(deps-dev): bump pytest from 9.0.3 to 9.1.1 (#244)
  • 922dcd7 chore(deps-ci): bump the github-actions group with 4 updates (#245)
  • 70461c5 ci: drop .github/FUNDING.yml to inherit org-wide config (#247)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=aiohappyeyeballs&package-manager=pip&previous-version=2.6.2&new-version=2.7.1)](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-mobile.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 be40c5bee8f..d52f9988399 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -6,7 +6,7 @@ # aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.2 +aiohappyeyeballs==2.7.1 # 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 5d8ba45de28..f1372b0b16d 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,7 +6,7 @@ # aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.2 +aiohappyeyeballs==2.7.1 # 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 140a21e8ea8..6b9179656a4 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -8,7 +8,7 @@ aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via # -r requirements/lint.in # -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.2 +aiohappyeyeballs==2.7.1 # 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 1aaac6debde..d09e4943d44 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,7 +8,7 @@ aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via # -r requirements/lint.in # -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.2 +aiohappyeyeballs==2.7.1 # 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 28afc656000..d1f98e8c7db 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -6,7 +6,7 @@ # aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.2 +aiohappyeyeballs==2.7.1 # 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 f8fb4d41bc4..b553c50ed4c 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -6,7 +6,7 @@ # aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.2 +aiohappyeyeballs==2.7.1 # via -r requirements/runtime-deps.in aiosignal==1.4.0 # via -r requirements/runtime-deps.in diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index 4dc437fd8f3..9a3e54e7840 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -6,7 +6,7 @@ # aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.2 +aiohappyeyeballs==2.7.1 # 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 a2f8623fd85..15a459e34ce 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -6,7 +6,7 @@ # aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -aiohappyeyeballs==2.6.2 +aiohappyeyeballs==2.7.1 # via -r requirements/runtime-deps.in aiosignal==1.4.0 # via -r requirements/runtime-deps.in From 7482f39a23e11095f1204cddd077ee938d0babfd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:03:52 +0000 Subject: [PATCH 067/137] Bump coverage from 7.14.3 to 7.15.0 (#13043) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.14.3 to 7.15.0.
Changelog

Sourced from coverage's changelog.

Version 7.15.0 β€” 2026-07-02

  • Since 7.14.0, reporting commands implicitly combine parallel data files. Now those commands have a new option --keep-combined to retain the data files after combining them instead of the default, which is to delete them. Finishes issue 2198_.

  • Fix: the LCOV report would incorrectly count excluded functions as uncovered, as described in issue 2205. This is now fixed thanks to Martin Kuntz Jacobsen <pull 2206_>.

  • When running your program, coverage now correctly sets yourmodule.__spec__.loader as strongly recommended <--loader--_>, avoiding the deprecation warning described in issue 2208. Thanks, A5rocks <pull 2209_>_.

  • Fix: with Python 3.10, running with the -I (isolated mode) option didn't correctly omit the current directory from the module search path, as described in issue 2103. That is now fixed thanks to Ilia Sorokin <pull 2211_>.

.. --loader--: https://docs.python.org/3/reference/datamodel.html#module.__loader_ .. _issue 2103: coveragepy/coveragepy#2103 .. _issue 2198: coveragepy/coveragepy#2198 .. _issue 2205: coveragepy/coveragepy#2205 .. _pull 2206: coveragepy/coveragepy#2206 .. _issue 2208: coveragepy/coveragepy#2208 .. _pull 2209: coveragepy/coveragepy#2209 .. _pull 2211: coveragepy/coveragepy#2211

.. _changes_7-14-3:

Commits
  • c8c8020 docs: sample HTML for 7.15.0
  • ae19db1 docs: prep for 7.15.0
  • 17b45a1 docs: --keep-combined in the man page
  • 6f9fa1e fix: preserve isolated sys.path on Python 3.10 (#2211)
  • 787af5f chore: bump actions/checkout in the action-dependencies group (#2210)
  • 1ed3998 fix: start attaching the loader on __spec__ #2208 (#2209)
  • 1ab1122 docs: remove stray comma
  • f24a91f feat: --keep-combined for reporting commands. #2198
  • 5e70751 test: canonicalize mock calls in test_cmdline to reduce diff noise
  • 65979cc fix: skip excluded functions in LCOV function totals (#2206)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coverage&package-manager=pip&previous-version=7.14.3&new-version=7.15.0)](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-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 6b9179656a4..6b2b9009583 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -56,7 +56,7 @@ click==8.4.2 # slotscheck # towncrier # wait-for-it -coverage==7.14.3 +coverage==7.15.0 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/dev.txt b/requirements/dev.txt index d09e4943d44..74aec2abbe0 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -56,7 +56,7 @@ click==8.4.2 # slotscheck # towncrier # wait-for-it -coverage==7.14.3 +coverage==7.15.0 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index 580621a8584..8d72c97efa7 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -6,7 +6,7 @@ # click==8.4.2 # via wait-for-it -coverage==7.14.3 +coverage==7.15.0 # via pytest-cov exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test-common.txt b/requirements/test-common.txt index a6e85edad5e..13aaaa3ce94 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -14,7 +14,7 @@ cffi==2.0.0 # via cryptography click==8.4.2 # via wait-for-it -coverage==7.14.3 +coverage==7.15.0 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index b553c50ed4c..2ae5ea0b2ea 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -30,7 +30,7 @@ cffi==2.0.0 # pycares click==8.4.2 # via wait-for-it -coverage==7.14.3 +coverage==7.15.0 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index 9a3e54e7840..b0a6d9f4f34 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.2 # via wait-for-it -coverage==7.14.3 +coverage==7.15.0 # via pytest-cov exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test.txt b/requirements/test.txt index 15a459e34ce..160b35275ea 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -30,7 +30,7 @@ cffi==2.0.0 # pycares click==8.4.2 # via wait-for-it -coverage==7.14.3 +coverage==7.15.0 # via # -r requirements/test-common.in # pytest-cov From fa9fc05e79c675f4e64d3a57d1bbf23eec8611bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:17:49 +0000 Subject: [PATCH 068/137] Bump filelock from 3.29.4 to 3.29.5 (#13046) 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.29.4 to 3.29.5.
Release notes

Sourced from filelock's releases.

3.29.5

What's Changed

New Contributors

Full Changelog: https://github.com/tox-dev/filelock/compare/3.29.4...3.29.5

Commits
  • be56227 lifetime: reject negative, non-numeric, and bool values at the setter (#573)
  • 1f6cde4 roll back a read acquire's open transaction when its SELECT fails (#575)
  • c76dee6 test: fix Windows type check broken by #577 (#580)
  • ea594a5 Keep Unix lock files after release (#577)
  • 7595a7b use a private break name in break_lock_file (#576)
  • 0b707fe [pre-commit.ci] pre-commit autoupdate (#572)
  • 22ecd6a don't complete a writer acquire on a peer's reclaimed marker (#571)
  • 70ecdb2 build(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#570)
  • dbcc83a [pre-commit.ci] pre-commit autoupdate (#568)
  • 022394c don't follow symlinks in raise_on_not_writable_file (#567)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=filelock&package-manager=pip&previous-version=3.29.4&new-version=3.29.5)](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 6b2b9009583..2bc9dd303b1 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -74,7 +74,7 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.29.4 +filelock==3.29.5 # via # python-discovery # virtualenv diff --git a/requirements/dev.txt b/requirements/dev.txt index 74aec2abbe0..85440f2b92d 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -72,7 +72,7 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.29.4 +filelock==3.29.5 # via # python-discovery # virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index 198f27a91f8..c13134c648a 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -30,7 +30,7 @@ distlib==0.4.3 # via virtualenv exceptiongroup==1.3.1 # via pytest -filelock==3.29.4 +filelock==3.29.5 # via # python-discovery # virtualenv From bbf87b3ab5f58a79480b980bea251d685995162d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:33:41 +0000 Subject: [PATCH 069/137] Bump astral-sh/setup-uv from 8.2.0 to 8.3.0 (#13062) Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.2.0 to 8.3.0.
Commits
  • d31148d Strip environment markers from detected uv dependency pins (#938)
  • 17c3989 Fix cache keys for Python version ranges (#937)
  • 3cc3c11 chore(deps): roll up Dependabot updates (#936)
  • 9225f84 chore(deps): bump release-drafter/release-drafter from 7.3.1 to 7.4.0 (#924)
  • fc16fa3 chore(deps): bump actions/checkout from 6.0.2 to 7.0.0 (#926)
  • a1a7345 ci: call docs update workflow from release (#933)
  • a5e9cbf docs: update version references to v8.2.0 (#932)
  • c5680ec chore: update known checksums for 0.11.26 (#930)
  • c86fe4e Add a threat model for setup-uv (#923)
  • 224c887 chore: update known checksums for 0.11.25 (#929)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=astral-sh/setup-uv&package-manager=github_actions&previous-version=8.2.0&new-version=8.3.0)](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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 28d776da237..d622eb9f3ee 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -175,7 +175,7 @@ jobs: # important: do not use system python env: UV_PYTHON_PREFERENCE: only-managed - uses: astral-sh/setup-uv@v8.2.0 + uses: astral-sh/setup-uv@v8.3.0 with: python-version: ${{ matrix.pyver }} activate-environment: true @@ -282,7 +282,7 @@ jobs: # important: do not use system python env: UV_PYTHON_PREFERENCE: only-managed - uses: astral-sh/setup-uv@v8.2.0 + uses: astral-sh/setup-uv@v8.3.0 with: python-version: ${{ matrix.pyver }} activate-environment: true @@ -354,7 +354,7 @@ jobs: # important: do not use system python env: UV_PYTHON_PREFERENCE: only-managed - uses: astral-sh/setup-uv@v8.2.0 + uses: astral-sh/setup-uv@v8.3.0 with: python-version: ${{ matrix.pyver }} activate-environment: true From aa360555826879b8378043b6f00c32cbaf5be918 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:34:16 +0000 Subject: [PATCH 070/137] Bump python-discovery from 1.4.2 to 1.4.3 (#13064) 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.4.2 to 1.4.3.
Release notes

Sourced from python-discovery's releases.

v1.4.3

What's Changed

Full Changelog: https://github.com/tox-dev/python-discovery/compare/1.4.2...1.4.3

Changelog

Sourced from python-discovery's changelog.

Packaging updates and notes for downstreams - 1.4.3

  • Constrain the hatchling build requirement per Python version so sdist builds resolve a compatible backend on the declared >=3.8 floor - hatchling>=1.28 dropped Python 3.9, leaving 3.8/3.9 unbuildable from source - by :user:gaborbernat. (:issue:92)

v1.4.2 (2026-06-11)


Commits
  • ad44e73 release 1.4.3
  • cabc631 πŸ› fix(build): match hatchling floor to python-requires (#94)
  • 952c6e5 [pre-commit.ci] pre-commit autoupdate (#93)
  • 67478b4 build(deps): bump actions/checkout from 6.0.3 to 7.0.0 (#91)
  • 8b1c352 [pre-commit.ci] pre-commit autoupdate (#90)
  • 936fabb [pre-commit.ci] pre-commit autoupdate (#89)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=python-discovery&package-manager=pip&previous-version=1.4.2&new-version=1.4.3)](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 2bc9dd303b1..9a9a34e1819 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -214,7 +214,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.4.2 +python-discovery==1.4.3 # via virtualenv python-on-whales==0.81.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 85440f2b92d..984ceb1b4af 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -209,7 +209,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.4.2 +python-discovery==1.4.3 # via virtualenv python-on-whales==0.81.0 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index c13134c648a..cb1ba80fc5f 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.4.2 +python-discovery==1.4.3 # via virtualenv python-on-whales==0.81.0 # via -r requirements/lint.in From d61dc3e97b2132ddaf33ba44274e82fc4f68585d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:36:32 +0000 Subject: [PATCH 071/137] Bump setuptools from 82.0.1 to 83.0.0 (#13065) 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.1 to 83.0.0.
Changelog

Sourced from setuptools's changelog.

v83.0.0

Features

  • Require Python 3.10 or later.

Bugfixes

  • MANIFEST.in matching (via FileList) is now insensitive to Unicode normalization form. A pattern authored in one form (e.g. NFC, as typically saved by editors) now matches a file whose name is stored on disk in another (e.g. NFD, as produced by macOS APFS/HFS+). Previously an exclude, global-exclude, recursive-exclude, or prune rule could silently fail to drop a non-ASCII-named file from the source distribution, publishing it despite the exclusion -- see GHSA-h35f-9h28-mq5c.

Deprecations and Removals

  • pypa/distutils#334
Commits
  • 6519f72 Bump version: 82.0.1 β†’ 83.0.0
  • d1151b1 Merge pull request #5250 from pypa/feature/distutils-d7633fbed
  • a2df31e Capture removal of dry_run parameter in changelog.
  • 00144dc Moved newsfragment to the release where it occurred.
  • a4a5a2b Add news fragment.
  • 77470c2 Merge https://github.com/pypa/distutils into feature/distutils-d7633fbed
  • 3c43897 Merge pull request #5247 from pypa/copilot/fix-pypy-version-issue
  • bb6ea66 Bump PyPy from 3.10 to 3.11 in CI workflow
  • a2bc3ac Fix broken intersphinx reference to build's installation docs
  • 2d6a739 Use stacked parametrize decorators instead of itertools.product
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=setuptools&package-manager=pip&previous-version=82.0.1&new-version=83.0.0)](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 9a9a34e1819..c1af1824dd1 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -231,7 +231,7 @@ requests==2.34.2 # sphinxcontrib-spelling rich==15.0.0 # via pytest-codspeed -setuptools==82.0.1 +setuptools==83.0.0 # via pip-tools setuptools-git==1.2 # via -r requirements/test-common-base.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 984ceb1b4af..d3a6fde77cd 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -224,7 +224,7 @@ requests==2.34.2 # via sphinx rich==15.0.0 # via pytest-codspeed -setuptools==82.0.1 +setuptools==83.0.0 # via pip-tools setuptools-git==1.2 # via -r requirements/test-common-base.in From 1e1b6690c13ec892f9151d10a5c4bdf6380a2ae6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:38:01 +0000 Subject: [PATCH 072/137] Bump slotscheck from 0.20.0 to 0.20.1 (#13066) Bumps [slotscheck](https://github.com/ariebovenberg/slotscheck) from 0.20.0 to 0.20.1.
Release notes

Sourced from slotscheck's releases.

v0.20.1

  • Removed the click dependency in favor of the standard library argparse. This makes slotscheck dependency-free (aside from tomli for Python <3.11).
  • Add a readme note about the differences between slotscheck and static linters like Ruff and Pylint.
Changelog

Sourced from slotscheck's changelog.

0.20.1 (2026-07-05)

  • Removed the click dependency in favor of the standard library argparse. This makes slotscheck dependency-free (aside from tomli for Python <3.11).
  • Add a readme note about the differences between slotscheck and static linters like Ruff and Pylint.
Commits
  • c97c0d2 fix release date in changelog
  • 557fba9 fix brittle test
  • 734db78 add changelog note
  • 7556199 use sphinx argparse to generate CLI docs
  • 4dce3cb add ruff/pylint notes
  • 1b53f94 make version lazy-computed
  • 960f995 remove click dependency in favor of stdlib
  • 4fb44c0 Bump typing-extensions from 4.15.0 to 4.16.0
  • 2312500 Bump click from 8.4.1 to 8.4.2
  • 223dccc Bump ruff from 0.15.18 to 0.15.20
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=slotscheck&package-manager=pip&previous-version=0.20.0&new-version=0.20.1)](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 | 4 +--- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index c1af1824dd1..d1321767ce7 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -53,7 +53,6 @@ charset-normalizer==3.4.7 click==8.4.2 # via # pip-tools - # slotscheck # towncrier # wait-for-it coverage==7.15.0 @@ -237,7 +236,7 @@ setuptools-git==1.2 # via -r requirements/test-common-base.in six==1.17.0 # via python-dateutil -slotscheck==0.20.0 +slotscheck==0.20.1 # via -r requirements/lint.in snowballstemmer==3.1.1 # via sphinx diff --git a/requirements/dev.txt b/requirements/dev.txt index d3a6fde77cd..d121afdc3f6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -53,7 +53,6 @@ charset-normalizer==3.4.7 click==8.4.2 # via # pip-tools - # slotscheck # towncrier # wait-for-it coverage==7.15.0 @@ -230,7 +229,7 @@ setuptools-git==1.2 # via -r requirements/test-common-base.in six==1.17.0 # via python-dateutil -slotscheck==0.20.0 +slotscheck==0.20.1 # via -r requirements/lint.in snowballstemmer==3.1.1 # via sphinx diff --git a/requirements/lint.txt b/requirements/lint.txt index cb1ba80fc5f..2bb294b6da9 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -22,8 +22,6 @@ cffi==2.0.0 # pycares cfgv==3.5.0 # via pre-commit -click==8.4.2 - # via slotscheck cryptography==49.0.0 # via trustme distlib==0.4.3 @@ -103,7 +101,7 @@ rich==15.0.0 # via pytest-codspeed six==1.17.0 # via python-dateutil -slotscheck==0.20.0 +slotscheck==0.20.1 # via -r requirements/lint.in tomli==2.4.1 # via From 40f89922d6d6a90ed68c00291910ba13135a5847 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:32:46 +0000 Subject: [PATCH 073/137] Bump astral-sh/setup-uv from 8.3.0 to 8.3.1 (#13072) Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.3.0 to 8.3.1.
Commits
  • f98e069 Change update-docs PR labels from 'update-docs' to 'documentation' (#945)
  • cd46263 chore: update known checksums for 0.11.27 (#944)
  • 11245c7 docs: update version references to v8.3.0 (#939)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=astral-sh/setup-uv&package-manager=github_actions&previous-version=8.3.0&new-version=8.3.1)](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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index d622eb9f3ee..9ce995594e6 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -175,7 +175,7 @@ jobs: # important: do not use system python env: UV_PYTHON_PREFERENCE: only-managed - uses: astral-sh/setup-uv@v8.3.0 + uses: astral-sh/setup-uv@v8.3.1 with: python-version: ${{ matrix.pyver }} activate-environment: true @@ -282,7 +282,7 @@ jobs: # important: do not use system python env: UV_PYTHON_PREFERENCE: only-managed - uses: astral-sh/setup-uv@v8.3.0 + uses: astral-sh/setup-uv@v8.3.1 with: python-version: ${{ matrix.pyver }} activate-environment: true @@ -354,7 +354,7 @@ jobs: # important: do not use system python env: UV_PYTHON_PREFERENCE: only-managed - uses: astral-sh/setup-uv@v8.3.0 + uses: astral-sh/setup-uv@v8.3.1 with: python-version: ${{ matrix.pyver }} activate-environment: true From 0c4ed9a7f1bfaa435923cb92ab9ba98412f85bb9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:37:10 +0000 Subject: [PATCH 074/137] Bump cffi from 2.0.0 to 2.1.0 (#13074) Bumps [cffi](https://github.com/python-cffi/cffi) from 2.0.0 to 2.1.0.
Release notes

Sourced from cffi's releases.

v2.1.0

  • Added support for Python 3.15 and support for C extensions generated by CFFI to target the new abi3t free-threaded ABI.
  • Dropped support for Python 3.9.
  • Added cffi-gen-src CLI to generate CFFI C extension source for alternate build backend support.
  • Fixed crashes inside __delitem__.
  • Fixed "string too big" error under MSVC.
  • Fixed mingw builds.
  • Added support for arm64 iOS wheels.
Commits
  • d9f6f70 New release 2.1.0
  • 02a7b0e Misc pre-2.1 release/packaging cleanup (#253)
  • 1362e5d Move cffi-gen-src release note to 2.1.0 notes
  • a797055 Make error message when embedding version test fails more friendly
  • f1f40a8 Update changelog
  • dc62c93 Delete missed cp39 Windows builds
  • a341180 Update version numbers to prepare for v2.1 release
  • 9f04d85 Mark test using inet_ntoa as thread-unsafe
  • 5f12702 Fix flaky pytest-run-parallel CI crash
  • 26b3d3a Merge branch 'main' into integrate-buildtool
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cffi&package-manager=pip&previous-version=2.0.0&new-version=2.1.0)](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-common.txt | 2 +- requirements/test-ft.txt | 2 +- requirements/test-mobile.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 d52f9988399..0c4b784c87d 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -18,7 +18,7 @@ backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_v # 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 +cffi==2.1.0 # via pycares frozenlist==1.8.0 # via diff --git a/requirements/base.txt b/requirements/base.txt index f1372b0b16d..dbaaea3f6a3 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -18,7 +18,7 @@ backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_v # 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 +cffi==2.1.0 # via pycares frozenlist==1.8.0 # via diff --git a/requirements/constraints.txt b/requirements/constraints.txt index d1321767ce7..c80bed15e5c 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -42,7 +42,7 @@ build==1.5.0 # via pip-tools certifi==2026.6.17 # via requests -cffi==2.0.0 +cffi==2.1.0 # via # cryptography # pycares diff --git a/requirements/dev.txt b/requirements/dev.txt index d121afdc3f6..0104adb6f6a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -42,7 +42,7 @@ build==1.5.0 # via pip-tools certifi==2026.6.17 # via requests -cffi==2.0.0 +cffi==2.1.0 # via # cryptography # pycares diff --git a/requirements/lint.txt b/requirements/lint.txt index 2bb294b6da9..ee2bee6c731 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -16,7 +16,7 @@ backports-zstd==1.3.0 ; implementation_name == "cpython" and python_version < "3 # via -r requirements/lint.in blockbuster==1.5.26 # via -r requirements/lint.in -cffi==2.0.0 +cffi==2.1.0 # via # cryptography # pycares diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index d1f98e8c7db..dd0a75aef3e 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -18,7 +18,7 @@ backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_v # 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 +cffi==2.1.0 # via pycares frozenlist==1.8.0 # via diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 13aaaa3ce94..0c08c85c327 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -10,7 +10,7 @@ ast-serialize==0.6.0 # via mypy blockbuster==1.5.26 # via -r requirements/test-common.in -cffi==2.0.0 +cffi==2.1.0 # via cryptography click==8.4.2 # via wait-for-it diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 2ae5ea0b2ea..8d1bff8f023 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -24,7 +24,7 @@ blockbuster==1.5.26 # via -r requirements/test-common.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 +cffi==2.1.0 # via # cryptography # pycares diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index b0a6d9f4f34..3fa1a5db302 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -20,7 +20,7 @@ backports-zstd==1.3.0 ; platform_python_implementation == "CPython" and python_v # 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" +cffi==2.1.0 ; sys_platform != "android" and sys_platform != "ios" # via # -r requirements/test-mobile.in # pycares diff --git a/requirements/test.txt b/requirements/test.txt index 160b35275ea..d4c42f19b95 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -24,7 +24,7 @@ blockbuster==1.5.26 # via -r requirements/test-common.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 +cffi==2.1.0 # via # cryptography # pycares From bc807b2c66e59d79e342e2df5e321d27eebbff8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:50:03 +0000 Subject: [PATCH 075/137] Bump virtualenv from 21.5.1 to 21.6.0 (#13080) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.5.1 to 21.6.0.
Release notes

Sourced from virtualenv's releases.

21.6.0

What's Changed

Full Changelog: https://github.com/pypa/virtualenv/compare/21.5.2...21.6.0

21.5.2

What's Changed

Full Changelog: https://github.com/pypa/virtualenv/compare/21.5.1...21.5.2

Changelog

Sourced from virtualenv's changelog.

Features - 21.6.0

  • Stop installing the _virtualenv.{py,pth} distutils import hook for Python 3.10 and later, where pip, setuptools and CPython already ignore the install config keys it guards against; this removes the hook's startup import cost. The hook is still installed for Python 3.9 - :issue:3181. (:issue:3181)

v21.5.2 (2026-07-06)


Bugfixes - 21.5.2

  • Upgrade embedded wheels:

    • setuptools to 83.0.0 (:issue:3180)

v21.5.1 (2026-06-16)


Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=virtualenv&package-manager=pip&previous-version=21.5.1&new-version=21.6.0)](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 c80bed15e5c..e7b5a496d82 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -306,7 +306,7 @@ uvloop==0.22.1 ; platform_system != "Windows" # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.5.1 +virtualenv==21.6.0 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common-base.in diff --git a/requirements/dev.txt b/requirements/dev.txt index 0104adb6f6a..b4edc960cf1 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -296,7 +296,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.5.1 +virtualenv==21.6.0 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common-base.in diff --git a/requirements/lint.txt b/requirements/lint.txt index ee2bee6c731..bfa89f249d0 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -126,7 +126,7 @@ uvloop==0.22.1 ; platform_system != "Windows" # via -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.5.1 +virtualenv==21.6.0 # via pre-commit zlib-ng==1.0.0 # via -r requirements/lint.in From 61a930fcb2a76b87e0aee261a1a4cb7038014142 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:52:56 +0000 Subject: [PATCH 076/137] Bump filelock from 3.29.5 to 3.29.6 (#13077) 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.29.5 to 3.29.6.
Release notes

Sourced from filelock's releases.

3.29.6

What's Changed

Full Changelog: https://github.com/tox-dev/filelock/compare/3.29.5...3.29.6

Commits
  • f8b0e93 test: silence fork DeprecationWarning on 3.15 (#585)
  • b68be65 _util: drop the dead st_mtime=0 short-circuit in raise_on_not_writable_file (...
  • a833dd9 serialise singleton construction in FileLockMeta (#581)
  • bb2f884 [pre-commit.ci] pre-commit autoupdate (#583)
  • da9c3ed :moneybag: Surface GitHub Sponsors + thanks.dev
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=filelock&package-manager=pip&previous-version=3.29.5&new-version=3.29.6)](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 e7b5a496d82..ff994e72c6d 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -73,7 +73,7 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.29.5 +filelock==3.29.6 # via # python-discovery # virtualenv diff --git a/requirements/dev.txt b/requirements/dev.txt index b4edc960cf1..c2876d733cf 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -71,7 +71,7 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.29.5 +filelock==3.29.6 # via # python-discovery # virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index bfa89f249d0..6788d69aa03 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -28,7 +28,7 @@ distlib==0.4.3 # via virtualenv exceptiongroup==1.3.1 # via pytest -filelock==3.29.5 +filelock==3.29.6 # via # python-discovery # virtualenv From 309ad617b8ec50a639e69304c4dbf9666324e19b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 11:53:19 +0000 Subject: [PATCH 077/137] Bump charset-normalizer from 3.4.7 to 3.4.8 (#13078) Bumps [charset-normalizer](https://github.com/jawah/charset_normalizer) from 3.4.7 to 3.4.8.
Release notes

Sourced from charset-normalizer's releases.

Version 3.4.8

3.4.8 (2026-07-06)

Fixed

  • Wall import time due to cascade codec imports for our multibyte first sort of iana supported codecs (#742)
  • Unnecessary json import at runtime (#753)
  • Inverse capitalization not seen by noise detector (#731)

Changed

  • No longer holding a global cache for our noise / coherence measurements. Relax RSS memory usage.
  • Micro-optimizations in our noise / coherence measurements.
  • No longer using regex search by default for our preemptive charset mark algorithm.
  • Raised upperbound of setuptools to v83.
  • Raised upperbound of mypy(c) to v2.1.

Removed

  • Redundant UTF7 BOM marker (#730)
Changelog

Sourced from charset-normalizer's changelog.

3.4.8 (2026-07-06)

Fixed

  • Wall import time due to cascade codec imports for our multibyte first sort of iana supported codecs (#742)
  • Unnecessary json import at runtime (#753)
  • Inverse capitalization not seen by noise detector (#731)

Changed

  • No longer holding a global cache for our noise / coherence measurements. Relax RSS memory usage.
  • Micro-optimizations in our noise / coherence measurements.
  • No longer using regex search by default for our preemptive charset mark algorithm.
  • Raised upperbound of setuptools to v83.
  • Raised upperbound of mypy(c) to v2.1.

Removed

  • Redundant UTF7 BOM marker (#730)
Commits
  • a6f8feb Merge pull request #770 from jawah/unblock-ci
  • 528e16c chore: add osv-scanner.toml
  • 5993498 chore: ast_serialize musl missing prebuilt riscv,s390x,ppc64le
  • aa2ddd8 Release 3.4.8 (#766)
  • a7f1781 chore: raise upperbound mypyc to 2.1
  • 6f7eb9e chore: raise upperbound setuptools to 83
  • 447b8aa chore: remove cooldown for dependabot github-actions ecosystem
  • c99135f chore: update release date
  • c15e6f0 chore: remove useless lock contention from char_info to utils functions
  • 82d2078 chore: harmonize truth chk style
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=charset-normalizer&package-manager=pip&previous-version=3.4.7&new-version=3.4.8)](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 ff994e72c6d..4a992432d06 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -48,7 +48,7 @@ cffi==2.1.0 # pycares cfgv==3.5.0 # via pre-commit -charset-normalizer==3.4.7 +charset-normalizer==3.4.8 # via requests click==8.4.2 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index c2876d733cf..bde0838ebff 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -48,7 +48,7 @@ cffi==2.1.0 # pycares cfgv==3.5.0 # via pre-commit -charset-normalizer==3.4.7 +charset-normalizer==3.4.8 # via requests click==8.4.2 # via diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index 4c35bc3fc56..d54a504c8b2 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -12,7 +12,7 @@ babel==2.18.0 # via sphinx certifi==2026.6.17 # via requests -charset-normalizer==3.4.7 +charset-normalizer==3.4.8 # via requests click==8.4.2 # via towncrier diff --git a/requirements/doc.txt b/requirements/doc.txt index c374c83ef83..17b1bef9fc4 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -12,7 +12,7 @@ babel==2.18.0 # via sphinx certifi==2026.6.17 # via requests -charset-normalizer==3.4.7 +charset-normalizer==3.4.8 # via requests click==8.4.2 # via towncrier From 4c7dc166c6bf3417a8b7481a46b01d7e4dc8e2a1 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:55:15 +0100 Subject: [PATCH 078/137] [PR #12706/ce99eac8 backport][3.15] Threat model chapter 4 (#13081) **This is a backport of PR #12706 as merged into master (ce99eac8db1073e4bbe4c8b3d0913900e0b3a758).** Co-authored-by: Sam Bull --- THREAT_MODEL.md | 152 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 143 insertions(+), 9 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 4b5af95cbd4..f7b0aed090a 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -313,6 +313,17 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and Werkzeug emit duplicate `Content-Type` / `Server`); fix disables the check on the response parser (lax mode) while keeping it on the request parser (strict). +- **GHSA-63hw-fmq6-xxg2 (CVE-2026-54277)** (3.14.1) β€” the + Cython / llhttp request parser failed to enforce `max_line_size` + when a single request or header line arrived across multiple TCP + segments, letting an attacker stream an oversized line in small + fragments to exhaust memory. +- **GHSA-4fvr-rgm6-gqmc (CVE-2026-54273)** (3.14.1) β€” no cap + on the number of HTTP/1 pipelined requests queued on a single + connection; the `_messages` deque in `web_protocol.RequestHandler` + could grow without bound. Memory was previously bounded only + transitively by `read_bufsize` ([Β§5.7](#57-server-connection-lifecycle) threat 7.4); + the fix adds an explicit pipeline-count cap. These are all currently in place; this section assumes no regression. @@ -546,20 +557,143 @@ client-side, the writer adds masks to outgoing frames. `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. +- **GHSA-xcgm-r5h9-7989 (CVE-2026-54274)** (3.14.1) β€” a peer could + send large WebSocket frames with incomplete payloads, bypassing the + per-frame memory limit because the buffer grew before the + `max_msg_size` check fired against the (still-unknown) final size. + Fixed by accounting for the in-flight buffer in the cap. - **PR #12976** β€” the client created its `WebSocketReader` without passing `compress`, so the reader defaulted to `compress=True` and decompressed RSV1 frames even when PMCE was never negotiated (threat 3.3; RFC 6455 Β§5.2 requires failing such frames). Fixed by passing `compress=bool(compress)` in `client.py:_ws_connect` and removing the `compress` / `decode_text` defaults on `WebSocketReader.__init__`. -- No formal CVE has been published against the WebSocket framing layer to - date. -**Open questions.** +--- + +### 5.4. Multipart parsing & encoding + +**Scope.** Parsing of incoming `multipart/*` bodies (`MultipartReader`, +`BodyPartReader`) and construction of outgoing multipart bodies +(`MultipartWriter`, `FormData`). This includes boundary handling, per-part +header parsing, `Content-Disposition` extraction, `Content-Transfer-Encoding` +decoding, charset detection, and the `Request.post()` path that wraps +`multipart/form-data`. Out of scope: the underlying compression codecs ([Β§5.5](#55-compression-codecs)), +streams/payloads ([Β§5.6](#56-streams--payloads)), and the connection-level read/write timeouts that +back-stop slow drips ([Β§5.7](#57-server-connection-lifecycle)). + +**Components covered.** + +- `aiohttp/multipart.py` β€” `MultipartReader`, `BodyPartReader`, + `MultipartWriter`, `parse_content_disposition`, + `content_disposition_filename`. +- `aiohttp/formdata.py` β€” `FormData` builder. +- `aiohttp/web_request.py` β€” `Request.post()`, `Request.multipart()`. +- `aiohttp/payload.py` β€” multipart payload wrapping (overlap with [Β§5.6](#56-streams--payloads)). + +**Trust boundaries & data flow.** + +```mermaid +flowchart LR + Wire([Untrusted body]) --> Reader[MultipartReader] + Reader --> Parts[BodyPartReader per part] + Parts -->|headers, name, filename| App([User handler]) + Parts -->|streamed body| App + Reader -. nested .-> Reader + + AppOut([User code]) --> FD[FormData] --> Writer[MultipartWriter] + Writer --> WireOut([Outbound body]) +``` + +The reader's input is fully attacker-controlled on the server side and +upstream-controlled on the client side. Per-part values surfaced to user +code (`headers`, `name`, `filename`, body bytes) are all untrusted. The +writer's input is trusted in the threat-model sense, but `FormData` is the +boundary at which user-supplied strings can become wire bytes. + +**Assets at risk (chunk-specific).** -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.) +- **Process memory** β€” number of parts, per-part body size, recursion depth + for nested multipart. +- **Filesystem safety in user code** β€” `filename` round-trips from wire to + application without sanitisation. +- **Outbound framing integrity** β€” boundary uniqueness, header validity in + `FormData.add_field`. + +**Threats (STRIDE).** + +| # | Component / Vector | STRIDE | Threat | Risk | +| :--- | :--- | :--- | :--- | :--- | +| 4.1 | Boundary parameter parsing | T | Malformed boundary parameter (oversized, missing, or containing bytes outside the RFC 2046 Β§5.1.1 safe set β€” digits, letters, and a small punctuation set) could enable multipart parser confusion or smuggling. | Low | +| 4.2 | Number of parts per body | D | A peer submits a body packed with many tiny parts (e.g. ten thousand 100-byte parts inside a 1 MiB body). Each part allocates a `BodyPartReader` plus header dict, so the live-Python-object footprint is far larger than the on-wire byte count. `client_max_size` caps the wire bytes but not the per-part allocation amplification. | Low | +| 4.3 | Nested multipart recursion | D | `MultipartReader.next()` recurses into nested multiparts without a depth cap; deeply nested input can hit `RecursionError`. `Request.post()` short-circuits this by rejecting any nested multipart it sees, but the bare API does not. | Medium | +| 4.4 | Per-part header block size | D | A peer submits a part with an oversized header block (very long field values, or hundreds of headers per part) to drive memory growth at parse time, multiplied across many parts. | Low | +| 4.5 | Per-part body size | D | A peer submits a single part with a body that grows arbitrarily large before any framing boundary β€” if size checking happens only after buffering the whole part, memory blows up before the cap fires. | Low | +| 4.6 | `Content-Disposition` filename | T / I | Filename is parsed (incl. RFC 2231 `filename*=`) and returned verbatim to the application. Path-traversal strings (`../`), absolute paths, NUL/CR/LF all reach user code unsanitised. | Medium | +| 4.7 | `Content-Disposition` name | T | Field name returned verbatim. CR/LF in a part's `name` doesn't cross the framing boundary (the multipart parser delimits parts by boundary, not by CR/LF in field names), but downstream loggers / dashboards may misrender. | Low | +| 4.8 | `_charset_` magic form field | T | An attacker can include a form field named `_charset_` whose value retroactively becomes the *default charset* used to decode subsequent text fields (`multipart.py:MultipartReader.next _charset_ form handling`). Surprising behaviour; documented in the standard. | Low | +| 4.9 | `Content-Transfer-Encoding` (base64 / quoted-printable) | T / D | A peer sets `Content-Transfer-Encoding: base64` (or `quoted-printable`) and submits malformed encoded data β€” the decoder raises mid-stream and the partially-decoded part is surfaced to the handler with an exception. | Low | +| 4.10 | `Content-Encoding` (gzip/deflate within a part) | D | A peer submits a multipart part with `Content-Encoding: gzip` (or `deflate`) carrying a zip-bomb payload β€” a small compressed body that expands to a vastly larger decoded body, driving memory exhaustion in the receiving handler. | Low | +| 4.11 | `MultipartWriter` boundary collision | T | If a part body contains a byte sequence matching the boundary string, the writer emits it as-is β€” a receiver parsing the multipart will see a fake part break early. Negligible against the default `uuid.uuid4().hex` boundary, but real if user code supplies a short or predictable one. | Low | +| 4.12 | `FormData.add_field` argument injection | T | When user code opts into `FormData(quote_fields=False)`, attacker-controlled `name` or `filename` reach the outbound `Content-Disposition` value with only `\` / `"` escaped. CR/LF/NUL would let an attacker inject extra disposition parameters or a fake-part break-out; `add_field` now rejects these bytes in `name`, `filename`, and `content_type` so the opt-out path is also defended. | Low–Med | +| 4.13 | Per-part header injection via `headers=` | T | Caller-supplied headers passed via `MultipartWriter.append(headers=...)` or `Payload(headers=...)` reach the wire through `Payload._binary_headers`, which serialises `f"{k}: {v}\r\n"` without CR/LF/NUL validation. An attacker-influenced header value can inject extra part headers or break out into a fake part. Distinct from 4.12, which only covers the `name` / `filename` parameters of `add_field`. | Medium | +| 4.14 | `Request.post()` temp-file lifetime | I / D | Uploaded files are streamed to `tempfile.TemporaryFile()` and lifetime is bound to the `FileField` object. If user code drops the field reference without reading or closing, garbage collection eventually closes the temp file. | Low | + +**Mitigations.** + +| # | Threat | Existing | Recommended | +| :--- | :--- | :--- | :--- | +| 4.1 | Boundary parameter | 70-char cap; missing-boundary raises; HTTP header layer ([Β§5.1](#51-http1-parser)) catches CR/LF/NUL. | None. | +| 4.2 | Many small parts | `client_max_size` caps total bytes. | Documented design decision: rely on `client_max_size` rather than introducing a `max_parts` knob. **User**: operators sensitive to live-object count should reduce `client_max_size`. | +| 4.3 | Nested-multipart recursion | `Request.post()` rejects any nested multipart with `ValueError` ("To decode nested multipart you need to use custom reader") (`web_request.py:BaseRequest.post`). | **Direct `MultipartReader` users get unlimited recursion. Add a `max_nesting_depth` parameter (default e.g. 10) to fail cleanly before `RecursionError`.** | +| 4.4 | Per-part headers bounded | `max_field_size` / `max_headers` plumbed since 5fe9dfb64 (Mar 2026). | None. | +| 4.5 | Per-part body bounded | Per-iteration size check since 9cc4b917c (Mar 2026). | None. | +| 4.6 | Filename path traversal | None; verbatim return is the documented behaviour. | **User**: sanitise `BodyPartReader.filename` before opening / saving / reflecting (strip path components, control bytes, known-bad sequences). | +| 4.7 | Field-name pass-through | None. | **User**: `request.post()` keys come from attacker-controlled `name` parameters; do not feed them directly to filesystem / DB / template paths. | +| 4.8 | `_charset_` magic field | Behaviour follows the HTML5 spec for multipart form charset selection. | **Document the surprising behaviour: an attacker who can post any field can change how subsequent fields are decoded.** **User**: if your form-handling code makes decisions based on string content, be aware that the decode charset is attacker-influenceable. | +| 4.9 | CTE decoding | `BodyPartReader._decode_content_transfer` dispatches to `base64.b64decode` / `binascii.a2b_qp` / pass-through for `binary`/`8bit`/`7bit`, and raises `RuntimeError` on any other token. Both decoders raise on malformed input; decoded output is still subject to the per-part `client_max_size` cap. For base64, `BodyPartReader.read_chunk` extends each chunk read until its base64-significant character count is a multiple of 4, so mid-quartet splits cannot silently corrupt the decoded output. | None. | +| 4.10 | Content-Encoding decompression | Per-chunk `max_decompress_size`, per-part `client_max_size` cap. | Inherits the [Β§5.5](#55-compression-codecs) caveat about backend `max_length` honouring. | +| 4.11 | Outbound boundary collision | UUID4-hex default; HMAC-grade entropy means collision with random body bytes is statistically negligible. | **User**: callers supplying their own boundary must use a random, sufficiently long value. | +| 4.12 | `FormData` argument injection | `add_field` runs `name`, `filename`, and `content_type` through `_safe_header` (`http_writer.py:_safe_header`), rejecting `\r` / `\n` / `\x00`. `name` / `filename` are additionally percent-encoded by `content_disposition_header` (`helpers.py:content_disposition_header`) when `quote_fields=True` (default), so the `add_field` check is what closes the `quote_fields=False` opt-out path. | None. | +| 4.13 | Per-part header injection | `Payload._binary_headers` (`payload.py:_binary_headers`) now applies `_safe_header` to every name and value, raising `ValueError` if CR / LF / NUL is present before any byte reaches the wire. Covers all paths that mutate the headers dict (constructor, direct assignment, `set_content_disposition`). | None. | +| 4.14 | Temp-file lifetime | `tempfile.TemporaryFile()` is unlinked at creation on POSIX; closing the FD (whether explicit or via GC) reclaims the disk. | **User**: explicitly close `FileField`s you don't intend to consume to release the temp file promptly under high load. | + +**Past advisories / hardening (recap).** + +- **GHSA-5m98-qgg9-wh84 (CVE-2024-30251)** (3.9.4) β€” infinite loop in multipart + `_read_chunk_from_length` on a malformed POST body. +- **GHSA-jj3x-wxrx-4x23 (CVE-2025-69227)** (3.13.3) β€” `Request.post()` entered + an infinite loop on malformed bodies when `PYTHONOPTIMIZE` stripped `assert` + statements (asserts were doing real control-flow work in the + multipart reader). Fixed by replacing the assertions with explicit + checks. Code audit follow-up: any other `assert` used for control + flow should be identified and removed. +- **GHSA-6jhg-hg63-jvvf (CVE-2025-69228)** (3.13.3) β€” DoS via large + `Request.post()` payloads. +- **GHSA-2vrm-gr82-f7m5 (CVE-2026-34514)** (3.13.4) β€” + `FormData.add_field` rejects CR/LF in `content_type` (header + injection on outbound multipart). +- **GHSA-m5qp-6w8w-w647 (CVE-2026-34516)** (3.13.4) β€” `MultipartReader` + now honours `max_field_size` and `max_headers` from the underlying + protocol, plugging an unbounded per-part header memory growth path. +- **GHSA-3wq7-rqq7-wx6j (CVE-2026-34517)** (3.13.4) β€” `Request.post()` enforces + `client_max_size` during iteration rather than after buffering, + plugging a memory-blow-up on large form fields. +- **GHSA-m6qw-4cw2-hm4m (CVE-2026-50269)** (3.14.0) β€” + `Payload._binary_headers` now rejects CR / LF / NUL in any per-part + header name or value via `_safe_header`, closing the outbound + multipart header-injection path through `MultipartWriter.append(headers=...)` + and `Payload(headers=...)` that the main HTTP writer's `_safe_header` + check did not cover (threat 4.13). +- **PR #12721** (3.14.0) β€” companion to GHSA-m6qw-4cw2-hm4m: + `FormData.add_field` now rejects CR / LF / NUL in `name` and + `filename` (in addition to the previous `content_type` check), so the + `quote_fields=False` opt-out path can no longer be used to inject + extra `Content-Disposition` parameters or a fake-part break-out + (threat 4.12). +- **PR #12794** (3.14.1) β€” precautionary hardening: + multipart body parts whose `Content-Length` header is not a plain + decimal sequence (e.g. `+5`, `-1`, `1_0`) are now rejected, matching + the main request parser's strictness per RFC 9110 Β§8.6. + +These are all currently in place; this section assumes no regression. From 4a2169c2aa3d4e8183f7ab59c59625bf57c4e991 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:54:03 +0100 Subject: [PATCH 079/137] [PR #13069/fd07a7a7 backport][3.15] Fix OWS in Content-Disposition (#13083) **This is a backport of PR #13069 as merged into master (fd07a7a7d7cdff58ca2a98c6c7a26149562e13a7).** Co-authored-by: Sam Bull --- CHANGES/13002.bugfix.rst | 1 + aiohttp/multipart.py | 5 +++-- tests/test_multipart_helpers.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 CHANGES/13002.bugfix.rst diff --git a/CHANGES/13002.bugfix.rst b/CHANGES/13002.bugfix.rst new file mode 100644 index 00000000000..1ac16a04fa7 --- /dev/null +++ b/CHANGES/13002.bugfix.rst @@ -0,0 +1 @@ +Fixed parsing optional whitespace in Content-Disposition -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 9116171e8cb..7bdae9644ca 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -154,9 +154,10 @@ def unescape(text: str, *, chars: str = "".join(map(re.escape, CHAR))) -> str: else: failed = True - if is_quoted(value): + rstripped = value.rstrip() + if is_quoted(rstripped): failed = False - value = unescape(value[1:-1].lstrip("\\/")) + value = unescape(rstripped[1:-1].lstrip("\\/")) elif is_token(value): failed = False elif parts: diff --git a/tests/test_multipart_helpers.py b/tests/test_multipart_helpers.py index 67bbc86c223..87c33778559 100644 --- a/tests/test_multipart_helpers.py +++ b/tests/test_multipart_helpers.py @@ -30,6 +30,16 @@ def test_semicolon(self) -> None: assert disptype == "form-data" assert params == {"name": "data", "filename": "file ; name.mp4"} + def test_ows_before_separator(self) -> None: + # https://github.com/aio-libs/aiohttp/issues/13002 + # Optional whitespace before the ";" separator (RFC 9110 Β§5.6.6) must + # not make the quoted-value repair heuristic swallow the next param. + disptype, params = parse_content_disposition( + 'attachment; filename="test.txt" ; name="field"' + ) + assert disptype == "attachment" + assert params == {"filename": "test.txt", "name": "field"} + def test_inlwithasciifilename(self) -> None: disptype, params = parse_content_disposition('inline; filename="foo.html"') assert "inline" == disptype From df1005069145decd94878c66f53dce39deb660d9 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:54:15 +0100 Subject: [PATCH 080/137] [PR #13069/fd07a7a7 backport][3.14] Fix OWS in Content-Disposition (#13082) **This is a backport of PR #13069 as merged into master (fd07a7a7d7cdff58ca2a98c6c7a26149562e13a7).** Co-authored-by: Sam Bull --- CHANGES/13002.bugfix.rst | 1 + aiohttp/multipart.py | 5 +++-- tests/test_multipart_helpers.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 CHANGES/13002.bugfix.rst diff --git a/CHANGES/13002.bugfix.rst b/CHANGES/13002.bugfix.rst new file mode 100644 index 00000000000..1ac16a04fa7 --- /dev/null +++ b/CHANGES/13002.bugfix.rst @@ -0,0 +1 @@ +Fixed parsing optional whitespace in Content-Disposition -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 9116171e8cb..7bdae9644ca 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -154,9 +154,10 @@ def unescape(text: str, *, chars: str = "".join(map(re.escape, CHAR))) -> str: else: failed = True - if is_quoted(value): + rstripped = value.rstrip() + if is_quoted(rstripped): failed = False - value = unescape(value[1:-1].lstrip("\\/")) + value = unescape(rstripped[1:-1].lstrip("\\/")) elif is_token(value): failed = False elif parts: diff --git a/tests/test_multipart_helpers.py b/tests/test_multipart_helpers.py index 67bbc86c223..87c33778559 100644 --- a/tests/test_multipart_helpers.py +++ b/tests/test_multipart_helpers.py @@ -30,6 +30,16 @@ def test_semicolon(self) -> None: assert disptype == "form-data" assert params == {"name": "data", "filename": "file ; name.mp4"} + def test_ows_before_separator(self) -> None: + # https://github.com/aio-libs/aiohttp/issues/13002 + # Optional whitespace before the ";" separator (RFC 9110 Β§5.6.6) must + # not make the quoted-value repair heuristic swallow the next param. + disptype, params = parse_content_disposition( + 'attachment; filename="test.txt" ; name="field"' + ) + assert disptype == "attachment" + assert params == {"filename": "test.txt", "name": "field"} + def test_inlwithasciifilename(self) -> None: disptype, params = parse_content_disposition('inline; filename="foo.html"') assert "inline" == disptype From d697a9eb245f051b81264cd3705e5d515977bc5c Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:34:15 +0100 Subject: [PATCH 081/137] [PR #12984/7d789204 backport][3.15] Fix Digest challenge with multiple schemes (#13091) **This is a backport of PR #12984 as merged into master (7d7892044c6fe663a566e78c58e5fd25ac6d57b1).** Co-authored-by: Sam Bull --- CHANGES/12984.bugfix.rst | 3 + aiohttp/client_middleware_digest_auth.py | 63 ++++++++++++--------- tests/test_client_middleware_digest_auth.py | 31 +++++++++- 3 files changed, 69 insertions(+), 28 deletions(-) create mode 100644 CHANGES/12984.bugfix.rst diff --git a/CHANGES/12984.bugfix.rst b/CHANGES/12984.bugfix.rst new file mode 100644 index 00000000000..a156216397b --- /dev/null +++ b/CHANGES/12984.bugfix.rst @@ -0,0 +1,3 @@ +Fixed :class:`~aiohttp.DigestAuthMiddleware` corrupting the ``Digest`` +challenge when a ``WWW-Authenticate`` response offered more than one +authentication scheme -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/client_middleware_digest_auth.py b/aiohttp/client_middleware_digest_auth.py index 6c3e37f7c00..21675107401 100644 --- a/aiohttp/client_middleware_digest_auth.py +++ b/aiohttp/client_middleware_digest_auth.py @@ -52,27 +52,23 @@ class DigestAuthChallenge(TypedDict, total=False): # Compile the regex pattern once at module level for performance _HEADER_PAIRS_PATTERN = re.compile( - r'(?:^|\s|,\s*)(\w+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))' + r'(?:^|\s|,\s*)(\w+)(?:\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+)))?' if sys.version_info < (3, 11) - else r'(?:^|\s|,\s*)((?>\w+))\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))' - # +------------|--------|--|-|-|--|----|------|----|--||-----|-> Match valid start/sep - # +--------|--|-|-|--|----|------|----|--||-----|-> alphanumeric key (atomic - # | | | | | | | | || | group reduces backtracking) - # +--|-|-|--|----|------|----|--||-----|-> maybe whitespace - # | | | | | | | || | - # +-|-|--|----|------|----|--||-----|-> = (delimiter) - # +-|--|----|------|----|--||-----|-> maybe whitespace - # | | | | | || | - # +--|----|------|----|--||-----|-> group quoted or unquoted - # | | | | || | - # +----|------|----|--||-----|-> if quoted... - # +------|----|--||-----|-> anything but " or \ - # +----|--||-----|-> escaped characters allowed - # +--||-----|-> or can be empty string - # || | - # +|-----|-> if unquoted... - # +-----|-> anything but , or - # +-> at least one char req'd + else r'(?:^|\s|,\s*)((?>\w+))(?:\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+)))?' + # +------------|--------|--|--||--|--|----|------|---|---||-----|-> Match valid start/sep + # +--------|--|--||--|--|----|------|---|---||-----|-> alphanumeric key (atomic group reduces backtracking) + # +--|--||--|--|----|------|---|---||-----|-> optional value; absent => bare auth-scheme token + # +--||--|--|----|------|---|---||-----|-> maybe whitespace + # +|--|--|----|------|---|---||-----|-> = (delimiter) + # +--|--|----|------|---|---||-----|-> maybe whitespace + # +--|----|------|---|---||-----|-> group quoted or unquoted + # +----|------|---|---||-----|-> if quoted... + # +------|---|---||-----|-> anything but " or \ + # +---|---||-----|-> escaped characters allowed + # +---||-----|-> or can be empty string + # +|-----|-> if unquoted... + # +-----|-> anything but , or + # +-> at least one char req'd ) @@ -117,12 +113,17 @@ def unescape_quotes(value: str) -> str: def parse_header_pairs(header: str) -> dict[str, str]: """ - Parse key-value pairs from WWW-Authenticate or similar HTTP headers. + Parse key-value pairs from the first challenge of a WWW-Authenticate header. This function handles the complex format of WWW-Authenticate header values, supporting both quoted and unquoted values, proper handling of commas in quoted values, and whitespace variations per RFC 7616. + A single header may carry several challenges + (https://www.rfc-editor.org/rfc/rfc7235#section-4.1). Parsing + stops at the next auth-scheme token so a later challenge's parameters cannot + overwrite the first challenge's values; a leading scheme token is skipped. + Examples of supported formats: - key1="value1", key2=value2 - key1 = "value1" , key2="value, with, commas" @@ -135,11 +136,21 @@ def parse_header_pairs(header: str) -> dict[str, str]: Returns: Dictionary mapping parameter names to their values """ - return { - stripped_key: unescape_quotes(quoted_val) if quoted_val else unquoted_val - for key, quoted_val, unquoted_val in _HEADER_PAIRS_PATTERN.findall(header) - if (stripped_key := key.strip()) - } + pairs: dict[str, str] = {} + for match in _HEADER_PAIRS_PATTERN.finditer(header): + key = match.group(1) + quoted_val, unquoted_val = match.group(2), match.group(3) + if quoted_val is None and unquoted_val is None: + # Bare token with no "=value": an auth-scheme name, not a parameter. + # Skip a leading scheme; once parameters exist, a new scheme marks + # the start of the next challenge, so stop here. + if pairs: + break + continue + pairs[key] = ( + unescape_quotes(quoted_val) if quoted_val is not None else unquoted_val + ) + return pairs class DigestAuthMiddleware: diff --git a/tests/test_client_middleware_digest_auth.py b/tests/test_client_middleware_digest_auth.py index cdbedee980f..bca026a53f8 100644 --- a/tests/test_client_middleware_digest_auth.py +++ b/tests/test_client_middleware_digest_auth.py @@ -121,6 +121,18 @@ def mock_md5_digest() -> Generator[mock.MagicMock, None, None]: True, {"realm": "", "nonce": "abc", "qop": "auth"}, ), + # Multi-scheme header: a second challenge (Basic) in the same + # WWW-Authenticate value must not overwrite the Digest realm/nonce. + # https://www.rfc-editor.org/rfc/rfc7235#section-4.1 + ( + 401, + { + "www-authenticate": 'Digest realm="protected", nonce="n1", ' + 'qop="auth", Basic realm="other"' + }, + True, + {"realm": "protected", "nonce": "n1", "qop": "auth"}, + ), # Non-401 status (200, {}, False, {}), # No challenge should be set ], @@ -469,6 +481,18 @@ async def test_digest_response_exact_match( ), # Empty header ("", {}), + # Multi-scheme header: parsing stops at the next auth-scheme so a later + # challenge cannot overwrite the first's values. + # https://www.rfc-editor.org/rfc/rfc7235#section-4.1 + ( + 'realm="protected", nonce="n1", qop="auth", Basic realm="other"', + {"realm": "protected", "nonce": "n1", "qop": "auth"}, + ), + # Multi-scheme header including the leading scheme token + ( + 'Digest realm="protected", nonce="n1", Basic realm="other"', + {"realm": "protected", "nonce": "n1"}, + ), ], ids=[ "fully_quoted_header", @@ -480,6 +504,8 @@ async def test_digest_response_exact_match( "escaped_quotes", "single_quotes_as_regular_chars", "empty_header", + "multi_scheme_second_challenge_ignored", + "multi_scheme_with_leading_scheme", ], ) def test_parse_header_pairs(header: str, expected_result: dict[str, str]) -> None: @@ -1563,5 +1589,6 @@ def test_regex_performance() -> None: f"Regex took {elapsed * 1000:.1f}ms, " f"expected <{REGEX_TIME_THRESHOLD_SECONDS * 1000:.0f}ms - potential ReDoS issue" ) - # This example shouldn't produce a match either. - assert not matches + # The lone run of word characters matches as a bare auth-scheme token + # with empty value groups, so it never becomes a key=value pair. + assert all(not quoted and not unquoted for _, quoted, unquoted in matches) From 4078e04f172d0b3d3c672cb2dd23eb98b4a79460 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:34:29 +0100 Subject: [PATCH 082/137] [PR #12983/20cde095 backport][3.15] Fix empty domain issue in DigestAuthMiddleware (#13089) **This is a backport of PR #12983 as merged into master (20cde095cf3e8f7fab2926a1bbbbe8e6ad189381).** Co-authored-by: Sam Bull --- CHANGES/12983.bugfix.rst | 1 + aiohttp/client_middleware_digest_auth.py | 8 ++- tests/test_client_middleware_digest_auth.py | 68 +++++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 CHANGES/12983.bugfix.rst diff --git a/CHANGES/12983.bugfix.rst b/CHANGES/12983.bugfix.rst new file mode 100644 index 00000000000..03f4083d83a --- /dev/null +++ b/CHANGES/12983.bugfix.rst @@ -0,0 +1 @@ +Fixed ``DigestAuthMiddleware`` raising an ``IndexError`` on empty domain -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/client_middleware_digest_auth.py b/aiohttp/client_middleware_digest_auth.py index 21675107401..f4904e363d8 100644 --- a/aiohttp/client_middleware_digest_auth.py +++ b/aiohttp/client_middleware_digest_auth.py @@ -445,21 +445,23 @@ def _authenticate(self, response: ClientResponse) -> bool: # Update protection space based on domain parameter or default to origin origin = response.url.origin() + self._protection_space = [] if domain := self._challenge.get("domain"): # Parse space-separated list of URIs - self._protection_space = [] for uri in domain.split(): # Remove quotes if present uri = uri.strip('"') + if not uri: + continue if uri.startswith("/"): # Path-absolute, relative to origin self._protection_space.append(str(origin.join(URL(uri)))) else: # Absolute URI self._protection_space.append(str(URL(uri))) - else: - # No domain specified, protection space is entire origin + + if not self._protection_space: self._protection_space = [str(origin)] # Return True only if we found at least one challenge parameter diff --git a/tests/test_client_middleware_digest_auth.py b/tests/test_client_middleware_digest_auth.py index bca026a53f8..f7094721e51 100644 --- a/tests/test_client_middleware_digest_auth.py +++ b/tests/test_client_middleware_digest_auth.py @@ -1523,6 +1523,74 @@ def test_in_protection_space_multiple_spaces( assert digest_auth_mw._in_protection_space(URL("http://example.com/other")) is False +@pytest.mark.parametrize( + "domain_value", + (r'"\""', '"'), + ids=("quoted_escaped_quote", "bare_quote"), +) +def test_authenticate_domain_only_quote_does_not_poison_protection_space( + digest_auth_mw: DigestAuthMiddleware, + domain_value: str, +) -> None: + response = mock.create_autospec(ClientResponse, spec_set=True, instance=True) + response.status = 401 + response.url = URL("http://example.com/resource") + response.headers = { + "www-authenticate": f'Digest realm="test", nonce="abc", domain={domain_value}' + } + + assert digest_auth_mw._authenticate(response) is True + assert digest_auth_mw._challenge["domain"] == '"' + assert digest_auth_mw._protection_space == ["http://example.com"] + assert "" not in digest_auth_mw._protection_space + # Must not raise IndexError and must still scope to the anchor origin. + assert digest_auth_mw._in_protection_space(URL("http://example.com/other")) is True + assert digest_auth_mw._in_protection_space(URL("http://other.com/x")) is False + + +async def test_double_quote_domain_does_not_break_future_requests( + aiohttp_server: AiohttpServer, +) -> None: + """End-to-end regression for a ``domain`` directive of just a double quote. + + The first request triggers the challenge and authenticates on retry. Before + the fix, the bogus ``domain`` left an empty string in the protection space, + so the next request's preemptive-auth check raised ``IndexError``. + """ + digest_auth_mw = DigestAuthMiddleware("user", "pass", preemptive=True) + auth_headers: list[str | None] = [] + + async def handler(request: Request) -> Response: + auth_headers.append(request.headers.get(hdrs.AUTHORIZATION)) + if request.headers.get(hdrs.AUTHORIZATION) is None: + challenge = ( + 'Digest realm="test", nonce="abc123", qop="auth", ' + 'algorithm=MD5, domain="\\""' + ) + return Response( + status=401, + headers={"WWW-Authenticate": challenge}, + text="Unauthorized", + ) + return Response(text="OK") + + app = Application() + app.router.add_get("/path1", handler) + app.router.add_get("/path2", handler) + server = await aiohttp_server(app) + + async with ClientSession(middlewares=(digest_auth_mw,)) as session: + async with session.get(server.make_url("/path1")) as resp: + assert resp.status == 200 + # Previously raised IndexError inside the preemptive-auth check. + async with session.get(server.make_url("/path2")) as resp: + assert resp.status == 200 + + assert auth_headers[0] is None # First request: no auth, gets challenge + assert auth_headers[1] is not None # Retry carries the digest response + assert auth_headers[2] is not None # Second request: preemptive auth + + async def test_case_sensitive_algorithm_server( aiohttp_server: AiohttpServer, ) -> None: From bbcdce7258635bc8c22d1665a39e817c2d004330 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:44:08 +0100 Subject: [PATCH 083/137] [PR #12984/7d789204 backport][3.14] Fix Digest challenge with multiple schemes (#13090) **This is a backport of PR #12984 as merged into master (7d7892044c6fe663a566e78c58e5fd25ac6d57b1).** Co-authored-by: Sam Bull --- CHANGES/12984.bugfix.rst | 3 + aiohttp/client_middleware_digest_auth.py | 63 ++++++++++++--------- tests/test_client_middleware_digest_auth.py | 31 +++++++++- 3 files changed, 69 insertions(+), 28 deletions(-) create mode 100644 CHANGES/12984.bugfix.rst diff --git a/CHANGES/12984.bugfix.rst b/CHANGES/12984.bugfix.rst new file mode 100644 index 00000000000..a156216397b --- /dev/null +++ b/CHANGES/12984.bugfix.rst @@ -0,0 +1,3 @@ +Fixed :class:`~aiohttp.DigestAuthMiddleware` corrupting the ``Digest`` +challenge when a ``WWW-Authenticate`` response offered more than one +authentication scheme -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/client_middleware_digest_auth.py b/aiohttp/client_middleware_digest_auth.py index 6c3e37f7c00..21675107401 100644 --- a/aiohttp/client_middleware_digest_auth.py +++ b/aiohttp/client_middleware_digest_auth.py @@ -52,27 +52,23 @@ class DigestAuthChallenge(TypedDict, total=False): # Compile the regex pattern once at module level for performance _HEADER_PAIRS_PATTERN = re.compile( - r'(?:^|\s|,\s*)(\w+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))' + r'(?:^|\s|,\s*)(\w+)(?:\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+)))?' if sys.version_info < (3, 11) - else r'(?:^|\s|,\s*)((?>\w+))\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))' - # +------------|--------|--|-|-|--|----|------|----|--||-----|-> Match valid start/sep - # +--------|--|-|-|--|----|------|----|--||-----|-> alphanumeric key (atomic - # | | | | | | | | || | group reduces backtracking) - # +--|-|-|--|----|------|----|--||-----|-> maybe whitespace - # | | | | | | | || | - # +-|-|--|----|------|----|--||-----|-> = (delimiter) - # +-|--|----|------|----|--||-----|-> maybe whitespace - # | | | | | || | - # +--|----|------|----|--||-----|-> group quoted or unquoted - # | | | | || | - # +----|------|----|--||-----|-> if quoted... - # +------|----|--||-----|-> anything but " or \ - # +----|--||-----|-> escaped characters allowed - # +--||-----|-> or can be empty string - # || | - # +|-----|-> if unquoted... - # +-----|-> anything but , or - # +-> at least one char req'd + else r'(?:^|\s|,\s*)((?>\w+))(?:\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+)))?' + # +------------|--------|--|--||--|--|----|------|---|---||-----|-> Match valid start/sep + # +--------|--|--||--|--|----|------|---|---||-----|-> alphanumeric key (atomic group reduces backtracking) + # +--|--||--|--|----|------|---|---||-----|-> optional value; absent => bare auth-scheme token + # +--||--|--|----|------|---|---||-----|-> maybe whitespace + # +|--|--|----|------|---|---||-----|-> = (delimiter) + # +--|--|----|------|---|---||-----|-> maybe whitespace + # +--|----|------|---|---||-----|-> group quoted or unquoted + # +----|------|---|---||-----|-> if quoted... + # +------|---|---||-----|-> anything but " or \ + # +---|---||-----|-> escaped characters allowed + # +---||-----|-> or can be empty string + # +|-----|-> if unquoted... + # +-----|-> anything but , or + # +-> at least one char req'd ) @@ -117,12 +113,17 @@ def unescape_quotes(value: str) -> str: def parse_header_pairs(header: str) -> dict[str, str]: """ - Parse key-value pairs from WWW-Authenticate or similar HTTP headers. + Parse key-value pairs from the first challenge of a WWW-Authenticate header. This function handles the complex format of WWW-Authenticate header values, supporting both quoted and unquoted values, proper handling of commas in quoted values, and whitespace variations per RFC 7616. + A single header may carry several challenges + (https://www.rfc-editor.org/rfc/rfc7235#section-4.1). Parsing + stops at the next auth-scheme token so a later challenge's parameters cannot + overwrite the first challenge's values; a leading scheme token is skipped. + Examples of supported formats: - key1="value1", key2=value2 - key1 = "value1" , key2="value, with, commas" @@ -135,11 +136,21 @@ def parse_header_pairs(header: str) -> dict[str, str]: Returns: Dictionary mapping parameter names to their values """ - return { - stripped_key: unescape_quotes(quoted_val) if quoted_val else unquoted_val - for key, quoted_val, unquoted_val in _HEADER_PAIRS_PATTERN.findall(header) - if (stripped_key := key.strip()) - } + pairs: dict[str, str] = {} + for match in _HEADER_PAIRS_PATTERN.finditer(header): + key = match.group(1) + quoted_val, unquoted_val = match.group(2), match.group(3) + if quoted_val is None and unquoted_val is None: + # Bare token with no "=value": an auth-scheme name, not a parameter. + # Skip a leading scheme; once parameters exist, a new scheme marks + # the start of the next challenge, so stop here. + if pairs: + break + continue + pairs[key] = ( + unescape_quotes(quoted_val) if quoted_val is not None else unquoted_val + ) + return pairs class DigestAuthMiddleware: diff --git a/tests/test_client_middleware_digest_auth.py b/tests/test_client_middleware_digest_auth.py index cdbedee980f..bca026a53f8 100644 --- a/tests/test_client_middleware_digest_auth.py +++ b/tests/test_client_middleware_digest_auth.py @@ -121,6 +121,18 @@ def mock_md5_digest() -> Generator[mock.MagicMock, None, None]: True, {"realm": "", "nonce": "abc", "qop": "auth"}, ), + # Multi-scheme header: a second challenge (Basic) in the same + # WWW-Authenticate value must not overwrite the Digest realm/nonce. + # https://www.rfc-editor.org/rfc/rfc7235#section-4.1 + ( + 401, + { + "www-authenticate": 'Digest realm="protected", nonce="n1", ' + 'qop="auth", Basic realm="other"' + }, + True, + {"realm": "protected", "nonce": "n1", "qop": "auth"}, + ), # Non-401 status (200, {}, False, {}), # No challenge should be set ], @@ -469,6 +481,18 @@ async def test_digest_response_exact_match( ), # Empty header ("", {}), + # Multi-scheme header: parsing stops at the next auth-scheme so a later + # challenge cannot overwrite the first's values. + # https://www.rfc-editor.org/rfc/rfc7235#section-4.1 + ( + 'realm="protected", nonce="n1", qop="auth", Basic realm="other"', + {"realm": "protected", "nonce": "n1", "qop": "auth"}, + ), + # Multi-scheme header including the leading scheme token + ( + 'Digest realm="protected", nonce="n1", Basic realm="other"', + {"realm": "protected", "nonce": "n1"}, + ), ], ids=[ "fully_quoted_header", @@ -480,6 +504,8 @@ async def test_digest_response_exact_match( "escaped_quotes", "single_quotes_as_regular_chars", "empty_header", + "multi_scheme_second_challenge_ignored", + "multi_scheme_with_leading_scheme", ], ) def test_parse_header_pairs(header: str, expected_result: dict[str, str]) -> None: @@ -1563,5 +1589,6 @@ def test_regex_performance() -> None: f"Regex took {elapsed * 1000:.1f}ms, " f"expected <{REGEX_TIME_THRESHOLD_SECONDS * 1000:.0f}ms - potential ReDoS issue" ) - # This example shouldn't produce a match either. - assert not matches + # The lone run of word characters matches as a bare auth-scheme token + # with empty value groups, so it never becomes a key=value pair. + assert all(not quoted and not unquoted for _, quoted, unquoted in matches) From e1cad9706ec297ece656d4a099b2010f0738c83d Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 22:56:18 +0100 Subject: [PATCH 084/137] [PR #12983/20cde095 backport][3.14] Fix empty domain issue in DigestAuthMiddleware (#13088) **This is a backport of PR #12983 as merged into master (20cde095cf3e8f7fab2926a1bbbbe8e6ad189381).** Co-authored-by: Sam Bull --- CHANGES/12983.bugfix.rst | 1 + aiohttp/client_middleware_digest_auth.py | 8 ++- tests/test_client_middleware_digest_auth.py | 68 +++++++++++++++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 CHANGES/12983.bugfix.rst diff --git a/CHANGES/12983.bugfix.rst b/CHANGES/12983.bugfix.rst new file mode 100644 index 00000000000..03f4083d83a --- /dev/null +++ b/CHANGES/12983.bugfix.rst @@ -0,0 +1 @@ +Fixed ``DigestAuthMiddleware`` raising an ``IndexError`` on empty domain -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/client_middleware_digest_auth.py b/aiohttp/client_middleware_digest_auth.py index 21675107401..f4904e363d8 100644 --- a/aiohttp/client_middleware_digest_auth.py +++ b/aiohttp/client_middleware_digest_auth.py @@ -445,21 +445,23 @@ def _authenticate(self, response: ClientResponse) -> bool: # Update protection space based on domain parameter or default to origin origin = response.url.origin() + self._protection_space = [] if domain := self._challenge.get("domain"): # Parse space-separated list of URIs - self._protection_space = [] for uri in domain.split(): # Remove quotes if present uri = uri.strip('"') + if not uri: + continue if uri.startswith("/"): # Path-absolute, relative to origin self._protection_space.append(str(origin.join(URL(uri)))) else: # Absolute URI self._protection_space.append(str(URL(uri))) - else: - # No domain specified, protection space is entire origin + + if not self._protection_space: self._protection_space = [str(origin)] # Return True only if we found at least one challenge parameter diff --git a/tests/test_client_middleware_digest_auth.py b/tests/test_client_middleware_digest_auth.py index bca026a53f8..f7094721e51 100644 --- a/tests/test_client_middleware_digest_auth.py +++ b/tests/test_client_middleware_digest_auth.py @@ -1523,6 +1523,74 @@ def test_in_protection_space_multiple_spaces( assert digest_auth_mw._in_protection_space(URL("http://example.com/other")) is False +@pytest.mark.parametrize( + "domain_value", + (r'"\""', '"'), + ids=("quoted_escaped_quote", "bare_quote"), +) +def test_authenticate_domain_only_quote_does_not_poison_protection_space( + digest_auth_mw: DigestAuthMiddleware, + domain_value: str, +) -> None: + response = mock.create_autospec(ClientResponse, spec_set=True, instance=True) + response.status = 401 + response.url = URL("http://example.com/resource") + response.headers = { + "www-authenticate": f'Digest realm="test", nonce="abc", domain={domain_value}' + } + + assert digest_auth_mw._authenticate(response) is True + assert digest_auth_mw._challenge["domain"] == '"' + assert digest_auth_mw._protection_space == ["http://example.com"] + assert "" not in digest_auth_mw._protection_space + # Must not raise IndexError and must still scope to the anchor origin. + assert digest_auth_mw._in_protection_space(URL("http://example.com/other")) is True + assert digest_auth_mw._in_protection_space(URL("http://other.com/x")) is False + + +async def test_double_quote_domain_does_not_break_future_requests( + aiohttp_server: AiohttpServer, +) -> None: + """End-to-end regression for a ``domain`` directive of just a double quote. + + The first request triggers the challenge and authenticates on retry. Before + the fix, the bogus ``domain`` left an empty string in the protection space, + so the next request's preemptive-auth check raised ``IndexError``. + """ + digest_auth_mw = DigestAuthMiddleware("user", "pass", preemptive=True) + auth_headers: list[str | None] = [] + + async def handler(request: Request) -> Response: + auth_headers.append(request.headers.get(hdrs.AUTHORIZATION)) + if request.headers.get(hdrs.AUTHORIZATION) is None: + challenge = ( + 'Digest realm="test", nonce="abc123", qop="auth", ' + 'algorithm=MD5, domain="\\""' + ) + return Response( + status=401, + headers={"WWW-Authenticate": challenge}, + text="Unauthorized", + ) + return Response(text="OK") + + app = Application() + app.router.add_get("/path1", handler) + app.router.add_get("/path2", handler) + server = await aiohttp_server(app) + + async with ClientSession(middlewares=(digest_auth_mw,)) as session: + async with session.get(server.make_url("/path1")) as resp: + assert resp.status == 200 + # Previously raised IndexError inside the preemptive-auth check. + async with session.get(server.make_url("/path2")) as resp: + assert resp.status == 200 + + assert auth_headers[0] is None # First request: no auth, gets challenge + assert auth_headers[1] is not None # Retry carries the digest response + assert auth_headers[2] is not None # Second request: preemptive auth + + async def test_case_sensitive_algorithm_server( aiohttp_server: AiohttpServer, ) -> None: From b2409db9ab3d2f2b4d1bffbc7b87d879c69b74ed Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 01:20:23 +0100 Subject: [PATCH 085/137] [PR #12985/4416d89e backport][3.15] Close request body Payload on exception (#13093) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **This is a backport of PR #12985 as merged into master (4416d89e73f4a09339eb08fabef3fa010ec9184f).** Co-authored-by: Sam Bull Co-authored-by: tonghuaroot (η«₯话) --- CHANGES/12985.bugfix.rst | 1 + aiohttp/client.py | 4 +++ tests/test_client_functional.py | 56 +++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 CHANGES/12985.bugfix.rst diff --git a/CHANGES/12985.bugfix.rst b/CHANGES/12985.bugfix.rst new file mode 100644 index 00000000000..055b8572e42 --- /dev/null +++ b/CHANGES/12985.bugfix.rst @@ -0,0 +1 @@ +Fixed client not closing cleanly after an exception -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/client.py b/aiohttp/client.py index b4c09d05e12..6f9d781a342 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -691,6 +691,7 @@ async def _request( await trace.send_request_start(method, url.update_query(params), headers) timer = tm.timer() + req: ClientRequest | None = None try: with timer: # https://www.rfc-editor.org/rfc/rfc9112.html#name-retrying-requests @@ -1014,6 +1015,9 @@ async def _connect_and_send_request( handle.cancel() handle = None + if req is not None and req._body is not None: + await req._body.close() + for trace in traces: await trace.send_request_exception( method, url.update_query(params), headers, e diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 524c4a2efcd..2064ec2aafe 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -5534,6 +5534,62 @@ async def redirect_handler(request: web.Request) -> web.Response: ), "Payload.close() was not called when InvalidUrlRedirectClientError (invalid origin) was raised" +async def test_request_body_closed_on_server_disconnect() -> None: + async def drop(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + # Slam the connection shut without sending a response. + writer.close() + + server = await asyncio.start_server(drop, "127.0.0.1", 0) + port = server.sockets[0].getsockname()[1] + payload = MockedBytesPayload(b"x" * 1024) + try: + async with aiohttp.ClientSession() as session: + with pytest.raises(aiohttp.ClientError): + await session.post(f"http://127.0.0.1:{port}/", data=payload) + finally: + server.close() + await server.wait_closed() + + assert ( + payload.close_called + ), "Payload.close() was not called after a mid-upload disconnect" + + +async def test_request_body_closed_on_cancellation() -> None: + accepted = asyncio.Event() + + async def stall(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + accepted.set() + try: + await reader.read() # wait for client EOF; never respond + finally: + writer.close() + + server = await asyncio.start_server(stall, "127.0.0.1", 0) + port = server.sockets[0].getsockname()[1] + payload = MockedBytesPayload(b"y" * 1024) + try: + async with aiohttp.ClientSession() as session: + task = asyncio.create_task( + session.post(f"http://127.0.0.1:{port}/", data=payload) + ) + await accepted.wait() + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + finally: + server.close() + await server.wait_closed() + + assert payload.close_called, "Payload.close() was not called after cancellation" + + +async def test_request_error_before_body_created_does_not_mask() -> None: + async with aiohttp.ClientSession() as session: + with pytest.raises(InvalidUrlClientError): + await session.get("http:///path") + + async def test_amazon_like_cookie_scenario(aiohttp_client: AiohttpClient) -> None: """Test real-world cookie scenario similar to Amazon.""" From e1d5b87997f2d8b26aff9b34aed695968de405e2 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 01:20:39 +0100 Subject: [PATCH 086/137] [PR #12985/4416d89e backport][3.14] Close request body Payload on exception (#13092) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **This is a backport of PR #12985 as merged into master (4416d89e73f4a09339eb08fabef3fa010ec9184f).** Co-authored-by: Sam Bull Co-authored-by: tonghuaroot (η«₯话) --- CHANGES/12985.bugfix.rst | 1 + aiohttp/client.py | 4 +++ tests/test_client_functional.py | 56 +++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 CHANGES/12985.bugfix.rst diff --git a/CHANGES/12985.bugfix.rst b/CHANGES/12985.bugfix.rst new file mode 100644 index 00000000000..055b8572e42 --- /dev/null +++ b/CHANGES/12985.bugfix.rst @@ -0,0 +1 @@ +Fixed client not closing cleanly after an exception -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/client.py b/aiohttp/client.py index 62bcd0ab585..1150e09a9dd 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -696,6 +696,7 @@ async def _request( await trace.send_request_start(method, url.update_query(params), headers) timer = tm.timer() + req: ClientRequest | None = None try: with timer: # https://www.rfc-editor.org/rfc/rfc9112.html#name-retrying-requests @@ -1019,6 +1020,9 @@ async def _connect_and_send_request( handle.cancel() handle = None + if req is not None and req._body is not None: + await req._body.close() + for trace in traces: await trace.send_request_exception( method, url.update_query(params), headers, e diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 524c4a2efcd..2064ec2aafe 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -5534,6 +5534,62 @@ async def redirect_handler(request: web.Request) -> web.Response: ), "Payload.close() was not called when InvalidUrlRedirectClientError (invalid origin) was raised" +async def test_request_body_closed_on_server_disconnect() -> None: + async def drop(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + # Slam the connection shut without sending a response. + writer.close() + + server = await asyncio.start_server(drop, "127.0.0.1", 0) + port = server.sockets[0].getsockname()[1] + payload = MockedBytesPayload(b"x" * 1024) + try: + async with aiohttp.ClientSession() as session: + with pytest.raises(aiohttp.ClientError): + await session.post(f"http://127.0.0.1:{port}/", data=payload) + finally: + server.close() + await server.wait_closed() + + assert ( + payload.close_called + ), "Payload.close() was not called after a mid-upload disconnect" + + +async def test_request_body_closed_on_cancellation() -> None: + accepted = asyncio.Event() + + async def stall(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: + accepted.set() + try: + await reader.read() # wait for client EOF; never respond + finally: + writer.close() + + server = await asyncio.start_server(stall, "127.0.0.1", 0) + port = server.sockets[0].getsockname()[1] + payload = MockedBytesPayload(b"y" * 1024) + try: + async with aiohttp.ClientSession() as session: + task = asyncio.create_task( + session.post(f"http://127.0.0.1:{port}/", data=payload) + ) + await accepted.wait() + task.cancel() + with pytest.raises(asyncio.CancelledError): + await task + finally: + server.close() + await server.wait_closed() + + assert payload.close_called, "Payload.close() was not called after cancellation" + + +async def test_request_error_before_body_created_does_not_mask() -> None: + async with aiohttp.ClientSession() as session: + with pytest.raises(InvalidUrlClientError): + await session.get("http:///path") + + async def test_amazon_like_cookie_scenario(aiohttp_client: AiohttpClient) -> None: """Test real-world cookie scenario similar to Amazon.""" From 9868c5f098918bcd624ef591c9b691f70feec3b5 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 02:23:52 +0100 Subject: [PATCH 087/137] [PR #13001/fe353185 backport][3.15] Fix IndexError in parser on edge case (#13095) **This is a backport of PR #13001 as merged into master (fe353185eb5c6eb87cb3cca92213c7098aa42437).** Co-authored-by: Sam Bull --- CHANGES/13001.bugfix.rst | 1 + aiohttp/http_parser.py | 29 ++++++++++++++--------------- tests/test_http_parser.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 CHANGES/13001.bugfix.rst diff --git a/CHANGES/13001.bugfix.rst b/CHANGES/13001.bugfix.rst new file mode 100644 index 00000000000..67511e3c95e --- /dev/null +++ b/CHANGES/13001.bugfix.rst @@ -0,0 +1 @@ +Fixed an :exc:`IndexError` in the pure-Python HTTP parser -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index 212b82228ea..e08b4c11aca 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -1157,19 +1157,20 @@ def feed_data(self, chunk: bytes, size: int) -> bool: self.size += size self.out.total_compressed_bytes = self.size - # RFC1950 - # bits 0..3 = CM = 0b1000 = 8 = "deflate" - # bits 4..7 = CINFO = 1..7 = windows size. - if ( - not self._started_decoding - and self.encoding == "deflate" - and chunk[0] & 0xF != 8 - ): - # Change the decoder to decompress incorrectly compressed data - # Actually we should issue a warning about non-RFC-compliant data. - self.decompressor = ZLibDecompressor( - encoding=self.encoding, suppress_deflate_header=True - ) + # Inspect the first real byte once to choose the decompressor. An empty + # chunk (e.g. a chunk-size line arriving without body bytes) has no + # header to sniff, so skip it and wait for the first data byte. + if not self._started_decoding and chunk: + # RFC1950 + # bits 0..3 = CM = 0b1000 = 8 = "deflate" + # bits 4..7 = CINFO = 1..7 = windows size. + if self.encoding == "deflate" and chunk[0] & 0xF != 8: + # Change the decoder to decompress incorrectly compressed data + # Actually we should issue a warning about non-RFC-compliant data. + self.decompressor = ZLibDecompressor( + encoding=self.encoding, suppress_deflate_header=True + ) + self._started_decoding = True low_water = self.out._low_water max_length = ( @@ -1182,8 +1183,6 @@ def feed_data(self, chunk: bytes, size: int) -> bool: "Can not decode content-encoding: %s" % self.encoding ) - self._started_decoding = True - if chunk: self.out.feed_data(chunk, len(chunk)) return self.decompressor.data_available # type: ignore[no-any-return] diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 0ed254557f5..407292079d1 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1321,6 +1321,37 @@ async def test_compressed_chunked_with_pending(response: HttpResponseParser) -> assert result == original +async def test_compressed_chunked_split_chunk_size_line( + response: HttpResponseParser, +) -> None: + """First chunk-size line arrives in a feed that carries no body bytes. + + Regression test for an ``IndexError`` in the pure-Python parser: the + chunked parser fed an empty chunk to ``DeflateBuffer`` before any data + had been decoded, and the deflate header sniff indexed ``chunk[0]`` on it. + """ + original = b"Hello, world! " * 4 + compressed = zlib.compress(original) + size_line = hex(len(compressed))[2:].encode() + 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" + ) + + msgs, upgrade, tail = response.feed_data(headers) + payload = msgs[0][-1] + # The chunk-size line lands with no body bytes in the same feed, so the + # parser transitions into the chunk body with an empty buffer. + response.feed_data(size_line) + response.feed_data(compressed + b"\r\n0\r\n\r\n") + + result = await payload.read() + assert result == original + assert payload.exception() is None + + 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. From 199628943cb6eab8032ce57fb163c98f10d8c53b Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 02:24:37 +0100 Subject: [PATCH 088/137] [PR #13001/fe353185 backport][3.14] Fix IndexError in parser on edge case (#13094) **This is a backport of PR #13001 as merged into master (fe353185eb5c6eb87cb3cca92213c7098aa42437).** Co-authored-by: Sam Bull --- CHANGES/13001.bugfix.rst | 1 + aiohttp/http_parser.py | 29 ++++++++++++++--------------- tests/test_http_parser.py | 31 +++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 15 deletions(-) create mode 100644 CHANGES/13001.bugfix.rst diff --git a/CHANGES/13001.bugfix.rst b/CHANGES/13001.bugfix.rst new file mode 100644 index 00000000000..67511e3c95e --- /dev/null +++ b/CHANGES/13001.bugfix.rst @@ -0,0 +1 @@ +Fixed an :exc:`IndexError` in the pure-Python HTTP parser -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index 212b82228ea..e08b4c11aca 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -1157,19 +1157,20 @@ def feed_data(self, chunk: bytes, size: int) -> bool: self.size += size self.out.total_compressed_bytes = self.size - # RFC1950 - # bits 0..3 = CM = 0b1000 = 8 = "deflate" - # bits 4..7 = CINFO = 1..7 = windows size. - if ( - not self._started_decoding - and self.encoding == "deflate" - and chunk[0] & 0xF != 8 - ): - # Change the decoder to decompress incorrectly compressed data - # Actually we should issue a warning about non-RFC-compliant data. - self.decompressor = ZLibDecompressor( - encoding=self.encoding, suppress_deflate_header=True - ) + # Inspect the first real byte once to choose the decompressor. An empty + # chunk (e.g. a chunk-size line arriving without body bytes) has no + # header to sniff, so skip it and wait for the first data byte. + if not self._started_decoding and chunk: + # RFC1950 + # bits 0..3 = CM = 0b1000 = 8 = "deflate" + # bits 4..7 = CINFO = 1..7 = windows size. + if self.encoding == "deflate" and chunk[0] & 0xF != 8: + # Change the decoder to decompress incorrectly compressed data + # Actually we should issue a warning about non-RFC-compliant data. + self.decompressor = ZLibDecompressor( + encoding=self.encoding, suppress_deflate_header=True + ) + self._started_decoding = True low_water = self.out._low_water max_length = ( @@ -1182,8 +1183,6 @@ def feed_data(self, chunk: bytes, size: int) -> bool: "Can not decode content-encoding: %s" % self.encoding ) - self._started_decoding = True - if chunk: self.out.feed_data(chunk, len(chunk)) return self.decompressor.data_available # type: ignore[no-any-return] diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 9e115620eba..b3e0ab67cff 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1293,6 +1293,37 @@ async def test_compressed_chunked_with_pending(response: HttpResponseParser) -> assert result == original +async def test_compressed_chunked_split_chunk_size_line( + response: HttpResponseParser, +) -> None: + """First chunk-size line arrives in a feed that carries no body bytes. + + Regression test for an ``IndexError`` in the pure-Python parser: the + chunked parser fed an empty chunk to ``DeflateBuffer`` before any data + had been decoded, and the deflate header sniff indexed ``chunk[0]`` on it. + """ + original = b"Hello, world! " * 4 + compressed = zlib.compress(original) + size_line = hex(len(compressed))[2:].encode() + 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" + ) + + msgs, upgrade, tail = response.feed_data(headers) + payload = msgs[0][-1] + # The chunk-size line lands with no body bytes in the same feed, so the + # parser transitions into the chunk body with an empty buffer. + response.feed_data(size_line) + response.feed_data(compressed + b"\r\n0\r\n\r\n") + + result = await payload.read() + assert result == original + assert payload.exception() is None + + 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. From 63157f503cd75c569a1cf96efd0af3015f276a16 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 02:25:06 +0100 Subject: [PATCH 089/137] [PR #13010/9e00085c backport][3.15] Fix parse_mimetype producing spurious empty-key parameter for whitespace-only segments after semicolons (#13097) **This is a backport of PR #13010 as merged into master (9e00085c958fbcd40de37867e773bf7cd3bbee0c).** Co-authored-by: JSap0914 <116227558+JSap0914@users.noreply.github.com> --- aiohttp/helpers.py | 2 +- tests/test_helpers.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 486b4976cac..fbdd6653fd0 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -373,7 +373,7 @@ def parse_mimetype(mimetype: str) -> MimeType: parts = mimetype.split(";") params: MultiDict[str] = MultiDict() for item in parts[1:]: - if not item: + if not item.strip(): continue key, _, value = item.partition("=") params.add(key.lower().strip(), value.strip(' "')) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 9c1193a7a87..197cbfaf61c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -60,6 +60,27 @@ "text/plain;base64", helpers.MimeType("text", "plain", "", MultiDict({"base64": ""})), ), + ( + "text/html; ", + helpers.MimeType("text", "html", "", MultiDictProxy(MultiDict())), + ), + ( + "text/html; ", + helpers.MimeType("text", "html", "", MultiDictProxy(MultiDict())), + ), + ( + "text/html;\t", + helpers.MimeType("text", "html", "", MultiDictProxy(MultiDict())), + ), + ( + "text/html; charset=utf-8; ", + helpers.MimeType( + "text", + "html", + "", + MultiDictProxy(MultiDict({"charset": "utf-8"})), + ), + ), ], ) def test_parse_mimetype(mimetype, expected) -> None: From d0b97523e4880faa34f1c83cb31a7bbf6e0de712 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:30:45 +0100 Subject: [PATCH 090/137] [PR #13010/9e00085c backport][3.14] Fix parse_mimetype producing spurious empty-key parameter for whitespace-only segments after semicolons (#13096) **This is a backport of PR #13010 as merged into master (9e00085c958fbcd40de37867e773bf7cd3bbee0c).** Co-authored-by: JSap0914 <116227558+JSap0914@users.noreply.github.com> --- aiohttp/helpers.py | 2 +- tests/test_helpers.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 0fa0e8fbf92..bf905ce4716 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -366,7 +366,7 @@ def parse_mimetype(mimetype: str) -> MimeType: parts = mimetype.split(";") params: MultiDict[str] = MultiDict() for item in parts[1:]: - if not item: + if not item.strip(): continue key, _, value = item.partition("=") params.add(key.lower().strip(), value.strip(' "')) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 9c1193a7a87..197cbfaf61c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -60,6 +60,27 @@ "text/plain;base64", helpers.MimeType("text", "plain", "", MultiDict({"base64": ""})), ), + ( + "text/html; ", + helpers.MimeType("text", "html", "", MultiDictProxy(MultiDict())), + ), + ( + "text/html; ", + helpers.MimeType("text", "html", "", MultiDictProxy(MultiDict())), + ), + ( + "text/html;\t", + helpers.MimeType("text", "html", "", MultiDictProxy(MultiDict())), + ), + ( + "text/html; charset=utf-8; ", + helpers.MimeType( + "text", + "html", + "", + MultiDictProxy(MultiDict({"charset": "utf-8"})), + ), + ), ], ) def test_parse_mimetype(mimetype, expected) -> None: From f312e0beea82967317a1d44c134c2424e3fe6c60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:32:22 +0000 Subject: [PATCH 091/137] Bump filelock from 3.29.6 to 3.29.7 (#13103) 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.29.6 to 3.29.7.
Release notes

Sourced from filelock's releases.

3.29.7

What's Changed

Full Changelog: https://github.com/tox-dev/filelock/compare/3.29.6...3.29.7

Commits
  • 1efb893 _util: drop the dead st_mtime=0 short-circuit in raise_on_not_writable_file (...
  • 3547d58 soft_rw: evict a non-regular write marker without reading it (#588)
  • 616400e asyncio: detect cross-instance reentrant deadlocks before the poll loop (#586)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=filelock&package-manager=pip&previous-version=3.29.6&new-version=3.29.7)](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 4a992432d06..3bf6cca6edc 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -73,7 +73,7 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.29.6 +filelock==3.29.7 # via # python-discovery # virtualenv diff --git a/requirements/dev.txt b/requirements/dev.txt index bde0838ebff..d6104be24b9 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -71,7 +71,7 @@ exceptiongroup==1.3.1 # via pytest execnet==2.1.2 # via pytest-xdist -filelock==3.29.6 +filelock==3.29.7 # via # python-discovery # virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index 6788d69aa03..5c5022c9d9d 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -28,7 +28,7 @@ distlib==0.4.3 # via virtualenv exceptiongroup==1.3.1 # via pytest -filelock==3.29.6 +filelock==3.29.7 # via # python-discovery # virtualenv From 3eafeb06d96430ab369b0058e3892f4320a86380 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:37:57 +0000 Subject: [PATCH 092/137] Bump astral-sh/setup-uv from 8.3.1 to 8.3.2 (#13101) Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 8.3.1 to 8.3.2.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=astral-sh/setup-uv&package-manager=github_actions&previous-version=8.3.1&new-version=8.3.2)](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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 9ce995594e6..24abc33bf70 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -175,7 +175,7 @@ jobs: # important: do not use system python env: UV_PYTHON_PREFERENCE: only-managed - uses: astral-sh/setup-uv@v8.3.1 + uses: astral-sh/setup-uv@v8.3.2 with: python-version: ${{ matrix.pyver }} activate-environment: true @@ -282,7 +282,7 @@ jobs: # important: do not use system python env: UV_PYTHON_PREFERENCE: only-managed - uses: astral-sh/setup-uv@v8.3.1 + uses: astral-sh/setup-uv@v8.3.2 with: python-version: ${{ matrix.pyver }} activate-environment: true @@ -354,7 +354,7 @@ jobs: # important: do not use system python env: UV_PYTHON_PREFERENCE: only-managed - uses: astral-sh/setup-uv@v8.3.1 + uses: astral-sh/setup-uv@v8.3.2 with: python-version: ${{ matrix.pyver }} activate-environment: true From 946bcd5d3cbc285bc8d87861f80dd6b23e095234 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Jul 2026 11:51:31 +0000 Subject: [PATCH 093/137] Bump charset-normalizer from 3.4.8 to 3.4.9 (#13108) Bumps [charset-normalizer](https://github.com/jawah/charset_normalizer) from 3.4.8 to 3.4.9.
Release notes

Sourced from charset-normalizer's releases.

Version 3.4.9

3.4.9 (2026-07-07)

Fixed

  • Regression in our fallback path leading to a decode error. (#771) We've yanked 3.4.8 as a result of that bug.
Changelog

Sourced from charset-normalizer's changelog.

3.4.9 (2026-07-07)

Fixed

  • Regression in our fallback path leading to a decode error. (#771) We've yanked 3.4.8 as a result of that bug.
Commits
  • cc68407 Merge pull request #772 from jawah/fix-regression-fallback-path
  • 152b923 chore: release 3.4.9
  • 2bc2607 fix: unicodedecodeerror in fallback path
  • be252d7 chore(deps): bump docker/setup-qemu-action from 4.1.0 to 4.2.0 (#767)
  • 71c7bdd chore(deps): bump actions/setup-python from 6.2.0 to 6.3.0 (#768)
  • aeea391 chore(deps): bump pypa/cibuildwheel from 3.4.1 to 4.1.0 (#758)
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=charset-normalizer&package-manager=pip&previous-version=3.4.8&new-version=3.4.9)](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 3bf6cca6edc..ef1104faa9b 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -48,7 +48,7 @@ cffi==2.1.0 # pycares cfgv==3.5.0 # via pre-commit -charset-normalizer==3.4.8 +charset-normalizer==3.4.9 # via requests click==8.4.2 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index d6104be24b9..eda31e5feee 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -48,7 +48,7 @@ cffi==2.1.0 # pycares cfgv==3.5.0 # via pre-commit -charset-normalizer==3.4.8 +charset-normalizer==3.4.9 # via requests click==8.4.2 # via diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index d54a504c8b2..ade053c613a 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -12,7 +12,7 @@ babel==2.18.0 # via sphinx certifi==2026.6.17 # via requests -charset-normalizer==3.4.8 +charset-normalizer==3.4.9 # via requests click==8.4.2 # via towncrier diff --git a/requirements/doc.txt b/requirements/doc.txt index 17b1bef9fc4..25ebe0e4a06 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -12,7 +12,7 @@ babel==2.18.0 # via sphinx certifi==2026.6.17 # via requests -charset-normalizer==3.4.8 +charset-normalizer==3.4.9 # via requests click==8.4.2 # via towncrier From d26417fb5eb0f9bef1f8ccccbda9dc250a3e13e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:54:43 +0000 Subject: [PATCH 094/137] Bump build from 1.5.0 to 1.5.1 (#13112) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [build](https://github.com/pypa/build) from 1.5.0 to 1.5.1.
Release notes

Sourced from build's releases.

1.5.1

What's Changed

... (truncated)

Changelog

Sourced from build's changelog.

#################### 1.5.1 (2026-07-09) ####################


Features


  • Add --report=PATH to write a machine-readable JSON report of built artifacts; --metadata now also accepts .whl files - by :user:gaborbernat (:issue:198)
  • The srcdir argument now accepts .tar.gz source distributions, extracting and building from them - by :user:gaborbernat (:issue:311)
  • The "Unmet dependencies" error from --no-isolation builds now shows the wanted version, found version, and interpreter - by :user:gaborbernat (:issue:504)
  • Add --sdist-extract-dir to extract the intermediate sdist into a persistent directory, enabling compiler cache reuse across rebuilds - by :user:gaborbernat (:issue:614)
  • Add --env-dir to place the isolated build environment at a fixed path, enabling compiler cache reuse across builds - by :user:gaborbernat (:issue:655)
  • Print a summary of resolved dependency versions (name==version) after installing them in isolated builds - by :user:gaborbernat (:issue:959)
  • On build failure, print a tip pointing to --env-dir and --sdist-extract-dir for debugging and link to the "Debug a failed build" how-to - reported by :user:dimpase, implemented by :user:gaborbernat (:issue:966)

Bugfixes


  • Drain verbose subprocess output inline instead of using a ThreadPoolExecutor, which silently swallowed logging errors - by :user:henryiii (:issue:1098)
  • Reject a file passed as --env-dir with a clear error instead of a raw FileExistsError - by :user:henryiii (:issue:1100)
  • Emit CLI warnings to stderr instead of stdout, so they no longer corrupt --metadata JSON output on stdout - by :user:ymyzk (:issue:1111)
  • Fix the Windows symlink support probe always returning False due to a stale object interpolated into the destination path - by :user:henryiii (:issue:1118)
  • Fix metadata_path's build-backend fallback returning a nonexistent dist-info path for wheels with a build tag - by :user:henryiii (:issue:1119)
  • Write pip/uv requirements and constraints files with \n instead of os.linesep, avoiding doubled \r\r\n line endings on Windows - by :user:henryiii (:issue:1120)
  • Batch of small robustness fixes: correct macOS release parsing for the minimum pip version, avoid sharing the mutable default build-system table between builders, keep the original error when isolated-environment setup fails early, and raise BuildException for an invalid wheel - by :user:henryiii (:issue:1121)
  • Decide color support independently for stdout and stderr instead of only checking stdout.isatty(), so redirecting one stream no longer disables or leaks ANSI colors on the other - by :user:henryiii (:issue:1123)

Deprecations and Removals


  • Deprecate :func:build.util.project_wheel_metadata; use python -m build --metadata instead - by :user:gaborbernat (:issue:557)
  • Warn on config settings without a value (e.g. -C--my-flag); use -C--my-flag= instead for compatibility with pip

... (truncated)

Commits
  • d08fcd7 chore: prepare for 1.5.1
  • 135801c test: isolate color tests from NO_COLOR and FORCE_COLOR (#1128)
  • 820d4e5 perf: memoize check_dependency (#1122)
  • 6c03264 pre-commit: bump repositories (#1127)
  • 43c337a refactor: simplify code paths found in review (#1124)
  • 260ae22 fix: minor robustness fixes from code review (#1121)
  • 100e86e refactor: open the wheel only once in metadata_path (#1126)
  • 272db6c fix: decide color support per output stream, not solely from stdout (#1123)
  • bc4ca7c fix: use LF instead of os.linesep when writing pip/uv requirements files (#1120)
  • 16d8143 fix: derive dist-info directory from wheel contents, not filename (#1119)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=build&package-manager=pip&previous-version=1.5.0&new-version=1.5.1)](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 ef1104faa9b..97f1a8fe6e5 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" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -build==1.5.0 +build==1.5.1 # via pip-tools certifi==2026.6.17 # via requests diff --git a/requirements/dev.txt b/requirements/dev.txt index eda31e5feee..627f066b7a7 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" and sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in -build==1.5.0 +build==1.5.1 # via pip-tools certifi==2026.6.17 # via requests From 0a507cbf3def56c8024890816be5f9c0344384eb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:24:17 +0000 Subject: [PATCH 095/137] Bump librt from 0.12.0 to 0.13.0 (#13115) Bumps [librt](https://github.com/mypyc/librt) from 0.12.0 to 0.13.0.
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=librt&package-manager=pip&previous-version=0.12.0&new-version=0.13.0)](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 97f1a8fe6e5..b29514ea01a 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -110,7 +110,7 @@ jinja2==3.1.6 # sphinx # sphinxcontrib-mermaid # towncrier -librt==0.12.0 +librt==0.13.0 # via mypy markdown-it-py==3.0.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 627f066b7a7..15df888941e 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -108,7 +108,7 @@ jinja2==3.1.6 # sphinx # sphinxcontrib-mermaid # towncrier -librt==0.12.0 +librt==0.13.0 # via mypy markdown-it-py==3.0.0 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index 5c5022c9d9d..d91198254e5 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -44,7 +44,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 # via -r requirements/lint.in -librt==0.12.0 +librt==0.13.0 # via mypy markdown-it-py==4.2.0 # via rich diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 0c08c85c327..7fcc689fb43 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" and implementation_name == "cpython" # via -r requirements/test-common.in -librt==0.12.0 +librt==0.13.0 # via mypy markdown-it-py==4.2.0 # via rich diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 8d1bff8f023..e56ae8699b0 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -58,7 +58,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in -librt==0.12.0 +librt==0.13.0 # via mypy markdown-it-py==4.2.0 # via rich diff --git a/requirements/test.txt b/requirements/test.txt index d4c42f19b95..cd2b1ce76ec 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -58,7 +58,7 @@ iniconfig==2.3.0 # via pytest isal==1.8.0 ; python_version < "3.14" and implementation_name == "cpython" # via -r requirements/test-common.in -librt==0.12.0 +librt==0.13.0 # via mypy markdown-it-py==4.2.0 # via rich From b9e0919dd00f0e1d24111fe9fa64b9d9f6656ec6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:25:41 +0000 Subject: [PATCH 096/137] Bump python-discovery from 1.4.3 to 1.4.4 (#13116) 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.4.3 to 1.4.4.
Release notes

Sourced from python-discovery's releases.

v1.4.4

What's Changed

New Contributors

Full Changelog: https://github.com/tox-dev/python-discovery/compare/1.4.3...1.4.4

Changelog

Sourced from python-discovery's changelog.

Bug fixes - 1.4.4

  • Parse the debug build flag in interpreter specs - python3.13d and Debian's python3.13-dbg / python3.13-debug now select a Py_DEBUG interpreter instead of being misread as an ISA named dbg. Resolving a virtualenv to its base interpreter also checks the free-threaded and debug ABI flags, so a debug or free-threaded environment no longer resolves to a release build of the same version - by :user:gaborbernat. (:issue:96)

v1.4.3 (2026-07-03)


Commits
  • 02a5c6e release 1.4.4
  • 4dbf5d1 πŸ› fix(spec): discover debug (Py_DEBUG) interpreters like python3.13-dbg (#96)
  • 1790c9f [pre-commit.ci] pre-commit autoupdate (#95)
  • d3cd2c9 :moneybag: Surface GitHub Sponsors + thanks.dev
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=python-discovery&package-manager=pip&previous-version=1.4.3&new-version=1.4.4)](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 b29514ea01a..91415beb7a3 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -213,7 +213,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.4.3 +python-discovery==1.4.4 # via virtualenv python-on-whales==0.81.0 # via diff --git a/requirements/dev.txt b/requirements/dev.txt index 15df888941e..ce02a3087c8 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -208,7 +208,7 @@ pytest-xdist==3.8.0 # via -r requirements/test-common.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.4.3 +python-discovery==1.4.4 # via virtualenv python-on-whales==0.81.0 # via diff --git a/requirements/lint.txt b/requirements/lint.txt index d91198254e5..b3291b2af9d 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -91,7 +91,7 @@ pytest-mock==3.15.1 # via -r requirements/lint.in python-dateutil==2.9.0.post0 # via freezegun -python-discovery==1.4.3 +python-discovery==1.4.4 # via virtualenv python-on-whales==0.81.0 # via -r requirements/lint.in From 0494d61eaaf74f1c65bc2df52c7622e5f54c41fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Jul 2026 11:44:25 +0000 Subject: [PATCH 097/137] Bump sphinxcontrib-mermaid from 2.0.2 to 2.0.3 (#13109) Bumps [sphinxcontrib-mermaid](https://github.com/mgaitan/sphinxcontrib-mermaid) from 2.0.2 to 2.0.3.
Changelog

Sourced from sphinxcontrib-mermaid's changelog.

2.0.3 (July 7, 2026)

  • Capture mmdc error message as string for nicer error display
  • Fix local JS module imports on root-level pages by ensuring a valid relative ES module specifier (#246)
  • Defer rendering of Mermaid diagrams hidden by a parent (e.g. Reveal.js slides, unopened tabs) until they become visible, fixing broken SVGs
Commits
  • f992df7 Merge pull request #249 from mgaitan/tkp/203
  • 7ced3de bump to 2.0.3
  • 7346af2 Merge pull request #227 from drillan/fix/lazy-render-hidden-elements
  • b6d1fdc Merge branch 'master' into fix/lazy-render-hidden-elements
  • 425076f Apply zoom to deferred diagrams and fix zoom completion loop
  • 9261af4 Merge pull request #248 from timkpaine/tkp/hf
  • a902799 Fix local JS module imports on root-level pages
  • 881b38a Merge pull request #245 from imphil/string-error-message
  • 4fa3848 fix: Capture mmdc error message as string
  • 1a6cad0 Backfill CHANGELOG for 2.0.2
  • 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 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 91415beb7a3..19efdfad395 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -255,7 +255,7 @@ sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-mermaid==2.0.2 +sphinxcontrib-mermaid==2.0.3 # via -r requirements/doc.in sphinxcontrib-qthelp==2.0.0 # via sphinx diff --git a/requirements/dev.txt b/requirements/dev.txt index ce02a3087c8..d0020b0d547 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -247,7 +247,7 @@ sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-mermaid==2.0.2 +sphinxcontrib-mermaid==2.0.3 # via -r requirements/doc.in sphinxcontrib-qthelp==2.0.0 # via sphinx diff --git a/requirements/doc-spelling.txt b/requirements/doc-spelling.txt index ade053c613a..2b6cd4e33a7 100644 --- a/requirements/doc-spelling.txt +++ b/requirements/doc-spelling.txt @@ -73,7 +73,7 @@ sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-mermaid==2.0.2 +sphinxcontrib-mermaid==2.0.3 # via -r requirements/doc.in sphinxcontrib-qthelp==2.0.0 # via sphinx diff --git a/requirements/doc.txt b/requirements/doc.txt index 25ebe0e4a06..a5e06ed48cf 100644 --- a/requirements/doc.txt +++ b/requirements/doc.txt @@ -68,7 +68,7 @@ sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-mermaid==2.0.2 +sphinxcontrib-mermaid==2.0.3 # via -r requirements/doc.in sphinxcontrib-qthelp==2.0.0 # via sphinx From 9ead8fbb44f37403441a52e04bccced70e978ddb Mon Sep 17 00:00:00 2001 From: Javid Khan Date: Fri, 10 Jul 2026 20:27:08 +0530 Subject: [PATCH 098/137] [PR #12895/64313cfc backport][3.15] reject websocket close codes above the valid range (#13120) **This is a backport of PR #12895 as merged into master (64313cfc5024a316d85f34e7e77c9995a683e35f).** --- CHANGES/12895.bugfix.rst | 4 ++++ CONTRIBUTORS.txt | 1 + aiohttp/_websocket/reader_py.py | 5 ++++- tests/test_websocket_parser.py | 16 ++++++++++++++-- 4 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 CHANGES/12895.bugfix.rst diff --git a/CHANGES/12895.bugfix.rst b/CHANGES/12895.bugfix.rst new file mode 100644 index 00000000000..af206e7f443 --- /dev/null +++ b/CHANGES/12895.bugfix.rst @@ -0,0 +1,4 @@ +Rejected WebSocket ``CLOSE`` frames carrying a status code above the +valid range (greater than ``4999``) with a protocol error, matching the +existing handling of out-of-range low codes, per :rfc:`6455#section-7.4.1` +-- by :user:`dxbjavid`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 6ce89d092ae..c6bfb2b8879 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -117,6 +117,7 @@ Dmitry Trofimov Dmytro Bohomiakov Dmytro Kuznetsov Dustin J. Mitchell +dxbjavid Earle Lowe Eduard Iskandarov Eli Ribble diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index 8c6a1e1148a..6a9f20dbcc2 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -291,7 +291,10 @@ def _handle_frame( elif opcode == OP_CODE_CLOSE: if len(payload) >= 2: close_code = UNPACK_CLOSE_CODE(payload[:2])[0] - if close_code < 3000 and close_code not in ALLOWED_CLOSE_CODES: + # https://datatracker.ietf.org/doc/html/rfc6455#section-7.4.2 + if close_code > 4999 or ( + close_code < 3000 and close_code not in ALLOWED_CLOSE_CODES + ): raise WebSocketError( WSCloseCode.PROTOCOL_ERROR, f"Invalid close code: {close_code}", diff --git a/tests/test_websocket_parser.py b/tests/test_websocket_parser.py index 16580002568..bd3f9030226 100644 --- a/tests/test_websocket_parser.py +++ b/tests/test_websocket_parser.py @@ -283,9 +283,9 @@ def test_close_frame(out: WebSocketDataQueue, parser: PatchableWebSocketReader) def test_close_frame_info( out: WebSocketDataQueue, parser: PatchableWebSocketReader ) -> None: - parser._handle_frame(True, WSMsgType.CLOSE, b"0112345", 0) + parser._handle_frame(True, WSMsgType.CLOSE, b"\x03\xe912345", 0) res = out._buffer[0] - assert res == (WSMessage(WSMsgType.CLOSE, 12337, "12345"), 0) + assert res == (WSMessage(WSMsgType.CLOSE, 1001, "12345"), 0) def test_close_frame_invalid( @@ -307,6 +307,18 @@ def test_close_frame_invalid_2( assert ctx.value.code == WSCloseCode.PROTOCOL_ERROR +@pytest.mark.parametrize("code", (5000, 9999, 65535)) +def test_close_frame_invalid_code_above_range( + parser: PatchableWebSocketReader, code: int +) -> None: + data = build_close_frame(code=code) + + with pytest.raises(WebSocketError) as ctx: + parser._feed_data(data) + + assert ctx.value.code == WSCloseCode.PROTOCOL_ERROR + + def test_close_frame_unicode_err(parser: PatchableWebSocketReader) -> None: data = build_close_frame(code=1000, message=b"\xf4\x90\x80\x80") From 7330fd710474a29ac94b7e82cc0018948a456af2 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Sun, 12 Jul 2026 22:18:09 +0100 Subject: [PATCH 099/137] Add _transport_sockname to ATTRS (#13125) --- aiohttp/web_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index d3d9cc2989f..ca925e2acdb 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -132,6 +132,7 @@ class BaseRequest(MutableMapping[str | RequestKey[Any], Any], HeadersMixin): "_loop", "_transport_sslcontext", "_transport_peername", + "_transport_sockname", ] ) _post: MultiDictProxy[_Post] | None = None From 3669bc11dc1c31c39f4257e6fdbcd18ce031aab3 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sun, 12 Jul 2026 23:01:32 +0100 Subject: [PATCH 100/137] [PR #13125/7330fd71 backport][3.14] Add _transport_sockname to ATTRS (#13126) **This is a backport of PR #13125 as merged into 3.15 (7330fd710474a29ac94b7e82cc0018948a456af2).** Co-authored-by: Sam Bull --- aiohttp/web_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index d3d9cc2989f..ca925e2acdb 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -132,6 +132,7 @@ class BaseRequest(MutableMapping[str | RequestKey[Any], Any], HeadersMixin): "_loop", "_transport_sslcontext", "_transport_peername", + "_transport_sockname", ] ) _post: MultiDictProxy[_Post] | None = None From 62b24cbad98113ff6f4b54e03b132c1ca2afa9a3 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:32:43 +0100 Subject: [PATCH 101/137] [PR #13128/bb3cb60f backport][3.15] Dependabot cooldown and versioning-strategy (#13130) **This is a backport of PR #13128 as merged into master (bb3cb60f57f237456a32d6e88b453f97107147d4).** Co-authored-by: Sam Bull --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index fddd531da59..1657d0444d4 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,6 +4,8 @@ updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" + cooldown: + default-days: 3 labels: - dependencies schedule: @@ -12,6 +14,9 @@ updates: # Maintain dependencies for Python - package-ecosystem: "pip" directory: "/" + versioning-strategy: "increase-if-necessary" + cooldown: + default-days: 3 allow: - dependency-type: "all" labels: @@ -23,6 +28,8 @@ updates: # Maintain dependencies for GitHub Actions aiohttp backport - package-ecosystem: "github-actions" directory: "/" + cooldown: + default-days: 3 labels: - dependencies target-branch: "3.15" @@ -33,6 +40,9 @@ updates: # Maintain dependencies for Python aiohttp backport - package-ecosystem: "pip" directory: "/" + versioning-strategy: "increase-if-necessary" + cooldown: + default-days: 3 allow: - dependency-type: "all" labels: @@ -44,6 +54,8 @@ updates: - package-ecosystem: "docker" directory: "/tests/autobahn/" + cooldown: + default-days: 3 labels: - dependencies schedule: @@ -51,6 +63,8 @@ updates: - package-ecosystem: "docker" directory: "/tests/autobahn/" + cooldown: + default-days: 3 labels: - dependencies target-branch: "3.15" From 79e3fd28db6f96589899daba15dec227ad3d7cc5 Mon Sep 17 00:00:00 2001 From: oppnc <129483880+oppnc@users.noreply.github.com> Date: Mon, 13 Jul 2026 07:35:50 +0800 Subject: [PATCH 102/137] [3.15] Fix Windows localhost resolution without active network (#13086) Backport of #12700 to 3.15. --- CHANGES/5357.bugfix.rst | 3 ++ CONTRIBUTORS.txt | 1 + aiohttp/resolver.py | 56 ++++++++++++++++------ tests/test_resolver.py | 102 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 147 insertions(+), 15 deletions(-) create mode 100644 CHANGES/5357.bugfix.rst diff --git a/CHANGES/5357.bugfix.rst b/CHANGES/5357.bugfix.rst new file mode 100644 index 00000000000..dd0125cb9ba --- /dev/null +++ b/CHANGES/5357.bugfix.rst @@ -0,0 +1,3 @@ +Fixed resolving ``localhost`` on Windows to fall back without ``AI_ADDRCONFIG`` +when the first lookup fails, so ``localhost`` still works without an active +network. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index c6bfb2b8879..a2b005f1b4e 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -289,6 +289,7 @@ NΓ‘ndor MΓ‘travΓΆlgyi Oisin Aylward Olaf Conradi Omkar Kabde +oppnc Pahaz Blinov Panagiotis Kolokotronis Pankaj Pandey diff --git a/aiohttp/resolver.py b/aiohttp/resolver.py index 84b5ffb667e..28a5cbba0fe 100644 --- a/aiohttp/resolver.py +++ b/aiohttp/resolver.py @@ -1,5 +1,6 @@ import asyncio import socket +import sys import weakref from typing import Any, Final, Optional @@ -22,6 +23,11 @@ _AI_ADDRCONFIG = socket.AI_ADDRCONFIG if hasattr(socket, "AI_MASK"): _AI_ADDRCONFIG &= socket.AI_MASK +_IS_WINDOWS = sys.platform == "win32" + + +def _is_windows_localhost(host: str) -> bool: + return _IS_WINDOWS and host.rstrip(".").casefold() == "localhost" class ThreadedResolver(AbstractResolver): @@ -37,13 +43,24 @@ def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None: async def resolve( self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET ) -> list[ResolveResult]: - infos = await self._loop.getaddrinfo( - host, - port, - type=socket.SOCK_STREAM, - family=family, - flags=_AI_ADDRCONFIG, - ) + try: + infos = await self._loop.getaddrinfo( + host, + port, + type=socket.SOCK_STREAM, + family=family, + flags=_AI_ADDRCONFIG, + ) + except socket.gaierror: + if not _is_windows_localhost(host): + raise + infos = await self._loop.getaddrinfo( + host, + port, + type=socket.SOCK_STREAM, + family=family, + flags=0, + ) hosts: list[ResolveResult] = [] for family, _, proto, _, address in infos: @@ -114,13 +131,24 @@ async def resolve( self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET ) -> list[ResolveResult]: try: - resp = await self._resolver.getaddrinfo( - host, - port=port, - type=socket.SOCK_STREAM, - family=family, - flags=_AI_ADDRCONFIG, - ) + try: + resp = await self._resolver.getaddrinfo( + host, + port=port, + type=socket.SOCK_STREAM, + family=family, + flags=_AI_ADDRCONFIG, + ) + except aiodns.error.DNSError: + if not _is_windows_localhost(host): + raise + resp = await self._resolver.getaddrinfo( + host, + port=port, + type=socket.SOCK_STREAM, + family=family, + flags=0, + ) except aiodns.error.DNSError as exc: msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed" raise OSError(None, msg) from exc diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 02a222d0d82..2b4fb9414ad 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable, Collection, Generator from ipaddress import ip_address from typing import Any, NamedTuple -from unittest.mock import Mock, create_autospec, patch +from unittest.mock import AsyncMock, Mock, call, create_autospec, patch import pytest @@ -233,6 +233,44 @@ async def test_async_resolver_negative_lookup(loop: asyncio.AbstractEventLoop) - await resolver.close() +@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required") +@pytest.mark.usefixtures("check_no_lingering_resolvers") +async def test_async_resolver_retries_localhost_without_addrconfig_on_windows( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr("aiohttp.resolver._IS_WINDOWS", True) + with patch("aiodns.DNSResolver") as mock: + mock().getaddrinfo.side_effect = [ + aiodns.error.DNSError(1, "addrconfig failed"), + fake_aiodns_getaddrinfo_ipv4_result(["127.0.0.1"]), + ] + resolver = AsyncResolver() + try: + real = await resolver.resolve("localhost", family=socket.AF_UNSPEC) + finally: + await resolver.close() + + assert real[0]["host"] == "127.0.0.1" + mock().getaddrinfo.assert_has_calls( + [ + call( + "localhost", + family=socket.AF_UNSPEC, + flags=socket.AI_ADDRCONFIG, + port=0, + type=socket.SOCK_STREAM, + ), + call( + "localhost", + family=socket.AF_UNSPEC, + flags=0, + port=0, + type=socket.SOCK_STREAM, + ), + ] + ) + + @pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required") @pytest.mark.usefixtures("check_no_lingering_resolvers") async def test_async_resolver_no_hosts_in_getaddrinfo( @@ -255,6 +293,68 @@ async def test_threaded_resolver_positive_lookup() -> None: ipaddress.ip_address(real[0]["host"]) +async def test_threaded_resolver_retries_localhost_without_addrconfig_on_windows( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr("aiohttp.resolver._IS_WINDOWS", True) + loop = Mock() + loop.getaddrinfo = AsyncMock( + side_effect=[ + socket.gaierror(), + [(socket.AF_INET, None, socket.SOCK_STREAM, None, ("127.0.0.1", 0))], + ] + ) + resolver = ThreadedResolver() + resolver._loop = loop + + real = await resolver.resolve("localhost", family=socket.AF_UNSPEC) + + assert real[0]["host"] == "127.0.0.1" + loop.getaddrinfo.assert_has_calls( + [ + call( + "localhost", + 0, + type=socket.SOCK_STREAM, + family=socket.AF_UNSPEC, + flags=socket.AI_ADDRCONFIG, + ), + call( + "localhost", + 0, + type=socket.SOCK_STREAM, + family=socket.AF_UNSPEC, + flags=0, + ), + ] + ) + + +@pytest.mark.parametrize( + ("host", "is_windows"), + (("localhost", False), ("www.python.org", True)), +) +async def test_threaded_resolver_keeps_addrconfig_without_localhost_windows_fallback( + host: str, is_windows: bool, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr("aiohttp.resolver._IS_WINDOWS", is_windows) + loop = Mock() + loop.getaddrinfo = AsyncMock(side_effect=socket.gaierror()) + resolver = ThreadedResolver() + resolver._loop = loop + + with pytest.raises(socket.gaierror): + await resolver.resolve(host, family=socket.AF_UNSPEC) + + loop.getaddrinfo.assert_called_once_with( + host, + 0, + type=socket.SOCK_STREAM, + family=socket.AF_UNSPEC, + flags=socket.AI_ADDRCONFIG, + ) + + async def test_threaded_resolver_positive_ipv6_link_local_lookup() -> None: loop = Mock() loop.getaddrinfo = fake_ipv6_addrinfo(["fe80::1"]) From b0d90c175ac2534c80b59248823662767ecf2aec Mon Sep 17 00:00:00 2001 From: oppnc <129483880+oppnc@users.noreply.github.com> Date: Mon, 13 Jul 2026 07:36:53 +0800 Subject: [PATCH 103/137] [3.14] Fix Windows localhost resolution without active network (#13087) Backport of #12700 to 3.14. --- CHANGES/5357.bugfix.rst | 3 ++ CONTRIBUTORS.txt | 1 + aiohttp/resolver.py | 56 ++++++++++++++++------ tests/test_resolver.py | 102 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 147 insertions(+), 15 deletions(-) create mode 100644 CHANGES/5357.bugfix.rst diff --git a/CHANGES/5357.bugfix.rst b/CHANGES/5357.bugfix.rst new file mode 100644 index 00000000000..dd0125cb9ba --- /dev/null +++ b/CHANGES/5357.bugfix.rst @@ -0,0 +1,3 @@ +Fixed resolving ``localhost`` on Windows to fall back without ``AI_ADDRCONFIG`` +when the first lookup fails, so ``localhost`` still works without an active +network. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 6ce89d092ae..9d36c7edfd2 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -288,6 +288,7 @@ NΓ‘ndor MΓ‘travΓΆlgyi Oisin Aylward Olaf Conradi Omkar Kabde +oppnc Pahaz Blinov Panagiotis Kolokotronis Pankaj Pandey diff --git a/aiohttp/resolver.py b/aiohttp/resolver.py index 84b5ffb667e..28a5cbba0fe 100644 --- a/aiohttp/resolver.py +++ b/aiohttp/resolver.py @@ -1,5 +1,6 @@ import asyncio import socket +import sys import weakref from typing import Any, Final, Optional @@ -22,6 +23,11 @@ _AI_ADDRCONFIG = socket.AI_ADDRCONFIG if hasattr(socket, "AI_MASK"): _AI_ADDRCONFIG &= socket.AI_MASK +_IS_WINDOWS = sys.platform == "win32" + + +def _is_windows_localhost(host: str) -> bool: + return _IS_WINDOWS and host.rstrip(".").casefold() == "localhost" class ThreadedResolver(AbstractResolver): @@ -37,13 +43,24 @@ def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None: async def resolve( self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET ) -> list[ResolveResult]: - infos = await self._loop.getaddrinfo( - host, - port, - type=socket.SOCK_STREAM, - family=family, - flags=_AI_ADDRCONFIG, - ) + try: + infos = await self._loop.getaddrinfo( + host, + port, + type=socket.SOCK_STREAM, + family=family, + flags=_AI_ADDRCONFIG, + ) + except socket.gaierror: + if not _is_windows_localhost(host): + raise + infos = await self._loop.getaddrinfo( + host, + port, + type=socket.SOCK_STREAM, + family=family, + flags=0, + ) hosts: list[ResolveResult] = [] for family, _, proto, _, address in infos: @@ -114,13 +131,24 @@ async def resolve( self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET ) -> list[ResolveResult]: try: - resp = await self._resolver.getaddrinfo( - host, - port=port, - type=socket.SOCK_STREAM, - family=family, - flags=_AI_ADDRCONFIG, - ) + try: + resp = await self._resolver.getaddrinfo( + host, + port=port, + type=socket.SOCK_STREAM, + family=family, + flags=_AI_ADDRCONFIG, + ) + except aiodns.error.DNSError: + if not _is_windows_localhost(host): + raise + resp = await self._resolver.getaddrinfo( + host, + port=port, + type=socket.SOCK_STREAM, + family=family, + flags=0, + ) except aiodns.error.DNSError as exc: msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed" raise OSError(None, msg) from exc diff --git a/tests/test_resolver.py b/tests/test_resolver.py index 02a222d0d82..2b4fb9414ad 100644 --- a/tests/test_resolver.py +++ b/tests/test_resolver.py @@ -5,7 +5,7 @@ from collections.abc import Awaitable, Callable, Collection, Generator from ipaddress import ip_address from typing import Any, NamedTuple -from unittest.mock import Mock, create_autospec, patch +from unittest.mock import AsyncMock, Mock, call, create_autospec, patch import pytest @@ -233,6 +233,44 @@ async def test_async_resolver_negative_lookup(loop: asyncio.AbstractEventLoop) - await resolver.close() +@pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required") +@pytest.mark.usefixtures("check_no_lingering_resolvers") +async def test_async_resolver_retries_localhost_without_addrconfig_on_windows( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr("aiohttp.resolver._IS_WINDOWS", True) + with patch("aiodns.DNSResolver") as mock: + mock().getaddrinfo.side_effect = [ + aiodns.error.DNSError(1, "addrconfig failed"), + fake_aiodns_getaddrinfo_ipv4_result(["127.0.0.1"]), + ] + resolver = AsyncResolver() + try: + real = await resolver.resolve("localhost", family=socket.AF_UNSPEC) + finally: + await resolver.close() + + assert real[0]["host"] == "127.0.0.1" + mock().getaddrinfo.assert_has_calls( + [ + call( + "localhost", + family=socket.AF_UNSPEC, + flags=socket.AI_ADDRCONFIG, + port=0, + type=socket.SOCK_STREAM, + ), + call( + "localhost", + family=socket.AF_UNSPEC, + flags=0, + port=0, + type=socket.SOCK_STREAM, + ), + ] + ) + + @pytest.mark.skipif(not getaddrinfo, reason="aiodns >=3.2.0 required") @pytest.mark.usefixtures("check_no_lingering_resolvers") async def test_async_resolver_no_hosts_in_getaddrinfo( @@ -255,6 +293,68 @@ async def test_threaded_resolver_positive_lookup() -> None: ipaddress.ip_address(real[0]["host"]) +async def test_threaded_resolver_retries_localhost_without_addrconfig_on_windows( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.setattr("aiohttp.resolver._IS_WINDOWS", True) + loop = Mock() + loop.getaddrinfo = AsyncMock( + side_effect=[ + socket.gaierror(), + [(socket.AF_INET, None, socket.SOCK_STREAM, None, ("127.0.0.1", 0))], + ] + ) + resolver = ThreadedResolver() + resolver._loop = loop + + real = await resolver.resolve("localhost", family=socket.AF_UNSPEC) + + assert real[0]["host"] == "127.0.0.1" + loop.getaddrinfo.assert_has_calls( + [ + call( + "localhost", + 0, + type=socket.SOCK_STREAM, + family=socket.AF_UNSPEC, + flags=socket.AI_ADDRCONFIG, + ), + call( + "localhost", + 0, + type=socket.SOCK_STREAM, + family=socket.AF_UNSPEC, + flags=0, + ), + ] + ) + + +@pytest.mark.parametrize( + ("host", "is_windows"), + (("localhost", False), ("www.python.org", True)), +) +async def test_threaded_resolver_keeps_addrconfig_without_localhost_windows_fallback( + host: str, is_windows: bool, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr("aiohttp.resolver._IS_WINDOWS", is_windows) + loop = Mock() + loop.getaddrinfo = AsyncMock(side_effect=socket.gaierror()) + resolver = ThreadedResolver() + resolver._loop = loop + + with pytest.raises(socket.gaierror): + await resolver.resolve(host, family=socket.AF_UNSPEC) + + loop.getaddrinfo.assert_called_once_with( + host, + 0, + type=socket.SOCK_STREAM, + family=socket.AF_UNSPEC, + flags=socket.AI_ADDRCONFIG, + ) + + async def test_threaded_resolver_positive_ipv6_link_local_lookup() -> None: loop = Mock() loop.getaddrinfo = fake_ipv6_addrinfo(["fe80::1"]) From 73c834796a57d71513701255ee7e70164bef3715 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:37:17 +0100 Subject: [PATCH 104/137] [PR #13128/bb3cb60f backport][3.14] Dependabot cooldown and versioning-strategy (#13129) **This is a backport of PR #13128 as merged into master (bb3cb60f57f237456a32d6e88b453f97107147d4).** Co-authored-by: Sam Bull --- .github/dependabot.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 6ac64362454..114fc2a532d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,6 +4,8 @@ updates: # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" + cooldown: + default-days: 3 labels: - dependencies schedule: @@ -12,6 +14,9 @@ updates: # Maintain dependencies for Python - package-ecosystem: "pip" directory: "/" + versioning-strategy: "increase-if-necessary" + cooldown: + default-days: 3 allow: - dependency-type: "all" labels: @@ -23,6 +28,8 @@ updates: # Maintain dependencies for GitHub Actions aiohttp backport - package-ecosystem: "github-actions" directory: "/" + cooldown: + default-days: 3 labels: - dependencies target-branch: "3.14" @@ -33,6 +40,9 @@ updates: # Maintain dependencies for Python aiohttp backport - package-ecosystem: "pip" directory: "/" + versioning-strategy: "increase-if-necessary" + cooldown: + default-days: 3 allow: - dependency-type: "all" labels: @@ -44,6 +54,8 @@ updates: - package-ecosystem: "docker" directory: "/tests/autobahn/" + cooldown: + default-days: 3 labels: - dependencies schedule: @@ -51,6 +63,8 @@ updates: - package-ecosystem: "docker" directory: "/tests/autobahn/" + cooldown: + default-days: 3 labels: - dependencies target-branch: "3.14" From 0af75fd64430ac79fbda76c488583c846e35998a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:26:52 +0000 Subject: [PATCH 105/137] Bump typing-extensions from 4.15.0 to 4.16.0 (#13035) Bumps [typing-extensions](https://github.com/python/typing_extensions) from 4.15.0 to 4.16.0.
Release notes

Sourced from typing-extensions's releases.

4.16.0

No changes since 4.16.0rc2.

Changes since 4.15.0:

  • Make typing_extensions.TypeAliasType's __module__ attribute writable. Backport of CPython PR #149172.
  • Fix setting of __required_keys__ and __optional_keys__ when inheriting keys with the same name.
  • Add support for AsyncIterator, io.Reader, io.Writer and os.PathLike protocols as bases for other protocols.
  • Fix incorrect behaviour on Python 3.9 and Python 3.10 that meant that calling isinstance with typing_extensions.Concatenate[...] or typing_extensions.Unpack[...] as the first argument could have a different result in some situations depending on whether or not a profiling function had been set using sys.setprofile. This affected both CPython and PyPy implementations. Patch by Brian Schubert.
  • Fix __init_subclass__() behavior in the presence of multiple inheritance involving an @deprecated-decorated base class. Backport of CPython PR #138210 by Brian Schubert.
  • Raise TypeError when attempting to subclass typing_extensions.ParamSpec on Python 3.9. The typing implementation has always raised an error, and the typing_extensions implementation has raised an error on Python 3.10+ since typing_extensions v4.6.0. Patch by Brian Schubert.
  • Add the bound, covariant, contravariant, and infer_variance parameters to TypeVarTuple.
  • Officially support the bound, covariant, contravariant and infer_variance parameters to ParamSpec. Improve the validation of these parameters at runtime.
  • Rename typing_extensions.Sentinel to typing_extensions.sentinel, following the name that has been adopted for builtins.sentinel on Python 3.15. typing_extensions.Sentinel is retained as a soft-deprecated alias for backwards compatibility.
  • Add support for pickling sentinels.
  • Sentinels now preserve their identity when copied or deep-copied.
  • Deprecate passing name as a keyword argument or repr as a positional argument to the sentinel constructor.
  • The default repr of a sentinel X = sentinel("X") is now X rather than <X>.
  • Deprecate arbitrary attribute assignments to sentinels.
  • Deprecate subclassing sentinels.
  • Add support for Python 3.15.
  • Avoid a DeprecationWarning when deprecated is applied to a coroutine function on Python 3.14.0.

4.16.0rc2

Changes since 4.16.0rc1:

  • Avoid a DeprecationWarning when deprecated is applied to a coroutine function on Python 3.14.0.

Changes since 4.15.0:

  • Make typing_extensions.TypeAliasType's __module__ attribute writable. Backport of CPython PR #149172.
  • Fix setting of __required_keys__ and __optional_keys__ when inheriting keys with the same name.
  • Add support for AsyncIterator, io.Reader, io.Writer and os.PathLike protocols as bases for other protocols.
  • Fix incorrect behaviour on Python 3.9 and Python 3.10 that meant that calling isinstance with typing_extensions.Concatenate[...] or typing_extensions.Unpack[...] as the first argument could have a different result in some situations depending on whether or not a profiling function had been set using sys.setprofile. This affected both CPython and PyPy implementations. Patch by Brian Schubert.
  • Fix __init_subclass__() behavior in the presence of multiple inheritance involving an @deprecated-decorated base class. Backport of CPython PR #138210 by Brian Schubert.
  • Raise TypeError when attempting to subclass typing_extensions.ParamSpec on Python 3.9. The typing implementation has always raised an error, and the typing_extensions implementation has raised an error on Python 3.10+ since typing_extensions v4.6.0. Patch by Brian Schubert.
  • Add the bound, covariant, contravariant, and infer_variance parameters to TypeVarTuple.
  • Officially support the bound, covariant, contravariant and infer_variance parameters to ParamSpec. Improve the validation of these parameters at runtime.
  • Rename typing_extensions.Sentinel to typing_extensions.sentinel, following the name that has been adopted for builtins.sentinel on Python 3.15. typing_extensions.Sentinel is retained as a soft-deprecated alias for backwards compatibility.
  • Add support for pickling sentinels.
  • Sentinels now preserve their identity when copied or deep-copied.
  • Deprecate passing name as a keyword argument or repr as a positional argument to the sentinel constructor.
  • The default repr of a sentinel X = sentinel("X") is now X rather than <X>.
  • Deprecate arbitrary attribute assignments to sentinels.
  • Deprecate subclassing sentinels.
  • Add support for Python 3.15.

4.16.0rc1

  • Make typing_extensions.TypeAliasType's __module__ attribute writable. Backport of CPython PR #149172.
  • Fix setting of __required_keys__ and __optional_keys__ when inheriting keys with the same name.

... (truncated)

Changelog

Sourced from typing-extensions's changelog.

Release 4.16.0 (July 2, 2025)

No user-facing changes since 4.16.0rc2.

Release 4.16.0rc2 (June 25, 2026)

  • Avoid a DeprecationWarning when deprecated is applied to a coroutine function on Python 3.14.0.

Release 4.16.0rc1 (June 24, 2026)

  • Make typing_extensions.TypeAliasType's __module__ attribute writable. Backport of CPython PR #149172.
  • Fix setting of __required_keys__ and __optional_keys__ when inheriting keys with the same name.
  • Add support for AsyncIterator, io.Reader, io.Writer and os.PathLike protocols as bases for other protocols.
  • Fix incorrect behaviour on Python 3.9 and Python 3.10 that meant that calling isinstance with typing_extensions.Concatenate[...] or typing_extensions.Unpack[...] as the first argument could have a different result in some situations depending on whether or not a profiling function had been set using sys.setprofile. This affected both CPython and PyPy implementations. Patch by Brian Schubert.
  • Fix __init_subclass__() behavior in the presence of multiple inheritance involving an @deprecated-decorated base class. Backport of CPython PR #138210 by Brian Schubert.
  • Raise TypeError when attempting to subclass typing_extensions.ParamSpec on Python 3.9. The typing implementation has always raised an error, and the typing_extensions implementation has raised an error on Python 3.10+ since typing_extensions v4.6.0. Patch by Brian Schubert.
  • Add the bound, covariant, contravariant, and infer_variance parameters to TypeVarTuple.
  • Officially support the bound, covariant, contravariant and infer_variance parameters to ParamSpec. Improve the validation of these parameters at runtime.
  • Rename typing_extensions.Sentinel to typing_extensions.sentinel, following the name that has been adopted for builtins.sentinel on Python 3.15. typing_extensions.Sentinel is retained as a soft-deprecated alias for backwards compatibility.
  • Add support for pickling sentinels.
  • Sentinels now preserve their identity when copied or deep-copied.
  • Deprecate passing name as a keyword argument or repr as a positional argument to the sentinel constructor.
  • The default repr of a sentinel X = sentinel("X") is now X rather than <X>.
  • Deprecate arbitrary attribute assignments to sentinels.
  • Deprecate subclassing sentinels.
  • Add support for Python 3.15.
Commits

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/cython.txt | 2 +- requirements/dev.txt | 2 +- requirements/lint.txt | 2 +- requirements/multidict.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 0c4b784c87d..7de739d9e66 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -42,7 +42,7 @@ pycares==5.0.1 # via aiodns pycparser==3.0 # via cffi -typing-extensions==4.15.0 ; python_version < "3.13" +typing-extensions==4.16.0 ; python_version < "3.13" # via # -r requirements/runtime-deps.in # aiosignal diff --git a/requirements/base.txt b/requirements/base.txt index dbaaea3f6a3..02c3e6d2da2 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -42,7 +42,7 @@ pycares==5.0.1 # via aiodns pycparser==3.0 # via cffi -typing-extensions==4.15.0 ; python_version < "3.13" +typing-extensions==4.16.0 ; python_version < "3.13" # via # -r requirements/runtime-deps.in # aiosignal diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 19efdfad395..5c7718dfbc9 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -283,7 +283,7 @@ trustme==1.2.1 ; platform_machine != "i686" # via # -r requirements/lint.in # -r requirements/test-common.in -typing-extensions==4.15.0 ; python_version < "3.13" +typing-extensions==4.16.0 ; python_version < "3.13" # via # -r requirements/runtime-deps.in # aiosignal diff --git a/requirements/cython.txt b/requirements/cython.txt index 2cf9f068891..d474981036f 100644 --- a/requirements/cython.txt +++ b/requirements/cython.txt @@ -8,5 +8,5 @@ cython==3.2.8 # via -r requirements/cython.in multidict==6.7.1 # via -r requirements/multidict.in -typing-extensions==4.15.0 +typing-extensions==4.16.0 # via multidict diff --git a/requirements/dev.txt b/requirements/dev.txt index d0020b0d547..1d424a51eee 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -273,7 +273,7 @@ trustme==1.2.1 ; platform_machine != "i686" # via # -r requirements/lint.in # -r requirements/test-common.in -typing-extensions==4.15.0 ; python_version < "3.13" +typing-extensions==4.16.0 ; python_version < "3.13" # via # -r requirements/runtime-deps.in # aiosignal diff --git a/requirements/lint.txt b/requirements/lint.txt index b3291b2af9d..47845fae073 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -110,7 +110,7 @@ tomli==2.4.1 # slotscheck trustme==1.2.1 # via -r requirements/lint.in -typing-extensions==4.15.0 +typing-extensions==4.16.0 # via # cryptography # exceptiongroup diff --git a/requirements/multidict.txt b/requirements/multidict.txt index 144a7966d26..fadab71b4dc 100644 --- a/requirements/multidict.txt +++ b/requirements/multidict.txt @@ -6,5 +6,5 @@ # multidict==6.7.1 # via -r requirements/multidict.in -typing-extensions==4.15.0 +typing-extensions==4.16.0 # via multidict diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index dd0a75aef3e..bcf58d7b7d6 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -38,7 +38,7 @@ pycares==5.0.1 # via aiodns pycparser==3.0 # via cffi -typing-extensions==4.15.0 ; python_version < "3.13" +typing-extensions==4.16.0 ; python_version < "3.13" # via # -r requirements/runtime-deps.in # aiosignal diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index 8d72c97efa7..ba6ce4e1573 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -48,7 +48,7 @@ tomli==2.4.1 # via # coverage # pytest -typing-extensions==4.15.0 +typing-extensions==4.16.0 # via exceptiongroup wait-for-it==2.3.0 # via -r requirements/test-common-base.in diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 7fcc689fb43..2d75098ef7e 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -101,7 +101,7 @@ tomli==2.4.1 # pytest trustme==1.2.1 ; platform_machine != "i686" # via -r requirements/test-common.in -typing-extensions==4.15.0 +typing-extensions==4.16.0 # via # cryptography # exceptiongroup diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index e56ae8699b0..4a2d7b5f340 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -137,7 +137,7 @@ tomli==2.4.1 # pytest trustme==1.2.1 ; platform_machine != "i686" # via -r requirements/test-common.in -typing-extensions==4.15.0 ; python_version < "3.13" +typing-extensions==4.16.0 ; python_version < "3.13" # via # -r requirements/runtime-deps.in # aiosignal diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index 3fa1a5db302..623dbc1b0b4 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -92,7 +92,7 @@ tomli==2.4.1 # via # coverage # pytest -typing-extensions==4.15.0 ; python_version < "3.13" +typing-extensions==4.16.0 ; python_version < "3.13" # via # -r requirements/runtime-deps.in # aiosignal diff --git a/requirements/test.txt b/requirements/test.txt index cd2b1ce76ec..77fbb0fe3c1 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -137,7 +137,7 @@ tomli==2.4.1 # pytest trustme==1.2.1 ; platform_machine != "i686" # via -r requirements/test-common.in -typing-extensions==4.15.0 ; python_version < "3.13" +typing-extensions==4.16.0 ; python_version < "3.13" # via # -r requirements/runtime-deps.in # aiosignal From 4a108e897fe1b242645b7fe7af72c9c47d575f42 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:23:00 +0100 Subject: [PATCH 106/137] [PR #13042/b48737e7 backport][3.15] catch unknown charset when decoding content-disposition params (#13132) **This is a backport of PR #13042 as merged into master (b48737e74f2a18d0345d2894566be02c5da47f6b).** Co-authored-by: arshsmith --- CHANGES/13042.bugfix.rst | 4 ++++ CONTRIBUTORS.txt | 1 + aiohttp/multipart.py | 15 +++++++++++++-- tests/test_multipart_helpers.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 CHANGES/13042.bugfix.rst diff --git a/CHANGES/13042.bugfix.rst b/CHANGES/13042.bugfix.rst new file mode 100644 index 00000000000..ecda7970b60 --- /dev/null +++ b/CHANGES/13042.bugfix.rst @@ -0,0 +1,4 @@ +Fixed :exc:`LookupError` (and an unguarded :exc:`UnicodeDecodeError`) escaping +``Content-Disposition`` parsing when a multipart part supplies an extended +parameter with an unknown charset, e.g. ``filename*=unknown-8bit''...`` +-- by :user:`arshsmith1`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index a2b005f1b4e..14ae92d7257 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -52,6 +52,7 @@ Anton Kasyanov Anton Zhdan-Pushkin Arcadiy Ivanov Arseny Timoniq +Arshiya Tabasum Artem Yushkovskiy Arthur Darcet Ashutosh Kumar Singh diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 7bdae9644ca..b5b87f71dd4 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -1,5 +1,6 @@ import base64 import binascii +import builtins import json import re import sys @@ -148,7 +149,10 @@ def unescape(text: str, *, chars: str = "".join(map(re.escape, CHAR))) -> str: try: value = unquote(value, encoding, "strict") - except UnicodeDecodeError: # pragma: nocover + except (builtins.LookupError, UnicodeDecodeError): + # The charset is attacker-controlled here; an unknown name + # raises the builtin LookupError (the bare name is shadowed in + # this module by payload.LookupError). warnings.warn(BadContentDispositionParam(item)) continue @@ -207,7 +211,14 @@ def content_disposition_filename( if "'" in value: encoding, _, value = value.split("'", 2) encoding = encoding or "utf-8" - return unquote(value, encoding, "strict") + try: + return unquote(value, encoding, "strict") + except (builtins.LookupError, UnicodeDecodeError): + # Both the charset name and the octets are attacker-controlled + # here; an unknown encoding raises the builtin LookupError + # (shadowed in this module by payload.LookupError) and + # undecodable bytes raise UnicodeDecodeError. + return None return value diff --git a/tests/test_multipart_helpers.py b/tests/test_multipart_helpers.py index 87c33778559..ffbb4c79f6a 100644 --- a/tests/test_multipart_helpers.py +++ b/tests/test_multipart_helpers.py @@ -535,6 +535,22 @@ def test_attwithfn2231nbadpct2(self) -> None: assert "attachment" == disptype assert {} == params + @pytest.mark.parametrize( + "header", + ( + # An unknown charset name is attacker-controlled and makes + # urllib.parse.unquote raise the builtin LookupError. + "attachment; filename*=unknown-8bit''foo-%c3%a4.html", + # Undecodable octets raise UnicodeDecodeError. + "attachment; filename*=UTF-8''%ff.html", + ), + ) + def test_attwithfn2231baddecode(self, header: str) -> None: + with pytest.warns(aiohttp.BadContentDispositionParam): + disptype, params = parse_content_disposition(header) + assert "attachment" == disptype + assert {} == params + def test_attwithfn2231dpct(self) -> None: disptype, params = parse_content_disposition( "attachment; filename*=UTF-8''A-%2541.html" @@ -707,6 +723,18 @@ def test_attfncontenc(self) -> None: params = {"filename*0*": "UTF-8''foo-%c3%a4", "filename*1": ".html"} assert "foo-Γ€.html" == content_disposition_filename(params) + @pytest.mark.parametrize( + "params", + ( + # Unknown charset name raises the builtin LookupError. + {"filename*0*": "unknown-8bit''foo-%c3%a4", "filename*1": ".html"}, + # Undecodable octets raise UnicodeDecodeError. + {"filename*0*": "UTF-8''%ff", "filename*1": ".html"}, + ), + ) + def test_attfncontenc_baddecode(self, params: dict[str, str]) -> None: + assert content_disposition_filename(params) is None + def test_attfncontlz(self) -> None: params = {"filename*0": "foo", "filename*01": "bar"} assert "foo" == content_disposition_filename(params) From 2781b9562ca57585ab6dc7a4207ec1a4b6fdc2da Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2026 16:23:21 +0100 Subject: [PATCH 107/137] [PR #13042/b48737e7 backport][3.14] catch unknown charset when decoding content-disposition params (#13131) **This is a backport of PR #13042 as merged into master (b48737e74f2a18d0345d2894566be02c5da47f6b).** Co-authored-by: arshsmith --- CHANGES/13042.bugfix.rst | 4 ++++ CONTRIBUTORS.txt | 1 + aiohttp/multipart.py | 15 +++++++++++++-- tests/test_multipart_helpers.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 CHANGES/13042.bugfix.rst diff --git a/CHANGES/13042.bugfix.rst b/CHANGES/13042.bugfix.rst new file mode 100644 index 00000000000..ecda7970b60 --- /dev/null +++ b/CHANGES/13042.bugfix.rst @@ -0,0 +1,4 @@ +Fixed :exc:`LookupError` (and an unguarded :exc:`UnicodeDecodeError`) escaping +``Content-Disposition`` parsing when a multipart part supplies an extended +parameter with an unknown charset, e.g. ``filename*=unknown-8bit''...`` +-- by :user:`arshsmith1`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 9d36c7edfd2..ffc5f66bffe 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -52,6 +52,7 @@ Anton Kasyanov Anton Zhdan-Pushkin Arcadiy Ivanov Arseny Timoniq +Arshiya Tabasum Artem Yushkovskiy Arthur Darcet Ashutosh Kumar Singh diff --git a/aiohttp/multipart.py b/aiohttp/multipart.py index 7bdae9644ca..b5b87f71dd4 100644 --- a/aiohttp/multipart.py +++ b/aiohttp/multipart.py @@ -1,5 +1,6 @@ import base64 import binascii +import builtins import json import re import sys @@ -148,7 +149,10 @@ def unescape(text: str, *, chars: str = "".join(map(re.escape, CHAR))) -> str: try: value = unquote(value, encoding, "strict") - except UnicodeDecodeError: # pragma: nocover + except (builtins.LookupError, UnicodeDecodeError): + # The charset is attacker-controlled here; an unknown name + # raises the builtin LookupError (the bare name is shadowed in + # this module by payload.LookupError). warnings.warn(BadContentDispositionParam(item)) continue @@ -207,7 +211,14 @@ def content_disposition_filename( if "'" in value: encoding, _, value = value.split("'", 2) encoding = encoding or "utf-8" - return unquote(value, encoding, "strict") + try: + return unquote(value, encoding, "strict") + except (builtins.LookupError, UnicodeDecodeError): + # Both the charset name and the octets are attacker-controlled + # here; an unknown encoding raises the builtin LookupError + # (shadowed in this module by payload.LookupError) and + # undecodable bytes raise UnicodeDecodeError. + return None return value diff --git a/tests/test_multipart_helpers.py b/tests/test_multipart_helpers.py index 87c33778559..ffbb4c79f6a 100644 --- a/tests/test_multipart_helpers.py +++ b/tests/test_multipart_helpers.py @@ -535,6 +535,22 @@ def test_attwithfn2231nbadpct2(self) -> None: assert "attachment" == disptype assert {} == params + @pytest.mark.parametrize( + "header", + ( + # An unknown charset name is attacker-controlled and makes + # urllib.parse.unquote raise the builtin LookupError. + "attachment; filename*=unknown-8bit''foo-%c3%a4.html", + # Undecodable octets raise UnicodeDecodeError. + "attachment; filename*=UTF-8''%ff.html", + ), + ) + def test_attwithfn2231baddecode(self, header: str) -> None: + with pytest.warns(aiohttp.BadContentDispositionParam): + disptype, params = parse_content_disposition(header) + assert "attachment" == disptype + assert {} == params + def test_attwithfn2231dpct(self) -> None: disptype, params = parse_content_disposition( "attachment; filename*=UTF-8''A-%2541.html" @@ -707,6 +723,18 @@ def test_attfncontenc(self) -> None: params = {"filename*0*": "UTF-8''foo-%c3%a4", "filename*1": ".html"} assert "foo-Γ€.html" == content_disposition_filename(params) + @pytest.mark.parametrize( + "params", + ( + # Unknown charset name raises the builtin LookupError. + {"filename*0*": "unknown-8bit''foo-%c3%a4", "filename*1": ".html"}, + # Undecodable octets raise UnicodeDecodeError. + {"filename*0*": "UTF-8''%ff", "filename*1": ".html"}, + ), + ) + def test_attfncontenc_baddecode(self, params: dict[str, str]) -> None: + assert content_disposition_filename(params) is None + def test_attfncontlz(self) -> None: params = {"filename*0": "foo", "filename*01": "bar"} assert "foo" == content_disposition_filename(params) From 0a8c8571b97a8c2c0a89993b9070812e053697c3 Mon Sep 17 00:00:00 2001 From: Taras Kozlov Date: Mon, 13 Jul 2026 21:41:06 +0200 Subject: [PATCH 108/137] [PR #12822/706ecdcc1 backport][3.15] Add aiofastnet to speedups extra #12822 (#13127) This is a backport of PR https://github.com/aio-libs/aiohttp/pull/12822 as merged into master (https://github.com/aio-libs/aiohttp/commit/706ecdcc10f9598c280a0af537437324a3e993fa). --------- Co-authored-by: taras Co-authored-by: Sam Bull --- .github/workflows/ci-cd.yml | 9 + CHANGES/12822.breaking.rst | 3 + CONTRIBUTORS.txt | 1 + aiohttp/connector.py | 94 ++++++- aiohttp/web_fileresponse.py | 19 +- aiohttp/web_runner.py | 51 +++- docs/faq.rst | 59 +++++ docs/spelling_wordlist.txt | 5 + pyproject.toml | 1 + requirements/base-ft.txt | 5 + requirements/base.txt | 5 + requirements/constraints.txt | 8 +- requirements/dev.txt | 8 +- requirements/lint.in | 1 + requirements/lint.txt | 10 +- requirements/runtime-deps.in | 1 + requirements/runtime-deps.txt | 5 + requirements/test-ft.txt | 6 +- requirements/test-mobile.txt | 6 +- requirements/test.txt | 6 +- tests/conftest.py | 5 + tests/test_benchmarks_web_fileresponse.py | 9 +- tests/test_connector.py | 189 +++++++++++--- tests/test_proxy.py | 285 ++++++++++++++-------- tests/test_proxy_functional.py | 22 +- tests/test_run_app.py | 208 ++++++++++++---- tests/test_web_runner.py | 21 +- tests/test_web_sendfile_functional.py | 45 ++-- 28 files changed, 859 insertions(+), 228 deletions(-) create mode 100644 CHANGES/12822.breaking.rst diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 24abc33bf70..7566a16c7a7 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -448,6 +448,15 @@ jobs: make cythonize - name: Install self run: python -m pip install -e . + - name: Load kernel TLS module + if: runner.os == 'Linux' + run: sudo modprobe tls + - name: Show kernel and OpenSSL build information + if: runner.os == 'Linux' + run: | + lsb_release -a + uname -r + openssl version -a - name: Run benchmarks uses: CodSpeedHQ/action@v4 with: diff --git a/CHANGES/12822.breaking.rst b/CHANGES/12822.breaking.rst new file mode 100644 index 00000000000..499736669ee --- /dev/null +++ b/CHANGES/12822.breaking.rst @@ -0,0 +1,3 @@ +Added ``aiofastnet`` package to ``speedups`` extra. aiofastnet provides faster alternatives to the standard loop functions, which are used to run server or establish connections. This is breaking because ``transport.get_extra_info('ssl_object')`` now returns ``aiofastnet.SSLObject`` when speedups are installed, which may lack some methods. If you experience any issues that you think might be related to this change, you can try to disable ``aiofastnet`` by uninstalling aiofastnet package. + +-- by :user:`tarasko`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 14ae92d7257..da471b46715 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -360,6 +360,7 @@ Sunit Deshpande Sviatoslav Bulbakha Sviatoslav Sydorenko Taha Jahangir +Taras Kozlov Taras Voinarovskyi Terence Honles Thanos Lefteris diff --git a/aiohttp/connector.py b/aiohttp/connector.py index 438c336da7f..c61f37a411c 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -49,6 +49,12 @@ from .log import client_logger from .resolver import DefaultResolver +try: + import aiofastnet +except ImportError: + aiofastnet = None # type: ignore[assignment] + + if sys.version_info >= (3, 12): from collections.abc import Buffer else: @@ -92,6 +98,82 @@ from .tracing import Trace +async def create_connection( + loop: asyncio.AbstractEventLoop, + protocol_factory: Callable[[], ResponseHandler], + *, + ssl: SSLContext | None, + sock: socket.socket, + server_hostname: str | None, + ssl_shutdown_timeout: float | None = None, +) -> tuple[asyncio.Transport, ResponseHandler]: + if aiofastnet is not None: + return await aiofastnet.create_connection( + loop, + protocol_factory, + ssl=ssl, + sock=sock, + server_hostname=server_hostname, + ssl_shutdown_timeout=ssl_shutdown_timeout, + ) + else: + if sys.version_info >= (3, 11): + return await loop.create_connection( + protocol_factory, + ssl=ssl, + sock=sock, + server_hostname=server_hostname, + ssl_shutdown_timeout=ssl_shutdown_timeout, + ) + else: + return await loop.create_connection( + protocol_factory, + ssl=ssl, + sock=sock, + server_hostname=server_hostname, + ) + + +async def start_tls( + loop: asyncio.AbstractEventLoop, + transport: asyncio.Transport, + protocol: ResponseHandler, + sslcontext: SSLContext, + *, + server_hostname: str | None, + ssl_handshake_timeout: float | None, + ssl_shutdown_timeout: float | None = None, +) -> asyncio.BaseTransport | None: + if aiofastnet is not None: + return await aiofastnet.start_tls( + loop, + transport, + protocol, + sslcontext, + server_hostname=server_hostname, + ssl_handshake_timeout=ssl_handshake_timeout, + ssl_shutdown_timeout=ssl_shutdown_timeout, + ) + else: + if sys.version_info >= (3, 11): + return await loop.start_tls( + transport, + protocol, + sslcontext, + server_hostname=server_hostname, + ssl_handshake_timeout=ssl_handshake_timeout, + ssl_shutdown_timeout=ssl_shutdown_timeout, + ) + else: + return await loop.start_tls( + transport, + protocol, + sslcontext, + server_hostname=server_hostname, + ssl_handshake_timeout=ssl_handshake_timeout, + ) + + class _DeprecationWaiter: __slots__ = ("_awaitable", "_awaited") @@ -1307,7 +1389,7 @@ async def _wrap_create_connection( and sys.version_info >= (3, 11) ): kwargs["ssl_shutdown_timeout"] = self._ssl_shutdown_timeout - return await self._loop.create_connection(*args, **kwargs, sock=sock) + return await create_connection(self._loop, *args, **kwargs, sock=sock) except cert_errors as exc: raise ClientConnectorCertificateError(req.connection_key, exc) from exc except ssl_errors as exc: @@ -1402,6 +1484,10 @@ def _warn_about_tls_in_tls( if type(underlying_transport).__module__.startswith("uvloop"): return + # Check if aiofastnet is being used, which supports TLS in TLS + if aiofastnet is not None: + return + # Support in asyncio was added in Python 3.11 (bpo-44011) asyncio_supports_tls_in_tls = sys.version_info >= (3, 11) or getattr( underlying_transport, @@ -1453,7 +1539,8 @@ async def _start_tls_connection( try: # ssl_shutdown_timeout is only available in Python 3.11+ if sys.version_info >= (3, 11) and self._ssl_shutdown_timeout: - tls_transport = await self._loop.start_tls( + tls_transport = await start_tls( + self._loop, underlying_transport, tls_proto, sslcontext, @@ -1462,7 +1549,8 @@ async def _start_tls_connection( ssl_shutdown_timeout=self._ssl_shutdown_timeout, ) else: - tls_transport = await self._loop.start_tls( + tls_transport = await start_tls( + self._loop, underlying_transport, tls_proto, sslcontext, diff --git a/aiohttp/web_fileresponse.py b/aiohttp/web_fileresponse.py index 8aa67cec7c1..dc705d0ebd9 100644 --- a/aiohttp/web_fileresponse.py +++ b/aiohttp/web_fileresponse.py @@ -13,6 +13,7 @@ TYPE_CHECKING, Any, Awaitable, + BinaryIO, Callable, Final, Iterator, @@ -43,6 +44,11 @@ if TYPE_CHECKING: from .web_request import BaseRequest +try: + import aiofastnet +except ImportError: + aiofastnet = None # type: ignore[assignment] + _T_OnChunkSent = Optional[Callable[[bytes], Awaitable[None]]] @@ -105,12 +111,12 @@ def __init__( self._path = pathlib.Path(path) self._chunk_size = chunk_size - def _seek_and_read(self, fobj: IO[Any], offset: int, chunk_size: int) -> bytes: + def _seek_and_read(self, fobj: BinaryIO, offset: int, chunk_size: int) -> bytes: fobj.seek(offset) - return fobj.read(chunk_size) # type: ignore[no-any-return] + return fobj.read(chunk_size) async def _sendfile_fallback( - self, writer: AbstractStreamWriter, fobj: IO[Any], offset: int, count: int + self, writer: AbstractStreamWriter, fobj: BinaryIO, offset: int, count: int ) -> AbstractStreamWriter: # To keep memory usage low,fobj is transferred in chunks # controlled by the constructor's chunk_size argument. @@ -131,7 +137,7 @@ async def _sendfile_fallback( return writer async def _sendfile( - self, request: "BaseRequest", fobj: IO[Any], offset: int, count: int + self, request: "BaseRequest", fobj: BinaryIO, offset: int, count: int ) -> AbstractStreamWriter: writer = await super().prepare(request) assert writer is not None @@ -145,7 +151,10 @@ async def _sendfile( raise ConnectionResetError("Connection lost") try: - await loop.sendfile(transport, fobj, offset, count) + if aiofastnet is not None: + await aiofastnet.sendfile(loop, transport, fobj, offset, count) + else: + await loop.sendfile(transport, fobj, offset, count) except NotImplementedError: return await self._sendfile_fallback(writer, fobj, offset, count) diff --git a/aiohttp/web_runner.py b/aiohttp/web_runner.py index b2128fa3cb1..021652567d8 100644 --- a/aiohttp/web_runner.py +++ b/aiohttp/web_runner.py @@ -3,6 +3,7 @@ import socket import warnings from abc import ABC, abstractmethod +from collections.abc import Callable from typing import TYPE_CHECKING, Any from yarl import URL @@ -21,6 +22,49 @@ except ImportError: # pragma: no cover SSLContext = object # type: ignore[misc,assignment] +try: + import aiofastnet +except ImportError: + aiofastnet = None # type: ignore[assignment] + + +async def create_server( + loop: asyncio.AbstractEventLoop, + protocol_factory: Callable[[], asyncio.Protocol], + host: str | None = None, + port: int | None = None, + *, + sock: socket.socket | None = None, + ssl: SSLContext | None = None, + backlog: int = 100, + reuse_address: bool | None = None, + reuse_port: bool | None = None, +) -> asyncio.Server: + if aiofastnet is not None: + return await aiofastnet.create_server( + loop, + protocol_factory, + host, + port, + sock=sock, + ssl=ssl, + backlog=backlog, + reuse_address=reuse_address, + reuse_port=reuse_port, + ) + else: + return await loop.create_server( + protocol_factory, + host, + port, + sock=sock, + ssl=ssl, + backlog=backlog, + reuse_address=reuse_address, + reuse_port=reuse_port, + ) + + __all__ = ( "BaseSite", "TCPSite", @@ -135,7 +179,8 @@ async def start(self) -> None: loop = asyncio.get_event_loop() server = self._runner.server assert server is not None - self._server = await loop.create_server( + self._server = await create_server( + loop, server, self._host, self._port, @@ -255,8 +300,8 @@ async def start(self) -> None: loop = asyncio.get_event_loop() server = self._runner.server assert server is not None - self._server = await loop.create_server( - server, sock=self._sock, ssl=self._ssl_context, backlog=self._backlog + self._server = await create_server( + loop, server, sock=self._sock, ssl=self._ssl_context, backlog=self._backlog ) diff --git a/docs/faq.rst b/docs/faq.rst index 009169a98ae..fa082b59f8c 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -263,6 +263,65 @@ enable compression in NGINX (you are deploying aiohttp behind reverse proxy, right?). +How do I enable Kernel TLS, and should I do it? +----------------------------------------------- + +Kernel TLS (KTLS) allows aiohttp to move encryption and decryption of +TLS traffic from user space to the kernel. + +KTLS will be beneficial if you run an HTTPS server that often returns +:class:`~aiohttp.web.FileResponse` objects or you have a high-end NIC that can +offload TLS encryption. For ordinary +dynamic responses, small files, or deployments behind a TLS-terminating reverse +proxy, it is unlikely to help and may actually slightly degrade performance. + +KTLS is supported through the ``aiofastnet`` package, which is installed as +part of the ``speedups`` extra. + +To enable KTLS, check the following: + +* Verify that ``aiofastnet`` is installed and was able to locate OpenSSL + dynamic libraries: + + .. code-block:: bash + + python -c "import aiofastnet; print(aiofastnet.OPENSSL_DYN_LIBS)" + + This should not print ``None``. For example: + + .. code-block:: bash + + OpenSSLDynLibs(libssl='/usr/lib/libssl.so.3', libcrypto='/usr/lib/libcrypto.so.3') + + KTLS requires a Python build that is dynamically linked against OpenSSL. + This is generally true for system Python installations, Conda distributions, + ``pyenv``, and ``actions/setup-python`` in GitHub Actions, but not for + Python installations managed by ``uv``. + +* Make sure the Linux ``tls`` kernel module is loaded:: + + sudo modprobe tls + +* Make sure the ``ssl.OP_ENABLE_KTLS`` option is enabled in ``SSLContext`` + (available since Python 3.12):: + + sslcontext.options |= ssl.OP_ENABLE_KTLS + +* Make sure Python is using OpenSSL 3.0 or newer. Typically, Python is using + the system OpenSSL on Linux, but sometimes distributions ship their own + OpenSSL. The following commands will help identify the OpenSSL version + and which ``libssl`` and ``libcrypto`` are being used by the ``ssl`` module:: + + python -c "import ssl; print(ssl.OPENSSL_VERSION)" + ldd "$(python -c 'import _ssl; print(_ssl.__file__)')" + + +If ``ssl.OP_ENABLE_KTLS`` was requested in ``sslcontext``, but ``aiofastnet`` +could not enable KTLS, it will log a warning suggesting the possible reason. + +After enabling it, run your own benchmarks and verify that KTLS actually +speeds things up in your case. + How do I manage a ClientSession within a web server? ---------------------------------------------------- diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 9ff16a5df2a..f063f4532fc 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -4,6 +4,7 @@ ABI addons aiodns aioes +aiofastnet aiohttp aiohttpdemo aiohttp’s @@ -81,6 +82,7 @@ codspeed Codings committer committers +Conda config Config configs @@ -184,6 +186,7 @@ keepalive keepalived keepalives keepaliving +KTLS kib KiB kwarg @@ -229,6 +232,7 @@ namedtuple nameservers namespace netrc +NIC nginx Nginx Nikolay @@ -238,6 +242,7 @@ nowait OAuth Online optimizations +OpenSSL orjson os outcoming diff --git a/pyproject.toml b/pyproject.toml index bf490bf7cdc..f91e8c08a16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ dynamic = [ [project.optional-dependencies] speedups = [ "aiodns >= 3.3.0; sys_platform != 'android' and sys_platform != 'ios'", + "aiofastnet >= 0.19.0; platform_python_implementation == 'CPython' 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'", "backports.zstd; platform_python_implementation == 'CPython' and python_version < '3.14' and sys_platform != 'android' and sys_platform != 'ios'", diff --git a/requirements/base-ft.txt b/requirements/base-ft.txt index 7de739d9e66..4ec9298409e 100644 --- a/requirements/base-ft.txt +++ b/requirements/base-ft.txt @@ -6,6 +6,8 @@ # aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in +aiofastnet==0.19.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" + # via -r requirements/runtime-deps.in aiohappyeyeballs==2.7.1 # via -r requirements/runtime-deps.in aiosignal==1.4.0 @@ -20,6 +22,8 @@ brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != # via -r requirements/runtime-deps.in cffi==2.1.0 # via pycares +exceptiongroup==1.3.1 + # via aiofastnet frozenlist==1.8.0 # via # -r requirements/runtime-deps.in @@ -46,6 +50,7 @@ typing-extensions==4.16.0 ; python_version < "3.13" # via # -r requirements/runtime-deps.in # aiosignal + # exceptiongroup # multidict yarl==1.24.2 # via -r requirements/runtime-deps.in diff --git a/requirements/base.txt b/requirements/base.txt index 02c3e6d2da2..c08ba7a3ea8 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -6,6 +6,8 @@ # aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in +aiofastnet==0.19.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" + # via -r requirements/runtime-deps.in aiohappyeyeballs==2.7.1 # via -r requirements/runtime-deps.in aiosignal==1.4.0 @@ -20,6 +22,8 @@ brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != # via -r requirements/runtime-deps.in cffi==2.1.0 # via pycares +exceptiongroup==1.3.1 + # via aiofastnet frozenlist==1.8.0 # via # -r requirements/runtime-deps.in @@ -46,6 +50,7 @@ typing-extensions==4.16.0 ; python_version < "3.13" # via # -r requirements/runtime-deps.in # aiosignal + # exceptiongroup # multidict uvloop==0.22.1 ; platform_system != "Windows" and implementation_name == "cpython" # via -r requirements/base.in diff --git a/requirements/constraints.txt b/requirements/constraints.txt index 5c7718dfbc9..09f245d8abe 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -8,6 +8,10 @@ aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via # -r requirements/lint.in # -r requirements/runtime-deps.in +aiofastnet==0.19.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" + # via + # -r requirements/lint.in + # -r requirements/runtime-deps.in aiohappyeyeballs==2.7.1 # via -r requirements/runtime-deps.in aiohttp-theme==0.1.7 @@ -70,7 +74,9 @@ docutils==0.21.2 # myst-parser # sphinx exceptiongroup==1.3.1 - # via pytest + # via + # aiofastnet + # pytest execnet==2.1.2 # via pytest-xdist filelock==3.29.7 diff --git a/requirements/dev.txt b/requirements/dev.txt index 1d424a51eee..8d1f05af83a 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -8,6 +8,10 @@ aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via # -r requirements/lint.in # -r requirements/runtime-deps.in +aiofastnet==0.19.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" + # via + # -r requirements/lint.in + # -r requirements/runtime-deps.in aiohappyeyeballs==2.7.1 # via -r requirements/runtime-deps.in aiohttp-theme==0.1.7 @@ -68,7 +72,9 @@ docutils==0.21.2 # myst-parser # sphinx exceptiongroup==1.3.1 - # via pytest + # via + # aiofastnet + # pytest execnet==2.1.2 # via pytest-xdist filelock==3.29.7 diff --git a/requirements/lint.in b/requirements/lint.in index 97ee198b9c9..7e29c454335 100644 --- a/requirements/lint.in +++ b/requirements/lint.in @@ -1,4 +1,5 @@ aiodns +aiofastnet >= 0.19.0 backports.zstd; implementation_name == "cpython" and python_version < "3.14" blockbuster freezegun diff --git a/requirements/lint.txt b/requirements/lint.txt index 47845fae073..a0cf5a5ff70 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -6,6 +6,12 @@ # aiodns==4.0.4 # via -r requirements/lint.in +aiofastnet==0.19.0 + # via -r requirements/lint.in +aiohappyeyeballs==2.7.1 + # via aiohttp +aiosignal==1.4.0 + # via aiohttp annotated-types==0.7.0 # via pydantic ast-serialize==0.6.0 @@ -27,7 +33,9 @@ cryptography==49.0.0 distlib==0.4.3 # via virtualenv exceptiongroup==1.3.1 - # via pytest + # via + # aiofastnet + # pytest filelock==3.29.7 # via # python-discovery diff --git a/requirements/runtime-deps.in b/requirements/runtime-deps.in index 49ba703ef65..957595a1030 100644 --- a/requirements/runtime-deps.in +++ b/requirements/runtime-deps.in @@ -1,6 +1,7 @@ # Extracted from `pyproject.toml` via `make sync-direct-runtime-deps` aiodns >= 3.3.0; sys_platform != 'android' and sys_platform != 'ios' +aiofastnet >= 0.19.0; platform_python_implementation == 'CPython' and 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' diff --git a/requirements/runtime-deps.txt b/requirements/runtime-deps.txt index bcf58d7b7d6..9af389ad565 100644 --- a/requirements/runtime-deps.txt +++ b/requirements/runtime-deps.txt @@ -6,6 +6,8 @@ # aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in +aiofastnet==0.19.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" + # via -r requirements/runtime-deps.in aiohappyeyeballs==2.7.1 # via -r requirements/runtime-deps.in aiosignal==1.4.0 @@ -20,6 +22,8 @@ brotli==1.2.0 ; platform_python_implementation == "CPython" and sys_platform != # via -r requirements/runtime-deps.in cffi==2.1.0 # via pycares +exceptiongroup==1.3.1 + # via aiofastnet frozenlist==1.8.0 # via # -r requirements/runtime-deps.in @@ -42,6 +46,7 @@ typing-extensions==4.16.0 ; python_version < "3.13" # via # -r requirements/runtime-deps.in # aiosignal + # exceptiongroup # multidict yarl==1.24.2 # via -r requirements/runtime-deps.in diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 4a2d7b5f340..ad3b32059b7 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -6,6 +6,8 @@ # aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in +aiofastnet==0.19.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" + # via -r requirements/runtime-deps.in aiohappyeyeballs==2.7.1 # via -r requirements/runtime-deps.in aiosignal==1.4.0 @@ -37,7 +39,9 @@ coverage==7.15.0 cryptography==49.0.0 # via trustme exceptiongroup==1.3.1 - # via pytest + # via + # aiofastnet + # pytest execnet==2.1.2 # via pytest-xdist forbiddenfruit==0.1.4 diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index 623dbc1b0b4..f8b30954946 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -6,6 +6,8 @@ # aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in +aiofastnet==0.19.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" + # via -r requirements/runtime-deps.in aiohappyeyeballs==2.7.1 # via -r requirements/runtime-deps.in aiosignal==1.4.0 @@ -29,7 +31,9 @@ click==8.4.2 coverage==7.15.0 # via pytest-cov exceptiongroup==1.3.1 - # via pytest + # via + # aiofastnet + # pytest freezegun==1.5.5 # via -r requirements/test-common-base.in frozenlist==1.8.0 diff --git a/requirements/test.txt b/requirements/test.txt index 77fbb0fe3c1..e2026736651 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -6,6 +6,8 @@ # aiodns==4.0.4 ; sys_platform != "android" and sys_platform != "ios" # via -r requirements/runtime-deps.in +aiofastnet==0.19.0 ; platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios" + # via -r requirements/runtime-deps.in aiohappyeyeballs==2.7.1 # via -r requirements/runtime-deps.in aiosignal==1.4.0 @@ -37,7 +39,9 @@ coverage==7.15.0 cryptography==49.0.0 # via trustme exceptiongroup==1.3.1 - # via pytest + # via + # aiofastnet + # pytest execnet==2.1.2 # via pytest-xdist forbiddenfruit==0.1.4 diff --git a/tests/conftest.py b/tests/conftest.py index a1cd6a17a56..2cf3a52590e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -108,6 +108,11 @@ def blockbuster(request: pytest.FixtureRequest) -> Iterator[None]: # synchronization in async code. # Allow lock.acquire calls to prevent these false positives bb.functions["threading.Lock.acquire"].deactivate() + + # aiofastnet uses os.sendfile only on non-blocking sockets. + # aiofastnet transports set non-blocking flag for all received socket objects. + # blockbuster triggers anyway. + bb.functions["os.sendfile"].deactivate() yield diff --git a/tests/test_benchmarks_web_fileresponse.py b/tests/test_benchmarks_web_fileresponse.py index 09df89daa01..faed5162db5 100644 --- a/tests/test_benchmarks_web_fileresponse.py +++ b/tests/test_benchmarks_web_fileresponse.py @@ -3,6 +3,8 @@ import asyncio import os import pathlib +import ssl +import sys from dataclasses import dataclass from typing import TYPE_CHECKING @@ -45,8 +47,13 @@ def test_simple_web_file_response( benchmark: BenchmarkFixture, conn_type: ConnectionType, benchmark_file: BenchmarkFile, + pytestconfig: pytest.Config, ) -> None: - """Benchmark simple web.FileResponse.""" + """Benchmark creating 100 simple web.FileResponse.""" + server_ssl_context = conn_type.s_kwargs.get("ssl") + if server_ssl_context is not None: + if sys.version_info >= (3, 12): + server_ssl_context.options |= ssl.OP_ENABLE_KTLS async def handler(request: web.Request) -> web.FileResponse: return web.FileResponse(path=benchmark_file.path) diff --git a/tests/test_connector.py b/tests/test_connector.py index 4d0817f55f9..276717b9114 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -356,6 +356,26 @@ async def test_close_with_proto_closed_none(key: ConnectionKey) -> None: assert conn.closed +async def test_close_logs_closed_waiter_exception( + loop: asyncio.AbstractEventLoop, key: ConnectionKey +) -> None: + exc = RuntimeError("close failed") + + proto = mock.create_autospec(ResponseHandler, instance=True) + proto.closed = loop.create_future() + proto.closed.set_exception(exc) + + conn = aiohttp.BaseConnector() + conn._conns[key] = deque([(proto, 0)]) + + with mock.patch.object(connector_module.client_logger, "debug") as debug: # type: ignore[attr-defined] + await conn.close() + + proto.close.assert_called_once() + debug.assert_called_once_with("Error while closing connector: %r", exc) + assert conn.closed + + async def test_get(loop: asyncio.AbstractEventLoop, key: ConnectionKey) -> None: conn = aiohttp.BaseConnector() try: @@ -673,14 +693,16 @@ async def test_tcp_connector_certificate_error( ) -> None: req = ClientRequest("GET", URL("https://127.0.0.1:443"), loop=loop) - async def certificate_error(*args, **kwargs): - raise ssl.CertificateError - - conn = aiohttp.TCPConnector(loop=loop) - conn._loop.create_connection = certificate_error - - with pytest.raises(aiohttp.ClientConnectorCertificateError) as ctx: - await conn.connect(req, [], ClientTimeout()) + conn = aiohttp.TCPConnector() + with mock.patch.object( + connector_module, + "create_connection", + autospec=True, + spec_set=True, + side_effect=ssl.CertificateError, + ): + with pytest.raises(aiohttp.ClientConnectorCertificateError) as ctx: + await conn.connect(req, [], ClientTimeout()) assert isinstance(ctx.value, ssl.CertificateError) assert isinstance(ctx.value.certificate_error, ssl.CertificateError) @@ -695,7 +717,7 @@ async def test_tcp_connector_server_hostname_default( conn = aiohttp.TCPConnector() with mock.patch.object( - conn._loop, "create_connection", autospec=True, spec_set=True + connector_module, "create_connection", autospec=True, spec_set=True ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() @@ -713,7 +735,7 @@ async def test_tcp_connector_server_hostname_override( conn = aiohttp.TCPConnector() with mock.patch.object( - conn._loop, "create_connection", autospec=True, spec_set=True + connector_module, "create_connection", autospec=True, spec_set=True ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() @@ -864,7 +886,7 @@ def get_extra_info(param): side_effect=_resolve_host, ), mock.patch.object( - conn._loop, + connector_module, "create_connection", autospec=True, spec_set=True, @@ -951,10 +973,24 @@ async def create_connection(*args, **kwargs): pr = create_mocked_conn(loop) return tr, pr - conn._loop.sock_connect = sock_connect - conn._loop.create_connection = create_connection - - established_connection = await conn.connect(req, [], ClientTimeout()) + with mock.patch.object( + conn, "_resolve_host", autospec=True, spec_set=True, side_effect=_resolve_host + ): + with mock.patch.object( + conn._loop, + "sock_connect", + autospec=True, + spec_set=True, + side_effect=sock_connect, + ): + with mock.patch.object( + connector_module, + "create_connection", + autospec=True, + spec_set=True, + side_effect=create_connection, + ): + established_connection = await conn.connect(req, [], ClientTimeout()) assert addrs_tried == [(ip1, 443, 0, 0), (ip2, 443)] @@ -1034,7 +1070,7 @@ async def create_connection(*args, **kwargs): side_effect=_resolve_host, ), mock.patch.object( - conn._loop, + connector_module, "create_connection", autospec=True, spec_set=True, @@ -1100,10 +1136,24 @@ async def create_connection(*args, **kwargs): pr = create_mocked_conn(loop) return tr, pr - conn._loop.sock_connect = sock_connect - conn._loop.create_connection = create_connection - - established_connection = await conn.connect(req, [], ClientTimeout()) + with mock.patch.object( + conn, "_resolve_host", autospec=True, spec_set=True, side_effect=_resolve_host + ): + with mock.patch.object( + conn._loop, + "sock_connect", + autospec=True, + spec_set=True, + side_effect=sock_connect, + ): + with mock.patch.object( + connector_module, + "create_connection", + autospec=True, + spec_set=True, + side_effect=create_connection, + ): + established_connection = await conn.connect(req, [], ClientTimeout()) # We should only try the IPv4 address since we specified # the family to be AF_INET @@ -1210,7 +1260,7 @@ async def create_connection( side_effect=_resolve_host, ), mock.patch.object( - conn._loop, + connector_module, "create_connection", autospec=True, spec_set=True, @@ -2133,7 +2183,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_passed_to_create_connection( conn = aiohttp.TCPConnector(ssl_shutdown_timeout=2.5) with mock.patch.object( - conn._loop, "create_connection", autospec=True, spec_set=True + connector_module, "create_connection", autospec=True, spec_set=True ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() @@ -2151,7 +2201,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_passed_to_create_connection( conn = aiohttp.TCPConnector(ssl_shutdown_timeout=None) with mock.patch.object( - conn._loop, "create_connection", autospec=True, spec_set=True + connector_module, "create_connection", autospec=True, spec_set=True ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() @@ -2170,7 +2220,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_passed_to_create_connection( conn = aiohttp.TCPConnector(ssl_shutdown_timeout=2.5) with mock.patch.object( - conn._loop, "create_connection", autospec=True, spec_set=True + connector_module, "create_connection", autospec=True, spec_set=True ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() @@ -2197,7 +2247,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_not_passed_pre_311( assert any(issubclass(warn.category, RuntimeWarning) for warn in w) with mock.patch.object( - conn._loop, "create_connection", autospec=True, spec_set=True + connector_module, "create_connection", autospec=True, spec_set=True ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() @@ -2362,7 +2412,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_zero_not_passed( conn = aiohttp.TCPConnector(ssl_shutdown_timeout=0) with mock.patch.object( - conn._loop, "create_connection", autospec=True, spec_set=True + connector_module, "create_connection", autospec=True, spec_set=True ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() @@ -2393,7 +2443,7 @@ async def test_tcp_connector_ssl_shutdown_timeout_nonzero_passed( conn = aiohttp.TCPConnector(ssl_shutdown_timeout=5.0) with mock.patch.object( - conn._loop, "create_connection", autospec=True, spec_set=True + connector_module, "create_connection", autospec=True, spec_set=True ) as create_connection: create_connection.return_value = mock.Mock(), mock.Mock() @@ -2466,7 +2516,9 @@ async def test_start_tls_exception_with_ssl_shutdown_timeout_zero( mock.patch.object( conn, "_get_ssl_context", return_value=ssl.create_default_context() ), - mock.patch.object(conn._loop, "start_tls", side_effect=OSError("TLS failed")), + mock.patch.object( + connector_module, "start_tls", side_effect=OSError("TLS failed") + ), ): with pytest.raises(OSError): await conn._start_tls_connection(underlying_transport, req, ClientTimeout()) @@ -2500,7 +2552,9 @@ async def test_start_tls_exception_with_ssl_shutdown_timeout_nonzero( mock.patch.object( conn, "_get_ssl_context", return_value=ssl.create_default_context() ), - mock.patch.object(conn._loop, "start_tls", side_effect=OSError("TLS failed")), + mock.patch.object( + connector_module, "start_tls", side_effect=OSError("TLS failed") + ), ): with pytest.raises(OSError): await conn._start_tls_connection(underlying_transport, req, ClientTimeout()) @@ -2537,7 +2591,9 @@ async def test_start_tls_exception_with_ssl_shutdown_timeout_nonzero_pre_311( mock.patch.object( conn, "_get_ssl_context", return_value=ssl.create_default_context() ), - mock.patch.object(conn._loop, "start_tls", side_effect=OSError("TLS failed")), + mock.patch.object( + connector_module, "start_tls", side_effect=OSError("TLS failed") + ), ): with pytest.raises(OSError): await conn._start_tls_connection(underlying_transport, req, ClientTimeout()) @@ -3956,15 +4012,11 @@ async def _resolve_host(host, port, traces=None): url = srv.make_url("/") r = await session.get(url.with_host(host), ssl=client_ssl_ctx) - r.release() first_conn = next(iter(conn._conns.values()))[0][0] - try: - _sslcontext = first_conn.transport._ssl_protocol._sslcontext - except AttributeError: - _sslcontext = first_conn.transport._sslcontext - + assert first_conn.transport is not None + _sslcontext = first_conn.transport.get_extra_info("sslcontext") assert _sslcontext is client_ssl_ctx r.close() @@ -4416,7 +4468,7 @@ async def test_tcp_connector_socket_factory( ) with mock.patch.object( - conn._loop, + connector_module, "create_connection", autospec=True, spec_set=True, @@ -4602,3 +4654,66 @@ async def close_connector() -> None: # After close, new resolves should raise ClientConnectionError with pytest.raises(aiohttp.ClientConnectionError, match="Connector is closed"): await connector._resolve_host("localhost", 80) + + +async def test_create_connection_uses_loop_when_aiofastnet_missing() -> None: + loop = mock.Mock() + loop.create_connection = mock.AsyncMock(return_value=(mock.Mock(), mock.Mock())) + protocol_factory = mock.Mock() + sock = mock.Mock() + + with mock.patch.object(connector_module, "aiofastnet", None): + result = await connector_module.create_connection( + loop, + protocol_factory, + ssl=None, + sock=sock, + server_hostname="example.com", + ssl_shutdown_timeout=1.0, + ) + + assert result is loop.create_connection.return_value + expected_kwargs: dict[str, Any] = { + "ssl": None, + "sock": sock, + "server_hostname": "example.com", + } + if sys.version_info >= (3, 11): + expected_kwargs["ssl_shutdown_timeout"] = 1.0 + loop.create_connection.assert_awaited_once_with( + protocol_factory, + **expected_kwargs, + ) + + +async def test_start_tls_uses_loop_when_aiofastnet_missing() -> None: + loop = mock.Mock() + loop.start_tls = mock.AsyncMock(return_value=mock.Mock()) + transport = mock.Mock() + protocol = mock.Mock() + sslcontext = ssl.create_default_context() + + with mock.patch.object(connector_module, "aiofastnet", None): + result = await connector_module.start_tls( + loop, + transport, + protocol, + sslcontext, + server_hostname="example.com", + ssl_handshake_timeout=1.0, + ssl_shutdown_timeout=2.0, + ) + + assert result is loop.start_tls.return_value + expected_kwargs: dict[str, Any] = { + "server_hostname": "example.com", + "ssl_handshake_timeout": 1.0, + } + if sys.version_info >= (3, 11): + expected_kwargs["ssl_shutdown_timeout"] = 2.0 + loop.start_tls.assert_awaited_once_with( + transport, + protocol, + sslcontext, + **expected_kwargs, + ) diff --git a/tests/test_proxy.py b/tests/test_proxy.py index 6f4b80c37b1..4f7148b8593 100644 --- a/tests/test_proxy.py +++ b/tests/test_proxy.py @@ -11,6 +11,7 @@ from yarl import URL import aiohttp +from aiohttp import connector as connector_module from aiohttp.abc import AbstractStreamWriter from aiohttp.client_reqrep import ClientRequest, ClientResponse, Fingerprint from aiohttp.connector import _SSL_CONTEXT_VERIFIED @@ -81,12 +82,15 @@ async def make_conn(): "transport.get_extra_info.return_value": False, } ) - self.loop.create_connection = mock.AsyncMock( - return_value=(proto.transport, proto) - ) - conn = self.loop.run_until_complete( - connector.connect(req, None, aiohttp.ClientTimeout()) - ) + with mock.patch.object( + connector_module, + "create_connection", + autospec=True, + return_value=(proto.transport, proto), + ): + conn = self.loop.run_until_complete( + connector.connect(req, None, aiohttp.ClientTimeout()) + ) self.assertEqual(req.url, URL("http://www.python.org")) self.assertIs(conn._protocol, proto) self.assertIs(conn.transport, proto.transport) @@ -141,12 +145,15 @@ async def make_conn(): "transport.get_extra_info.return_value": False, } ) - self.loop.create_connection = mock.AsyncMock( - return_value=(proto.transport, proto) - ) - conn = self.loop.run_until_complete( - connector.connect(req, None, aiohttp.ClientTimeout()) - ) + with mock.patch.object( + connector_module, + "create_connection", + autospec=True, + return_value=(proto.transport, proto), + ): + conn = self.loop.run_until_complete( + connector.connect(req, None, aiohttp.ClientTimeout()) + ) self.assertEqual(req.url, URL("http://www.python.org")) self.assertIs(conn._protocol, proto) self.assertIs(conn.transport, proto.transport) @@ -232,17 +239,21 @@ async def make_conn(): } ] ) - connector._loop.create_connection = mock.AsyncMock( - side_effect=OSError("dont take it serious") - ) - req = ClientRequest( "GET", URL("http://www.python.org"), proxy=URL("http://proxy.example.com"), loop=self.loop, ) - with self.assertRaises(aiohttp.ClientProxyConnectionError): + with ( + mock.patch.object( + connector_module, + "create_connection", + autospec=True, + side_effect=OSError("dont take it serious"), + ), + self.assertRaises(aiohttp.ClientProxyConnectionError), + ): self.loop.run_until_complete( connector.connect(req, None, aiohttp.ClientTimeout()) ) @@ -296,21 +307,32 @@ async def make_conn(): ) tr, proto = mock.Mock(), mock.Mock() - self.loop.create_connection = mock.AsyncMock(return_value=(tr, proto)) - self.loop.start_tls = mock.AsyncMock(return_value=mock.Mock()) - req = ClientRequest( "GET", URL("https://www.python.org"), proxy=URL("http://proxy.example.com"), loop=self.loop, ) - self.loop.run_until_complete( - connector._create_connection(req, None, aiohttp.ClientTimeout()) - ) + with ( + mock.patch.object( + connector_module, + "create_connection", + autospec=True, + return_value=(tr, proto), + ), + mock.patch.object( + connector_module, + "start_tls", + autospec=True, + return_value=mock.Mock(), + ) as start_tls_mock, + ): + self.loop.run_until_complete( + connector._create_connection(req, None, aiohttp.ClientTimeout()) + ) self.assertEqual( - self.loop.start_tls.call_args.kwargs["server_hostname"], "www.python.org" + start_tls_mock.call_args.kwargs["server_hostname"], "www.python.org" ) self.loop.run_until_complete(proxy_req.close()) @@ -368,9 +390,6 @@ async def make_conn(): ) tr, proto = mock.Mock(), mock.Mock() - self.loop.create_connection = mock.AsyncMock(return_value=(tr, proto)) - self.loop.start_tls = mock.AsyncMock(return_value=mock.Mock()) - req = ClientRequest( "GET", URL("https://www.python.org"), @@ -378,12 +397,26 @@ async def make_conn(): server_hostname="server-hostname.example.com", loop=self.loop, ) - self.loop.run_until_complete( - connector._create_connection(req, None, aiohttp.ClientTimeout()) - ) + with ( + mock.patch.object( + connector_module, + "create_connection", + autospec=True, + return_value=(tr, proto), + ), + mock.patch.object( + connector_module, + "start_tls", + autospec=True, + return_value=mock.Mock(), + ) as start_tls_mock, + ): + self.loop.run_until_complete( + connector._create_connection(req, None, aiohttp.ClientTimeout()) + ) self.assertEqual( - self.loop.start_tls.call_args.kwargs["server_hostname"], + start_tls_mock.call_args.kwargs["server_hostname"], "server-hostname.example.com", ) @@ -475,20 +508,6 @@ def close(self) -> None: spec_set=True, return_value=fingerprint_mock, ), - mock.patch.object( # Called on connection to http://proxy.example.com - self.loop, - "create_connection", - autospec=True, - spec_set=True, - return_value=(mock.Mock(), mock.Mock()), - ), - mock.patch.object( # Called on connection to https://www.python.org - self.loop, - "start_tls", - autospec=True, - spec_set=True, - return_value=TransportMock(), - ), ): req = ClientRequest( "GET", @@ -496,7 +515,21 @@ def close(self) -> None: proxy=URL("http://proxy.example.com"), loop=self.loop, ) - with self.assertRaises(aiohttp.ServerFingerprintMismatch): + with ( + mock.patch.object( # Called on connection to http://proxy.example.com + connector_module, + "create_connection", + autospec=True, + return_value=(mock.Mock(), mock.Mock()), + ), + mock.patch.object( # Called on connection to https://www.python.org + connector_module, + "start_tls", + autospec=True, + return_value=TransportMock(), + ), + self.assertRaises(aiohttp.ServerFingerprintMismatch), + ): self.loop.run_until_complete( connector._create_connection( req, [], aiohttp.ClientTimeout() @@ -552,18 +585,29 @@ async def make_conn(): ) tr, proto = mock.Mock(), mock.Mock() - self.loop.create_connection = mock.AsyncMock(return_value=(tr, proto)) - self.loop.start_tls = mock.AsyncMock(return_value=mock.Mock()) - req = ClientRequest( "GET", URL("https://www.python.org"), proxy=URL("http://proxy.example.com"), loop=self.loop, ) - self.loop.run_until_complete( - connector._create_connection(req, None, aiohttp.ClientTimeout()) - ) + with ( + mock.patch.object( + connector_module, + "create_connection", + autospec=True, + return_value=(tr, proto), + ), + mock.patch.object( + connector_module, + "start_tls", + autospec=True, + return_value=mock.Mock(), + ), + ): + self.loop.run_until_complete( + connector._create_connection(req, None, aiohttp.ClientTimeout()) + ) self.assertEqual(req.url.path, "/") self.assertEqual(proxy_req.method, "CONNECT") @@ -621,20 +665,27 @@ async def make_conn(): ] ) - # Called on connection to http://proxy.example.com - self.loop.create_connection = mock.AsyncMock( - return_value=(mock.Mock(), mock.Mock()) - ) - # Called on connection to https://www.python.org - self.loop.start_tls = mock.AsyncMock(side_effect=ssl.CertificateError) - req = ClientRequest( "GET", URL("https://www.python.org"), proxy=URL("http://proxy.example.com"), loop=self.loop, ) - with self.assertRaises(aiohttp.ClientConnectorCertificateError): + with ( + mock.patch.object( # Called on connection to http://proxy.example.com + connector_module, + "create_connection", + autospec=True, + return_value=(mock.Mock(), mock.Mock()), + ), + mock.patch.object( # Called on connection to https://www.python.org + connector_module, + "start_tls", + autospec=True, + side_effect=ssl.CertificateError, + ), + self.assertRaises(aiohttp.ClientConnectorCertificateError), + ): self.loop.run_until_complete( connector._create_connection(req, None, aiohttp.ClientTimeout()) ) @@ -687,20 +738,27 @@ async def make_conn(): ] ) - # Called on connection to http://proxy.example.com - self.loop.create_connection = mock.AsyncMock( - return_value=(mock.Mock(), mock.Mock()), - ) - # Called on connection to https://www.python.org - self.loop.start_tls = mock.AsyncMock(side_effect=ssl.SSLError) - req = ClientRequest( "GET", URL("https://www.python.org"), proxy=URL("http://proxy.example.com"), loop=self.loop, ) - with self.assertRaises(aiohttp.ClientConnectorSSLError): + with ( + mock.patch.object( # Called on connection to http://proxy.example.com + connector_module, + "create_connection", + autospec=True, + return_value=(mock.Mock(), mock.Mock()), + ), + mock.patch.object( # Called on connection to https://www.python.org + connector_module, + "start_tls", + autospec=True, + side_effect=ssl.SSLError, + ), + self.assertRaises(aiohttp.ClientConnectorSSLError), + ): self.loop.run_until_complete( connector._create_connection(req, None, aiohttp.ClientTimeout()) ) @@ -757,8 +815,6 @@ async def make_conn(): tr, proto = mock.Mock(), mock.Mock() tr.get_extra_info.return_value = None - self.loop.create_connection = mock.AsyncMock(return_value=(tr, proto)) - req = ClientRequest( "GET", URL("https://www.python.org"), @@ -768,9 +824,15 @@ async def make_conn(): with self.assertRaisesRegex( aiohttp.ClientHttpProxyError, "400, message='bad request'" ): - self.loop.run_until_complete( - connector._create_connection(req, None, aiohttp.ClientTimeout()) - ) + with mock.patch.object( + connector_module, + "create_connection", + autospec=True, + return_value=(tr, proto), + ): + self.loop.run_until_complete( + connector._create_connection(req, None, aiohttp.ClientTimeout()) + ) self.loop.run_until_complete(proxy_req.close()) proxy_resp.close() @@ -826,8 +888,6 @@ async def make_conn(): tr, proto = mock.Mock(), mock.Mock() tr.get_extra_info.return_value = None - self.loop.create_connection = mock.AsyncMock(return_value=(tr, proto)) - req = ClientRequest( "GET", URL("https://www.python.org"), @@ -835,9 +895,15 @@ async def make_conn(): loop=self.loop, ) with self.assertRaisesRegex(OSError, "error message"): - self.loop.run_until_complete( - connector._create_connection(req, None, aiohttp.ClientTimeout()) - ) + with mock.patch.object( + connector_module, + "create_connection", + autospec=True, + return_value=(tr, proto), + ): + self.loop.run_until_complete( + connector._create_connection(req, None, aiohttp.ClientTimeout()) + ) @mock.patch("aiohttp.connector.ClientRequest") @mock.patch( @@ -870,17 +936,21 @@ async def make_conn(): tr, proto = mock.Mock(), mock.Mock() tr.get_extra_info.return_value = None - self.loop.create_connection = mock.AsyncMock(return_value=(tr, proto)) - req = ClientRequest( "GET", URL("http://localhost:1234/path"), proxy=URL("http://proxy.example.com"), loop=self.loop, ) - self.loop.run_until_complete( - connector._create_connection(req, None, aiohttp.ClientTimeout()) - ) + with mock.patch.object( + connector_module, + "create_connection", + autospec=True, + return_value=(tr, proto), + ): + self.loop.run_until_complete( + connector._create_connection(req, None, aiohttp.ClientTimeout()) + ) self.assertEqual(req.url, URL("http://localhost:1234/path")) def test_proxy_auth_property(self) -> None: @@ -953,20 +1023,32 @@ async def make_conn(): ) tr, proto = mock.Mock(), mock.Mock() - self.loop.create_connection = mock.AsyncMock(return_value=(tr, proto)) - self.loop.start_tls = mock.AsyncMock(return_value=mock.Mock()) - req = ClientRequest( "GET", URL("https://www.python.org"), proxy=URL("http://proxy.example.com"), loop=self.loop, ) - self.loop.run_until_complete( - connector._create_connection(req, None, aiohttp.ClientTimeout()) - ) + with ( + mock.patch.object( + connector_module, + "create_connection", + autospec=True, + return_value=(tr, proto), + ), + mock.patch.object( + connector_module, + "start_tls", + autospec=True, + return_value=mock.Mock(), + ) as start_tls_mock, + ): + self.loop.run_until_complete( + connector._create_connection(req, None, aiohttp.ClientTimeout()) + ) # ssl_shutdown_timeout=0 is not passed to start_tls - self.loop.start_tls.assert_called_with( + start_tls_mock.assert_called_with( + mock.ANY, mock.ANY, mock.ANY, _SSL_CONTEXT_VERIFIED, @@ -1034,9 +1116,6 @@ async def make_conn(): ) tr, proto = mock.Mock(), mock.Mock() - self.loop.create_connection = mock.AsyncMock(return_value=(tr, proto)) - self.loop.start_tls = mock.AsyncMock(return_value=mock.Mock()) - self.assertIn("AUTHORIZATION", proxy_req.headers) self.assertNotIn("PROXY-AUTHORIZATION", proxy_req.headers) @@ -1048,9 +1127,23 @@ async def make_conn(): ) self.assertNotIn("AUTHORIZATION", req.headers) self.assertNotIn("PROXY-AUTHORIZATION", req.headers) - self.loop.run_until_complete( - connector._create_connection(req, None, aiohttp.ClientTimeout()) - ) + with ( + mock.patch.object( + connector_module, + "create_connection", + autospec=True, + return_value=(tr, proto), + ), + mock.patch.object( + connector_module, + "start_tls", + autospec=True, + return_value=mock.Mock(), + ), + ): + self.loop.run_until_complete( + connector._create_connection(req, None, aiohttp.ClientTimeout()) + ) self.assertEqual(req.url.path, "/") self.assertNotIn("AUTHORIZATION", req.headers) diff --git a/tests/test_proxy_functional.py b/tests/test_proxy_functional.py index 33cd31872c9..55d3f6fc659 100644 --- a/tests/test_proxy_functional.py +++ b/tests/test_proxy_functional.py @@ -5,7 +5,7 @@ import ssl import sys from collections.abc import Awaitable, Callable -from contextlib import suppress +from contextlib import ExitStack, suppress from re import match as match_regex from typing import TYPE_CHECKING from unittest import mock @@ -202,6 +202,7 @@ async def test_https_proxy_unsupported_tls_in_tls( ) with ( + mock.patch.object(aiohttp.connector, "aiofastnet", None), pytest.warns( RuntimeWarning, match=expected_warning_text, @@ -231,7 +232,10 @@ async def test_https_proxy_unsupported_tls_in_tls( # Filter out the warning from # https://github.com/abhinavsingh/proxy.py/blob/30574fd0414005dfa8792a6e797023e862bdcf43/proxy/common/utils.py#L226 # otherwise this test will fail because the proxy will die with an error. +# Parameterizing on use_aiofastnet improves test coverage +@pytest.mark.parametrize("use_aiofastnet", [True, False], ids=["aiofastnet", "native"]) async def test_uvloop_secure_https_proxy( + use_aiofastnet: bool, client_ssl_ctx: ssl.SSLContext, ssl_ctx: ssl.SSLContext, secure_proxy_url: URL, @@ -252,11 +256,17 @@ async def handler(request: web.Request) -> web.Response: conn = aiohttp.TCPConnector(force_close=True) sess = aiohttp.ClientSession(connector=conn) try: - async with sess.get( - url, proxy=secure_proxy_url, ssl=client_ssl_ctx - ) as response: - assert response.status == 200 - assert await response.text() == payload + with ExitStack() as stack: + if not use_aiofastnet: + stack.enter_context( + mock.patch.object(aiohttp.connector, "aiofastnet", None) + ) + + async with sess.get( + url, proxy=secure_proxy_url, ssl=client_ssl_ctx + ) as response: + assert response.status == 200 + assert await response.text() == payload finally: await sess.close() await conn.close() diff --git a/tests/test_run_app.py b/tests/test_run_app.py index 19a5d021627..9be245d510a 100644 --- a/tests/test_run_app.py +++ b/tests/test_run_app.py @@ -24,6 +24,7 @@ ServerDisconnectedError, WSCloseCode, web, + web_runner as web_runner_module, ) from aiohttp.web_runner import BaseRunner @@ -63,6 +64,22 @@ def skip_if_on_windows(): pytest.skip("the test is not valid for Windows") +@pytest.fixture +def create_server_mock() -> Iterator[mock.AsyncMock]: + server = mock.create_autospec(asyncio.Server, spec_set=True, instance=True) + server.wait_closed.return_value = None + server.sockets = [] + + with mock.patch.object( + web_runner_module, + "create_server", + autospec=True, + spec_set=True, + return_value=server, + ) as create_server_mock: + yield create_server_mock + + @pytest.fixture def patched_loop( loop: asyncio.AbstractEventLoop, @@ -97,7 +114,9 @@ def f(*args): return f -def test_run_app_http(patched_loop) -> None: +def test_run_app_http( + patched_loop: asyncio.AbstractEventLoop, create_server_mock: mock.AsyncMock +) -> None: app = web.Application() startup_handler = mock.AsyncMock() app.on_startup.append(startup_handler) @@ -106,19 +125,35 @@ def test_run_app_http(patched_loop) -> None: web.run_app(app, print=stopper(patched_loop), loop=patched_loop) - patched_loop.create_server.assert_called_with( - mock.ANY, None, 8080, ssl=None, backlog=128, reuse_address=None, reuse_port=None + create_server_mock.assert_called_with( + patched_loop, + mock.ANY, + None, + 8080, + ssl=None, + backlog=128, + reuse_address=None, + reuse_port=None, ) startup_handler.assert_called_once_with(app) cleanup_handler.assert_called_once_with(app) -def test_run_app_close_loop(patched_loop) -> None: +def test_run_app_close_loop( + patched_loop: asyncio.AbstractEventLoop, create_server_mock: mock.AsyncMock +) -> None: app = web.Application() web.run_app(app, print=stopper(patched_loop), loop=patched_loop) - patched_loop.create_server.assert_called_with( - mock.ANY, None, 8080, ssl=None, backlog=128, reuse_address=None, reuse_port=None + create_server_mock.assert_called_with( + patched_loop, + mock.ANY, + None, + 8080, + ssl=None, + backlog=128, + reuse_address=None, + reuse_port=None, ) assert patched_loop.is_closed() @@ -154,6 +189,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: ] mock_server_single = [ mock.call( + mock.ANY, mock.ANY, "127.0.0.1", 8080, @@ -165,6 +201,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: ] mock_server_multi = [ mock.call( + mock.ANY, mock.ANY, "127.0.0.1", 8080, @@ -174,6 +211,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: reuse_port=None, ), mock.call( + mock.ANY, mock.ANY, "192.168.1.1", 8080, @@ -185,7 +223,14 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: ] mock_server_default_8989 = [ mock.call( - mock.ANY, None, 8989, ssl=None, backlog=128, reuse_address=None, reuse_port=None + mock.ANY, + mock.ANY, + None, + 8989, + ssl=None, + backlog=128, + reuse_address=None, + reuse_port=None, ) ] mock_socket = mock.Mock(getsockname=lambda: ("mock-socket", 123)) @@ -195,6 +240,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: {}, [ mock.call( + mock.ANY, mock.ANY, None, 8080, @@ -253,6 +299,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: }, [ mock.call( + mock.ANY, mock.ANY, "127.0.0.1", 8000, @@ -262,6 +309,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: reuse_port=None, ), mock.call( + mock.ANY, mock.ANY, "192.168.1.1", 8000, @@ -276,7 +324,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: ( "Only socket", {"sock": [mock_socket]}, - [mock.call(mock.ANY, ssl=None, sock=mock_socket, backlog=128)], + [mock.call(mock.ANY, mock.ANY, ssl=None, sock=mock_socket, backlog=128)], [], ), ( @@ -284,6 +332,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: {"sock": [mock_socket], "port": 8765}, [ mock.call( + mock.ANY, mock.ANY, None, 8765, @@ -292,7 +341,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: reuse_address=None, reuse_port=None, ), - mock.call(mock.ANY, sock=mock_socket, ssl=None, backlog=128), + mock.call(mock.ANY, mock.ANY, sock=mock_socket, ssl=None, backlog=128), ], [], ), @@ -301,6 +350,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: {"sock": [mock_socket], "host": "localhost"}, [ mock.call( + mock.ANY, mock.ANY, "localhost", 8080, @@ -309,7 +359,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: reuse_address=None, reuse_port=None, ), - mock.call(mock.ANY, sock=mock_socket, ssl=None, backlog=128), + mock.call(mock.ANY, mock.ANY, sock=mock_socket, ssl=None, backlog=128), ], [], ), @@ -318,6 +368,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: {"reuse_port": True}, [ mock.call( + mock.ANY, mock.ANY, None, 8080, @@ -334,6 +385,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: {"reuse_address": False}, [ mock.call( + mock.ANY, mock.ANY, None, 8080, @@ -350,6 +402,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: {"reuse_address": True, "reuse_port": True}, [ mock.call( + mock.ANY, mock.ANY, None, 8080, @@ -366,6 +419,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: {"port": 8989, "reuse_port": True}, [ mock.call( + mock.ANY, mock.ANY, None, 8989, @@ -382,6 +436,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: {"host": ("127.0.0.1", "192.168.1.1"), "reuse_port": True}, [ mock.call( + mock.ANY, mock.ANY, "127.0.0.1", 8080, @@ -391,6 +446,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: reuse_port=True, ), mock.call( + mock.ANY, mock.ANY, "192.168.1.1", 8080, @@ -411,6 +467,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: }, [ mock.call( + mock.ANY, mock.ANY, None, 8989, @@ -432,6 +489,7 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: }, [ mock.call( + mock.ANY, mock.ANY, "127.0.0.1", 8080, @@ -453,17 +511,23 @@ async def failing_ctx(_app: web.Application) -> AsyncIterator[None]: mixed_bindings_test_params, ids=mixed_bindings_test_ids, ) -def test_run_app_mixed_bindings( - run_app_kwargs, expected_server_calls, expected_unix_server_calls, patched_loop -): +def test_run_app_mixed_bindings( # type: ignore[misc] + run_app_kwargs: dict[str, Any], + expected_server_calls: list[mock._Call], + expected_unix_server_calls: list[mock._Call], + patched_loop: asyncio.AbstractEventLoop, + create_server_mock: mock.AsyncMock, +) -> None: app = web.Application() web.run_app(app, print=stopper(patched_loop), **run_app_kwargs, loop=patched_loop) - assert patched_loop.create_unix_server.mock_calls == expected_unix_server_calls - assert patched_loop.create_server.mock_calls == expected_server_calls + assert patched_loop.create_unix_server.mock_calls == expected_unix_server_calls # type: ignore[attr-defined] + assert create_server_mock.mock_calls == expected_server_calls -def test_run_app_https(patched_loop) -> None: +def test_run_app_https( + patched_loop: asyncio.AbstractEventLoop, create_server_mock: mock.AsyncMock +) -> None: app = web.Application() ssl_context = ssl.create_default_context() @@ -471,7 +535,8 @@ def test_run_app_https(patched_loop) -> None: app, ssl_context=ssl_context, print=stopper(patched_loop), loop=patched_loop ) - patched_loop.create_server.assert_called_with( + create_server_mock.assert_called_with( + patched_loop, mock.ANY, None, 8443, @@ -483,7 +548,9 @@ def test_run_app_https(patched_loop) -> None: def test_run_app_nondefault_host_port( - patched_loop: asyncio.AbstractEventLoop, unused_port_socket: socket.socket + patched_loop: asyncio.AbstractEventLoop, + unused_port_socket: socket.socket, + create_server_mock: mock.AsyncMock, ) -> None: port = unused_port_socket.getsockname()[1] host = "127.0.0.1" @@ -493,13 +560,22 @@ def test_run_app_nondefault_host_port( app, host=host, port=port, print=stopper(patched_loop), loop=patched_loop ) - patched_loop.create_server.assert_called_with( - mock.ANY, host, port, ssl=None, backlog=128, reuse_address=None, reuse_port=None + create_server_mock.assert_called_with( + patched_loop, + mock.ANY, + host, + port, + ssl=None, + backlog=128, + reuse_address=None, + reuse_port=None, ) def test_run_app_with_sock( - patched_loop: asyncio.AbstractEventLoop, unused_port_socket: socket.socket + patched_loop: asyncio.AbstractEventLoop, + unused_port_socket: socket.socket, + create_server_mock: mock.AsyncMock, ) -> None: sock = unused_port_socket app = web.Application() @@ -510,12 +586,14 @@ def test_run_app_with_sock( loop=patched_loop, ) - patched_loop.create_server.assert_called_with( # type: ignore[attr-defined] - mock.ANY, sock=sock, ssl=None, backlog=128 + create_server_mock.assert_called_with( + patched_loop, mock.ANY, sock=sock, ssl=None, backlog=128 ) -def test_run_app_multiple_hosts(patched_loop: asyncio.AbstractEventLoop) -> None: +def test_run_app_multiple_hosts( + patched_loop: asyncio.AbstractEventLoop, create_server_mock: mock.AsyncMock +) -> None: hosts = ("127.0.0.1", "127.0.0.2") app = web.Application() @@ -523,6 +601,7 @@ def test_run_app_multiple_hosts(patched_loop: asyncio.AbstractEventLoop) -> None calls = map( lambda h: mock.call( + patched_loop, mock.ANY, h, 8080, @@ -533,15 +612,24 @@ def test_run_app_multiple_hosts(patched_loop: asyncio.AbstractEventLoop) -> None ), hosts, ) - patched_loop.create_server.assert_has_calls(calls) + create_server_mock.assert_has_calls(list(calls)) -def test_run_app_custom_backlog(patched_loop) -> None: +def test_run_app_custom_backlog( + patched_loop: asyncio.AbstractEventLoop, create_server_mock: mock.AsyncMock +) -> None: app = web.Application() web.run_app(app, backlog=10, print=stopper(patched_loop), loop=patched_loop) - patched_loop.create_server.assert_called_with( - mock.ANY, None, 8080, ssl=None, backlog=10, reuse_address=None, reuse_port=None + create_server_mock.assert_called_with( + patched_loop, + mock.ANY, + None, + 8080, + ssl=None, + backlog=10, + reuse_address=None, + reuse_port=None, ) @@ -608,7 +696,11 @@ def test_run_app_abstract_linux_socket(patched_loop) -> None: ) -def test_run_app_preexisting_inet_socket(patched_loop, mocker) -> None: +def test_run_app_preexisting_inet_socket( + patched_loop, + mocker, + create_server_mock: mock.AsyncMock, +) -> None: app = web.Application() sock = socket.socket() @@ -619,14 +711,16 @@ def test_run_app_preexisting_inet_socket(patched_loop, mocker) -> None: printer = mock.Mock(wraps=stopper(patched_loop)) web.run_app(app, sock=sock, print=printer, loop=patched_loop) - patched_loop.create_server.assert_called_with( - mock.ANY, sock=sock, backlog=128, ssl=None + create_server_mock.assert_called_with( + patched_loop, mock.ANY, sock=sock, backlog=128, ssl=None ) assert f"http://127.0.0.1:{port}" in printer.call_args[0][0] @pytest.mark.skipif(not HAS_IPV6, reason="IPv6 is not available") -def test_run_app_preexisting_inet6_socket(patched_loop) -> None: +def test_run_app_preexisting_inet6_socket( + patched_loop: asyncio.AbstractEventLoop, create_server_mock: mock.AsyncMock +) -> None: app = web.Application() sock = socket.socket(socket.AF_INET6) @@ -637,14 +731,19 @@ def test_run_app_preexisting_inet6_socket(patched_loop) -> None: printer = mock.Mock(wraps=stopper(patched_loop)) web.run_app(app, sock=sock, print=printer, loop=patched_loop) - patched_loop.create_server.assert_called_with( - mock.ANY, sock=sock, backlog=128, ssl=None + create_server_mock.assert_called_with( + patched_loop, mock.ANY, sock=sock, backlog=128, ssl=None ) assert f"http://[::1]:{port}" in printer.call_args[0][0] @pytest.mark.skipif(not hasattr(socket, "AF_UNIX"), reason="requires UNIX sockets") -def test_run_app_preexisting_unix_socket(patched_loop, unix_sockname, mocker) -> None: +def test_run_app_preexisting_unix_socket( + patched_loop: asyncio.AbstractEventLoop, + unix_sockname: str, + mocker, + create_server_mock: mock.AsyncMock, +) -> None: app = web.Application() sock = socket.socket(socket.AF_UNIX) @@ -655,13 +754,15 @@ def test_run_app_preexisting_unix_socket(patched_loop, unix_sockname, mocker) -> printer = mock.Mock(wraps=stopper(patched_loop)) web.run_app(app, sock=sock, print=printer, loop=patched_loop) - patched_loop.create_server.assert_called_with( - mock.ANY, sock=sock, backlog=128, ssl=None + create_server_mock.assert_called_with( + patched_loop, mock.ANY, sock=sock, backlog=128, ssl=None ) assert f"http://unix:{unix_sockname}:" in printer.call_args[0][0] -def test_run_app_multiple_preexisting_sockets(patched_loop) -> None: +def test_run_app_multiple_preexisting_sockets( + patched_loop: asyncio.AbstractEventLoop, create_server_mock: mock.AsyncMock +) -> None: app = web.Application() sock1 = socket.socket() @@ -675,10 +776,10 @@ def test_run_app_multiple_preexisting_sockets(patched_loop) -> None: printer = mock.Mock(wraps=stopper(patched_loop)) web.run_app(app, sock=(sock1, sock2), print=printer, loop=patched_loop) - patched_loop.create_server.assert_has_calls( + create_server_mock.assert_has_calls( [ - mock.call(mock.ANY, sock=sock1, backlog=128, ssl=None), - mock.call(mock.ANY, sock=sock2, backlog=128, ssl=None), + mock.call(patched_loop, mock.ANY, sock=sock1, backlog=128, ssl=None), + mock.call(patched_loop, mock.ANY, sock=sock2, backlog=128, ssl=None), ] ) assert f"http://127.0.0.1:{port1}" in printer.call_args[0][0] @@ -727,8 +828,10 @@ def test_sigterm() -> None: assert proc.wait() == 0 -def test_startup_cleanup_signals_even_on_failure(patched_loop) -> None: - patched_loop.create_server = mock.Mock(side_effect=RuntimeError()) +def test_startup_cleanup_signals_even_on_failure( + patched_loop: asyncio.AbstractEventLoop, create_server_mock: mock.AsyncMock +) -> None: + create_server_mock.side_effect = RuntimeError() app = web.Application() startup_handler = mock.AsyncMock() @@ -743,7 +846,9 @@ def test_startup_cleanup_signals_even_on_failure(patched_loop) -> None: cleanup_handler.assert_called_once_with(app) -def test_run_app_coro(patched_loop) -> None: +def test_run_app_coro( + patched_loop: asyncio.AbstractEventLoop, create_server_mock: mock.AsyncMock +) -> None: startup_handler = cleanup_handler = None async def make_app(): @@ -757,8 +862,15 @@ async def make_app(): web.run_app(make_app(), print=stopper(patched_loop), loop=patched_loop) - patched_loop.create_server.assert_called_with( - mock.ANY, None, 8080, ssl=None, backlog=128, reuse_address=None, reuse_port=None + create_server_mock.assert_called_with( + patched_loop, + mock.ANY, + None, + 8080, + ssl=None, + backlog=128, + reuse_address=None, + reuse_port=None, ) startup_handler.assert_called_once_with(mock.ANY) cleanup_handler.assert_called_once_with(mock.ANY) @@ -871,7 +983,7 @@ async def on_startup(app): assert task.cancelled() -def test_run_app_cancels_done_tasks(patched_loop): +def test_run_app_cancels_done_tasks(patched_loop: asyncio.AbstractEventLoop) -> None: app = web.Application() task = None @@ -889,7 +1001,7 @@ async def on_startup(app): assert task.done() -def test_run_app_cancels_failed_tasks(patched_loop): +def test_run_app_cancels_failed_tasks(patched_loop: asyncio.AbstractEventLoop) -> None: app = web.Application() task = None diff --git a/tests/test_web_runner.py b/tests/test_web_runner.py index d9e16a82f39..1dda1c4eb7e 100644 --- a/tests/test_web_runner.py +++ b/tests/test_web_runner.py @@ -6,7 +6,7 @@ import pytest -from aiohttp import web +from aiohttp import web, web_runner as web_runner_module from aiohttp.test_utils import get_unused_port_socket @@ -167,16 +167,19 @@ async def test_tcpsite_default_host(make_runner: Any) -> None: site = web.TCPSite(runner) assert site.name == "http://0.0.0.0:8080" - m = mock.create_autospec(asyncio.AbstractEventLoop, spec_set=True, instance=True) - m.create_server.return_value = mock.create_autospec(asyncio.Server, spec_set=True) - with mock.patch( - "asyncio.get_event_loop", autospec=True, spec_set=True, return_value=m - ): + server = mock.create_autospec(asyncio.Server, spec_set=True) + with mock.patch.object( + web_runner_module, + "create_server", + autospec=True, + spec_set=True, + return_value=server, + ) as create_server: await site.start() - m.create_server.assert_called_once() - args, kwargs = m.create_server.call_args - assert args == (runner.server, None, 8080) + create_server.assert_called_once() + args, kwargs = create_server.call_args + assert args == (asyncio.get_running_loop(), runner.server, None, 8080) async def test_tcpsite_empty_str_host(make_runner: Any) -> None: diff --git a/tests/test_web_sendfile_functional.py b/tests/test_web_sendfile_functional.py index 5c6fd637944..00ccc056d1a 100644 --- a/tests/test_web_sendfile_functional.py +++ b/tests/test_web_sendfile_functional.py @@ -14,6 +14,7 @@ from aiohttp import web from aiohttp.compression_utils import ZLibBackend from aiohttp.pytest_plugin import AiohttpClient +from aiohttp.typedefs import PathLike from aiohttp.web_fileresponse import NOSENDFILE try: @@ -29,6 +30,11 @@ except ImportError: ssl = None # type: ignore +try: + import aiofastnet +except ImportError: + aiofastnet = None # type: ignore[assignment] + HELLO_AIOHTTP = b"Hello aiohttp! :-)\n" @@ -73,22 +79,33 @@ def sendfile(transport, fobj, offset, count): def sender(request: Any, loop: Any): sendfile_mock = None - def maker(*args, **kwargs): - ret = web.FileResponse(*args, **kwargs) - rloop = asyncio.get_running_loop() - is_patched = rloop.sendfile is sendfile_mock - assert is_patched if request.param == "no_sendfile" else not is_patched - return ret + def maker(path: PathLike, chunk_size: int = 256 * 1024) -> web.FileResponse: + if request.param == "no_sendfile": + if aiofastnet is None: + assert loop.sendfile is sendfile_mock # type: ignore[unreachable, unused-ignore] + else: + assert aiofastnet.sendfile is sendfile_mock # type: ignore[unreachable, unused-ignore] + return web.FileResponse(path, chunk_size=chunk_size) if request.param == "no_sendfile": - with mock.patch.object( - loop, - "sendfile", - autospec=True, - spec_set=True, - side_effect=NotImplementedError, - ) as sendfile_mock: - yield maker + if aiofastnet is None: + with mock.patch.object( # type: ignore[unreachable, unused-ignore] + loop, + "sendfile", + autospec=True, + spec_set=True, + side_effect=NotImplementedError, + ) as sendfile_mock: + yield maker + else: + with mock.patch.object( # type: ignore[unreachable, unused-ignore] + aiofastnet, + "sendfile", + autospec=True, + spec_set=True, + side_effect=NotImplementedError, + ) as sendfile_mock: + yield maker else: yield maker From 04027c004f1ae3f65578da855f9605c718c67fbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2026 11:16:41 +0000 Subject: [PATCH 109/137] Bump softprops/action-gh-release from 3 to 3.0.1 (#13144) 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 3 to 3.0.1.
Release notes

Sourced from softprops/action-gh-release's releases.

v3.0.1

3.0.1

  • maintenance release with updated dependencies
Changelog

Sourced from softprops/action-gh-release's changelog.

3.0.1

  • maintenance release with updated dependencies

3.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. v2.6.2 was the final Node 20-compatible release and is no longer maintained or supported.

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; freeze v2 at the final v2.6.2 release

2.6.2

2.6.2 is the final v2 release and is no longer maintained or supported. Upgrade to v3 for the supported Node 24 runtime and current fixes.

What's Changed

Other Changes πŸ”„

2.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 πŸ›

2.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.

... (truncated)

Commits
  • b430933 release: cut v3.0.0 for Node 24 upgrade (#670)
  • c2e35e0 chore(deps): bump the npm group across 1 directory with 7 updates (#783)
  • 3bb1273 release 2.6.2
  • c34030f chore: bump node to 24.14.1
  • 8975bd0 chore(deps): bump vite from 8.0.0 to 8.0.5 (#781)
  • f71937f chore(deps): bump brace-expansion from 5.0.4 to 5.0.5 (#777)
  • 3f0d239 chore(deps): bump picomatch from 4.0.3 to 4.0.4 (#775)
  • 153bb8e release 2.6.1
  • 569deb8 fix: preserve discussion category when publishing releases (#765)
  • 26e8ad2 release 2.6.0
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=softprops/action-gh-release&package-manager=github_actions&previous-version=3&new-version=3.0.1)](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 7566a16c7a7..a97d0f8ed01 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -800,7 +800,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@v3 + uses: softprops/action-gh-release@v3.0.1 with: # dist/ contains the built packages, which smoketest-artifacts/ # contains the signatures and certificates. From 5d8fa372da633573b6ebd2cfe652218ec51e2ab3 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 14 Jul 2026 15:08:52 +0100 Subject: [PATCH 110/137] Fix Python parser not rejecting LF early (#13136) (#13147) (cherry picked from commit cba121d5864966d95b48f4a790ff0cefad7f041f) --- CHANGES/13136.bugfix.rst | 1 + THREAT_MODEL.md | 11 +++++++++-- aiohttp/http_parser.py | 17 +++++++++++++++++ tests/test_http_parser.py | 37 ++++++++++++++++++++++++++++++++++++- 4 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 CHANGES/13136.bugfix.rst diff --git a/CHANGES/13136.bugfix.rst b/CHANGES/13136.bugfix.rst new file mode 100644 index 00000000000..55fdd4228ca --- /dev/null +++ b/CHANGES/13136.bugfix.rst @@ -0,0 +1 @@ +Fixed Python parser not rejecting a bare ``LF`` in the request line -- by :user:`Dreamsorcerer`. diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index f7b0aed090a..422cb7a2993 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -267,8 +267,8 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and | 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 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.11 | Parser error reflection | `http_parser.py` truncates to `[:100]` only for `LineTooLong`; `BadStatusLine` / `InvalidHeader` / `TransferEncodingError` carry the offending line up to `max_line_size` / `max_field_size`. | **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*`), bare-LF line endings (`test_reject_bare_lf_no_cross_request_leak`). | 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.** | @@ -324,6 +324,13 @@ into `StreamReader`) is then handed to `web_protocol.RequestHandler` and could grow without bound. Memory was previously bounded only transitively by `read_bufsize` ([Β§5.7](#57-server-connection-lifecycle) threat 7.4); the fix adds an explicit pipeline-count cap. +- **PR #13136** (3.14.2) β€” the pure-Python request parser buffered a + bare `LF` line ending (where `CRLF` is required) across reads instead of + rejecting it, so bytes from a following request on the same connection + could be concatenated into the failed parse and disclosed in the error. + Now rejected at the point it is seen, matching llhttp (request line, + headers, chunk-size, trailers). Not a vulnerability because no proxy would + pipeline requests from multiple clients and forward raw LFs to the backend. These are all currently in place; this section assumes no regression. diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index e08b4c11aca..0c9fd086b3b 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -527,6 +527,11 @@ def get_content_length() -> int | None: should_close = msg.should_close else: self._tail = data[start_pos:] + # A bare LF here means CRLF was required: + # reject instead of buffering, else a following request's + # bytes get appended to this line and leak in the error. + if b"\n" in self._tail: + raise BadHttpMessage("Bad line ending, expected CRLF") if len(self._tail) > self.max_line_size: raise LineTooLong(self._tail[:100] + b"...", self.max_line_size) data = EMPTY @@ -1016,6 +1021,12 @@ def feed_data( self._chunk_size = size self.payload.begin_http_chunk_receiving() else: + if b"\n" in chunk: + exc = TransferEncodingError( + "Bad chunk-size line ending, expected CRLF" + ) + set_exception(self.payload, exc) + raise exc self._chunk_tail = chunk return PayloadState.PAYLOAD_NEEDS_INPUT, b"" @@ -1062,6 +1073,12 @@ def feed_data( if self._chunk == ChunkState.PARSE_TRAILERS: pos = chunk.find(SEP) if pos < 0: # No line found + if b"\n" in chunk: + exc = TransferEncodingError( + "Bad trailer line ending, expected CRLF" + ) + set_exception(self.payload, exc) + raise exc self._chunk_tail = chunk return PayloadState.PAYLOAD_NEEDS_INPUT, b"" diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 407292079d1..c9e9ca51407 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -323,7 +323,42 @@ def test_invalid_linebreak( parser.feed_data(text) -def test_cve_2023_37276(parser: Any) -> None: +def test_partial_request_split_still_parses(parser: HttpRequestParser) -> None: + """A legitimate request split mid-line must still stream.""" + messages, _, _ = parser.feed_data(b"GET /split HTTP/1.1\r\nHo") + assert not messages + messages, _, _ = parser.feed_data(b"st: a\r\n\r\n") + assert len(messages) == 1 + assert messages[0][0].path == "/split" + + +@pytest.mark.parametrize( + "attacker", + ( + pytest.param( + b"GET /attacker HTTP/1.1\nHost: a\nX-Foo: bar\n\n", id="request_line" + ), + pytest.param( + b"POST /attacker HTTP/1.1\r\nHost: a\r\n" + b"Transfer-Encoding: chunked\r\n\r\n5\n", + id="chunk_size", + ), + pytest.param( + b"POST /attacker HTTP/1.1\r\nHost: a\r\n" + b"Transfer-Encoding: chunked\r\n\r\n0\r\nX-Trailer: bad\n", + id="trailer", + ), + ), +) +def test_reject_bare_lf_at_point_seen( + parser: HttpRequestParser, attacker: bytes +) -> None: + """A bare LF where CRLF is required is rejected at the point it's seen.""" + with pytest.raises(http_exceptions.BadHttpMessage): + parser.feed_data(attacker) + + +def test_cve_2023_37276(parser: HttpRequestParser) -> None: text = b"""POST / HTTP/1.1\r\nHost: localhost:8080\r\nX-Abc: \rxTransfer-Encoding: chunked\r\n\r\n""" with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(text) From c620e623db233f245ab9ce6990ed7ac014ac6d13 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Tue, 14 Jul 2026 15:09:01 +0100 Subject: [PATCH 111/137] Fix Python parser not rejecting LF early (#13136) (#13148) (cherry picked from commit cba121d5864966d95b48f4a790ff0cefad7f041f) --- CHANGES/13136.bugfix.rst | 1 + aiohttp/http_parser.py | 17 +++++++++++++++++ tests/test_http_parser.py | 37 ++++++++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 CHANGES/13136.bugfix.rst diff --git a/CHANGES/13136.bugfix.rst b/CHANGES/13136.bugfix.rst new file mode 100644 index 00000000000..55fdd4228ca --- /dev/null +++ b/CHANGES/13136.bugfix.rst @@ -0,0 +1 @@ +Fixed Python parser not rejecting a bare ``LF`` in the request line -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index e08b4c11aca..0c9fd086b3b 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -527,6 +527,11 @@ def get_content_length() -> int | None: should_close = msg.should_close else: self._tail = data[start_pos:] + # A bare LF here means CRLF was required: + # reject instead of buffering, else a following request's + # bytes get appended to this line and leak in the error. + if b"\n" in self._tail: + raise BadHttpMessage("Bad line ending, expected CRLF") if len(self._tail) > self.max_line_size: raise LineTooLong(self._tail[:100] + b"...", self.max_line_size) data = EMPTY @@ -1016,6 +1021,12 @@ def feed_data( self._chunk_size = size self.payload.begin_http_chunk_receiving() else: + if b"\n" in chunk: + exc = TransferEncodingError( + "Bad chunk-size line ending, expected CRLF" + ) + set_exception(self.payload, exc) + raise exc self._chunk_tail = chunk return PayloadState.PAYLOAD_NEEDS_INPUT, b"" @@ -1062,6 +1073,12 @@ def feed_data( if self._chunk == ChunkState.PARSE_TRAILERS: pos = chunk.find(SEP) if pos < 0: # No line found + if b"\n" in chunk: + exc = TransferEncodingError( + "Bad trailer line ending, expected CRLF" + ) + set_exception(self.payload, exc) + raise exc self._chunk_tail = chunk return PayloadState.PAYLOAD_NEEDS_INPUT, b"" diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index b3e0ab67cff..f1ef31f1df6 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -323,7 +323,42 @@ def test_invalid_linebreak( parser.feed_data(text) -def test_cve_2023_37276(parser: Any) -> None: +def test_partial_request_split_still_parses(parser: HttpRequestParser) -> None: + """A legitimate request split mid-line must still stream.""" + messages, _, _ = parser.feed_data(b"GET /split HTTP/1.1\r\nHo") + assert not messages + messages, _, _ = parser.feed_data(b"st: a\r\n\r\n") + assert len(messages) == 1 + assert messages[0][0].path == "/split" + + +@pytest.mark.parametrize( + "attacker", + ( + pytest.param( + b"GET /attacker HTTP/1.1\nHost: a\nX-Foo: bar\n\n", id="request_line" + ), + pytest.param( + b"POST /attacker HTTP/1.1\r\nHost: a\r\n" + b"Transfer-Encoding: chunked\r\n\r\n5\n", + id="chunk_size", + ), + pytest.param( + b"POST /attacker HTTP/1.1\r\nHost: a\r\n" + b"Transfer-Encoding: chunked\r\n\r\n0\r\nX-Trailer: bad\n", + id="trailer", + ), + ), +) +def test_reject_bare_lf_at_point_seen( + parser: HttpRequestParser, attacker: bytes +) -> None: + """A bare LF where CRLF is required is rejected at the point it's seen.""" + with pytest.raises(http_exceptions.BadHttpMessage): + parser.feed_data(attacker) + + +def test_cve_2023_37276(parser: HttpRequestParser) -> None: text = b"""POST / HTTP/1.1\r\nHost: localhost:8080\r\nX-Abc: \rxTransfer-Encoding: chunked\r\n\r\n""" with pytest.raises(http_exceptions.BadHttpMessage): parser.feed_data(text) From e79e0da4c74a55cc3c6abc234b22bd8e5145ee2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:13:26 +0000 Subject: [PATCH 112/137] Bump actions/setup-node from 6 to 7 (#13156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7.
Release notes

Sourced from actions/setup-node's releases.

v7.0.0

What's Changed

Enhancements:

Bug fixes:

Documentation updates:

Dependency update:

New Contributors

Full Changelog: https://github.com/actions/setup-node/compare/v6...v7.0.0

v6.5.0

What's Changed

Full Changelog: https://github.com/actions/setup-node/compare/v6.4.0...v6.5.0

v6.4.0

What's Changed

Dependency updates:

New Contributors

Full Changelog: https://github.com/actions/setup-node/compare/v6...v6.4.0

v6.3.0

What's Changed

Enhancements:

... (truncated)

Commits
  • 8207627 Migrate to ESM and upgrade dependencies (#1574)
  • 04be95c Add cache-primary-key and cache-matched-key as outputs (#1577)
  • 7c2c68d docs: Update caching recommendations to mitigate cache poisoning risks (#1567)
  • 6a61c03 Merge pull request #1569 from jasongin/update-actions-cache-5.1.0
  • 30eb73b Resolve high-severity audit issues
  • 4e1a87a Update dist
  • 360237f Strict equality
  • 4f8aac5 Bump @​actions/cache to 5.1.0, log cache write denied
  • f4a67bb Only use mirrorToken in getManifest if it's provided (#1548)
  • 0355742 Remove dummy NODE_AUTH_TOKEN export (#1558)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-node&package-manager=github_actions&previous-version=6&new-version=7)](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 a97d0f8ed01..7d64de5c670 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -124,7 +124,7 @@ jobs: path: vendor/llhttp/build - name: Setup NodeJS if: steps.cache.outputs.cache-hit != 'true' - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 18 - name: Generate llhttp sources From 8069270a4cf9e739589e4b9a4196409f1251e1f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 11:25:07 +0000 Subject: [PATCH 113/137] Bump coverage from 7.15.0 to 7.15.1 (#13158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.15.0 to 7.15.1.
Release notes

Sourced from coverage's releases.

7.15.1

Version 7.15.1 β€” 2026-07-12

  • Fix: in the HTML report with show_contexts enabled, a context label containing </script> (for example a parametrized pytest node id) could close the inline <script> element in a file page early, injecting markup. Context labels are now fully escaped. Thanks, Rajath Mohare.
  • A number of performance improvements thanks to Paul Kehrer, in pull requests 2213, 2214, 2215, 2216, 2218, 2220, and 2221.

:arrow_right:Β  PyPI page: coverage 7.15.1. :arrow_right:Β  To install: python3 -m pip install coverage==7.15.1

Changelog

Sourced from coverage's changelog.

Version 7.15.1 β€” 2026-07-12

  • Fix: in the HTML report with show_contexts enabled, a context label containing </script> (for example a parametrized pytest node id) could close the inline <script> element in a file page early, injecting markup. Context labels are now fully escaped. Thanks, Rajath Mohare <pull 2224_>_.

  • A number of performance improvements thanks to Paul Kehrer, in pull requests 2213 <pull 2213_>, 2214 <pull 2214_>, 2215 <pull 2215_>, 2216 <pull 2216_>, 2218 <pull 2218_>, 2220 <pull 2220_>, and 2221 <pull 2221_>_.

.. _pull 2213: coveragepy/coveragepy#2213 .. _pull 2214: coveragepy/coveragepy#2214 .. _pull 2215: coveragepy/coveragepy#2215 .. _pull 2216: coveragepy/coveragepy#2216 .. _pull 2218: coveragepy/coveragepy#2218 .. _pull 2220: coveragepy/coveragepy#2220 .. _pull 2221: coveragepy/coveragepy#2221 .. _pull 2224: coveragepy/coveragepy#2224

.. _changes_7-15-0:

Commits
  • da63bed docs: sample HTML for 7.15.1
  • bc35e64 docs: prep for 7.15.1
  • 182b010 perf: resolve sysmon branch events lazily, one pair at a time (#2221)
  • ee271ee perf: compute multiline maps cheaply in the sysmon core (#2220)
  • 1441b96 chore: bump the action-dependencies group with 6 updates (#2225)
  • dd80635 fix: escape context labels in html report inline script block (#2224)
  • 7c3ae71 docs: normalize earlier history order (#2222)
  • 088dc60 chore: make upgrade
  • f7e15a8 test: upload .json and .xml coverage reports
  • 3b62112 docs: thanks, Paul Kehrer
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coverage&package-manager=pip&previous-version=7.15.0&new-version=7.15.1)](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-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 09f245d8abe..cae1f051326 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -59,7 +59,7 @@ click==8.4.2 # pip-tools # towncrier # wait-for-it -coverage==7.15.0 +coverage==7.15.1 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/dev.txt b/requirements/dev.txt index 8d1f05af83a..cec89340be6 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -59,7 +59,7 @@ click==8.4.2 # pip-tools # towncrier # wait-for-it -coverage==7.15.0 +coverage==7.15.1 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index ba6ce4e1573..d2e66067bbe 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -6,7 +6,7 @@ # click==8.4.2 # via wait-for-it -coverage==7.15.0 +coverage==7.15.1 # via pytest-cov exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 2d75098ef7e..169b0a411c6 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -14,7 +14,7 @@ cffi==2.1.0 # via cryptography click==8.4.2 # via wait-for-it -coverage==7.15.0 +coverage==7.15.1 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index ad3b32059b7..92208b78ccb 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -32,7 +32,7 @@ cffi==2.1.0 # pycares click==8.4.2 # via wait-for-it -coverage==7.15.0 +coverage==7.15.1 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index f8b30954946..c04b3f9398a 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -28,7 +28,7 @@ cffi==2.1.0 ; sys_platform != "android" and sys_platform != "ios" # pycares click==8.4.2 # via wait-for-it -coverage==7.15.0 +coverage==7.15.1 # via pytest-cov exceptiongroup==1.3.1 # via diff --git a/requirements/test.txt b/requirements/test.txt index e2026736651..869ecc64290 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -32,7 +32,7 @@ cffi==2.1.0 # pycares click==8.4.2 # via wait-for-it -coverage==7.15.0 +coverage==7.15.1 # via # -r requirements/test-common.in # pytest-cov From c727b9c4e1bc4e039a1169060dc44c841f7f8913 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:05:10 +0000 Subject: [PATCH 114/137] Bump github/codeql-action from 4 to 4.37.0 (#13163) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4 to 4.37.0.
Release notes

Sourced from github/codeql-action's releases.

v4.37.0

  • Update default CodeQL bundle version to 2.26.0. #3995
  • In addition to the existing input format, the config-file input for the codeql-action/init step will soon support a new [owner/]repo[@ref][:path] format. All components except the repository name are optional. If omitted, owner defaults to the same owner as the repository the analysis is running for, ref to main, and path to .github/codeql-action.yaml. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. #3973

v4.36.3

No user facing changes.

v4.36.2

  • Cache CodeQL CLI version information across Actions steps. #3943
  • Reduce requests while waiting for analysis processing by using exponential backoff when polling SARIF processing status. #3937
  • Update default CodeQL bundle version to 2.25.6. #3948

v4.36.1

No user facing changes.

v4.36.0

  • Breaking change: Bump the minimum required CodeQL bundle version to 2.19.4. #3894
  • Add support for SHA-256 Git object IDs. #3893
  • Update default CodeQL bundle version to 2.25.5. #3926

v4.35.5

  • We have improved how the JavaScript bundles for the CodeQL Action are generated to avoid duplication across bundles and reduce the size of the repository by around 70%. This should have no effect on the runtime behaviour of the CodeQL Action. #3899
  • For performance and accuracy reasons, improved incremental analysis will now only be enabled on a pull request when diff-informed analysis is also enabled for that run. If diff-informed analysis is unavailable (for example, because the PR diff ranges could not be computed), the action will fall back to a full analysis. #3791
  • If multiple inputs are provided for the GitHub-internal analysis-kinds input, only code-scanning will be enabled. The analysis-kinds input is experimental, for GitHub-internal use only, and may change without notice at any time. #3892
  • Added an experimental change which, when running a Code Scanning analysis for a PR with improved incremental analysis enabled, prefers CodeQL CLI versions that have a cached overlay-base database for the configured languages. This speeds up analysis for a repository when there is not yet a cached overlay-base database for the latest CLI version. We expect to roll this change out to everyone in May. #3880

v4.35.4

  • Update default CodeQL bundle version to 2.25.4. #3881

v4.35.3

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.19.3 and earlier. These versions of CodeQL were discontinued on 9 April 2026 alongside GitHub Enterprise Server 3.15, and will be unsupported by the next minor release of the CodeQL Action. #3837
  • Configurations for private registries that use Cloudsmith or GCP OIDC are now accepted. #3850
  • Best-effort connection tests for private registries now use GET requests instead of HEAD for better compatibility with various registry implementations. For NuGet feeds, the test is now always performed against the service index. #3853
  • Fixed a bug where two diagnostics produced within the same millisecond could overwrite each other on disk, causing one of them to be lost. #3852
  • Update default CodeQL bundle version to 2.25.3. #3865

v4.35.2

  • The undocumented TRAP cache cleanup feature that could be enabled using the CODEQL_ACTION_CLEANUP_TRAP_CACHES environment variable is deprecated and will be removed in May 2026. If you are affected by this, we recommend disabling TRAP caching by passing the trap-caching: false input to the init Action. #3795
  • The Git version 2.36.0 requirement for improved incremental analysis now only applies to repositories that contain submodules. #3789
  • Python analysis on GHES no longer extracts the standard library, relying instead on models of the standard library. This should result in significantly faster extraction and analysis times, while the effect on alerts should be minimal. #3794
  • Fixed a bug in the validation of OIDC configurations for private registries that was added in CodeQL Action 4.33.0 / 3.33.0. #3807
  • Update default CodeQL bundle version to 2.25.2. #3823

v4.35.1

v4.35.0

... (truncated)

Changelog

Sourced from github/codeql-action's changelog.

4.37.0 - 08 Jul 2026

  • Update default CodeQL bundle version to 2.26.0. #3995
  • In addition to the existing input format, the config-file input for the codeql-action/init step will soon support a new [owner/]repo[@ref][:path] format. All components except the repository name are optional. If omitted, owner defaults to the same owner as the repository the analysis is running for, ref to main, and path to .github/codeql-action.yaml. Support for this format ships in this version of the CodeQL Action, but will only be enabled over the coming weeks. #3973

4.36.3 - 01 Jul 2026

No user facing changes.

Commits
  • 99df26d Merge pull request #3996 from github/update-v4.37.0-c7c896d71
  • 31c2707 Add changenote for #3973
  • 72df218 Update changelog for v4.37.0
  • c7c896d Merge pull request #3995 from github/update-bundle/codeql-bundle-v2.26.0
  • 3f34ff0 Add changelog note
  • 43bec09 Update default bundle to codeql-bundle-v2.26.0
  • f58f0d1 Merge pull request #3973 from github/mbg/repo-props/config-file-shorthands
  • 7dc37cb Merge remote-tracking branch 'origin/main' into mbg/repo-props/config-file-sh...
  • 8e22350 Thread ActionState to initConfig
  • 69c9e8c Mark some status-report imports as type-only to avoid circular dependencies
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action&package-manager=github_actions&previous-version=4&new-version=4.37.0)](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/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 445a2efd906..bb4c76fb366 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,17 +29,17 @@ jobs: uses: actions/checkout@v7 - name: Initialize CodeQL - uses: github/codeql-action/init@v4 + uses: github/codeql-action/init@v4.37.0 with: languages: ${{ matrix.language }} config-file: ./.github/codeql.yml queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v4 + uses: github/codeql-action/autobuild@v4.37.0 if: ${{ matrix.language == 'python' || matrix.language == 'javascript' }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4 + uses: github/codeql-action/analyze@v4.37.0 with: category: "/language:${{ matrix.language }}" From 3d93fae13b4dc20426e00a90d38370febfe94191 Mon Sep 17 00:00:00 2001 From: arshsmith Date: Sat, 18 Jul 2026 18:03:04 +0530 Subject: [PATCH 115/137] [3.15] keep websocket compression state across interleaved control frames (#13139) --- CHANGES/12988.bugfix.rst | 1 + CONTRIBUTORS.txt | 1 + aiohttp/_websocket/reader_py.py | 6 +++++- tests/test_websocket_parser.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12988.bugfix.rst diff --git a/CHANGES/12988.bugfix.rst b/CHANGES/12988.bugfix.rst new file mode 100644 index 00000000000..ff07271fa31 --- /dev/null +++ b/CHANGES/12988.bugfix.rst @@ -0,0 +1 @@ +Fixed control frames breaking fragmented WebSocket messages -- by :user:`arshsmith1`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index da471b46715..7efbcc10425 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -52,6 +52,7 @@ Anton Kasyanov Anton Zhdan-Pushkin Arcadiy Ivanov Arseny Timoniq +Arsh Smith Arshiya Tabasum Artem Yushkovskiy Arthur Darcet diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index 6a9f20dbcc2..10ebd19c485 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -407,7 +407,11 @@ def _feed_data(self, data: bytes) -> None: "Received frame with non-zero reserved bits", ) - self._frame_fin = bool(fin) + # Control frames (opcode > 0x7) may be interleaved between the + # fragments of a data message. + # https://datatracker.ietf.org/doc/html/rfc6455#section-5.4 + if opcode <= 0x7: + self._frame_fin = bool(fin) self._frame_opcode = opcode self._has_mask = bool(has_mask) self._payload_len_flag = length diff --git a/tests/test_websocket_parser.py b/tests/test_websocket_parser.py index bd3f9030226..ead5051f9bb 100644 --- a/tests/test_websocket_parser.py +++ b/tests/test_websocket_parser.py @@ -612,6 +612,34 @@ def test_parse_compress_frame_multi(parser: PatchableWebSocketReader) -> None: assert (1, 1, b"1234", False) == (fin, opcode, payload, not not compress) +def test_compressed_continuation_with_ping( + out: WebSocketDataQueue, parser: PatchableWebSocketReader +) -> None: + # A control frame may be interleaved between the fragments of a data + # message. The continuation must still be decompressed. + # https://datatracker.ietf.org/doc/html/rfc6455#section-5.4 + message = b"hello compressed world " * 4 + compressobj = ZLibBackend.compressobj(wbits=-9) + compressed = compressobj.compress(message) + compressed += compressobj.flush(ZLibBackend.Z_SYNC_FLUSH) + assert compressed.endswith(WS_DEFLATE_TRAILING) + compressed = compressed[:-4] + half = len(compressed) // 2 + + # first fragment: compressed binary, RSV1 set, not final + parser.feed_data(PACK_LEN1(0x40 | WSMsgType.BINARY, half) + compressed[:half]) + # interleaved ping + parser.feed_data(PACK_LEN1(0x80 | WSMsgType.PING, 0)) + # final continuation fragment + parser.feed_data( + PACK_LEN1(0x80 | WSMsgType.CONTINUATION, len(compressed) - half) + + compressed[half:] + ) + + assert out._buffer[0] == (WSMessage(WSMsgType.PING, b"", ""), 0) + assert out._buffer[1] == (WSMessage(WSMsgType.BINARY, message, ""), len(message)) + + def test_parse_compress_error_frame(parser: PatchableWebSocketReader) -> None: parser.parse_frame(struct.pack("!BB", 0b01000001, 0b00000001)) parser.parse_frame(b"1") From ea2b4aea7c0775c47546bf2486c17445d82093e0 Mon Sep 17 00:00:00 2001 From: arshsmith Date: Sat, 18 Jul 2026 18:03:55 +0530 Subject: [PATCH 116/137] [3.14] keep websocket compression state across interleaved control frames (#13138) --- CHANGES/12988.bugfix.rst | 1 + CONTRIBUTORS.txt | 1 + aiohttp/_websocket/reader_py.py | 6 +++++- tests/test_websocket_parser.py | 28 ++++++++++++++++++++++++++++ 4 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 CHANGES/12988.bugfix.rst diff --git a/CHANGES/12988.bugfix.rst b/CHANGES/12988.bugfix.rst new file mode 100644 index 00000000000..ff07271fa31 --- /dev/null +++ b/CHANGES/12988.bugfix.rst @@ -0,0 +1 @@ +Fixed control frames breaking fragmented WebSocket messages -- by :user:`arshsmith1`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index ffc5f66bffe..3e3893856cc 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -52,6 +52,7 @@ Anton Kasyanov Anton Zhdan-Pushkin Arcadiy Ivanov Arseny Timoniq +Arsh Smith Arshiya Tabasum Artem Yushkovskiy Arthur Darcet diff --git a/aiohttp/_websocket/reader_py.py b/aiohttp/_websocket/reader_py.py index 8c6a1e1148a..ff5a5b362c0 100644 --- a/aiohttp/_websocket/reader_py.py +++ b/aiohttp/_websocket/reader_py.py @@ -404,7 +404,11 @@ def _feed_data(self, data: bytes) -> None: "Received frame with non-zero reserved bits", ) - self._frame_fin = bool(fin) + # Control frames (opcode > 0x7) may be interleaved between the + # fragments of a data message. + # https://datatracker.ietf.org/doc/html/rfc6455#section-5.4 + if opcode <= 0x7: + self._frame_fin = bool(fin) self._frame_opcode = opcode self._has_mask = bool(has_mask) self._payload_len_flag = length diff --git a/tests/test_websocket_parser.py b/tests/test_websocket_parser.py index 99f57dd5aed..81f59172b73 100644 --- a/tests/test_websocket_parser.py +++ b/tests/test_websocket_parser.py @@ -600,6 +600,34 @@ def test_parse_compress_frame_multi(parser: PatchableWebSocketReader) -> None: assert (1, 1, b"1234", False) == (fin, opcode, payload, not not compress) +def test_compressed_continuation_with_ping( + out: WebSocketDataQueue, parser: PatchableWebSocketReader +) -> None: + # A control frame may be interleaved between the fragments of a data + # message. The continuation must still be decompressed. + # https://datatracker.ietf.org/doc/html/rfc6455#section-5.4 + message = b"hello compressed world " * 4 + compressobj = ZLibBackend.compressobj(wbits=-9) + compressed = compressobj.compress(message) + compressed += compressobj.flush(ZLibBackend.Z_SYNC_FLUSH) + assert compressed.endswith(WS_DEFLATE_TRAILING) + compressed = compressed[:-4] + half = len(compressed) // 2 + + # first fragment: compressed binary, RSV1 set, not final + parser.feed_data(PACK_LEN1(0x40 | WSMsgType.BINARY, half) + compressed[:half]) + # interleaved ping + parser.feed_data(PACK_LEN1(0x80 | WSMsgType.PING, 0)) + # final continuation fragment + parser.feed_data( + PACK_LEN1(0x80 | WSMsgType.CONTINUATION, len(compressed) - half) + + compressed[half:] + ) + + assert out._buffer[0] == (WSMessage(WSMsgType.PING, b"", ""), 0) + assert out._buffer[1] == (WSMessage(WSMsgType.BINARY, message, ""), len(message)) + + def test_parse_compress_error_frame(parser: PatchableWebSocketReader) -> None: parser.parse_frame(struct.pack("!BB", 0b01000001, 0b00000001)) parser.parse_frame(b"1") From 498940406cd56463f54b20e0d97591cd4ef74184 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:41:22 +0000 Subject: [PATCH 117/137] Bump virtualenv from 21.6.0 to 21.6.1 (#13146) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [virtualenv](https://github.com/pypa/virtualenv) from 21.6.0 to 21.6.1.
Release notes

Sourced from virtualenv's releases.

21.6.1

What's Changed

Full Changelog: https://github.com/pypa/virtualenv/compare/21.6.0...21.6.1

Changelog

Sourced from virtualenv's changelog.

Bugfixes - 21.6.1

  • Harden the fish activator prompt against user functions that shadow builtins. Routing functions, printf, string, echo and source/. through builtin stops a shadowing function (such as a dot-style directory navigator that redefines .) from hijacking the prompt and dropping the previous command's exit status, matching the CPython fix in [gh-140006](https://github.com/pypa/virtualenv/issues/140006) <https://github.com/python/cpython/issues/140006>_. (:issue:3185)

v21.6.0 (2026-07-06)


Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=virtualenv&package-manager=pip&previous-version=21.6.0&new-version=21.6.1)](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 | 6 +----- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/requirements/constraints.txt b/requirements/constraints.txt index cae1f051326..31c29e2b1f9 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -312,7 +312,7 @@ uvloop==0.22.1 ; platform_system != "Windows" # -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.6.0 +virtualenv==21.6.1 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common-base.in diff --git a/requirements/dev.txt b/requirements/dev.txt index cec89340be6..d1c77329add 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -302,7 +302,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.6.0 +virtualenv==21.6.1 # via pre-commit wait-for-it==2.3.0 # via -r requirements/test-common-base.in diff --git a/requirements/lint.txt b/requirements/lint.txt index a0cf5a5ff70..72aefded7ca 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -8,10 +8,6 @@ aiodns==4.0.4 # via -r requirements/lint.in aiofastnet==0.19.0 # via -r requirements/lint.in -aiohappyeyeballs==2.7.1 - # via aiohttp -aiosignal==1.4.0 - # via aiohttp annotated-types==0.7.0 # via pydantic ast-serialize==0.6.0 @@ -134,7 +130,7 @@ uvloop==0.22.1 ; platform_system != "Windows" # via -r requirements/lint.in valkey==6.1.1 # via -r requirements/lint.in -virtualenv==21.6.0 +virtualenv==21.6.1 # via pre-commit zlib-ng==1.0.0 # via -r requirements/lint.in From 4aaef755ba3deab4b87e6aca8bf6fd01fb33b075 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 15:02:28 +0100 Subject: [PATCH 118/137] Bump softprops/action-gh-release from 3.0.1 to 3.0.2 (#13157) 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 3.0.1 to 3.0.2.
Release notes

Sourced from softprops/action-gh-release's releases.

v3.0.2

3.0.2 is a patch release focused on release reliability and compatibility. It reuses existing draft releases when publishing prereleases, supports replacing release assets on Gitea, hardens streamed asset uploads, and provides clearer release-creation diagnostics. It also includes TypeScript, coverage, and tooling maintenance merged since 3.0.1.

This release fixes #795, #438, and #803. The upload transport hardening covers the historical failure reported in #790, although current hosted Node 24 runners did not reproduce it naturally. The diagnostics work is related to #786 and does not claim a reproducible release-creation fix.

What's Changed

Exciting New Features πŸŽ‰

Bug fixes πŸ›

Other Changes πŸ”„

Changelog

Sourced from softprops/action-gh-release's changelog.

3.0.2

3.0.2 is a patch release focused on release reliability and compatibility. It reuses existing draft releases when publishing prereleases, supports replacing release assets on Gitea, hardens streamed asset uploads, and provides clearer release-creation diagnostics. It also includes TypeScript, coverage, and tooling maintenance merged since 3.0.1.

This release fixes #795, #438, and #803. The upload transport hardening covers the historical failure reported in #790, although current hosted Node 24 runners did not reproduce it naturally. The diagnostics work is related to #786 and does not claim a reproducible release-creation fix.

What's Changed

Exciting New Features πŸŽ‰

Bug fixes πŸ›

Other Changes πŸ”„

Commits
  • 3d0d988 release 3.0.2 (#818)
  • 7e13ed4 fix: clarify release creation 404 errors (#817)
  • e6c70a5 fix: replace existing release assets on Gitea (#816)
  • f345337 fix: publish existing draft releases as prereleases (#801)
  • d8a89a2 fix: upload small checksum assets reliably (#815)
  • 45ece40 chore(deps): remove unused TypeScript tooling (#814)
  • f6b913c feat: improve release error reporting and test coverage (#813)
  • 15f193d chore(deps): upgrade TypeScript to 7 (#812)
  • cc8268d chore(deps): bump actions/checkout in the github-actions group (#810)
  • fd0ed1e chore(deps): bump the npm group with 3 updates (#811)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=softprops/action-gh-release&package-manager=github_actions&previous-version=3.0.1&new-version=3.0.2)](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 7d64de5c670..dcdd2fccd73 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -800,7 +800,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@v3.0.1 + uses: softprops/action-gh-release@v3.0.2 with: # dist/ contains the built packages, which smoketest-artifacts/ # contains the signatures and certificates. From 5b3261135838ca3fc2405d39c340d17177791054 Mon Sep 17 00:00:00 2001 From: Rodrigo Nogueira Date: Sat, 18 Jul 2026 11:06:04 -0300 Subject: [PATCH 119/137] [PR #13122/bc3b5cf2 backport][3.15] Add the ssl parameter to ClientSession (#13166) --- CHANGES/13006.feature.rst | 4 ++ CHANGES/13122.feature.rst | 1 + aiohttp/client.py | 29 ++++++++++-- docs/client_reference.rst | 27 +++++++++++ tests/test_client_session.py | 92 ++++++++++++++++++++++++++++++++++++ 5 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 CHANGES/13006.feature.rst create mode 120000 CHANGES/13122.feature.rst diff --git a/CHANGES/13006.feature.rst b/CHANGES/13006.feature.rst new file mode 100644 index 00000000000..19496bd98d0 --- /dev/null +++ b/CHANGES/13006.feature.rst @@ -0,0 +1,4 @@ +Added the ``ssl`` parameter to :class:`~aiohttp.ClientSession`, setting the +default SSL validation mode for every request made through the session; a +per-request ``ssl`` argument still takes precedence -- by +:user:`rodrigobnogueira`. diff --git a/CHANGES/13122.feature.rst b/CHANGES/13122.feature.rst new file mode 120000 index 00000000000..3fb8272861c --- /dev/null +++ b/CHANGES/13122.feature.rst @@ -0,0 +1 @@ +13006.feature.rst \ No newline at end of file diff --git a/aiohttp/client.py b/aiohttp/client.py index 6f9d781a342..116005def1f 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -69,6 +69,7 @@ ) from .client_middlewares import ClientMiddlewareType, build_client_middlewares from .client_reqrep import ( + SSL_ALLOWED_TYPES, ClientRequest as ClientRequest, ClientResponse as ClientResponse, Fingerprint as Fingerprint, @@ -186,7 +187,7 @@ class _RequestOptions(TypedDict, total=False): proxy: StrOrURL | None proxy_auth: BasicAuth | None timeout: "ClientTimeout | _SENTINEL | None" - ssl: SSLContext | bool | Fingerprint + ssl: SSLContext | bool | Fingerprint | _SENTINEL server_hostname: str | None proxy_headers: LooseHeaders | None trace_request_ctx: object @@ -212,7 +213,7 @@ class _WSConnectOptions(TypedDict, total=False): headers: LooseHeaders | None proxy: StrOrURL | None proxy_auth: BasicAuth | None - ssl: SSLContext | bool | Fingerprint + ssl: SSLContext | bool | Fingerprint | _SENTINEL verify_ssl: bool | None fingerprint: bytes | None ssl_context: SSLContext | None @@ -291,6 +292,7 @@ class ClientSession: "_max_headers", "_resolve_charset", "_default_proxy", + "_default_ssl", "_default_proxy_auth", "_retry_connection", "_middlewares", @@ -311,6 +313,7 @@ def __init__( headers: LooseHeaders | None = None, proxy: StrOrURL | None = None, proxy_auth: BasicAuth | None = None, + ssl: SSLContext | bool | Fingerprint = True, skip_auto_headers: Iterable[str] | None = None, auth: BasicAuth | None = None, json_serialize: JSONEncoder = json.dumps, @@ -357,6 +360,12 @@ def __init__( if self._base_url is not None and not self._base_url.path.endswith("/"): raise ValueError("base_url must have a trailing '/'") + if not isinstance(ssl, SSL_ALLOWED_TYPES): + raise TypeError( + "ssl should be SSLContext, bool, Fingerprint or None, " + f"got {ssl!r} instead." + ) + if timeout is sentinel or timeout is None: self._timeout = DEFAULT_TIMEOUT if read_timeout is not sentinel: @@ -474,6 +483,7 @@ def __init__( self._default_proxy = proxy self._default_proxy_auth = proxy_auth + self._default_ssl = ssl self._retry_connection: bool = True self._middlewares = middlewares @@ -556,7 +566,7 @@ async def _request( verify_ssl: bool | None = None, fingerprint: bytes | None = None, ssl_context: SSLContext | None = None, - ssl: SSLContext | bool | Fingerprint = True, + ssl: SSLContext | bool | Fingerprint | _SENTINEL = sentinel, server_hostname: str | None = None, proxy_headers: LooseHeaders | None = None, trace_request_ctx: object = None, @@ -576,6 +586,11 @@ async def _request( raise RuntimeError("Session is closed") method = method.upper() + # Resolve the session default before merging the legacy parameters: + # _merge_ssl_params treats any non-True ssl as explicit and would + # reject the sentinel when a legacy parameter is also given. + if ssl is sentinel: + ssl = self._default_ssl ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint) if auth is not None: @@ -1070,7 +1085,7 @@ def ws_connect( headers: LooseHeaders | None = None, proxy: StrOrURL | None = None, proxy_auth: BasicAuth | None = None, - ssl: SSLContext | bool | Fingerprint = True, + ssl: SSLContext | bool | Fingerprint | _SENTINEL = sentinel, verify_ssl: bool | None = None, fingerprint: bytes | None = None, ssl_context: SSLContext | None = None, @@ -1155,7 +1170,7 @@ async def _ws_connect( headers: LooseHeaders | None = None, proxy: StrOrURL | None = None, proxy_auth: BasicAuth | None = None, - ssl: SSLContext | bool | Fingerprint = True, + ssl: SSLContext | bool | Fingerprint | _SENTINEL = sentinel, verify_ssl: bool | None = None, fingerprint: bytes | None = None, ssl_context: SSLContext | None = None, @@ -1238,6 +1253,10 @@ async def _ws_connect( stacklevel=2, ) ssl = True + # Resolve the session default before merging the legacy parameters, + # mirroring _request (the merge treats any non-True ssl as explicit). + if ssl is sentinel: + ssl = self._default_ssl ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint) # send request diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 734201128d6..f4cf74d05b9 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -50,6 +50,7 @@ The client session supports the context manager protocol for self closing. raise_for_status=False, \ timeout=sentinel, \ auto_decompress=True, \ + ssl=True, \ trust_env=False, \ requote_redirect_url=True, \ trace_configs=None, \ @@ -196,6 +197,18 @@ The client session supports the context manager protocol for self closing. .. versionadded:: 2.3 + :param ssl: Default SSL validation mode for requests made through this + session. ``True`` for default SSL check + (:func:`ssl.create_default_context` is used), + ``False`` for skip SSL certificate validation, + :class:`aiohttp.Fingerprint` for fingerprint + validation, :class:`ssl.SSLContext` for custom SSL + certificate validation (``True`` by default). + + A per-request ``ssl`` argument overrides this value. + + .. versionadded:: 3.15 + :param bool trust_env: Trust environment settings for proxy configuration if the parameter is ``True`` (``False`` by default). See :ref:`aiohttp-client-proxy-support` for more information. @@ -553,8 +566,15 @@ The client session supports the context manager protocol for self closing. Supersedes *verify_ssl*, *ssl_context* and *fingerprint* parameters. + When not given, the session's ``ssl`` value is used + (``True`` unless set). + .. versionadded:: 3.0 + .. versionchanged:: 3.15 + + Defaults to the session's ``ssl`` value. + :param str server_hostname: Sets or overrides the host name that the target server's certificate will be matched against. @@ -816,8 +836,15 @@ The client session supports the context manager protocol for self closing. Supersedes *verify_ssl*, *ssl_context* and *fingerprint* parameters. + When not given, the session's ``ssl`` value is used + (``True`` unless set). + .. versionadded:: 3.0 + .. versionchanged:: 3.15 + + Defaults to the session's ``ssl`` value. + :param bool verify_ssl: Perform SSL certificate validation for *HTTPS* requests (enabled by default). May be disabled to skip validation for sites with invalid certificates. diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 6b351687995..53709ffe1fe 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -3,6 +3,7 @@ import gc import io import json +import ssl import sys import warnings from collections import deque @@ -942,6 +943,97 @@ class OnCall(Exception): await session.close() +async def test_default_ssl() -> None: + ssl_ctx = ssl.create_default_context() + + class OnCall(Exception): + pass + + request_class_mock = mock.Mock(side_effect=OnCall()) + session = ClientSession(ssl=ssl_ctx, request_class=request_class_mock) + + assert session._default_ssl is ssl_ctx, "`ClientSession._default_ssl` not set" + + with pytest.raises(OnCall): + await session.get("http://example.com") + + assert request_class_mock.called, "request class not called" + assert ( + request_class_mock.call_args[1].get("ssl") is ssl_ctx + ), "`ClientSession._request` does not use the session default ssl" + + request_class_mock.reset_mock() + with pytest.raises(OnCall): + await session.get("http://example.com", ssl=False) + + assert request_class_mock.called, "request class not called" + assert ( + request_class_mock.call_args[1].get("ssl") is False + ), "`ClientSession._request` uses session default ssl not the per-request one" + + request_class_mock.reset_mock() + with pytest.raises(OnCall): + await session.get("http://example.com", ssl=True) + + assert request_class_mock.called, "request class not called" + assert ( + request_class_mock.call_args[1].get("ssl") is True + ), "explicit `ssl=True` should not be replaced by the session default" + + await session.close() + + +async def test_default_ssl_not_set() -> None: + class OnCall(Exception): + pass + + request_class_mock = mock.Mock(side_effect=OnCall()) + session = ClientSession(request_class=request_class_mock) + + with pytest.raises(OnCall): + await session.get("http://example.com") + + assert request_class_mock.called, "request class not called" + assert ( + request_class_mock.call_args[1].get("ssl") is True + ), "the default ssl mode should stay `True` when not configured" + + await session.close() + + +async def test_default_ssl_invalid_type() -> None: + with pytest.raises( + TypeError, match="ssl should be SSLContext, bool, Fingerprint or None" + ): + ClientSession(ssl="/some/cert.pem") # type: ignore[arg-type] + + +async def test_request_ssl_invalid_type() -> None: + session = ClientSession() + + with pytest.raises( + TypeError, match="ssl should be SSLContext, bool, Fingerprint or None" + ): + await session.get( + "http://example.com", ssl="/some/cert.pem" # type: ignore[arg-type] + ) + + await session.close() + + +async def test_ws_connect_ssl_invalid_type() -> None: + session = ClientSession() + + with pytest.raises( + TypeError, match="ssl should be SSLContext, bool, Fingerprint or None" + ): + await session.ws_connect( + "http://example.com", ssl="/some/cert.pem" # type: ignore[arg-type] + ) + + await session.close() + + async def test_request_tracing(loop: asyncio.AbstractEventLoop, aiohttp_client) -> None: async def handler(request: web.Request) -> web.Response: return web.json_response({"ok": True}) From dff371291882d0b6cc2f82c8c0df7669646fed5f Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 00:33:12 +0100 Subject: [PATCH 120/137] [PR #13169/1adc0cd7 backport][3.15] Upgrade http:// to https:// in README.rst (#13178) **This is a backport of PR #13169 as merged into master (1adc0cd753cd19c627a198495d4b6a974f81bedf).** Co-authored-by: MOHAMMED HANAN M T P <91409429+hanu-14@users.noreply.github.com> --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index d5c8dd835bc..c10a0a8b841 100644 --- a/README.rst +++ b/README.rst @@ -59,7 +59,7 @@ To get something from the web: async def main(): async with aiohttp.ClientSession() as session: - async with session.get('http://python.org') as response: + async with session.get('https://python.org') as response: print("Status:", response.status) print("Content-type:", response.headers['content-type']) @@ -134,11 +134,11 @@ External links ============== * `Third party libraries - `_ + `_ * `Built with aiohttp - `_ + `_ * `Powered by aiohttp - `_ + `_ Feel free to make a Pull Request for adding your link to these pages! From 64a03fb620b623e5a5a1b7103c07ae3e536a0d40 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 00:33:28 +0100 Subject: [PATCH 121/137] [PR #13169/1adc0cd7 backport][3.14] Upgrade http:// to https:// in README.rst (#13177) **This is a backport of PR #13169 as merged into master (1adc0cd753cd19c627a198495d4b6a974f81bedf).** Co-authored-by: MOHAMMED HANAN M T P <91409429+hanu-14@users.noreply.github.com> --- README.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index d5c8dd835bc..c10a0a8b841 100644 --- a/README.rst +++ b/README.rst @@ -59,7 +59,7 @@ To get something from the web: async def main(): async with aiohttp.ClientSession() as session: - async with session.get('http://python.org') as response: + async with session.get('https://python.org') as response: print("Status:", response.status) print("Content-type:", response.headers['content-type']) @@ -134,11 +134,11 @@ External links ============== * `Third party libraries - `_ + `_ * `Built with aiohttp - `_ + `_ * `Powered by aiohttp - `_ + `_ Feel free to make a Pull Request for adding your link to these pages! From b4e8edf7ce1f59ecac9a91148d7b71652997ff5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:19:24 +0000 Subject: [PATCH 122/137] Bump actions/setup-python from 6 to 6.3.0 (#13183) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 6.3.0.
Release notes

Sourced from actions/setup-python's releases.

v6.3.0

What's Changed

Enhancement

Dependency update

Documentation

New Contributors

Full Changelog: https://github.com/actions/setup-python/compare/v6.2.0...v6.3.0

v6.2.0

What's Changed

Dependency Upgrades

Full Changelog: https://github.com/actions/setup-python/compare/v6...v6.2.0

v6.1.0

What's Changed

Enhancements:

Dependency and Documentation updates:

New Contributors

Full Changelog: https://github.com/actions/setup-python/compare/v6...v6.1.0

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-python&package-manager=github_actions&previous-version=6&new-version=6.3.0)](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 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index dcdd2fccd73..411d60c2931 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -67,7 +67,7 @@ jobs: make sync-direct-runtime-deps git diff --exit-code -- requirements/runtime-deps.in - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v6.3.0 with: python-version: 3.11 - name: Cache PyPI @@ -430,7 +430,7 @@ jobs: submodules: true - name: Setup Python 3.13.2 id: python-install - uses: actions/setup-python@v6 + uses: actions/setup-python@v6.3.0 with: python-version: 3.13.2 cache: pip @@ -482,7 +482,7 @@ jobs: submodules: true - name: Setup Python id: python-install - uses: actions/setup-python@v6 + uses: actions/setup-python@v6.3.0 with: python-version: '3.12' - name: Install dependencies @@ -568,7 +568,7 @@ jobs: with: submodules: true - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v6.3.0 - name: Install build tooling and cython run: >- python -m @@ -666,7 +666,7 @@ jobs: fi shell: bash - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v6.3.0 with: python-version: 3.x - name: Install build tooling and cython From 157cdb24937b6bfa5b8648633c0a49ed2181b81a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:45:05 +0000 Subject: [PATCH 123/137] Bump filelock from 3.29.7 to 3.30.2 (#13186) 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.29.7 to 3.30.2.
Release notes

Sourced from filelock's releases.

3.30.2

What's Changed

Full Changelog: https://github.com/tox-dev/filelock/compare/3.30.1...3.30.2

3.30.1

What's Changed

Full Changelog: https://github.com/tox-dev/filelock/compare/3.30.0...3.30.1

3.30.0

What's Changed

... (truncated)

Changelog

Sourced from filelock's changelog.

########### Changelog ###########

.. towncrier-draft-entries:: Unreleased

.. towncrier release notes start


3.31.1 (2026-07-20)


  • A SoftFileLease acquired on one thread keeps its claim when another thread fails to acquire the same lease object, so its heartbeat carries on refreshing the marker instead of being torn down and letting a peer take the live claim. :pr:680

3.31.0 (2026-07-18)


  • Support Termux/Android, whose CPython ships without os.link and reports sys.platform == "android". import filelock and both FileLock and SoftFileLock now work there, StrictSoftFileLock reports its missing hard-link support only when acquired, and process liveness reads /proc on Android instead of PID-only checks. :pr:678
  • StrictSoftFileLock no longer lets two processes hold the lock at once under heavy contention. A holder now keeps its intent claim for the whole hold, so a contender whose directory scan races the holder's freshly linked claim can no longer miss it and win alongside it. A held lock now exposes both an intent and a held claim. :pr:678

3.30.3 (2026-07-17)


  • AsyncFileLock and AsyncSoftFileLock no longer raise a deadlock RuntimeError when a different asyncio task waits for a lock they hold; the check now scopes holders to the owning task, so only a same-task reacquire is refused. :pr:676
  • Keep both tables of contents on screen at any browser font size. The widened body column sized itself in em against the reader's font size while the breakpoints that fold the layout resolve em against a fixed 16px, so a reader whose default font is larger than 16px had the right-hand table of contents clipped off the edge. The body column now flexes instead, and the right-hand table of contents hides only at the mobile breakpoint. :pr:673
  • Color the mermaid diagrams from whichever theme is active. They previously carried a light palette baked into each diagram's source, which stayed light on a dark page. :pr:674

3.30.2 (2026-07-16)


  • Stop :class:~filelock.SoftFileLease deleting a live marker whose mode it does not implement. An unrecognized mode now names its owner instead of reading as malformed, so a record written by a newer filelock survives the grace window rather than being evicted after two seconds. :pr:672
  • Stop :class:~filelock.StrictSoftFileLock and :class:~filelock.AsyncStrictSoftFileLock calling themselves a native OS lock when they warn that they ignore lifetime; they now say a strict claim is only ever cleared by force_break(). :pr:672
  • Cover every lock type in the tutorials and how-to guides, with examples drawn from projects that use filelock, and color the mermaid diagrams. Correct the claims that StrictSoftFileLock exposes owner, that :class:~filelock.SoftFileLock evicts a strict sentinel, that :class:~filelock.ReadWriteLock requires a .db extension, and that every log record is DEBUG. :pr:672

3.30.1 (2026-07-16)


  • StrictSoftFileLock and AsyncStrictSoftFileLock no longer abort acquisition when a peer's claim vanishes as an

... (truncated)

Commits
  • 9a71d06 Release 3.30.2
  • 0dfa1b2 Document every lock type, and stop evicting a marker we cannot read (#672)
  • 47d435f Release 3.30.1
  • 9b7b3b1 reject non-finite lease duration in marker records (#670)
  • cfafd70 Match fork-reset protocol on the class object (#671)
  • e4c0eb3 πŸ› fix: tolerate NFSv3 stale handle in strict claim read (#669)
  • d0450d5 πŸ“ docs: separate changelog releases with a blank line (#668)
  • 1b09406 Release 3.30.0
  • 5f2d663 πŸ‘· ci(matrix): add SMB, capability, and matrix docs (#665)
  • 2345750 Replace prettier with mdformat and yamlfmt (#667)
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=filelock&package-manager=pip&previous-version=3.29.7&new-version=3.30.2)](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 31c29e2b1f9..33860a9066d 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -79,7 +79,7 @@ exceptiongroup==1.3.1 # pytest execnet==2.1.2 # via pytest-xdist -filelock==3.29.7 +filelock==3.30.2 # via # python-discovery # virtualenv diff --git a/requirements/dev.txt b/requirements/dev.txt index d1c77329add..f2507eed0af 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -77,7 +77,7 @@ exceptiongroup==1.3.1 # pytest execnet==2.1.2 # via pytest-xdist -filelock==3.29.7 +filelock==3.30.2 # via # python-discovery # virtualenv diff --git a/requirements/lint.txt b/requirements/lint.txt index 72aefded7ca..da04bfc171e 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -32,7 +32,7 @@ exceptiongroup==1.3.1 # via # aiofastnet # pytest -filelock==3.29.7 +filelock==3.30.2 # via # python-discovery # virtualenv From 8d47297bf7df392d6a35117c6349bd727140adf2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:50:41 +0000 Subject: [PATCH 124/137] Bump coverage from 7.15.1 to 7.15.2 (#13188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.15.1 to 7.15.2.
Release notes

Sourced from coverage's releases.

7.15.2

Version 7.15.2 β€” 2026-07-15

  • Fix: one of the performance improvements in 7.15.1 (pull 2215) dramatically increased memory use during reporting for large projects. Now we use a different approach that is both faster and slimmer than 7.15.0. Fixes issue 2229.

:arrow_right:Β  PyPI page: coverage 7.15.2. :arrow_right:Β  To install: python3 -m pip install coverage==7.15.2

Changelog

Sourced from coverage's changelog.

Version 7.15.2 β€” 2026-07-15

  • Fix: one of the performance improvements in 7.15.1 (pull 2215) dramatically increased memory use during reporting for large projects. Now we use a different approach that is both faster and slimmer than 7.15.0. Fixes issue 2229_.

.. _issue 2229: coveragepy/coveragepy#2229

.. _changes_7-15-1:

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=coverage&package-manager=pip&previous-version=7.15.1&new-version=7.15.2)](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-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 33860a9066d..89cc73b184f 100644 --- a/requirements/constraints.txt +++ b/requirements/constraints.txt @@ -59,7 +59,7 @@ click==8.4.2 # pip-tools # towncrier # wait-for-it -coverage==7.15.1 +coverage==7.15.2 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/dev.txt b/requirements/dev.txt index f2507eed0af..60e7d4435be 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -59,7 +59,7 @@ click==8.4.2 # pip-tools # towncrier # wait-for-it -coverage==7.15.1 +coverage==7.15.2 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-common-base.txt b/requirements/test-common-base.txt index d2e66067bbe..eb0c089765c 100644 --- a/requirements/test-common-base.txt +++ b/requirements/test-common-base.txt @@ -6,7 +6,7 @@ # click==8.4.2 # via wait-for-it -coverage==7.15.1 +coverage==7.15.2 # via pytest-cov exceptiongroup==1.3.1 # via pytest diff --git a/requirements/test-common.txt b/requirements/test-common.txt index 169b0a411c6..cd729ff5953 100644 --- a/requirements/test-common.txt +++ b/requirements/test-common.txt @@ -14,7 +14,7 @@ cffi==2.1.0 # via cryptography click==8.4.2 # via wait-for-it -coverage==7.15.1 +coverage==7.15.2 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-ft.txt b/requirements/test-ft.txt index 92208b78ccb..99b1ab02696 100644 --- a/requirements/test-ft.txt +++ b/requirements/test-ft.txt @@ -32,7 +32,7 @@ cffi==2.1.0 # pycares click==8.4.2 # via wait-for-it -coverage==7.15.1 +coverage==7.15.2 # via # -r requirements/test-common.in # pytest-cov diff --git a/requirements/test-mobile.txt b/requirements/test-mobile.txt index c04b3f9398a..13c580c7152 100644 --- a/requirements/test-mobile.txt +++ b/requirements/test-mobile.txt @@ -28,7 +28,7 @@ cffi==2.1.0 ; sys_platform != "android" and sys_platform != "ios" # pycares click==8.4.2 # via wait-for-it -coverage==7.15.1 +coverage==7.15.2 # via pytest-cov exceptiongroup==1.3.1 # via diff --git a/requirements/test.txt b/requirements/test.txt index 869ecc64290..8203da6e9fa 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -32,7 +32,7 @@ cffi==2.1.0 # pycares click==8.4.2 # via wait-for-it -coverage==7.15.1 +coverage==7.15.2 # via # -r requirements/test-common.in # pytest-cov From 795e0448f40878e0fd380d176557cabbecc7a367 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:31:25 +0100 Subject: [PATCH 125/137] [PR #13172/a57747ed backport][3.15] Fix C parser folding fragment into query_string for empty-query origin-form target (#13191) **This is a backport of PR #13172 as merged into master (a57747ed361c7d16b72053f168808d6e62b7929a).** Co-authored-by: giulio d'erme --- CHANGES/13171.bugfix.rst | 3 +++ aiohttp/_http_parser.pyx | 2 +- tests/test_http_parser.py | 13 +++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 CHANGES/13171.bugfix.rst diff --git a/CHANGES/13171.bugfix.rst b/CHANGES/13171.bugfix.rst new file mode 100644 index 00000000000..fead0685a8e --- /dev/null +++ b/CHANGES/13171.bugfix.rst @@ -0,0 +1,3 @@ +Fixed the C HTTP parser folding the fragment into the query string for an +origin-form request target with an empty query (e.g. ``/path?#frag``), +which diverged from the pure-Python parser -- by :user:`GiulioDER`. diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 01ade16338e..2dcc0f1a2a5 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -734,7 +734,7 @@ cdef class HttpRequestParser(HttpParser): else: path = self._path[0:idx1] idx1 += 1 - idx2 = self._path.find("#", idx1+1) + idx2 = self._path.find("#", idx1) if idx2 == -1: query = self._path[idx1:] fragment = "" diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index c9e9ca51407..89d531eb286 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -2439,6 +2439,19 @@ def test_parse_uri_utf8_percent_encoded(parser) -> None: assert msg.url.fragment == "Ρ„Ρ€Π°Π³" +def test_parse_uri_empty_query_with_fragment(parser: HttpRequestParser) -> None: + # Origin-form target with an empty query but a fragment: the ``#`` sits + # immediately after ``?``. Regression for the C parser folding ``#frag`` + # into the query string instead of the fragment. + text = b"GET /path?#frag HTTP/1.1\r\nHost: a\r\n\r\n" + messages, upgrade, tail = parser.feed_data(text) + msg = messages[0][0] + + assert msg.url.path == "/path" + assert msg.url.query == {} + assert msg.url.fragment == "frag" + + @pytest.mark.skipif( "HttpRequestParserC" not in dir(aiohttp.http_parser), reason="C based HTTP parser not available", From 9ffa1d9fd39cdee72344e1e27afff3581df20b4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:45:00 +0000 Subject: [PATCH 126/137] Bump github/codeql-action from 4.37.0 to 4.37.1 (#13184) Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.37.0 to 4.37.1.
Release notes

Sourced from github/codeql-action's releases.

v4.37.1

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. #3956
  • Update default CodeQL bundle version to 2.26.1. #4019
Changelog

Sourced from github/codeql-action's changelog.

4.37.1 - 16 Jul 2026

  • Upcoming breaking change: Add a deprecation warning for customers using CodeQL version 2.20.6 and earlier. These versions of CodeQL were discontinued on 1 July 2026 alongside GitHub Enterprise Server 3.16, and will be unsupported by the next minor release of the CodeQL Action. #3956
  • Update default CodeQL bundle version to 2.26.1. #4019
Commits
  • 7188fc3 Merge pull request #4020 from github/update-v4.37.1-9e7c07009
  • c8b5f69 Update changelog for v4.37.1
  • 9e7c070 Merge pull request #4014 from github/mbg/explicit-remote-prefix
  • 3492b7e Change REMOTE_PATH_PREFIX to remote=
  • 3654baa Merge remote-tracking branch 'origin/main' into mbg/explicit-remote-prefix
  • 2d682ac Merge pull request #4017 from github/dependabot/github_actions/dot-github/wor...
  • 23f6a50 Merge pull request #4009 from github/mbg/action-state/additions
  • 1ee3c75 Merge pull request #4018 from github/dependabot/github_actions/dot-github/wor...
  • e053684 Merge pull request #4015 from github/dependabot/npm_and_yarn/npm-minor-fd2e83...
  • 6803c56 Merge pull request #4019 from github/update-bundle/codeql-bundle-v2.26.1
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github/codeql-action&package-manager=github_actions&previous-version=4.37.0&new-version=4.37.1)](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/codeql.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bb4c76fb366..6d4bf162d59 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -29,17 +29,17 @@ jobs: uses: actions/checkout@v7 - name: Initialize CodeQL - uses: github/codeql-action/init@v4.37.0 + uses: github/codeql-action/init@v4.37.1 with: languages: ${{ matrix.language }} config-file: ./.github/codeql.yml queries: +security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v4.37.0 + uses: github/codeql-action/autobuild@v4.37.1 if: ${{ matrix.language == 'python' || matrix.language == 'javascript' }} - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v4.37.0 + uses: github/codeql-action/analyze@v4.37.1 with: category: "/language:${{ matrix.language }}" From 71b57b40d85a0723c92b0a5a37ebdf518210d2ea Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:58:08 +0100 Subject: [PATCH 127/137] [PR #13172/a57747ed backport][3.14] Fix C parser folding fragment into query_string for empty-query origin-form target (#13190) **This is a backport of PR #13172 as merged into master (a57747ed361c7d16b72053f168808d6e62b7929a).** Co-authored-by: giulio d'erme --- CHANGES/13171.bugfix.rst | 3 +++ aiohttp/_http_parser.pyx | 2 +- tests/test_http_parser.py | 13 +++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 CHANGES/13171.bugfix.rst diff --git a/CHANGES/13171.bugfix.rst b/CHANGES/13171.bugfix.rst new file mode 100644 index 00000000000..fead0685a8e --- /dev/null +++ b/CHANGES/13171.bugfix.rst @@ -0,0 +1,3 @@ +Fixed the C HTTP parser folding the fragment into the query string for an +origin-form request target with an empty query (e.g. ``/path?#frag``), +which diverged from the pure-Python parser -- by :user:`GiulioDER`. diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 01ade16338e..2dcc0f1a2a5 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -734,7 +734,7 @@ cdef class HttpRequestParser(HttpParser): else: path = self._path[0:idx1] idx1 += 1 - idx2 = self._path.find("#", idx1+1) + idx2 = self._path.find("#", idx1) if idx2 == -1: query = self._path[idx1:] fragment = "" diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index f1ef31f1df6..65052bfdd4d 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -2411,6 +2411,19 @@ def test_parse_uri_utf8_percent_encoded(parser) -> None: assert msg.url.fragment == "Ρ„Ρ€Π°Π³" +def test_parse_uri_empty_query_with_fragment(parser: HttpRequestParser) -> None: + # Origin-form target with an empty query but a fragment: the ``#`` sits + # immediately after ``?``. Regression for the C parser folding ``#frag`` + # into the query string instead of the fragment. + text = b"GET /path?#frag HTTP/1.1\r\nHost: a\r\n\r\n" + messages, upgrade, tail = parser.feed_data(text) + msg = messages[0][0] + + assert msg.url.path == "/path" + assert msg.url.query == {} + assert msg.url.fragment == "frag" + + @pytest.mark.skipif( "HttpRequestParserC" not in dir(aiohttp.http_parser), reason="C based HTTP parser not available", From ef0e0c574197bc7efa5e50f87f62d07102969a90 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 13:58:30 +0100 Subject: [PATCH 128/137] [PR #13170/2b906869 backport][3.15] Fix StreamResponse.last_modified rounding inconsistency between datetime and timestamp (#13193) **This is a backport of PR #13170 as merged into master (2b9068690af7f868ccd01552c58cf045afc6d9f6).** Co-authored-by: agu2347 <94227848+agu2347@users.noreply.github.com> --- CHANGES/5303.bugfix.rst | 7 +++++++ aiohttp/web_response.py | 2 ++ tests/test_web_response.py | 17 +++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 CHANGES/5303.bugfix.rst diff --git a/CHANGES/5303.bugfix.rst b/CHANGES/5303.bugfix.rst new file mode 100644 index 00000000000..fcb1e7c8ae9 --- /dev/null +++ b/CHANGES/5303.bugfix.rst @@ -0,0 +1,7 @@ +Fixed :py:attr:`~aiohttp.web.StreamResponse.last_modified` rounding a +:class:`datetime.datetime` with a fractional second down while rounding an +equivalent :class:`float` timestamp up, so the two produced different +``Last-Modified`` headers for the same instant. Both now round up to the +next whole second, consistent with how :py:class:`~aiohttp.web.FileResponse` +relies on this rounding to avoid falsely reporting a resource as +unmodified. diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py index cea31be1733..ec0f9a4eb2e 100644 --- a/aiohttp/web_response.py +++ b/aiohttp/web_response.py @@ -371,6 +371,8 @@ def last_modified( "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value)) ) elif isinstance(value, datetime.datetime): + if value.microsecond: + value = value.replace(microsecond=0) + datetime.timedelta(seconds=1) self._headers[hdrs.LAST_MODIFIED] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", value.utctimetuple() ) diff --git a/tests/test_web_response.py b/tests/test_web_response.py index c5ca849b6b9..b5fac7e5f66 100644 --- a/tests/test_web_response.py +++ b/tests/test_web_response.py @@ -339,6 +339,23 @@ def test_last_modified_datetime() -> None: assert resp.last_modified == dt +def test_last_modified_datetime_and_timestamp_round_consistently() -> None: + dt = datetime.datetime(2020, 12, 2, 9, 51, 2, 500000, datetime.timezone.utc) + + resp_dt = web.StreamResponse() + resp_dt.last_modified = dt + + resp_ts = web.StreamResponse() + resp_ts.last_modified = dt.timestamp() + + assert resp_dt.headers["Last-Modified"] == resp_ts.headers["Last-Modified"] + # Both should round up to the next whole second, so that Last-Modified + # is never earlier than the file's true st_mtime, invalidating the caching. + assert resp_dt.last_modified == datetime.datetime( + 2020, 12, 2, 9, 51, 3, 0, datetime.timezone.utc + ) + + def test_last_modified_reset() -> None: resp = StreamResponse() From aa4cf29b6a5ad6f4d21fa1dd3f69193dc2f5d505 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 14:00:19 +0100 Subject: [PATCH 129/137] [PR #13170/2b906869 backport][3.14] Fix StreamResponse.last_modified rounding inconsistency between datetime and timestamp (#13192) **This is a backport of PR #13170 as merged into master (2b9068690af7f868ccd01552c58cf045afc6d9f6).** Co-authored-by: agu2347 <94227848+agu2347@users.noreply.github.com> --- CHANGES/5303.bugfix.rst | 7 +++++++ aiohttp/web_response.py | 2 ++ tests/test_web_response.py | 17 +++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 CHANGES/5303.bugfix.rst diff --git a/CHANGES/5303.bugfix.rst b/CHANGES/5303.bugfix.rst new file mode 100644 index 00000000000..fcb1e7c8ae9 --- /dev/null +++ b/CHANGES/5303.bugfix.rst @@ -0,0 +1,7 @@ +Fixed :py:attr:`~aiohttp.web.StreamResponse.last_modified` rounding a +:class:`datetime.datetime` with a fractional second down while rounding an +equivalent :class:`float` timestamp up, so the two produced different +``Last-Modified`` headers for the same instant. Both now round up to the +next whole second, consistent with how :py:class:`~aiohttp.web.FileResponse` +relies on this rounding to avoid falsely reporting a resource as +unmodified. diff --git a/aiohttp/web_response.py b/aiohttp/web_response.py index cea31be1733..ec0f9a4eb2e 100644 --- a/aiohttp/web_response.py +++ b/aiohttp/web_response.py @@ -371,6 +371,8 @@ def last_modified( "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value)) ) elif isinstance(value, datetime.datetime): + if value.microsecond: + value = value.replace(microsecond=0) + datetime.timedelta(seconds=1) self._headers[hdrs.LAST_MODIFIED] = time.strftime( "%a, %d %b %Y %H:%M:%S GMT", value.utctimetuple() ) diff --git a/tests/test_web_response.py b/tests/test_web_response.py index c5ca849b6b9..b5fac7e5f66 100644 --- a/tests/test_web_response.py +++ b/tests/test_web_response.py @@ -339,6 +339,23 @@ def test_last_modified_datetime() -> None: assert resp.last_modified == dt +def test_last_modified_datetime_and_timestamp_round_consistently() -> None: + dt = datetime.datetime(2020, 12, 2, 9, 51, 2, 500000, datetime.timezone.utc) + + resp_dt = web.StreamResponse() + resp_dt.last_modified = dt + + resp_ts = web.StreamResponse() + resp_ts.last_modified = dt.timestamp() + + assert resp_dt.headers["Last-Modified"] == resp_ts.headers["Last-Modified"] + # Both should round up to the next whole second, so that Last-Modified + # is never earlier than the file's true st_mtime, invalidating the caching. + assert resp_dt.last_modified == datetime.datetime( + 2020, 12, 2, 9, 51, 3, 0, datetime.timezone.utc + ) + + def test_last_modified_reset() -> None: resp = StreamResponse() From 2a6c672fc47bcb1ce7c13f045c95abe9b7a7a2ac Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Jul 2026 14:21:38 +0100 Subject: [PATCH 130/137] Fix raw path (#13175) (#13189) (cherry picked from commit 663d356fb61fd48e9e0d2ec3ab60292b011c92f7) --- CHANGES/13175.bugfix.rst | 4 ++++ aiohttp/http_parser.py | 5 +++-- aiohttp/web_request.py | 21 +++++++++++++++++++- tests/test_http_parser.py | 8 +++++++- tests/test_web_middleware.py | 28 +++++++++++++++++++++++++++ tests/test_web_request.py | 37 +++++++++++++++++++++++++++++++++++- 6 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 CHANGES/13175.bugfix.rst diff --git a/CHANGES/13175.bugfix.rst b/CHANGES/13175.bugfix.rst new file mode 100644 index 00000000000..ce39fa8f3cd --- /dev/null +++ b/CHANGES/13175.bugfix.rst @@ -0,0 +1,4 @@ +Fixed :attr:`~aiohttp.web.BaseRequest.raw_path` including the scheme and host +for absolute-form request targets, and made the pure-Python parser reject +authority-form targets (``host:port``) for methods other than ``CONNECT`` +-- by :user:`Dreamsorcerer`. diff --git a/aiohttp/http_parser.py b/aiohttp/http_parser.py index 0c9fd086b3b..15422c5d88a 100644 --- a/aiohttp/http_parser.py +++ b/aiohttp/http_parser.py @@ -709,8 +709,9 @@ def parse_message(self, lines: list[bytes]) -> RawRequestMessage: # absolute-form for proxy maybe, # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2 url = URL(path, encoded=True) - if url.scheme == "": - # not absolute-form + if not url.absolute: + # authority-form is only allowed with CONNECT + # https://www.rfc-editor.org/info/rfc9112/#section-3.2.3-1 raise InvalidURLError( path.encode(errors="surrogateescape").decode("latin1") ) diff --git a/aiohttp/web_request.py b/aiohttp/web_request.py index ca925e2acdb..92f687b3999 100644 --- a/aiohttp/web_request.py +++ b/aiohttp/web_request.py @@ -507,7 +507,26 @@ def raw_path(self) -> str: E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters`` """ - return self._message.path + path = self._message.path + + # An absolute-form target carries a "scheme://authority" that must not + # leak into the path. Strip it, keeping the remainder byte-for-byte, + # exactly as an origin-form target. Authority-form is used only by + # CONNECT and is left unchanged. + # https://www.rfc-editor.org/info/rfc9112/#section-3.2.2-9 + # https://www.rfc-editor.org/info/rfc9112/#name-authority-form + if self._message.url.absolute and self._method != "CONNECT": + # absolute-form always contains "://" (guaranteed by the parser). + scheme_sep = path.find("://") + assert scheme_sep != -1 + cursor = scheme_sep + 3 + rel = len(path) + for delimiter in "/?#": + found = path.find(delimiter, cursor) + if found != -1: + rel = min(rel, found) + return path[rel:] + return path @reify def query(self) -> "MultiMapping[str]": diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 89d531eb286..5a7c782fc38 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -1055,7 +1055,13 @@ def test_url_absolute(parser: Any) -> None: assert msg.url == URL("https://www.google.com/path/to.html") -def test_headers_old_websocket_key1(parser: Any) -> None: +def test_url_authority_form_only_connect(parser: HttpRequestParser) -> None: + # https://www.rfc-editor.org/info/rfc9112/#section-3.2.3-1 + with pytest.raises(http_exceptions.InvalidURLError): + parser.feed_data(b"GET www.google.com:443 HTTP/1.1\r\nHost: a\r\n\r\n") + + +def test_headers_old_websocket_key1(parser: HttpRequestParser) -> None: 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): diff --git a/tests/test_web_middleware.py b/tests/test_web_middleware.py index 13acc589da9..35842269eb2 100644 --- a/tests/test_web_middleware.py +++ b/tests/test_web_middleware.py @@ -1,4 +1,6 @@ +import asyncio import re +from contextlib import suppress from typing import Any, NoReturn import pytest @@ -393,6 +395,32 @@ async def handle(request: web.Request) -> web.StreamResponse: assert resp.headers["Location"] == "/google.com" assert resp.url.query == URL("//google.com").query + async def test_open_redirect_absolute_form_target( + self, aiohttp_server: Any + ) -> None: + async def handle(request: web.Request) -> web.Response: + assert False + + app = web.Application(middlewares=[web.normalize_path_middleware()]) + app.add_routes([web.get("/google.com/", handle)]) + server = await aiohttp_server(app) + + reader, writer = await asyncio.open_connection(server.host, server.port) + try: + writer.write( + b"GET http://google.com/google.com HTTP/1.1\r\n" + b"Host: localhost\r\nConnection: close\r\n\r\n" + ) + await writer.drain() + head = (await reader.readuntil(b"\r\n\r\n")).decode("ascii") + finally: + writer.close() + with suppress(ConnectionResetError, BrokenPipeError): + await writer.wait_closed() + + assert head.startswith("HTTP/1.1 308 ") + assert "\r\nLocation: /google.com/\r\n" in head + async def test_old_style_middleware(loop, aiohttp_client) -> None: async def handler(request): diff --git a/tests/test_web_request.py b/tests/test_web_request.py index d025c8c4812..925ba3f7552 100644 --- a/tests/test_web_request.py +++ b/tests/test_web_request.py @@ -11,7 +11,7 @@ from multidict import CIMultiDict, CIMultiDictProxy, MultiDict from yarl import URL -from aiohttp import HttpVersion +from aiohttp import HttpVersion, web from aiohttp.base_protocol import BaseProtocol from aiohttp.helpers import DEFAULT_CHUNK_SIZE from aiohttp.http_exceptions import BadHttpMessage, LineTooLong @@ -224,6 +224,41 @@ def test_absolute_url() -> None: assert req.rel_url == URL.build(path="/path/to", query={"a": "1"}) +def test_absolute_form_raw_path() -> None: + # An absolute-form target (RFC 9112 3.2.2) must not leak the scheme/host + # into raw_path. The path, query and fragment are kept byte-for-byte, the + # same raw form an origin-form target yields. + req = make_mocked_request("GET", "https://example.com/path/to?a=1#frag") + assert req.raw_path == "/path/to?a=1#frag" + assert req.raw_path == make_mocked_request("GET", "/path/to?a=1#frag").raw_path + + +def test_connect_authority_form_raw_path() -> None: + # Authority-form is only used by CONNECT (RFC 9112 3.2.3); its target is a + # bare host:port with no scheme prefix, so raw_path returns it unchanged. + message = RawRequestMessage( + "CONNECT", + "example.com:443", + HttpVersion(1, 1), + CIMultiDict(), + (), + False, + None, + False, + False, + URL.build(authority="example.com:443", encoded=True), + ) + protocol = mock.Mock() + protocol.ssl_context = None + protocol.peername = None + protocol.sockname = ("127.0.0.1", 80) + req = web.BaseRequest( + message, mock.Mock(), protocol, mock.Mock(), mock.Mock(), mock.Mock() + ) + assert req._message.url.absolute + assert req.raw_path == "example.com:443" + + def test_clone_absolute_scheme() -> None: req = make_mocked_request("GET", "https://example.com/path/to?a=1") assert req.scheme == "https" From 84d606a8ad497ad41c111fc07bcfb387627d8786 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Jul 2026 15:06:28 +0100 Subject: [PATCH 131/137] Make llhttp method array size dynamic (#13174) (#13195) (cherry picked from commit b3a1c675ba3795c8b32d3f1e8eaa6f200ab5f465) --- CHANGES/13174.bugfix.rst | 1 + aiohttp/_cparser.pxd | 82 +++------------------------------------ aiohttp/_http_parser.pyx | 29 +++++++++----- tests/test_http_parser.py | 17 +++++++- 4 files changed, 42 insertions(+), 87 deletions(-) create mode 100644 CHANGES/13174.bugfix.rst diff --git a/CHANGES/13174.bugfix.rst b/CHANGES/13174.bugfix.rst new file mode 100644 index 00000000000..56cf18eea32 --- /dev/null +++ b/CHANGES/13174.bugfix.rst @@ -0,0 +1 @@ +Fixed the C parser reporting newer HTTP methods such as ``QUERY`` as ````; the method table is now derived from the vendored llhttp instead of a hand-maintained count -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/_cparser.pxd b/aiohttp/_cparser.pxd index cc7ef58d664..903f35c4590 100644 --- a/aiohttp/_cparser.pxd +++ b/aiohttp/_cparser.pxd @@ -1,29 +1,20 @@ -from libc.stdint cimport int32_t, uint8_t, uint16_t, uint64_t +# This file contains just the definitions needed in our Cython code. + +from libc.stdint cimport uint8_t, uint16_t, uint64_t cdef extern from "llhttp.h": struct llhttp__internal_s: - int32_t _index - void* _span_pos0 - void* _span_cb0 - int32_t error - const char* reason - const char* error_pos void* data - void* _current uint64_t content_length uint8_t type uint8_t method uint8_t http_major uint8_t http_minor - uint8_t header_state - uint8_t lenient_flags uint8_t upgrade - uint8_t finish uint16_t flags uint16_t status_code - void* settings ctypedef llhttp__internal_s llhttp__internal_t ctypedef llhttp__internal_t llhttp_t @@ -43,20 +34,10 @@ cdef extern from "llhttp.h": llhttp_cb on_chunk_header llhttp_cb on_chunk_complete - llhttp_cb on_url_complete - llhttp_cb on_status_complete - llhttp_cb on_header_field_complete - llhttp_cb on_header_value_complete - ctypedef llhttp_settings_s llhttp_settings_t enum llhttp_errno: HPE_OK, - HPE_INTERNAL, - HPE_STRICT, - HPE_LF_EXPECTED, - HPE_UNEXPECTED_CONTENT_LENGTH, - HPE_CLOSED_CONNECTION, HPE_INVALID_METHOD, HPE_INVALID_URL, HPE_INVALID_CONSTANT, @@ -73,8 +54,7 @@ cdef extern from "llhttp.h": HPE_CB_CHUNK_HEADER, HPE_CB_CHUNK_COMPLETE, HPE_PAUSED, - HPE_PAUSED_UPGRADE, - HPE_USER + HPE_PAUSED_UPGRADE ctypedef llhttp_errno llhttp_errno_t @@ -84,58 +64,10 @@ cdef extern from "llhttp.h": enum llhttp_type: HTTP_REQUEST, - HTTP_RESPONSE, - HTTP_BOTH + HTTP_RESPONSE enum llhttp_method: - HTTP_DELETE, - HTTP_GET, - HTTP_HEAD, - HTTP_POST, - HTTP_PUT, - HTTP_CONNECT, - HTTP_OPTIONS, - HTTP_TRACE, - HTTP_COPY, - HTTP_LOCK, - HTTP_MKCOL, - HTTP_MOVE, - HTTP_PROPFIND, - HTTP_PROPPATCH, - HTTP_SEARCH, - HTTP_UNLOCK, - HTTP_BIND, - HTTP_REBIND, - HTTP_UNBIND, - HTTP_ACL, - HTTP_REPORT, - HTTP_MKACTIVITY, - HTTP_CHECKOUT, - HTTP_MERGE, - HTTP_MSEARCH, - HTTP_NOTIFY, - HTTP_SUBSCRIBE, - HTTP_UNSUBSCRIBE, - HTTP_PATCH, - HTTP_PURGE, - HTTP_MKCALENDAR, - HTTP_LINK, - HTTP_UNLINK, - HTTP_SOURCE, - HTTP_PRI, - HTTP_DESCRIBE, - HTTP_ANNOUNCE, - HTTP_SETUP, - HTTP_PLAY, - HTTP_PAUSE, - HTTP_TEARDOWN, - HTTP_GET_PARAMETER, - HTTP_SET_PARAMETER, - HTTP_REDIRECT, - HTTP_RECORD, - HTTP_FLUSH - - ctypedef llhttp_method llhttp_method_t; + HTTP_CONNECT void llhttp_settings_init(llhttp_settings_t* settings) void llhttp_init(llhttp_t* parser, llhttp_type type, @@ -152,8 +84,6 @@ cdef extern from "llhttp.h": const char* llhttp_get_error_reason(const llhttp_t* parser) const char* llhttp_get_error_pos(const llhttp_t* parser) - const char* llhttp_method_name(llhttp_method_t method) - void llhttp_set_lenient_headers(llhttp_t* parser, int enabled) void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled) void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled) diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 2dcc0f1a2a5..d772e60aeae 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -98,20 +98,29 @@ cdef inline object extend(object buf, const char* at, size_t length): memcpy(ptr + s, at, length) -DEF METHODS_COUNT = 46; +# The method-name table and its length come straight from llhttp's canonical +# HTTP_ALL_METHOD_MAP, so they track the vendored llhttp version automatically +# instead of relying on a hand-maintained method count. +cdef extern from *: + """ + #include "llhttp.h" + + #define _AIOHTTP_METHOD_NAME(NUM, NAME, STRING) [NUM] = #STRING, + static const char* const _aiohttp_method_names[] = { + HTTP_ALL_METHOD_MAP(_AIOHTTP_METHOD_NAME) + }; + #undef _AIOHTTP_METHOD_NAME + """ + const char* _aiohttp_method_names[] + const int METHODS_COUNT "((int)(sizeof(_aiohttp_method_names) / sizeof(_aiohttp_method_names[0])))" + cdef list _http_method = [] for i in range(METHODS_COUNT): - _http_method.append( - cparser.llhttp_method_name( i).decode('ascii')) - + assert _aiohttp_method_names[i] is not NULL + _http_method.append(_aiohttp_method_names[i].decode('ascii')) -cdef inline str http_method_str(int i): - if i < METHODS_COUNT: - return _http_method[i] - else: - return "" cdef inline object find_header(bytes raw_header): cdef Py_ssize_t size @@ -504,7 +513,7 @@ cdef class HttpParser: encoding = enc if self._cparser.type == cparser.HTTP_REQUEST: - method = http_method_str(self._cparser.method) + method = _http_method[self._cparser.method] msg = _new_request_message( method, self._path, http_version, headers, raw_headers, diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 5a7c782fc38..07ec7a56945 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -647,7 +647,22 @@ def test_parse(parser: HttpRequestParser) -> None: assert msg.headers["Host"] == "a" -async def test_parse_body(parser) -> None: +def test_parse_query_method(parser: HttpRequestParser) -> None: + """QUERY has the highest llhttp method id; both parsers must decode its name. + + Regression test for a stale hand-maintained method count in the C parser + that mapped the last method(s) to "". + """ + text = b"QUERY /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][0] + assert msg.method == "QUERY" + assert msg.path == "/test" + assert msg.version == (1, 1) + + +async def test_parse_body(parser: HttpRequestParser) -> None: 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 From e1e1bee363dfba04a9a75c8801717da2ed5bdcb9 Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Jul 2026 15:06:39 +0100 Subject: [PATCH 132/137] Make llhttp method array size dynamic (#13174) (#13196) (cherry picked from commit b3a1c675ba3795c8b32d3f1e8eaa6f200ab5f465) --- CHANGES/13174.bugfix.rst | 1 + aiohttp/_cparser.pxd | 82 +++------------------------------------ aiohttp/_http_parser.pyx | 29 +++++++++----- tests/test_http_parser.py | 17 +++++++- 4 files changed, 42 insertions(+), 87 deletions(-) create mode 100644 CHANGES/13174.bugfix.rst diff --git a/CHANGES/13174.bugfix.rst b/CHANGES/13174.bugfix.rst new file mode 100644 index 00000000000..56cf18eea32 --- /dev/null +++ b/CHANGES/13174.bugfix.rst @@ -0,0 +1 @@ +Fixed the C parser reporting newer HTTP methods such as ``QUERY`` as ````; the method table is now derived from the vendored llhttp instead of a hand-maintained count -- by :user:`Dreamsorcerer`. diff --git a/aiohttp/_cparser.pxd b/aiohttp/_cparser.pxd index cc7ef58d664..903f35c4590 100644 --- a/aiohttp/_cparser.pxd +++ b/aiohttp/_cparser.pxd @@ -1,29 +1,20 @@ -from libc.stdint cimport int32_t, uint8_t, uint16_t, uint64_t +# This file contains just the definitions needed in our Cython code. + +from libc.stdint cimport uint8_t, uint16_t, uint64_t cdef extern from "llhttp.h": struct llhttp__internal_s: - int32_t _index - void* _span_pos0 - void* _span_cb0 - int32_t error - const char* reason - const char* error_pos void* data - void* _current uint64_t content_length uint8_t type uint8_t method uint8_t http_major uint8_t http_minor - uint8_t header_state - uint8_t lenient_flags uint8_t upgrade - uint8_t finish uint16_t flags uint16_t status_code - void* settings ctypedef llhttp__internal_s llhttp__internal_t ctypedef llhttp__internal_t llhttp_t @@ -43,20 +34,10 @@ cdef extern from "llhttp.h": llhttp_cb on_chunk_header llhttp_cb on_chunk_complete - llhttp_cb on_url_complete - llhttp_cb on_status_complete - llhttp_cb on_header_field_complete - llhttp_cb on_header_value_complete - ctypedef llhttp_settings_s llhttp_settings_t enum llhttp_errno: HPE_OK, - HPE_INTERNAL, - HPE_STRICT, - HPE_LF_EXPECTED, - HPE_UNEXPECTED_CONTENT_LENGTH, - HPE_CLOSED_CONNECTION, HPE_INVALID_METHOD, HPE_INVALID_URL, HPE_INVALID_CONSTANT, @@ -73,8 +54,7 @@ cdef extern from "llhttp.h": HPE_CB_CHUNK_HEADER, HPE_CB_CHUNK_COMPLETE, HPE_PAUSED, - HPE_PAUSED_UPGRADE, - HPE_USER + HPE_PAUSED_UPGRADE ctypedef llhttp_errno llhttp_errno_t @@ -84,58 +64,10 @@ cdef extern from "llhttp.h": enum llhttp_type: HTTP_REQUEST, - HTTP_RESPONSE, - HTTP_BOTH + HTTP_RESPONSE enum llhttp_method: - HTTP_DELETE, - HTTP_GET, - HTTP_HEAD, - HTTP_POST, - HTTP_PUT, - HTTP_CONNECT, - HTTP_OPTIONS, - HTTP_TRACE, - HTTP_COPY, - HTTP_LOCK, - HTTP_MKCOL, - HTTP_MOVE, - HTTP_PROPFIND, - HTTP_PROPPATCH, - HTTP_SEARCH, - HTTP_UNLOCK, - HTTP_BIND, - HTTP_REBIND, - HTTP_UNBIND, - HTTP_ACL, - HTTP_REPORT, - HTTP_MKACTIVITY, - HTTP_CHECKOUT, - HTTP_MERGE, - HTTP_MSEARCH, - HTTP_NOTIFY, - HTTP_SUBSCRIBE, - HTTP_UNSUBSCRIBE, - HTTP_PATCH, - HTTP_PURGE, - HTTP_MKCALENDAR, - HTTP_LINK, - HTTP_UNLINK, - HTTP_SOURCE, - HTTP_PRI, - HTTP_DESCRIBE, - HTTP_ANNOUNCE, - HTTP_SETUP, - HTTP_PLAY, - HTTP_PAUSE, - HTTP_TEARDOWN, - HTTP_GET_PARAMETER, - HTTP_SET_PARAMETER, - HTTP_REDIRECT, - HTTP_RECORD, - HTTP_FLUSH - - ctypedef llhttp_method llhttp_method_t; + HTTP_CONNECT void llhttp_settings_init(llhttp_settings_t* settings) void llhttp_init(llhttp_t* parser, llhttp_type type, @@ -152,8 +84,6 @@ cdef extern from "llhttp.h": const char* llhttp_get_error_reason(const llhttp_t* parser) const char* llhttp_get_error_pos(const llhttp_t* parser) - const char* llhttp_method_name(llhttp_method_t method) - void llhttp_set_lenient_headers(llhttp_t* parser, int enabled) void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled) void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled) diff --git a/aiohttp/_http_parser.pyx b/aiohttp/_http_parser.pyx index 2dcc0f1a2a5..d772e60aeae 100644 --- a/aiohttp/_http_parser.pyx +++ b/aiohttp/_http_parser.pyx @@ -98,20 +98,29 @@ cdef inline object extend(object buf, const char* at, size_t length): memcpy(ptr + s, at, length) -DEF METHODS_COUNT = 46; +# The method-name table and its length come straight from llhttp's canonical +# HTTP_ALL_METHOD_MAP, so they track the vendored llhttp version automatically +# instead of relying on a hand-maintained method count. +cdef extern from *: + """ + #include "llhttp.h" + + #define _AIOHTTP_METHOD_NAME(NUM, NAME, STRING) [NUM] = #STRING, + static const char* const _aiohttp_method_names[] = { + HTTP_ALL_METHOD_MAP(_AIOHTTP_METHOD_NAME) + }; + #undef _AIOHTTP_METHOD_NAME + """ + const char* _aiohttp_method_names[] + const int METHODS_COUNT "((int)(sizeof(_aiohttp_method_names) / sizeof(_aiohttp_method_names[0])))" + cdef list _http_method = [] for i in range(METHODS_COUNT): - _http_method.append( - cparser.llhttp_method_name( i).decode('ascii')) - + assert _aiohttp_method_names[i] is not NULL + _http_method.append(_aiohttp_method_names[i].decode('ascii')) -cdef inline str http_method_str(int i): - if i < METHODS_COUNT: - return _http_method[i] - else: - return "" cdef inline object find_header(bytes raw_header): cdef Py_ssize_t size @@ -504,7 +513,7 @@ cdef class HttpParser: encoding = enc if self._cparser.type == cparser.HTTP_REQUEST: - method = http_method_str(self._cparser.method) + method = _http_method[self._cparser.method] msg = _new_request_message( method, self._path, http_version, headers, raw_headers, diff --git a/tests/test_http_parser.py b/tests/test_http_parser.py index 65052bfdd4d..7f3e73f3d5b 100644 --- a/tests/test_http_parser.py +++ b/tests/test_http_parser.py @@ -647,7 +647,22 @@ def test_parse(parser: HttpRequestParser) -> None: assert msg.headers["Host"] == "a" -async def test_parse_body(parser) -> None: +def test_parse_query_method(parser: HttpRequestParser) -> None: + """QUERY has the highest llhttp method id; both parsers must decode its name. + + Regression test for a stale hand-maintained method count in the C parser + that mapped the last method(s) to "". + """ + text = b"QUERY /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][0] + assert msg.method == "QUERY" + assert msg.path == "/test" + assert msg.version == (1, 1) + + +async def test_parse_body(parser: HttpRequestParser) -> None: 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 From ee53d6558083e245f3aae7e864ed5523d138c9db Mon Sep 17 00:00:00 2001 From: arshsmith Date: Mon, 20 Jul 2026 22:56:20 +0530 Subject: [PATCH 133/137] drop every copy of credential headers on cross-origin redirect (#13180) --- CHANGES/13180.bugfix.rst | 2 ++ aiohttp/client.py | 6 ++--- tests/test_client_functional.py | 47 ++++++++++++++++++++++++++++++++- 3 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 CHANGES/13180.bugfix.rst diff --git a/CHANGES/13180.bugfix.rst b/CHANGES/13180.bugfix.rst new file mode 100644 index 00000000000..d1e676e5be8 --- /dev/null +++ b/CHANGES/13180.bugfix.rst @@ -0,0 +1,2 @@ +Fixed the client dropping only the first ``Authorization``, ``Cookie`` and +``Proxy-Authorization`` header when a redirect crosses an origin -- by :user:`arshsmith1`. diff --git a/aiohttp/client.py b/aiohttp/client.py index 464b8c89b30..481663128fd 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -837,9 +837,9 @@ async def _request( if url.origin() != redirect_origin: cookies = None - headers.pop(hdrs.AUTHORIZATION, None) - headers.pop(hdrs.COOKIE, None) - headers.pop(hdrs.PROXY_AUTHORIZATION, None) + headers.popall(hdrs.AUTHORIZATION, None) + headers.popall(hdrs.COOKIE, None) + headers.popall(hdrs.PROXY_AUTHORIZATION, None) url = parsed_redirect_url params = {} diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index e06cd019fa5..e8d3cbe7515 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -37,7 +37,7 @@ ZstdCompressor = None # type: ignore[assignment,misc] # pragma: no cover import pytest -from multidict import MultiDict +from multidict import CIMultiDict, MultiDict from pytest_aiohttp import AiohttpClient, AiohttpServer from pytest_mock import MockerFixture from yarl import URL, Query @@ -3576,6 +3576,51 @@ async def close(self) -> None: assert resp.status == 200 +async def test_drop_duplicate_auth_headers_on_redirect_to_other_host( + aiohttp_server: AiohttpServer, +) -> None: + """Every copy of a credential header is dropped, not just the first one.""" + + async def srv_from(request: web.Request) -> NoReturn: + assert list(request.headers.getall("Authorization")) == [ + "Basic first", + "Basic second", + ] + raise web.HTTPFound(server_to.make_url("/path2")) + + async def srv_to(request: web.Request) -> web.Response: + assert "Authorization" not in request.headers, "Header wasn't dropped" + assert "Proxy-Authorization" not in request.headers + assert "Cookie" not in request.headers + return web.Response() + + app_from = web.Application() + app_from.router.add_get("/path1", srv_from) + server_from = await aiohttp_server(app_from) + + app_to = web.Application() + app_to.router.add_get("/path2", srv_to) + server_to = await aiohttp_server(app_to) + + # Two servers on the same host but different ports are different origins, + # so following the redirect must strip the credential headers. + headers = CIMultiDict( + ( + ("Authorization", "Basic first"), + ("Authorization", "Basic second"), + ("Proxy-Authorization", "Basic first"), + ("Proxy-Authorization", "Basic second"), + ("Cookie", "a=b"), + ("Cookie", "c=d"), + ) + ) + + url = server_from.make_url("/path1") + async with aiohttp.ClientSession() as client: + async with client.get(url, headers=headers) as resp: + assert resp.status == 200 + + async def test_drop_session_authorization_header_on_redirect_to_other_host( create_server_for_url_and_handler: Callable[[URL, Handler], Awaitable[TestServer]], ) -> None: From 00c3ede302fb359d6063f005e1bb00d8053cc887 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:48:50 +0100 Subject: [PATCH 134/137] [PR #13054/ed8b040c backport][3.15] escape backslashes in digest auth quoted-string values (#13198) **This is a backport of PR #13054 as merged into master (ed8b040c81dfdce5793271c7d6c3aa36c97a2ba3).** Co-authored-by: Javid Khan --- CHANGES/13054.bugfix.rst | 4 +++ CONTRIBUTORS.txt | 1 + aiohttp/client_middleware_digest_auth.py | 8 +++--- tests/test_client_middleware_digest_auth.py | 30 ++++++++++++++++++++- 4 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 CHANGES/13054.bugfix.rst diff --git a/CHANGES/13054.bugfix.rst b/CHANGES/13054.bugfix.rst new file mode 100644 index 00000000000..75196b9bfe6 --- /dev/null +++ b/CHANGES/13054.bugfix.rst @@ -0,0 +1,4 @@ +Fixed ``escape_quotes`` in the Digest authentication middleware not escaping +backslashes, so a ``WWW-Authenticate`` challenge value containing a backslash +could break out of its quoted-string in the generated ``Authorization`` header +-- by :user:`dxbjavid`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 7efbcc10425..271b8b60af1 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -190,6 +190,7 @@ Jan Buchar Jan Gosmann Jarno Elonen Jashandeep Sohi +Javid Khan Javier Torres Jean-Baptiste Estival Jens Steinhauser diff --git a/aiohttp/client_middleware_digest_auth.py b/aiohttp/client_middleware_digest_auth.py index f4904e363d8..fbcdf549b4a 100644 --- a/aiohttp/client_middleware_digest_auth.py +++ b/aiohttp/client_middleware_digest_auth.py @@ -102,13 +102,13 @@ class DigestAuthChallenge(TypedDict, total=False): def escape_quotes(value: str) -> str: - """Escape double quotes for HTTP header values.""" - return value.replace('"', '\\"') + """Escape backslashes and double quotes for HTTP quoted-strings.""" + return value.replace("\\", "\\\\").replace('"', '\\"') def unescape_quotes(value: str) -> str: - """Unescape double quotes in HTTP header values.""" - return value.replace('\\"', '"') + """Unescape backslashes and double quotes in HTTP quoted-strings.""" + return value.replace('\\"', '"').replace("\\\\", "\\") def parse_header_pairs(header: str) -> dict[str, str]: diff --git a/tests/test_client_middleware_digest_auth.py b/tests/test_client_middleware_digest_auth.py index f7094721e51..856f5df6954 100644 --- a/tests/test_client_middleware_digest_auth.py +++ b/tests/test_client_middleware_digest_auth.py @@ -551,6 +551,32 @@ async def test_escaping_quotes_in_auth_header() -> None: assert 'opaque="opaque\\"with\\"quotes"' in header +async def test_backslash_in_challenge_cannot_break_quoting() -> None: + """A backslash in a server-supplied value must not escape the closing quote.""" + auth = DigestAuthMiddleware("bob", "secret") + # A malicious/compromised server could return directives ending in a + # backslash; without escaping it the trailing quote of the field would be + # turned into an escaped quote, letting the value run into later directives. + auth._challenge = DigestAuthChallenge( + realm="realm\\", + nonce="n0", + qop="auth", + algorithm="MD5", + opaque="op\\", + ) + + header = await auth._encode("GET", URL("http://example.com/path"), b"") + + assert 'realm="realm\\\\"' in header + assert 'opaque="op\\\\"' in header + # Re-parsing the header must recover the exact realm value, i.e. the + # backslash did not consume the closing quote and swallow ``nonce``. + params = parse_header_pairs(header[len("Digest ") :]) + assert params["realm"] == "realm\\" + assert params["nonce"] == "n0" + assert params["opaque"] == "op\\" + + async def test_template_based_header_construction( auth_mw_with_challenge: DigestAuthMiddleware, mock_sha1_digest: mock.MagicMock, @@ -624,7 +650,9 @@ async def test_template_based_header_construction( ), ('""', '\\"\\"', "Just double quotes"), ('"', '\\"', "Single double quote"), - ('already\\"escaped', 'already\\\\"escaped', "Already escaped quotes"), + ('already\\"escaped', 'already\\\\\\"escaped', "Already escaped quotes"), + ("back\\slash", "back\\\\slash", "Embedded backslash"), + ("trailing\\", "trailing\\\\", "Trailing backslash"), ], ) def test_quote_escaping_functions( From 380d4b55e8df48dfd62f1addfb530426f6bc4106 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:49:03 +0100 Subject: [PATCH 135/137] [PR #13054/ed8b040c backport][3.14] escape backslashes in digest auth quoted-string values (#13197) **This is a backport of PR #13054 as merged into master (ed8b040c81dfdce5793271c7d6c3aa36c97a2ba3).** Co-authored-by: Javid Khan --- CHANGES/13054.bugfix.rst | 4 +++ CONTRIBUTORS.txt | 1 + aiohttp/client_middleware_digest_auth.py | 8 +++--- tests/test_client_middleware_digest_auth.py | 30 ++++++++++++++++++++- 4 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 CHANGES/13054.bugfix.rst diff --git a/CHANGES/13054.bugfix.rst b/CHANGES/13054.bugfix.rst new file mode 100644 index 00000000000..75196b9bfe6 --- /dev/null +++ b/CHANGES/13054.bugfix.rst @@ -0,0 +1,4 @@ +Fixed ``escape_quotes`` in the Digest authentication middleware not escaping +backslashes, so a ``WWW-Authenticate`` challenge value containing a backslash +could break out of its quoted-string in the generated ``Authorization`` header +-- by :user:`dxbjavid`. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 3e3893856cc..4bb43d068cf 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -189,6 +189,7 @@ Jan Buchar Jan Gosmann Jarno Elonen Jashandeep Sohi +Javid Khan Javier Torres Jean-Baptiste Estival Jens Steinhauser diff --git a/aiohttp/client_middleware_digest_auth.py b/aiohttp/client_middleware_digest_auth.py index f4904e363d8..fbcdf549b4a 100644 --- a/aiohttp/client_middleware_digest_auth.py +++ b/aiohttp/client_middleware_digest_auth.py @@ -102,13 +102,13 @@ class DigestAuthChallenge(TypedDict, total=False): def escape_quotes(value: str) -> str: - """Escape double quotes for HTTP header values.""" - return value.replace('"', '\\"') + """Escape backslashes and double quotes for HTTP quoted-strings.""" + return value.replace("\\", "\\\\").replace('"', '\\"') def unescape_quotes(value: str) -> str: - """Unescape double quotes in HTTP header values.""" - return value.replace('\\"', '"') + """Unescape backslashes and double quotes in HTTP quoted-strings.""" + return value.replace('\\"', '"').replace("\\\\", "\\") def parse_header_pairs(header: str) -> dict[str, str]: diff --git a/tests/test_client_middleware_digest_auth.py b/tests/test_client_middleware_digest_auth.py index f7094721e51..856f5df6954 100644 --- a/tests/test_client_middleware_digest_auth.py +++ b/tests/test_client_middleware_digest_auth.py @@ -551,6 +551,32 @@ async def test_escaping_quotes_in_auth_header() -> None: assert 'opaque="opaque\\"with\\"quotes"' in header +async def test_backslash_in_challenge_cannot_break_quoting() -> None: + """A backslash in a server-supplied value must not escape the closing quote.""" + auth = DigestAuthMiddleware("bob", "secret") + # A malicious/compromised server could return directives ending in a + # backslash; without escaping it the trailing quote of the field would be + # turned into an escaped quote, letting the value run into later directives. + auth._challenge = DigestAuthChallenge( + realm="realm\\", + nonce="n0", + qop="auth", + algorithm="MD5", + opaque="op\\", + ) + + header = await auth._encode("GET", URL("http://example.com/path"), b"") + + assert 'realm="realm\\\\"' in header + assert 'opaque="op\\\\"' in header + # Re-parsing the header must recover the exact realm value, i.e. the + # backslash did not consume the closing quote and swallow ``nonce``. + params = parse_header_pairs(header[len("Digest ") :]) + assert params["realm"] == "realm\\" + assert params["nonce"] == "n0" + assert params["opaque"] == "op\\" + + async def test_template_based_header_construction( auth_mw_with_challenge: DigestAuthMiddleware, mock_sha1_digest: mock.MagicMock, @@ -624,7 +650,9 @@ async def test_template_based_header_construction( ), ('""', '\\"\\"', "Just double quotes"), ('"', '\\"', "Single double quote"), - ('already\\"escaped', 'already\\\\"escaped', "Already escaped quotes"), + ('already\\"escaped', 'already\\\\\\"escaped', "Already escaped quotes"), + ("back\\slash", "back\\\\slash", "Embedded backslash"), + ("trailing\\", "trailing\\\\", "Trailing backslash"), ], ) def test_quote_escaping_functions( From b786c39f66d2582f33a388fad8b6b83973d85792 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 18:51:23 +0100 Subject: [PATCH 136/137] [pre-commit.ci] pre-commit autoupdate (#13199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/codespell-project/codespell: v2.4.2 β†’ v2.4.3](https://github.com/codespell-project/codespell/compare/v2.4.2...v2.4.3) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99014a20b50..ebb9ede885c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -121,7 +121,7 @@ repos: exclude: >- ^CHANGES\.rst$ - repo: https://github.com/codespell-project/codespell - rev: v2.4.2 + rev: v2.4.3 hooks: - id: codespell additional_dependencies: From c1b9212ad3d93c24b5fc66ad0849597166bc816e Mon Sep 17 00:00:00 2001 From: Sam Bull Date: Mon, 20 Jul 2026 19:51:04 +0100 Subject: [PATCH 137/137] Release v3.14.2 (#13201) --- CHANGES.rst | 233 ++++++++++++++++++++++++++++++++++++ CHANGES/12794.bugfix.rst | 4 - CHANGES/12879.bugfix.rst | 1 - CHANGES/12914.contrib.rst | 3 - CHANGES/12931.bugfix.rst | 1 - CHANGES/12948.bugfix.rst | 3 - CHANGES/12953.bugfix.rst | 6 - CHANGES/12954.bugfix.rst | 1 - CHANGES/12956.packaging.rst | 1 - CHANGES/12976.bugfix.rst | 1 - CHANGES/12983.bugfix.rst | 1 - CHANGES/12984.bugfix.rst | 3 - CHANGES/12985.bugfix.rst | 1 - CHANGES/12988.bugfix.rst | 1 - CHANGES/12996.bugfix.rst | 5 - CHANGES/13001.bugfix.rst | 1 - CHANGES/13002.bugfix.rst | 1 - CHANGES/13016.bugfix.rst | 1 - CHANGES/13042.bugfix.rst | 4 - CHANGES/13054.bugfix.rst | 4 - CHANGES/13136.bugfix.rst | 1 - CHANGES/13171.bugfix.rst | 3 - CHANGES/13174.bugfix.rst | 1 - CHANGES/5303.bugfix.rst | 7 -- CHANGES/5357.bugfix.rst | 3 - aiohttp/__init__.py | 2 +- 26 files changed, 234 insertions(+), 59 deletions(-) delete mode 100644 CHANGES/12794.bugfix.rst delete mode 100644 CHANGES/12879.bugfix.rst delete mode 100644 CHANGES/12914.contrib.rst delete mode 100644 CHANGES/12931.bugfix.rst delete mode 100644 CHANGES/12948.bugfix.rst delete mode 100644 CHANGES/12953.bugfix.rst delete mode 120000 CHANGES/12954.bugfix.rst delete mode 100644 CHANGES/12956.packaging.rst delete mode 100644 CHANGES/12976.bugfix.rst delete mode 100644 CHANGES/12983.bugfix.rst delete mode 100644 CHANGES/12984.bugfix.rst delete mode 100644 CHANGES/12985.bugfix.rst delete mode 100644 CHANGES/12988.bugfix.rst delete mode 100644 CHANGES/12996.bugfix.rst delete mode 100644 CHANGES/13001.bugfix.rst delete mode 100644 CHANGES/13002.bugfix.rst delete mode 100644 CHANGES/13016.bugfix.rst delete mode 100644 CHANGES/13042.bugfix.rst delete mode 100644 CHANGES/13054.bugfix.rst delete mode 100644 CHANGES/13136.bugfix.rst delete mode 100644 CHANGES/13171.bugfix.rst delete mode 100644 CHANGES/13174.bugfix.rst delete mode 100644 CHANGES/5303.bugfix.rst delete mode 100644 CHANGES/5357.bugfix.rst diff --git a/CHANGES.rst b/CHANGES.rst index 09d5c2efcaf..03aac46c4d1 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,6 +10,239 @@ .. towncrier release notes start +3.14.2 (2026-07-20) +=================== + +Bug fixes +--------- + +- Fixed :py:attr:`~aiohttp.web.StreamResponse.last_modified` rounding a + :class:`datetime.datetime` with a fractional second down. + + + *Related issues and pull requests on GitHub:* + :issue:`5303`. + + + +- Fixed resolving ``localhost`` on Windows to fall back without ``AI_ADDRCONFIG`` + when the first lookup fails, so ``localhost`` still works without an active + network. + + + *Related issues and pull requests on GitHub:* + :issue:`5357`. + + + +- Rejected multipart body parts whose ``Content-Length`` header is not a + plain sequence of digits (e.g. ``+5``, ``-1``, ``1_0``), matching the + strictness of the main request parser per :rfc:`9110#section-8.6` + -- by :user:`dxbjavid`. + + + *Related issues and pull requests on GitHub:* + :issue:`12794`. + + + +- Fixed ``GunicornWebWorker`` endlessly reloading when app fails during startup -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12879`. + + + +- Fixed some inconsistent case sensitivity on request methods -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12931`. + + + +- Fixed ``IndexError: string index out of range`` in ``parse_content_disposition`` + when a header parameter has an empty value (e.g. ``filename=``). + -- by :user:`JSap0914`. + + + *Related issues and pull requests on GitHub:* + :issue:`12948`. + + + +- Fixed the ``sock_read`` timeout being re-armed on a keep-alive connection after + it had been returned to the pool. An idle pooled connection could be left with a + pending read timeout that fired and poisoned it, so the next request reusing the + connection failed immediately with :exc:`aiohttp.SocketTimeoutError`. The read + timeout is now only rescheduled when resuming a transport that was actually + paused -- by :user:`daragok`. + + + *Related issues and pull requests on GitHub:* + :issue:`12953`, :issue:`12954`. + + + +- Fixed the client decompressing frames when ``permessage-deflate`` was not negotiated -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12976`. + + + +- Fixed ``DigestAuthMiddleware`` raising an ``IndexError`` on empty domain -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12983`. + + + +- Fixed :class:`~aiohttp.DigestAuthMiddleware` corrupting the ``Digest`` + challenge when a ``WWW-Authenticate`` response offered more than one + authentication scheme -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12984`. + + + +- Fixed client not closing cleanly after an exception -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12985`. + + + +- Fixed control frames breaking fragmented WebSocket messages -- by :user:`arshsmith1`. + + + *Related issues and pull requests on GitHub:* + :issue:`12988`. + + + +- Fixed ``parse_content_disposition`` rejecting otherwise-valid + ``Content-Disposition`` header values that contain optional whitespace (OWS) + around the disposition type (e.g. ``"form-data ; name=\"field\""``). + The disposition type is now stripped before token validation, consistent with + how parameter keys are already handled -- by :user:`JSap0914`. + + + *Related issues and pull requests on GitHub:* + :issue:`12996`. + + + +- Fixed an :exc:`IndexError` in the pure-Python HTTP parser -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`13001`. + + + +- Fixed parsing optional whitespace in Content-Disposition -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`13002`. + + + +- Fixed request body not being read on rejected WebSocket upgrades -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`13016`. + + + +- Fixed :exc:`LookupError` (and an unguarded :exc:`UnicodeDecodeError`) escaping + ``Content-Disposition`` parsing when a multipart part supplies an extended + parameter with an unknown charset + -- by :user:`arshsmith1`. + + + *Related issues and pull requests on GitHub:* + :issue:`13042`. + + + +- Fixed ``escape_quotes`` in the Digest authentication middleware not escaping + backslashes, so a ``WWW-Authenticate`` challenge value containing a backslash + could break out of its quoted-string in the generated ``Authorization`` header + -- by :user:`dxbjavid`. + + + *Related issues and pull requests on GitHub:* + :issue:`13054`. + + + +- Fixed Python parser not rejecting a bare ``LF`` in the request line -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`13136`. + + + +- Fixed the C HTTP parser folding the fragment into the query string for an + origin-form request target with an empty query (e.g. ``/path?#frag``), + which diverged from the pure-Python parser -- by :user:`GiulioDER`. + + + *Related issues and pull requests on GitHub:* + :issue:`13171`. + + + +- Fixed the C parser reporting newer HTTP methods such as ``QUERY`` as ````; + the method table is now derived from the vendored llhttp instead of a hand-maintained count + -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`13174`. + + + + +Packaging updates and notes for downstreams +------------------------------------------- + +- Upgraded ``llhttp`` to v9.4.2 -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12956`. + + + + +Contributor-facing changes +-------------------------- + +- Added admin documentation on incident response and on running reproducer code + safely, covering security vulnerability handling and supply-chain, account, and + CI/infrastructure compromise -- by :user:`Dreamsorcerer`. + + + *Related issues and pull requests on GitHub:* + :issue:`12914`. + + + + +---- + + 3.14.1 (2026-06-07) =================== diff --git a/CHANGES/12794.bugfix.rst b/CHANGES/12794.bugfix.rst deleted file mode 100644 index ec72efc1f3c..00000000000 --- a/CHANGES/12794.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -Rejected multipart body parts whose ``Content-Length`` header is not a -plain sequence of digits (e.g. ``+5``, ``-1``, ``1_0``), matching the -strictness of the main request parser per :rfc:`9110#section-8.6` --- by :user:`dxbjavid`. diff --git a/CHANGES/12879.bugfix.rst b/CHANGES/12879.bugfix.rst deleted file mode 100644 index 9dc8057a64d..00000000000 --- a/CHANGES/12879.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed ``GunicornWebWorker`` endlessly reloading when app fails during startup -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12914.contrib.rst b/CHANGES/12914.contrib.rst deleted file mode 100644 index 5ef030c2786..00000000000 --- a/CHANGES/12914.contrib.rst +++ /dev/null @@ -1,3 +0,0 @@ -Added admin documentation on incident response and on running reproducer code -safely, covering security vulnerability handling and supply-chain, account, and -CI/infrastructure compromise -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12931.bugfix.rst b/CHANGES/12931.bugfix.rst deleted file mode 100644 index 6b62e87dac7..00000000000 --- a/CHANGES/12931.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed some inconsistent case sensitivity on request methods -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12948.bugfix.rst b/CHANGES/12948.bugfix.rst deleted file mode 100644 index 8cd00cfe0a6..00000000000 --- a/CHANGES/12948.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed ``IndexError: string index out of range`` in ``parse_content_disposition`` -when a header parameter has an empty value (e.g. ``filename=``). --- by :user:`JSap0914`. diff --git a/CHANGES/12953.bugfix.rst b/CHANGES/12953.bugfix.rst deleted file mode 100644 index 20edc2dda87..00000000000 --- a/CHANGES/12953.bugfix.rst +++ /dev/null @@ -1,6 +0,0 @@ -Fixed the ``sock_read`` timeout being re-armed on a keep-alive connection after -it had been returned to the pool. An idle pooled connection could be left with a -pending read timeout that fired and poisoned it, so the next request reusing the -connection failed immediately with :exc:`aiohttp.SocketTimeoutError`. The read -timeout is now only rescheduled when resuming a transport that was actually -paused -- by :user:`daragok`. diff --git a/CHANGES/12954.bugfix.rst b/CHANGES/12954.bugfix.rst deleted file mode 120000 index baccfb94cc8..00000000000 --- a/CHANGES/12954.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -12953.bugfix.rst \ No newline at end of file diff --git a/CHANGES/12956.packaging.rst b/CHANGES/12956.packaging.rst deleted file mode 100644 index 7ea0cd11ac6..00000000000 --- a/CHANGES/12956.packaging.rst +++ /dev/null @@ -1 +0,0 @@ -Upgraded ``llhttp`` to v9.4.2 -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12976.bugfix.rst b/CHANGES/12976.bugfix.rst deleted file mode 100644 index 008095a3351..00000000000 --- a/CHANGES/12976.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed the client decompressing frames when ``permessage-deflate`` was not negotiated -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12983.bugfix.rst b/CHANGES/12983.bugfix.rst deleted file mode 100644 index 03f4083d83a..00000000000 --- a/CHANGES/12983.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed ``DigestAuthMiddleware`` raising an ``IndexError`` on empty domain -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12984.bugfix.rst b/CHANGES/12984.bugfix.rst deleted file mode 100644 index a156216397b..00000000000 --- a/CHANGES/12984.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed :class:`~aiohttp.DigestAuthMiddleware` corrupting the ``Digest`` -challenge when a ``WWW-Authenticate`` response offered more than one -authentication scheme -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12985.bugfix.rst b/CHANGES/12985.bugfix.rst deleted file mode 100644 index 055b8572e42..00000000000 --- a/CHANGES/12985.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed client not closing cleanly after an exception -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/12988.bugfix.rst b/CHANGES/12988.bugfix.rst deleted file mode 100644 index ff07271fa31..00000000000 --- a/CHANGES/12988.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed control frames breaking fragmented WebSocket messages -- by :user:`arshsmith1`. diff --git a/CHANGES/12996.bugfix.rst b/CHANGES/12996.bugfix.rst deleted file mode 100644 index c43d973fb77..00000000000 --- a/CHANGES/12996.bugfix.rst +++ /dev/null @@ -1,5 +0,0 @@ -Fixed ``parse_content_disposition`` rejecting otherwise-valid -``Content-Disposition`` header values that contain optional whitespace (OWS) -around the disposition type (e.g. ``"form-data ; name=\"field\""``). -The disposition type is now stripped before token validation, consistent with -how parameter keys are already handled -- by :user:`JSap0914`. diff --git a/CHANGES/13001.bugfix.rst b/CHANGES/13001.bugfix.rst deleted file mode 100644 index 67511e3c95e..00000000000 --- a/CHANGES/13001.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed an :exc:`IndexError` in the pure-Python HTTP parser -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/13002.bugfix.rst b/CHANGES/13002.bugfix.rst deleted file mode 100644 index 1ac16a04fa7..00000000000 --- a/CHANGES/13002.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed parsing optional whitespace in Content-Disposition -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/13016.bugfix.rst b/CHANGES/13016.bugfix.rst deleted file mode 100644 index a984f64e333..00000000000 --- a/CHANGES/13016.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed request body not being read on rejected WebSocket upgrades -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/13042.bugfix.rst b/CHANGES/13042.bugfix.rst deleted file mode 100644 index ecda7970b60..00000000000 --- a/CHANGES/13042.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fixed :exc:`LookupError` (and an unguarded :exc:`UnicodeDecodeError`) escaping -``Content-Disposition`` parsing when a multipart part supplies an extended -parameter with an unknown charset, e.g. ``filename*=unknown-8bit''...`` --- by :user:`arshsmith1`. diff --git a/CHANGES/13054.bugfix.rst b/CHANGES/13054.bugfix.rst deleted file mode 100644 index 75196b9bfe6..00000000000 --- a/CHANGES/13054.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -Fixed ``escape_quotes`` in the Digest authentication middleware not escaping -backslashes, so a ``WWW-Authenticate`` challenge value containing a backslash -could break out of its quoted-string in the generated ``Authorization`` header --- by :user:`dxbjavid`. diff --git a/CHANGES/13136.bugfix.rst b/CHANGES/13136.bugfix.rst deleted file mode 100644 index 55fdd4228ca..00000000000 --- a/CHANGES/13136.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed Python parser not rejecting a bare ``LF`` in the request line -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/13171.bugfix.rst b/CHANGES/13171.bugfix.rst deleted file mode 100644 index fead0685a8e..00000000000 --- a/CHANGES/13171.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed the C HTTP parser folding the fragment into the query string for an -origin-form request target with an empty query (e.g. ``/path?#frag``), -which diverged from the pure-Python parser -- by :user:`GiulioDER`. diff --git a/CHANGES/13174.bugfix.rst b/CHANGES/13174.bugfix.rst deleted file mode 100644 index 56cf18eea32..00000000000 --- a/CHANGES/13174.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed the C parser reporting newer HTTP methods such as ``QUERY`` as ````; the method table is now derived from the vendored llhttp instead of a hand-maintained count -- by :user:`Dreamsorcerer`. diff --git a/CHANGES/5303.bugfix.rst b/CHANGES/5303.bugfix.rst deleted file mode 100644 index fcb1e7c8ae9..00000000000 --- a/CHANGES/5303.bugfix.rst +++ /dev/null @@ -1,7 +0,0 @@ -Fixed :py:attr:`~aiohttp.web.StreamResponse.last_modified` rounding a -:class:`datetime.datetime` with a fractional second down while rounding an -equivalent :class:`float` timestamp up, so the two produced different -``Last-Modified`` headers for the same instant. Both now round up to the -next whole second, consistent with how :py:class:`~aiohttp.web.FileResponse` -relies on this rounding to avoid falsely reporting a resource as -unmodified. diff --git a/CHANGES/5357.bugfix.rst b/CHANGES/5357.bugfix.rst deleted file mode 100644 index dd0125cb9ba..00000000000 --- a/CHANGES/5357.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed resolving ``localhost`` on Windows to fall back without ``AI_ADDRCONFIG`` -when the first lookup fails, so ``localhost`` still works without an active -network. diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index 5dfcd3841b9..4c63f90e1fa 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "3.14.1.dev0" +__version__ = "3.14.2" from typing import TYPE_CHECKING