From 35aba242a163fb4a991bb42fb456e87e0c31177b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=BD=E3=83=8C?= Date: Sun, 20 Sep 2026 13:53:48 +0530 Subject: [PATCH 1/2] pyodide: preserve User-Agent header in httpx and httpx2 requests In Python Workers, outbound requests using httpx or httpx2 silently dropped custom User-Agent headers. Direct calls via fetch(req) preserved the header as expected. In the Emscripten fetch transports used by httpx (0.28.1) and httpx2 (via httpx2_jsfetch), HEADERS_TO_IGNORE = ("user-agent",) explicitly stripped the header to avoid browser console warnings, where User-Agent is considered a forbidden request header. In workerd and Node.js, fetch() fully allows setting custom User-Agent headers without restriction. Register import execution patches for both httpx._transports.emscripten and httpx2_jsfetch in entropy_import_context_packages.py, clearing module.HEADERS_TO_IGNORE to an empty tuple upon module execution. Add regression unit test in test_entropy_patches.py and wire the unit-test-entropy-patches target in src/pyodide/BUILD.bazel. Fixes #7242 --- src/pyodide/BUILD.bazel | 14 ++++++ src/pyodide/internal/test_entropy_patches.py | 44 +++++++++++++++++++ .../entropy_import_context_packages.py | 18 ++++++++ 3 files changed, 76 insertions(+) create mode 100644 src/pyodide/internal/test_entropy_patches.py diff --git a/src/pyodide/BUILD.bazel b/src/pyodide/BUILD.bazel index 4f0fa134c20..e6fec05ed32 100644 --- a/src/pyodide/BUILD.bazel +++ b/src/pyodide/BUILD.bazel @@ -91,3 +91,17 @@ py_test( main = "internal/test_frozen_sdk.py", target_compatible_with = ["@platforms//os:linux"], ) + +py_test( + name = "unit-test-entropy-patches", + srcs = [ + "internal/test_entropy_patches.py", + "internal/topLevelEntropy/__init__.py", + "internal/topLevelEntropy/allow_entropy.py", + "internal/topLevelEntropy/entropy_import_context.py", + "internal/topLevelEntropy/entropy_import_context_packages.py", + "internal/topLevelEntropy/entropy_patches.py", + "internal/topLevelEntropy/import_patch_manager.py", + ], + main = "internal/test_entropy_patches.py", +) diff --git a/src/pyodide/internal/test_entropy_patches.py b/src/pyodide/internal/test_entropy_patches.py new file mode 100644 index 00000000000..dffeb6b70e4 --- /dev/null +++ b/src/pyodide/internal/test_entropy_patches.py @@ -0,0 +1,44 @@ +# Copyright (c) 2026 Cloudflare, Inc. +# Licensed under the Apache 2.0 license found in the LICENSE file or at: +# https://opensource.org/licenses/Apache-2.0 + +import sys +import unittest +from pathlib import Path +from types import SimpleNamespace + +DIR = Path(__file__).parent +sys.path.insert(0, str(DIR)) + +from topLevelEntropy.import_patch_manager import patches +import topLevelEntropy.entropy_import_context_packages # noqa: F401 + + +class TestEntropyPatches(unittest.TestCase): + def test_httpx_user_agent_patch(self): + """Ensure httpx._transports.emscripten has HEADERS_TO_IGNORE cleared.""" + self.assertIn("httpx._transports.emscripten", patches) + mock_httpx = SimpleNamespace(HEADERS_TO_IGNORE=("user-agent",)) + with patches["httpx._transports.emscripten"].exec(mock_httpx): + pass + self.assertEqual( + mock_httpx.HEADERS_TO_IGNORE, + (), + "Expected HEADERS_TO_IGNORE to be cleared for httpx", + ) + + def test_httpx2_user_agent_patch(self): + """Ensure httpx2_jsfetch has HEADERS_TO_IGNORE cleared.""" + self.assertIn("httpx2_jsfetch", patches) + mock_httpx2 = SimpleNamespace(HEADERS_TO_IGNORE=("user-agent",)) + with patches["httpx2_jsfetch"].exec(mock_httpx2): + pass + self.assertEqual( + mock_httpx2.HEADERS_TO_IGNORE, + (), + "Expected HEADERS_TO_IGNORE to be cleared for httpx2_jsfetch", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/pyodide/internal/topLevelEntropy/entropy_import_context_packages.py b/src/pyodide/internal/topLevelEntropy/entropy_import_context_packages.py index b6351af72ba..fa03cb74c4f 100644 --- a/src/pyodide/internal/topLevelEntropy/entropy_import_context_packages.py +++ b/src/pyodide/internal/topLevelEntropy/entropy_import_context_packages.py @@ -190,3 +190,21 @@ def langchain_openai_chat_models_base_context(module): yield else: yield + + +@register_exec_patch("httpx._transports.emscripten") +@contextmanager +def httpx_emscripten_context(module): + yield + # In browser environments, User-Agent is a forbidden request header and causes console warnings. + # In workerd, fetch() natively supports custom User-Agent headers without restriction. + module.HEADERS_TO_IGNORE = () + + +@register_exec_patch("httpx2_jsfetch") +@contextmanager +def httpx2_jsfetch_context(module): + yield + # In browser environments, User-Agent is a forbidden request header and causes console warnings. + # In workerd, fetch() natively supports custom User-Agent headers without restriction. + module.HEADERS_TO_IGNORE = () From 9ea09fc1444e580f576495b98bfa7d88f5656897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=BD=E3=83=8C?= Date: Sun, 20 Sep 2026 13:53:59 +0530 Subject: [PATCH 2/2] ci: add GitHub Actions workflow for Python runtime and container tests Add a dedicated workflow in .github/workflows/python-tests.yml running on ubuntu-latest. The workflow executes matrix unit testing across Python 3.12 and 3.13, runs the containerized test suite in python:3.12-slim via Docker, and verifies git diff whitespace hygiene. Add /.context/ to .gitignore to exclude local investigation scratchpads. --- .github/workflows/python-tests.yml | 49 ++++++++++++++++++++++++++++++ .gitignore | 1 + 2 files changed, 50 insertions(+) create mode 100644 .github/workflows/python-tests.yml diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 00000000000..cf1747d178f --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,49 @@ +name: Python Runtime Tests + +on: + pull_request: + push: + branches: + - main + workflow_dispatch: + +concurrency: + group: python-tests-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + python-unit-tests: + name: Python Unit Tests (Python ${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.12', '3.13'] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Run Python internal unit tests + run: | + python3 -m unittest discover -s src/pyodide/internal -p "test_*.py" + + - name: Check git diff and whitespace hygiene + run: | + git diff --check + + docker-tests: + name: Containerized Test Suite (Docker) + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run tests inside Docker container + run: | + docker run --rm -v "$GITHUB_WORKSPACE":/workerd -w /workerd python:3.12-slim \ + python3 -m unittest discover -s src/pyodide/internal -p "test_*.py" diff --git a/.gitignore b/.gitignore index 1013d3449fe..55615d1245f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ MODULE.bazel.lock /.cache/ /.bazel-cache +/.context/ /artifacts /.gitlab-ci-ca-file /.gitlab-ci-gitconfig