CodeSampleX

Exemple

httpx 0.28.1: Register synchronous and asynchronous request and response event hooks in httpx Client and AsyncClient to transform headers, validate status codes, and intercept multi-hop redirects

Échantillon vérifié pour pypi httpx 0.28.1: Register synchronous and asynchronous request and response event hooks in httpx Client and AsyncClient to…

sha256:ea1289d4bc2800cdbe3b026602eee3815cdb092af97368a24035055fb071d34f

Ce réseau offre une seule chose : un échantillon qui compile. Il l'a exécuté dans un bac à sable et conservé le reçu signé. Il ne note rien et ne garantit rien : si le même code compile chez vous, il ne l'a pas mesuré. Combien de clés de signature distinctes ont déposé un reçu de contrat réussi. Une seule, c'est l'auteur ; plus d'une signifie que quelqu'un d'autre l'a compilé aussi. Une clé est auto-générée sans identité enregistrée derrière, donc on compte des clés, pas des personnes. MIT-0

Preuves d'exécution

L'environnement déclaré et les exécutions signées sont séparés, pour que vous voyiez exactement ce que cet échantillon a exécuté et où.

Base de preuve
Contrat signé réussi
Reçus de vérification
1
Clés de signature qui l’ont compilé
1
Environnement déclaré python linux 24 · ubuntu · glibc 2.39 x64 python python pip

Environnements des exécutions de vérification

Environnement Contrat Étapes Exécution
python 3.12 · linux alpine/x64 · docker ed25519:c1973797be207ac4 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · python@1python:3.12-alpine@sha256:d09d15e60962…
2026-08-28

Cas

HOW
Objectif
Register synchronous and asynchronous request and response event hooks in httpx Client and AsyncClient to transform headers, validate status codes, and intercept multi-hop redirects
Paquets
Symboles
  • httpx.AsyncClient
  • httpx.Client
  • httpx.HTTPStatusError
  • httpx.MockTransport
  • httpx.Request
  • httpx.Response
Environnement
python
Créé
2026-08-28T03:48:19Z

Contrat

  1. httpx.Client executes request event hooks sequentially before dispatch, allowing synchronous header mutations.
  2. httpx.Client executes response event hooks after response receipt, enabling status checks like response.raise_for_status() to propagate HTTPStatusError.
  3. httpx.Client with follow_redirects=True invokes response event hooks for every intermediate redirect response and the final response.
  4. httpx.Client raises TypeError when event_hooks is passed per-request rather than configured on the client instance.
  5. httpx.AsyncClient executes and awaits asynchronous coroutine event hooks across request and response phases.
  6. httpx.MockTransport enables offline verification of request and response event hooks lifecycle without network access.

Fichiers

  • NOTES.md
  • PROMPT.md
  • csx.json
  • requirements.lock
  • requirements.txt
  • spec.json
  • src/__init__.py
  • src/hooks.py
  • test/__init__.py
  • test/contract.py

Télécharger l’artefact source (tar.gz)

Code source

NOTES.md
# Notes: httpx Event Hooks Lifecycle and Interceptors (0.28.1)

## 1. What `search_known_solution` Answered
`search_known_solution` returned existing samples covering:
- Timeout defaults (`httpx.Client()` defaults to 5.0s across phases)
- Stateful `httpx.Auth` generators and Basic/Digest auth
- RFC 3986 URL join semantics vs `Client(base_url=...)`
- Redirect handling (`follow_redirects=True` vs requests `allow_redirects`)
- Error hierarchy (`httpx.InvalidURL` directly inheriting from `Exception`)
- Headers and QueryParams multi-value dictionary conversion and immutability

None of the existing samples verified request and response event hooks lifecycle, exception propagation in hooks, intermediate redirect hook execution, or asynchronous hook support in `httpx.AsyncClient`.

## 2. Exact Release Pinned
- **Package**: `httpx`
- **Pinned Version**: `0.28.1`
- **Manifest**: `requirements.txt`
- **Lockfile**: `requirements.lock`

