From 8ff2b87506528f96508c1c0454658d562a8ac1ce Mon Sep 17 00:00:00 2001 From: "chenxiao.2025" Date: Fri, 3 Jul 2026 19:31:28 +0800 Subject: [PATCH] Fix docs API paths for mounted apps --- fasta2a/applications.py | 10 ++++++++-- fasta2a/static/docs.html | 13 +++++++++---- tests/test_applications.py | 26 ++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/fasta2a/applications.py b/fasta2a/applications.py index 89a3cda..b835869 100644 --- a/fasta2a/applications.py +++ b/fasta2a/applications.py @@ -1,5 +1,7 @@ from __future__ import annotations as _annotations +import html +import json from collections.abc import AsyncIterator, Sequence from contextlib import asynccontextmanager from pathlib import Path @@ -8,7 +10,7 @@ from starlette.applications import Starlette from starlette.middleware import Middleware from starlette.requests import Request -from starlette.responses import FileResponse, Response, StreamingResponse +from starlette.responses import Response, StreamingResponse from starlette.routing import Route from starlette.types import ExceptionHandler, Lifespan, Receive, Scope, Send @@ -112,7 +114,11 @@ async def _agent_card_endpoint(self, request: Request) -> Response: async def _docs_endpoint(self, request: Request) -> Response: """Serve the documentation interface.""" docs_path = Path(__file__).parent / 'static' / 'docs.html' - return FileResponse(docs_path, media_type='text/html') + root_path = request.scope.get('root_path', '').rstrip('/') + content = docs_path.read_text() + content = content.replace('__FASTA2A_API_ROOT_JSON__', json.dumps(root_path)) + content = content.replace('__FASTA2A_API_ROOT__', html.escape(root_path, quote=True)) + return Response(content=content, media_type='text/html') async def _agent_run_endpoint(self, request: Request) -> Response: """This is the main endpoint for the A2A server. diff --git a/fasta2a/static/docs.html b/fasta2a/static/docs.html index a1862a2..5e320e2 100644 --- a/fasta2a/static/docs.html +++ b/fasta2a/static/docs.html @@ -489,7 +489,7 @@

📋 Agent Information

Loading agent information...
@@ -526,11 +526,16 @@

💬 Chat Interface

let agentCard = null; let currentTaskId = null; let contextId = null; + const apiRoot = __FASTA2A_API_ROOT_JSON__; + + function apiUrl(path) { + return `${apiRoot}${path}`; + } // Load agent card information async function loadAgentCard() { try { - const response = await fetch('/.well-known/agent-card.json'); + const response = await fetch(apiUrl('/.well-known/agent-card.json')); if (!response.ok) throw new Error('Failed to load agent card'); agentCard = await response.json(); @@ -596,7 +601,7 @@

Skills:

try { // Send message to A2A endpoint - const response = await fetch('/', { + const response = await fetch(apiUrl('/'), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -660,7 +665,7 @@

Skills:

if (!currentTaskId) return; try { - const response = await fetch('/', { + const response = await fetch(apiUrl('/'), { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/tests/test_applications.py b/tests/test_applications.py index 2385138..18a0fc7 100644 --- a/tests/test_applications.py +++ b/tests/test_applications.py @@ -6,6 +6,7 @@ import pytest from asgi_lifespan import LifespanManager from inline_snapshot import snapshot +from starlette.applications import Starlette from fasta2a.applications import FastA2A from fasta2a.broker import InMemoryBroker @@ -56,6 +57,8 @@ async def test_docs_endpoint_default(self): async with create_test_client(app) as client: response = await client.get('/docs') assert response.status_code == 200 + assert '__FASTA2A_API_ROOT__' not in response.text + assert 'const apiRoot = "";' in response.text async def test_docs_endpoint_custom_url(self): app = FastA2A(storage=InMemoryStorage(), broker=InMemoryBroker(), docs_url='/custom-docs') @@ -63,6 +66,29 @@ async def test_docs_endpoint_custom_url(self): response = await client.get('/custom-docs') assert response.status_code == 200 + async def test_docs_endpoint_mounted_app_uses_root_path(self): + a2a_app = FastA2A(storage=InMemoryStorage(), broker=InMemoryBroker()) + + @asynccontextmanager + async def lifespan(_app: Starlette): + async with a2a_app.router.lifespan_context(a2a_app): + yield + + app = Starlette(lifespan=lifespan) + app.mount('/agent', a2a_app) + + async with LifespanManager(app=app) as manager: + transport = httpx.ASGITransport(app=manager.app) + async with httpx.AsyncClient(transport=transport, base_url='http://testclient') as client: + response = await client.get('/agent/docs') + assert response.status_code == 200 + assert '__FASTA2A_API_ROOT__' not in response.text + assert 'const apiRoot = "/agent";' in response.text + assert 'href="/agent/.well-known/agent-card.json"' in response.text + + response = await client.get('/agent/.well-known/agent-card.json') + assert response.status_code == 200 + async def test_docs_endpoint_disabled(self): app = FastA2A(storage=InMemoryStorage(), broker=InMemoryBroker(), docs_url=None) async with create_test_client(app) as client: