ProvenPin Install

Starlette routes and middleware tested offline with httpx ASGITransport: working versions (tested 2026-10-15)

A Starlette app with BaseHTTPMiddleware and lifespan state, exercised in-process via httpx ASGITransport without a real server.

passed6 passed · 0 failed2026-10-15 16:05 UTC
Status
passed
Last passed
2026-10-15 16:05 UTC
Tests
6 / 6
Language
Python
Runtime
Python 3.13.16
pinned versions4
starlette@1.8.0httpx@0.28.1pytest@9.1.1pytest-asyncio@1.4.0
run it
uv venv -q .venv && uv pip install -q --python .venv/bin/python -r requirements.txt
.venv/bin/python -m pytest -q
test environment
Python 3.13.16
linux 6.17.0-1022-azure
x64
runner: github-actions 38764699834

The code

Exactly the files that passed. Your agent can pull them with get_recipe("starlette-httpx-asgi-client") (install), or see them on GitHub.

requirements.txt
starlette==1.8.0
httpx==0.28.1
pytest==9.1.1
pytest-asyncio==1.4.0
app.py
from contextlib import asynccontextmanager

from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from starlette.routing import Route


class TraceMiddleware(BaseHTTPMiddleware):
    def __init__(self, app, name):
        super().__init__(app)
        self.name = name

    async def dispatch(self, request, call_next):
        request.app.state.trace.append(f"{self.name}:in")
        response = await call_next(request)
        request.app.state.trace.append(f"{self.name}:out")
        return response


@asynccontextmanager
async def lifespan(app):
    app.state.trace = []
    app.state.items = {1: "one", 2: "two"}
    try:
        yield
    finally:
        app.state.trace.clear()
        del app.state.items


async def get_item(request):
    item_id = request.path_params["item_id"]
    if item_id not in request.app.state.items:
        return JSONResponse({"detail": "not found"}, status_code=404)
    return JSONResponse({"id": item_id, "name": request.app.state.items[item_id]})


async def crash(request):
    raise ValueError("boom")


def create_app():
    return Starlette(
        routes=[Route("/items/{item_id:int}", get_item), Route("/crash", crash)],
        middleware=[
            Middleware(TraceMiddleware, "outer"),
            Middleware(TraceMiddleware, "inner"),
        ],
        lifespan=lifespan,
    )
tests/test_app.py
import httpx
import pytest

from app import create_app


@pytest.fixture
async def app():
    application = create_app()
    async with application.router.lifespan_context(application):
        yield application


@pytest.fixture
async def client(app):
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        yield client


async def test_route_returns_json(client):
    response = await client.get("/items/1")
    assert response.status_code == 200
    assert response.json() == {"id": 1, "name": "one"}


async def test_unknown_item_is_404(client):
    response = await client.get("/items/9")
    assert response.status_code == 404
    assert response.json() == {"detail": "not found"}


async def test_middleware_runs_outermost_first(app, client):
    app.state.trace.clear()
    await client.get("/items/1")
    assert app.state.trace == ["outer:in", "inner:in", "inner:out", "outer:out"]


async def test_transport_does_not_run_lifespan():
    bare = create_app()
    transport = httpx.ASGITransport(app=bare)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        with pytest.raises(AttributeError):
            await client.get("/items/1")
        async with bare.router.lifespan_context(bare):
            assert (await client.get("/items/1")).status_code == 200


async def test_unhandled_exception_propagates(client):
    with pytest.raises(ValueError):
        await client.get("/crash")


async def test_raise_app_exceptions_false_returns_500():
    app = create_app()
    transport = httpx.ASGITransport(app=app, raise_app_exceptions=False)
    async with httpx.AsyncClient(transport=transport, base_url="http://test") as client:
        response = await client.get("/crash")
        assert response.status_code == 500
pytest.ini
[pytest]
pythonpath = .
asyncio_mode = auto

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.