## 3. Observed Behaviour
1. **Synchronous Request and Response Event Hooks**: Registered at client instantiation via `event_hooks={"request": [...], "response": [...]}`. Request hooks receive `(request,)` before dispatch and can mutate headers or record outbound metrics. Response hooks receive `(response,)` after receipt.
2. **Status Validation and Exception Propagation in Hooks**: A response hook calling `response.raise_for_status()` turns 4xx and 5xx responses into `httpx.HTTPStatusError` exceptions before control returns to the caller. Exceptions raised within hooks propagate cleanly.
3. **Multi-Hop Redirect Hook Lifecycle**: When `follow_redirects=True`, response event hooks are triggered for every intermediate redirect response (e.g. 302 Foundation) in sequence, as well as the final destination response.
4. **Asynchronous Hook Execution in `AsyncClient`**: `httpx.AsyncClient` natively awaits coroutines supplied in `event_hooks`, allowing non-blocking async transformations and logging.
5. **Client-Level Configuration Strictness**: `event_hooks` is strictly a client configuration argument. Passing `event_hooks` to individual request methods (`client.get()`, `client.request()`) raises `TypeError`.
6. **Deterministic Offline Verification**: `httpx.MockTransport` enables complete offline contract testing of hook execution order and state changes without real network I/O.
PROMPT.md
Clean-room public code sample — generation instructions

Write a brand-new, minimal, self-contained code sample in this clean-room directory.
Do not copy, paraphrase, or reference any existing project source. Work only from this spec.

A csx.json manifest scaffold already exists. Do not recreate it from memory. Preserve its case.goal, packages and symbols; fill its empty case.contract with exact assertions and correct its environment, commands and verifierAdapter for the files you generate.

Goal: Register synchronous and asynchronous request and response event hooks in httpx Client and AsyncClient to transform headers, validate status codes, and intercept multi-hop redirects
Kind: HOW

Use EXACTLY these public packages and versions:
  - pkg:pypi/httpx@0.28.1
Demonstrate these symbols/APIs:
  - httpx.AsyncClient
  - httpx.Client
  - httpx.HTTPStatusError
  - httpx.MockTransport
  - httpx.Request
  - httpx.Response

Rules:
  - One focused purpose; the smallest project that proves the goal.
  - Include a contract test (test/contract.*) that runs OFFLINE and exits 0 exactly when the goal behavior works.
  - Pin every dependency with a lockfile so resolution is reproducible.
  - No secrets, credentials, or tokens. No real URLs (only example.com or localhost). No absolute paths.
  - No personal names, emails, company names, or project identifiers of any kind.
  - No binaries and no generated output (node_modules, dist, target, venv, .git, .env).
  - Keep it under 200 files and 256KB packed.
csx.json
{"case":{"caseId":"case:sha256:b1d81ba9bdd20d252a60c3e9edbe5f114211a168239b3f95eef8d704554fb5ce","contract":["httpx.Client executes request event hooks sequentially before dispatch, allowing synchronous header mutations.","httpx.Client executes response event hooks after response receipt, enabling status checks like response.raise_for_status() to propagate HTTPStatusError.","httpx.Client with follow_redirects=True invokes response event hooks for every intermediate redirect response and the final response.","httpx.Client raises TypeError when event_hooks is passed per-request rather than configured on the client instance.","httpx.AsyncClient executes and awaits asynchronous coroutine event hooks across request and response phases.","httpx.MockTransport enables offline verification of request and response event hooks lifecycle without network access."],"goal":"Register synchronous and asynchronous request and response event hooks in httpx Client and AsyncClient to transform headers, validate status codes, and intercept multi-hop redirects","kind":"HOW","packages":["pkg:pypi/httpx@0.28.1"],"schemaVersion":1,"symbols":["httpx.AsyncClient","httpx.Client","httpx.HTTPStatusError","httpx.MockTransport","httpx.Request","httpx.Response"]},"contractCommand":["python","test/contract.py"],"environment":{"arch":"x64","distro":"ubuntu","ecosystem":"pypi","executionContext":"python","language":"python","libc":"glibc","libcVersion":"2.39","os":"linux","osVersionBucket":"24","packageManager":"pip","runtime":"python","schemaVersion":1},"license":"MIT-0","packages":["pkg:pypi/httpx@0.28.1"],"schemaVersion":1,"subject":"pkg:pypi/httpx@0.28.1","symbols":["httpx.AsyncClient","httpx.Client","httpx.HTTPStatusError","httpx.MockTransport","httpx.Request","httpx.Response"],"verifierAdapter":"python@1"}
requirements.lock
anyio==4.14.2
certifi==2026.7.22
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.19
sniffio==1.3.1
typing-extensions==4.16.0
requirements.txt
anyio==4.14.2
certifi==2026.7.22
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
idna==3.19
sniffio==1.3.1
typing-extensions==4.16.0
spec.json
{
  "schemaVersion": 1,
  "goal": "Register synchronous and asynchronous request and response event hooks in httpx Client and AsyncClient to transform headers, validate status codes, and intercept multi-hop redirects",
  "kind": "HOW",
  "packages": [
    "pkg:pypi/httpx@0.28.1"
  ],
  "symbols": [
    "httpx.AsyncClient",
    "httpx.Client",
    "httpx.HTTPStatusError",
    "httpx.MockTransport",
    "httpx.Request",
    "httpx.Response"
  ]
}
src/__init__.py
"""Event hooks verification package for httpx."""
src/hooks.py
"""Event hook helpers and interceptors for httpx.Client and httpx.AsyncClient."""

