fix(server): preserve Starlette lifespan when public_url is callable#680
Conversation
create_a2a_server returned a raw ASGI callable when public_url was a PublicUrlResolver, so the unified-transport composer's a2a_inner.router.lifespan_context access raised AttributeError at startup and the process exited 0 with no requests served. Install the per-request card intercept as a Starlette middleware via add_middleware so the returned object stays a Starlette app with .router intact. The static and None public_url paths are unchanged. Fixes #676 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Expert review summarycode-reviewer: approved — no blockers. Starlette middleware semantics correct, lifespan composition correct (router-level lifespan bypasses middleware stack which is the right behavior), regression test exercises the bug path. Nits (non-blocking): hot-path imports inside python-expert: approved — no blockers. Confirmed CI: 16/16 green across Python 3.10–3.13, conformance, storyboards. Merging. |
|
Thanks for the review summary, @bokelley. No action needed on my end — happy to see the experts aligned and CI clean. Triaged by Claude Code. Session: https://claude.ai/code/${CLAUDE_CODE_REMOTE_SESSION_ID} Generated by Claude Code |
Summary
Fixes #676.
serve(transport="both", public_url=<callable>)crashed at lifespan startup withAttributeError: 'function' object has no attribute 'router'and the process exited 0 with no requests served.Root cause:
create_a2a_serverreturned a raw ASGI callable (the per-request card middleware) whenpublic_urlwas aPublicUrlResolver, but the unified-transport composer in_serve_mcp_and_a2areachesa2a_inner.router.lifespan_contextdirectly. Only the static-string andNonepaths returned a Starlette app.This implements option 1 from the issue: install the per-request card intercept as a proper Starlette middleware (
app.add_middleware(_PerRequestCardMiddleware, ...)) so the returned object stays a Starlette instance with.routerintact. The composer keeps working unchanged.Changes
src/adcp/server/a2a_server.py— replaced_wrap_with_per_request_cardclosure with a class-based ASGI middleware_PerRequestCardMiddleware;create_a2a_servernow installs it viaapp.add_middleware(...)instead of wrapping. The card-serving behaviour is unchanged.tests/test_unified_mcp_a2a.py— addedtest_unified_app_starts_with_callable_public_urlregression test. Verified it fails on the pre-fix source with the sameAttributeErrorreported in the issue, and passes on the fix.Test plan
pytest tests/test_a2a_public_url_resolver.py tests/test_unified_mcp_a2a.py— 24 passedpytest tests/test_auth_middleware.py tests/test_mcp_middleware_composition.py tests/test_discovery_endpoint.py— 69 passedruff checkclean on modified filesmypy src/adcp/clean