What happened?
Context: I am building an A2A server extension for Quarkus LangChain4j. It started from a stalled PR by another contributor, who is not involved in this work; that PR predates the 1.x SDK, and most of the current code is my own, bringing it to 1.2.0.Final. This surfaced when I added gRPC coverage to the integration tests. The goal is to land the extension in quarkus-langchain4j proper.
The three Quarkus reference implementations disagree about when the Agent Card is resolved, and the gRPC one resolves it earliest, early enough that an application whose card depends on the address the HTTP server bound to cannot start at all.
| Module |
How it obtains the card |
a2a-java-sdk-reference-jsonrpc |
A2AServerRoutes holds no AgentCard field; the card is resolved per request |
a2a-java-sdk-reference-rest |
same |
a2a-java-sdk-reference-grpc |
QuarkusGrpcHandler declares private final AgentCard agentCard and private final AgentCard extendedAgentCard, both constructor parameters |
What makes this awkward is that the constructor already receives Instance<AgentCard> for the extended card, the very mechanism that exists to defer resolution, and still resolves it inside the constructor and stores it in a final field.
Why it breaks startup. Quarkus's GrpcServerRecorder.collectServiceDefinitions runs at RUNTIME_INIT and instantiates the handler. Quarkus fires HttpServerStart with fireAsync from WebDeploymentVerticle, that is, later. So any @PublicAgentCard producer that waits for the bound address is invoked before the address exists, and the application never starts.
Deriving the card's URL from the address actually bound is not an exotic thing to want: it is the only way to be correct with quarkus.http.port=0, and it is what makes the card right in tests (port 8081) and in production (port 8080) without configuring the URL twice.
Suggested fix
Hold Instance<AgentCard> in both fields and resolve inside getAgentCard() / getExtendedAgentCard(), which would also bring the gRPC module in line with the JSON-RPC and REST ones.
My workaround
I fall back to quarkus.http.host / quarkus.http.port when the startup event has not arrived yet, and still fail as before when the port is 0 or plain HTTP is disabled, cases where a guessed URL would be wrong and a client may cache it. I intend to keep that fallback even after an upstream fix, because fireAsync makes this a race for the other transports too.
Verified against 1.2.0.Final
QuarkusGrpcHandler still declares two private final AgentCard fields; the JSON-RPC and REST A2AServerRoutes declare none.
Happy to send a PR for the Instance<AgentCard> change if you agree with the direction.
Relevant log output
java.lang.IllegalStateException: The address the HTTP server is bound to is not known yet
at ConfigCardBuilderCustomizer.determineUrl
at AgentCardProducer.agentCard
at QuarkusGrpcHandler_Bean.create
at io.quarkus.grpc.runtime.GrpcServerRecorder.collectServiceDefinitions(GrpcServerRecorder.java:487)
Environment
- a2a-java
1.2.0.Final (originally found on 1.1.0.Final)
- Quarkus 3.33.2
Code of Conduct
What happened?
Context: I am building an A2A server extension for Quarkus LangChain4j. It started from a stalled PR by another contributor, who is not involved in this work; that PR predates the 1.x SDK, and most of the current code is my own, bringing it to
1.2.0.Final. This surfaced when I added gRPC coverage to the integration tests. The goal is to land the extension in quarkus-langchain4j proper.The three Quarkus reference implementations disagree about when the Agent Card is resolved, and the gRPC one resolves it earliest, early enough that an application whose card depends on the address the HTTP server bound to cannot start at all.
a2a-java-sdk-reference-jsonrpcA2AServerRoutesholds noAgentCardfield; the card is resolved per requesta2a-java-sdk-reference-resta2a-java-sdk-reference-grpcQuarkusGrpcHandlerdeclaresprivate final AgentCard agentCardandprivate final AgentCard extendedAgentCard, both constructor parametersWhat makes this awkward is that the constructor already receives
Instance<AgentCard>for the extended card, the very mechanism that exists to defer resolution, and still resolves it inside the constructor and stores it in afinalfield.Why it breaks startup. Quarkus's
GrpcServerRecorder.collectServiceDefinitionsruns atRUNTIME_INITand instantiates the handler. Quarkus firesHttpServerStartwithfireAsyncfromWebDeploymentVerticle, that is, later. So any@PublicAgentCardproducer that waits for the bound address is invoked before the address exists, and the application never starts.Deriving the card's URL from the address actually bound is not an exotic thing to want: it is the only way to be correct with
quarkus.http.port=0, and it is what makes the card right in tests (port 8081) and in production (port 8080) without configuring the URL twice.Suggested fix
Hold
Instance<AgentCard>in both fields and resolve insidegetAgentCard()/getExtendedAgentCard(), which would also bring the gRPC module in line with the JSON-RPC and REST ones.My workaround
I fall back to
quarkus.http.host/quarkus.http.portwhen the startup event has not arrived yet, and still fail as before when the port is0or plain HTTP is disabled, cases where a guessed URL would be wrong and a client may cache it. I intend to keep that fallback even after an upstream fix, becausefireAsyncmakes this a race for the other transports too.Verified against 1.2.0.Final
QuarkusGrpcHandlerstill declares twoprivate final AgentCardfields; the JSON-RPC and RESTA2AServerRoutesdeclare none.Happy to send a PR for the
Instance<AgentCard>change if you agree with the direction.Relevant log output
java.lang.IllegalStateException: The address the HTTP server is bound to is not known yet at ConfigCardBuilderCustomizer.determineUrl at AgentCardProducer.agentCard at QuarkusGrpcHandler_Bean.create at io.quarkus.grpc.runtime.GrpcServerRecorder.collectServiceDefinitions(GrpcServerRecorder.java:487)Environment
1.2.0.Final(originally found on1.1.0.Final)Code of Conduct