fix(server): serve(transport="both", public_url=callable) crashes at startup#681
Closed
bokelley wants to merge 3 commits into
Closed
fix(server): serve(transport="both", public_url=callable) crashes at startup#681bokelley wants to merge 3 commits into
bokelley wants to merge 3 commits into
Conversation
#676) When public_url is a callable, create_a2a_server returns the _wrap_with_per_request_card middleware wrapper (a plain async function) instead of a Starlette app. _build_mcp_and_a2a_app's lifespan composer then called a2a_inner.router.lifespan_context(), which fails because plain functions have no .router. Fix: attach a ._starlette_app back-reference to the middleware wrapper so _composed_lifespan can reach the inner Starlette app's router. Use getattr(a2a_inner, '_starlette_app', a2a_inner) in the composer so the static/None path is unchanged. Adds a regression test in test_a2a_public_url_resolver.py. https://claude.ai/code/session_01He3UUyDxcy4pmyExJKCHNY
Per pre-PR review nits: - Add @pytest.mark.asyncio to new regression test for consistency with all other async tests in test_a2a_public_url_resolver.py - Add comment at _wrap_with_per_request_card's return site noting that create_a2a_server attaches ._starlette_app on the returned function https://claude.ai/code/session_01He3UUyDxcy4pmyExJKCHNY
CI mypy flagged the [attr-defined] suppression as unused — the wrapper function's typed return is permissive enough that mypy accepts the attribute assignment without the suppression. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Closing as superseded. Main already merged a cleaner fix for #676 (closed at 2026-05-12T00:52:05Z): That's the architecturally better solution. Closing this PR rather than rebasing on top of the conflict, per the parallel-race convention in CLAUDE.md ("Stale claude-triaged draft PRs lose parallel races... Prefer closing-as-superseded over rebasing."). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #676
Summary
serve(transport="both", public_url=callable)exited 0 at startup withAttributeError: 'function' object has no attribute 'router'in_composed_lifespan.Root cause: When
public_urlis a callable,create_a2a_serverbuilds a Starlette app then wraps it with_wrap_with_per_request_card, which returns a plainasync def _middleware(scope, receive, send)function — not a Starlette app. The lifespan composer in_build_mcp_and_a2a_appthen calleda2a_inner.router.lifespan_context(a2a_inner), which crashes because plain functions have no.router. The string andNonecases return a Starlette app directly, which is why they were unaffected.Fix:
a2a_server.py: in the callable branch ofcreate_a2a_server, keep the Starlette app in a local (starlette_app) before wrapping, then attach._starlette_app = starlette_appon the returned middleware function.serve.py_composed_lifespan: usegetattr(a2a_inner, "_starlette_app", a2a_inner)to reach the inner Starlette app's router. Falls back toa2a_inneritself for the static/Nonepath — no behaviour change there._wrap_with_per_request_cardcomment noting the post-hoc attribute attachment.What was tested
pytest tests/test_a2a_public_url_resolver.py tests/test_unified_mcp_a2a.py tests/test_serve_auth_both.py -v— 56 passed (including new regression test)ruff check src/adcp/server/a2a_server.py src/adcp/server/serve.py— no issuesmypy src/adcp/server/a2a_server.py src/adcp/server/serve.py --ignore-missing-imports— no new errors in changed filesNits surfaced by pre-PR review (not fixed in this PR)
auth=BearerTokenAuth(...)+public_url=callableontransport="both"together — fix logic is auth-independent but the coverage gap exists; follow-up issue recommended._starlette_appas a duck-typed cross-module attribute is fragile if_wrap_with_per_request_cardis ever converted to a class without reading the comment; aProtocolwould make the contract explicitPre-PR review
starlette_app(not middleware wrapper) as thelifespan_contextargument; nits noted above@pytest.mark.asyncioadded for consistency per nitSession: https://claude.ai/code/session_01He3UUyDxcy4pmyExJKCHNY
Generated by Claude Code