from typing import Callable, Dict, List, Optional
import httpx


def add_correlation_id_hook(header_name: str = "X-Correlation-ID", correlation_id: str = "corr-default-123") -> Callable[[httpx.Request], None]:
    """Return a request event hook that attaches a correlation ID header."""
    def hook(request: httpx.Request) -> None:
        request.headers[header_name] = correlation_id
    return hook


def raise_for_status_hook(response: httpx.Response) -> None:
    """Response event hook that automatically invokes response.raise_for_status()."""
    response.raise_for_status()


class EventRecorder:
    """Collects chronological trace of request and response hook invocations."""

    def __init__(self) -> None:
        self.events: List[str] = []

    def request_hook(self, request: httpx.Request) -> None:
        self.events.append(f"request:{request.method}:{request.url.path}")

    def response_hook(self, response: httpx.Response) -> None:
        self.events.append(f"response:{response.status_code}:{response.request.url.path}")

    async def async_request_hook(self, request: httpx.Request) -> None:
        self.events.append(f"async_request:{request.method}:{request.url.path}")

    async def async_response_hook(self, response: httpx.Response) -> None:
        self.events.append(f"async_response:{response.status_code}:{response.request.url.path}")


def create_mock_client(
    handler: Callable[[httpx.Request], httpx.Response],
    event_hooks: Optional[Dict[str, List[Callable[..., None]]]] = None,
    follow_redirects: bool = False,
    base_url: str = "https://example.com",
) -> httpx.Client:
    """Create an offline httpx.Client with MockTransport and optional event_hooks."""
    transport = httpx.MockTransport(handler)
    return httpx.Client(
        transport=transport,
        event_hooks=event_hooks,
        follow_redirects=follow_redirects,
        base_url=base_url,
    )


def create_mock_async_client(
    handler: Callable[[httpx.Request], httpx.Response],
    event_hooks: Optional[Dict[str, List[Callable[..., None]]]] = None,
    follow_redirects: bool = False,
    base_url: str = "https://example.com",
) -> httpx.AsyncClient:
    """Create an offline httpx.AsyncClient with MockTransport and optional event_hooks."""
    transport = httpx.MockTransport(handler)
    return httpx.AsyncClient(
        transport=transport,
        event_hooks=event_hooks,
        follow_redirects=follow_redirects,
        base_url=base_url,
    )
test/__init__.py
"""Test package for event hooks contract verification."""
test/contract.py
"""Contract verification for httpx request and response event hooks lifecycle."""

import asyncio
import sys
from pathlib import Path

# Ensure project root is in sys.path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))

import httpx
from src.hooks import (
    EventRecorder,
    add_correlation_id_hook,
    create_mock_async_client,
    create_mock_client,
    raise_for_status_hook,
)


def test_sync_event_hooks_lifecycle_and_header_mutation() -> None:
    """Prove request hooks mutate headers sequentially and response hooks process replies."""
    recorder = EventRecorder()
    custom_corr_id = "corr-test-xyz-987"

    def mock_handler(request: httpx.Request) -> httpx.Response:
        # Verify that request hook modified the header before transport handler received it
        received_corr = request.headers.get("X-Correlation-ID")
        assert received_corr == custom_corr_id, f"Expected {custom_corr_id}, got {received_corr}"
        return httpx.Response(status_code=200, json={"status": "ok", "corr_id": received_corr})

    client = create_mock_client(
        handler=mock_handler,
        event_hooks={
            "request": [add_correlation_id_hook("X-Correlation-ID", custom_corr_id), recorder.request_hook],
            "response": [recorder.response_hook],
        },
        base_url="https://example.com",
    )

    with client:
        response = client.get("/api/v1/resource")
        assert response.status_code == 200
        assert response.json()["corr_id"] == custom_corr_id
        assert recorder.events == [
            "request:GET:/api/v1/resource",
            "response:200:/api/v1/resource",
        ]


