Repository navigation
[Bug]: REST tenant routes are mounted before path_prefix, so tenant requests from the SDK client get 404 #1376
Description
What happened?
With a non-empty path_prefix, create_rest_routes serves the tenant routes in front of the prefix, at /{tenant}{path_prefix}/.... The SDK's own REST client sends the tenant after the prefix, at {path_prefix}/{tenant}/..., so a client whose AgentInterface sets tenant gets a 404 on every call.
In src/a2a/server/routes/rest_routes.py, the tenant routes come from Mount(path='/{tenant}', routes=base_route_objects), and base_route_objects already include path_prefix. On the client side, RestTransport._get_path returns /{tenant}{base_path}, which is appended to the interface URL. The HTTP bindings in a2a.proto also put the tenant right after the interface URL (post: "/{tenant}/message:send"). The hello_world_agent sample serves REST with path_prefix='/a2a/rest' and advertises an interface URL ending in /a2a/rest, so the prefix is meant to be part of the interface URL.
This changed in #892. Before it, the REST adapter built /{tenant}{path} and the FastAPI app added rpc_url in front, so the prefix came first. Without a prefix both layouts are the same, so only servers that set path_prefix are affected.
To reproduce, pip install "a2a-sdk[http-server]==1.2.3" and run:
import asyncio
from unittest.mock import AsyncMock
import httpx
from starlette.applications import Starlette
from a2a.client import ClientConfig, ClientFactory
from a2a.server.request_handlers import RequestHandler
from a2a.server.routes import create_rest_routes
from a2a.types import AgentCard, AgentInterface, ListTasksRequest, ListTasksResponse
handler = AsyncMock(spec=RequestHandler)
handler.on_list_tasks.return_value = ListTasksResponse()
app = Starlette(routes=create_rest_routes(handler, path_prefix="/a2a/rest"))
card = AgentCard(supported_interfaces=[AgentInterface(
url="http://test/a2a/rest", protocol_binding="HTTP+JSON",
protocol_version="1.0", tenant="acme")])
async def main():
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as http:
for path in ("/a2a/rest/acme/tasks", "/acme/a2a/rest/tasks"):
r = await http.get(path, headers={"A2A-Version": "1.0"})
print(path, r.status_code)
client = ClientFactory(ClientConfig(httpx_client=http, supported_protocol_bindings=["HTTP+JSON"])).create(card)
try:
await client.list_tasks(ListTasksRequest())
print("list_tasks: ok")
except Exception as e:
print(f"list_tasks: {type(e).__name__}: {e}")
asyncio.run(main())I expected /a2a/rest/acme/tasks to return 200 and list_tasks to succeed.
I have a fix with a test and will open a PR.
Relevant log output
/a2a/rest/acme/tasks 404
/acme/a2a/rest/tasks 200
list_tasks: MethodNotFoundError: Resource not found: http://test/a2a/rest/acme/tasksCode of Conduct
- I agree to follow this project's Code of Conduct
Activity
rohityan commentedon Oct 27, 2026
Hi @Revinand, thanks for reporting this!
I was able to reproduce the issue. You identified the exact root cause, in
create_rest_routes, nestingbase_route_objectsunderMount(path='/{tenant}', ...)registers endpoints at/{tenant}{path_prefix}/...instead of{path_prefix}/{tenant}/.... BecauseRestTransport._get_pathappends the tenant segment after the interface URL prefix, Starlette fails to match incoming client requests and returns 404.Your fix in PR #1377 looks correct and resolves the mismatch cleanly. Assigning this issue to you while the PR is under review!