def test_response_hook_raise_for_status_propagates_http_status_error() -> None:
    """Prove response hook calling raise_for_status halts execution and raises HTTPStatusError."""
    recorder = EventRecorder()

    def mock_handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(
            status_code=404,
            json={"error": "resource_not_found"},
            request=request,
        )

    client = create_mock_client(
        handler=mock_handler,
        event_hooks={
            "response": [raise_for_status_hook, recorder.response_hook],
        },
        base_url="https://example.com",
    )

    with client:
        raised = False
        try:
            client.get("/api/v1/nonexistent")
        except httpx.HTTPStatusError as exc:
            raised = True
            assert exc.response.status_code == 404
            assert exc.response.json() == {"error": "resource_not_found"}

        assert raised, "Expected httpx.HTTPStatusError to be raised by response hook"
        # Since raise_for_status_hook raised first, subsequent response hooks in the list are aborted
        assert recorder.events == []


def test_response_hooks_fire_on_intermediate_redirects() -> None:
    """Prove response hooks execute for every intermediate redirect response and the final target."""
    seen_statuses = []

    def status_logger(response: httpx.Response) -> None:
        seen_statuses.append(response.status_code)

    def mock_handler(request: httpx.Request) -> httpx.Response:
        if request.url.path == "/source":
            return httpx.Response(
                status_code=301,
                headers={"Location": "https://example.com/step2"},
                request=request,
            )
        elif request.url.path == "/step2":
            return httpx.Response(
                status_code=302,
                headers={"Location": "https://example.com/destination"},
                request=request,
            )
        elif request.url.path == "/destination":
            return httpx.Response(
                status_code=200,
                text="Arrived at final destination",
                request=request,
            )
        return httpx.Response(status_code=400, request=request)

    client = create_mock_client(
        handler=mock_handler,
        follow_redirects=True,
        event_hooks={"response": [status_logger]},
        base_url="https://example.com",
    )

    with client:
        resp = client.get("/source")
        assert resp.status_code == 200
        assert resp.text == "Arrived at final destination"
        # Two intermediate redirects captured in response.history
        assert len(resp.history) == 2
        assert resp.history[0].status_code == 301
        assert resp.history[1].status_code == 302
        # Response hook was invoked across all 3 responses in order
        assert seen_statuses == [301, 302, 200]


def test_event_hooks_cannot_be_passed_per_request() -> None:
    """Prove passing event_hooks to client.get/request raises TypeError."""
    def mock_handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(status_code=200, request=request)

    client = create_mock_client(handler=mock_handler, base_url="https://example.com")

    with client:
        type_error_raised = False
        try:
            client.get("/test", event_hooks={"request": []})  # type: ignore[call-arg]
        except TypeError as exc:
            type_error_raised = True
            assert "event_hooks" in str(exc)

        assert type_error_raised, "Expected TypeError when passing event_hooks per-request"


def test_async_client_event_hooks_await_coroutines() -> None:
    """Prove httpx.AsyncClient awaits async request and response event hooks."""
    recorder = EventRecorder()

    def mock_handler(request: httpx.Request) -> httpx.Response:
        return httpx.Response(
            status_code=200,
            json={"async_result": "success"},
            request=request,
        )

    async def run_async_test() -> None:
        client = create_mock_async_client(
            handler=mock_handler,
            event_hooks={
                "request": [recorder.async_request_hook],
                "response": [recorder.async_response_hook],
            },
            base_url="https://example.com",
        )
        async with client:
            resp = await client.get("/async/endpoint")
            assert resp.status_code == 200
            assert resp.json() == {"async_result": "success"}

    asyncio.run(run_async_test())
    assert recorder.events == [
        "async_request:GET:/async/endpoint",
        "async_response:200:/async/endpoint",
    ]


def main() -> None:
    test_sync_event_hooks_lifecycle_and_header_mutation()
    test_response_hook_raise_for_status_propagates_http_status_error()
    test_response_hooks_fire_on_intermediate_redirects()
    test_event_hooks_cannot_be_passed_per_request()
    test_async_client_event_hooks_await_coroutines()
    print("Contract pass: httpx request and response event hooks lifecycle verified cleanly.")


if __name__ == "__main__":
    main()

Seeder d'origine

anonyme