From 9efe42a896932327a97a0243ae8e8da0ff38cf43 Mon Sep 17 00:00:00 2001 From: Kabir Khan Date: Wed, 12 Aug 2026 12:54:07 +0100 Subject: [PATCH] fix: defer AgentCard resolution to prevent eager startup failures (#1033) All three handler implementations (gRPC, JSON-RPC, REST) now store Instance and resolve lazily on first access instead of eagerly in the constructor. This prevents startup failures when the AgentCard producer depends on the HTTP server's bound address, which is not yet available during gRPC service construction at RUNTIME_INIT. Transport validation is also deferred to first use via an AtomicBoolean guard, consistent with the existing pattern in GrpcHandler. This fixes #1033 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Kabir Khan --- .../grpc/quarkus/QuarkusGrpcHandler.java | 19 +++-- .../QuarkusGrpcHandlerLazyResolutionTest.java | 44 +++++++++++ .../sdk/server/ResolvedInstance.java | 78 +++++++++++++++++++ .../transport/grpc/handler/GrpcHandler.java | 9 ++- .../jsonrpc/handler/JSONRPCHandler.java | 56 ++++++++----- .../jsonrpc/handler/JSONRPCHandlerTest.java | 11 +++ .../transport/rest/handler/RestHandler.java | 54 ++++++++----- .../rest/handler/RestHandlerTest.java | 26 +++++-- 8 files changed, 237 insertions(+), 60 deletions(-) create mode 100644 reference/grpc/src/test/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandlerLazyResolutionTest.java create mode 100644 server-common/src/main/java/org/a2aproject/sdk/server/ResolvedInstance.java diff --git a/reference/grpc/src/main/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandler.java b/reference/grpc/src/main/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandler.java index 8df6e0699..cae365e2e 100644 --- a/reference/grpc/src/main/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandler.java +++ b/reference/grpc/src/main/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandler.java @@ -74,8 +74,8 @@ @Blocking public class QuarkusGrpcHandler extends GrpcHandler { - private final AgentCard agentCard; - private final AgentCard extendedAgentCard; + private final Instance agentCard; + private final Instance extendedAgentCard; private final RequestHandler requestHandler; private final Instance callContextFactoryInstance; private final Executor executor; @@ -106,17 +106,13 @@ public class QuarkusGrpcHandler extends GrpcHandler { * @param executor the executor for async operations (qualified with {@code @Internal}) */ @Inject - public QuarkusGrpcHandler(@PublicAgentCard AgentCard agentCard, + public QuarkusGrpcHandler(@PublicAgentCard Instance agentCard, @ExtendedAgentCard Instance extendedAgentCard, RequestHandler requestHandler, Instance callContextFactoryInstance, @Internal Executor executor) { this.agentCard = agentCard; - if (extendedAgentCard != null && extendedAgentCard.isResolvable()) { - this.extendedAgentCard = extendedAgentCard.get(); - } else { - this.extendedAgentCard = null; - } + this.extendedAgentCard = extendedAgentCard; this.requestHandler = requestHandler; this.callContextFactoryInstance = callContextFactoryInstance; this.executor = executor; @@ -129,12 +125,15 @@ protected RequestHandler getRequestHandler() { @Override protected AgentCard getAgentCard() { - return agentCard; + return agentCard.get(); } @Override protected AgentCard getExtendedAgentCard() { - return extendedAgentCard; + if (extendedAgentCard != null && extendedAgentCard.isResolvable()) { + return extendedAgentCard.get(); + } + return null; } @Override diff --git a/reference/grpc/src/test/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandlerLazyResolutionTest.java b/reference/grpc/src/test/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandlerLazyResolutionTest.java new file mode 100644 index 000000000..57d44396e --- /dev/null +++ b/reference/grpc/src/test/java/org/a2aproject/sdk/server/grpc/quarkus/QuarkusGrpcHandlerLazyResolutionTest.java @@ -0,0 +1,44 @@ +package org.a2aproject.sdk.server.grpc.quarkus; + +import jakarta.enterprise.inject.Instance; + +import org.a2aproject.sdk.server.ResolvedInstance; +import org.a2aproject.sdk.spec.AgentCard; +import org.a2aproject.sdk.transport.grpc.handler.CallContextFactory; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +/** + * Verifies that {@link QuarkusGrpcHandler} does not eagerly resolve {@link Instance} parameters + * during construction. This guards against the regression described in + * issue #1033, where eager + * resolution prevented startup when the AgentCard producer depended on the HTTP server address. + */ +class QuarkusGrpcHandlerLazyResolutionTest { + + @Test + void constructorDoesNotResolveAgentCardInstances() { + Instance throwOnGet = new ResolvedInstance<>(null) { + @Override + public AgentCard get() { + throw new AssertionError("Instance.get() must not be called during construction"); + } + + @Override + public boolean isUnsatisfied() { + return false; + } + }; + + @SuppressWarnings("unchecked") + Instance emptyCallContextFactory = (Instance) (Instance) new ResolvedInstance<>(null); + + assertDoesNotThrow(() -> new QuarkusGrpcHandler( + throwOnGet, + throwOnGet, + null, + emptyCallContextFactory, + Runnable::run)); + } +} diff --git a/server-common/src/main/java/org/a2aproject/sdk/server/ResolvedInstance.java b/server-common/src/main/java/org/a2aproject/sdk/server/ResolvedInstance.java new file mode 100644 index 000000000..3383858d9 --- /dev/null +++ b/server-common/src/main/java/org/a2aproject/sdk/server/ResolvedInstance.java @@ -0,0 +1,78 @@ +package org.a2aproject.sdk.server; + +import java.lang.annotation.Annotation; +import java.util.Collections; +import java.util.Iterator; + +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.UnsatisfiedResolutionException; +import jakarta.enterprise.util.TypeLiteral; + +import org.jspecify.annotations.Nullable; + +/** + * An {@link Instance} wrapper for a pre-resolved value, for use in non-CDI contexts + * such as convenience constructors and tests. + * + * @param the bean type + */ +public class ResolvedInstance implements Instance { + + private final @Nullable T value; + + public ResolvedInstance(@Nullable T value) { + this.value = value; + } + + @Override + public T get() { + if (value == null) { + throw new UnsatisfiedResolutionException("No value available"); + } + return value; + } + + @Override + public boolean isUnsatisfied() { + return value == null; + } + + @Override + public boolean isAmbiguous() { + return false; + } + + @Override + public Iterator iterator() { + return value != null ? Collections.singleton(value).iterator() : Collections.emptyIterator(); + } + + @Override + public void destroy(T instance) { + } + + @Override + public Instance select(Annotation... qualifiers) { + throw new UnsupportedOperationException(); + } + + @Override + public Instance select(Class subtype, Annotation... qualifiers) { + throw new UnsupportedOperationException(); + } + + @Override + public Instance select(TypeLiteral subtype, Annotation... qualifiers) { + throw new UnsupportedOperationException(); + } + + @Override + public Handle getHandle() { + throw new UnsupportedOperationException(); + } + + @Override + public Iterable> handles() { + throw new UnsupportedOperationException(); + } +} diff --git a/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandler.java b/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandler.java index 4b38ed6bd..587460e32 100644 --- a/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandler.java +++ b/transport/grpc/src/main/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandler.java @@ -833,8 +833,13 @@ private void handleInternalError(StreamObserver responseObserver, Throwab private AgentCard getAgentCardInternal() { AgentCard agentCard = getAgentCard(); if (initialised.compareAndSet(false, true)) { - // Validate transport configuration with proper classloader context - validateTransportConfigurationWithCorrectClassLoader(agentCard); + try { + // Validate transport configuration with proper classloader context + validateTransportConfigurationWithCorrectClassLoader(agentCard); + } catch (RuntimeException e) { + initialised.set(false); + throw e; + } } return agentCard; } diff --git a/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java b/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java index ec9fd3b85..cedf9eba1 100644 --- a/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java +++ b/transport/jsonrpc/src/main/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandler.java @@ -5,6 +5,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; @@ -35,6 +36,7 @@ import org.a2aproject.sdk.server.AgentCardValidator; import org.a2aproject.sdk.server.ExtendedAgentCard; import org.a2aproject.sdk.server.PublicAgentCard; +import org.a2aproject.sdk.server.ResolvedInstance; import org.a2aproject.sdk.server.ServerCallContext; import org.a2aproject.sdk.server.extensions.A2AExtensions; import org.a2aproject.sdk.server.requesthandlers.RequestHandler; @@ -135,10 +137,11 @@ public class JSONRPCHandler { // Fields set by constructor injection cannot be final. We need a noargs constructor for // Jakarta compatibility, and it seems that making fields set by constructor injection // final, is not proxyable in all runtimes - private AgentCard agentCard; + private Instance agentCardInstance; private @Nullable Instance extendedAgentCard; private RequestHandler requestHandler; private Executor executor; + private final AtomicBoolean transportValidated = new AtomicBoolean(false); /** * No-args constructor for CDI proxy creation. @@ -148,7 +151,7 @@ public class JSONRPCHandler { @SuppressWarnings("NullAway") protected JSONRPCHandler() { // For CDI proxy creation - this.agentCard = null; + this.agentCardInstance = null; this.extendedAgentCard = null; this.requestHandler = null; this.executor = null; @@ -157,21 +160,19 @@ protected JSONRPCHandler() { /** * Creates a JSON-RPC handler with full CDI injection support. * - * @param agentCard the public agent card containing agent capabilities + * @param agentCardInstance the public agent card instance containing agent capabilities * @param extendedAgentCard optional extended agent card instance * @param requestHandler the handler for processing A2A requests * @param executor the executor for asynchronous operations */ @Inject - public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, @Nullable @ExtendedAgentCard Instance extendedAgentCard, + public JSONRPCHandler(@PublicAgentCard Instance agentCardInstance, + @Nullable @ExtendedAgentCard Instance extendedAgentCard, RequestHandler requestHandler, @Internal Executor executor) { - this.agentCard = agentCard; + this.agentCardInstance = agentCardInstance; this.extendedAgentCard = extendedAgentCard; this.requestHandler = requestHandler; this.executor = executor; - - // Validate transport configuration - AgentCardValidator.validateTransportConfiguration(agentCard); } /** @@ -182,7 +183,7 @@ public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, @Nullable @ExtendedA * @param executor the executor for asynchronous operations */ public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, RequestHandler requestHandler, Executor executor) { - this(agentCard, null, requestHandler, executor); + this(new ResolvedInstance<>(agentCard), null, requestHandler, executor); } /** @@ -229,8 +230,8 @@ public JSONRPCHandler(@PublicAgentCard AgentCard agentCard, RequestHandler reque */ public SendMessageResponse onMessageSend(SendMessageRequest request, ServerCallContext context) { try { - A2AVersionValidator.validateProtocolVersion(agentCard, context); - A2AExtensions.validateRequiredExtensions(agentCard, context); + A2AVersionValidator.validateProtocolVersion(agentCard(), context); + A2AExtensions.validateRequiredExtensions(agentCard(), context); EventKind taskOrMessage = requestHandler.onMessageSend(request.getParams(), context); return new SendMessageResponse(request.getId(), taskOrMessage); } catch (A2AError e) { @@ -278,7 +279,7 @@ public SendMessageResponse onMessageSend(SendMessageRequest request, ServerCallC */ public Flow.Publisher onMessageSendStream( SendStreamingMessageRequest request, ServerCallContext context) { - if (!agentCard.capabilities().streaming()) { + if (!agentCard().capabilities().streaming()) { return ZeroPublisher.fromItems( new SendStreamingMessageResponse( request.getId(), @@ -286,8 +287,8 @@ public Flow.Publisher onMessageSendStream( } try { - A2AVersionValidator.validateProtocolVersion(agentCard, context); - A2AExtensions.validateRequiredExtensions(agentCard, context); + A2AVersionValidator.validateProtocolVersion(agentCard(), context); + A2AExtensions.validateRequiredExtensions(agentCard(), context); Flow.Publisher publisher = requestHandler.onMessageSendStream(request.getParams(), context); // We can't use the convertingProcessor convenience method since that propagates any errors as an error handled @@ -375,7 +376,7 @@ public CancelTaskResponse onCancelTask(CancelTaskRequest request, ServerCallCont */ public Flow.Publisher onSubscribeToTask( SubscribeToTaskRequest request, ServerCallContext context) throws A2AError { - if (!agentCard.capabilities().streaming()) { + if (!agentCard().capabilities().streaming()) { return ZeroPublisher.fromItems( new SendStreamingMessageResponse( request.getId(), @@ -422,7 +423,7 @@ public Flow.Publisher onSubscribeToTask( */ public GetTaskPushNotificationConfigResponse getPushNotificationConfig( GetTaskPushNotificationConfigRequest request, ServerCallContext context) { - if (!agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { return new GetTaskPushNotificationConfigResponse(request.getId(), new PushNotificationNotSupportedError()); } @@ -464,7 +465,7 @@ public GetTaskPushNotificationConfigResponse getPushNotificationConfig( */ public CreateTaskPushNotificationConfigResponse setPushNotificationConfig( CreateTaskPushNotificationConfigRequest request, ServerCallContext context) { - if (!agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { return new CreateTaskPushNotificationConfigResponse(request.getId(), new PushNotificationNotSupportedError()); } @@ -587,7 +588,7 @@ public ListTasksResponse onListTasks(ListTasksRequest request, ServerCallContext */ public ListTaskPushNotificationConfigsResponse listPushNotificationConfigs( ListTaskPushNotificationConfigsRequest request, ServerCallContext context) { - if ( !agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { return new ListTaskPushNotificationConfigsResponse(request.getId(), new PushNotificationNotSupportedError()); } @@ -629,7 +630,7 @@ public ListTaskPushNotificationConfigsResponse listPushNotificationConfigs( */ public DeleteTaskPushNotificationConfigResponse deletePushNotificationConfig( DeleteTaskPushNotificationConfigRequest request, ServerCallContext context) { - if ( !agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { return new DeleteTaskPushNotificationConfigResponse(request.getId(), new PushNotificationNotSupportedError()); } @@ -669,7 +670,7 @@ public DeleteTaskPushNotificationConfigResponse deletePushNotificationConfig( // TODO: Add authentication (https://github.com/a2aproject/a2a-java/issues/77) public GetExtendedAgentCardResponse onGetExtendedCardRequest( GetExtendedAgentCardRequest request, ServerCallContext context) { - if (!agentCard.capabilities().extendedAgentCard()) { + if (!agentCard().capabilities().extendedAgentCard()) { return new GetExtendedAgentCardResponse(request.getId(), new UnsupportedOperationError()); } if (extendedAgentCard == null || !extendedAgentCard.isResolvable()) { @@ -696,7 +697,20 @@ public GetExtendedAgentCardResponse onGetExtendedCardRequest( * @see AgentCard */ public AgentCard getAgentCard() { - return agentCard; + return agentCard(); + } + + private AgentCard agentCard() { + AgentCard card = agentCardInstance.get(); + if (transportValidated.compareAndSet(false, true)) { + try { + AgentCardValidator.validateTransportConfiguration(card); + } catch (RuntimeException e) { + transportValidated.set(false); + throw e; + } + } + return card; } private Flow.Publisher convertToSendStreamingMessageResponse( diff --git a/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java b/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java index 6cd6deb99..f3e927855 100644 --- a/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java +++ b/transport/jsonrpc/src/test/java/org/a2aproject/sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java @@ -1,5 +1,6 @@ package org.a2aproject.sdk.transport.jsonrpc.handler; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -80,6 +81,7 @@ import org.a2aproject.sdk.spec.TextPart; import org.a2aproject.sdk.spec.UnsupportedOperationError; import org.a2aproject.sdk.spec.VersionNotSupportedError; +import jakarta.enterprise.inject.Instance; import mutiny.zero.ZeroPublisher; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; @@ -2004,4 +2006,13 @@ public void testListTasksEmptyResultIncludesAllFields() { assertEquals(0, result.pageSize(), "pageSize should be 0"); // nextPageToken can be null for empty results } + + @Test + void constructorDoesNotResolveAgentCardInstance() { + @SuppressWarnings("unchecked") + Instance throwOnGet = Mockito.mock(Instance.class); + Mockito.when(throwOnGet.get()).thenThrow(new AssertionError("Instance.get() must not be called during construction")); + + assertDoesNotThrow(() -> new JSONRPCHandler(throwOnGet, null, requestHandler, internalExecutor)); + } } diff --git a/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java b/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java index f4f59dc58..2e7df900c 100644 --- a/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java +++ b/transport/rest/src/main/java/org/a2aproject/sdk/transport/rest/handler/RestHandler.java @@ -12,6 +12,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Flow; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; @@ -34,6 +35,7 @@ import org.a2aproject.sdk.server.AgentCardValidator; import org.a2aproject.sdk.server.ExtendedAgentCard; import org.a2aproject.sdk.server.PublicAgentCard; +import org.a2aproject.sdk.server.ResolvedInstance; import org.a2aproject.sdk.server.ServerCallContext; import org.a2aproject.sdk.server.auth.TaskOperation; import org.a2aproject.sdk.server.extensions.A2AExtensions; @@ -124,11 +126,12 @@ public class RestHandler { // Fields set by constructor injection cannot be final. We need a noargs constructor for // Jakarta compatibility, and it seems that making fields set by constructor injection // final, is not proxyable in all runtimes - private AgentCard agentCard; + private Instance agentCardInstance; private @Nullable Instance extendedAgentCard; private AgentCardCacheMetadata cacheMetadata; private RequestHandler requestHandler; private Executor executor; + private final AtomicBoolean transportValidated = new AtomicBoolean(false); /** * No-args constructor for CDI proxy creation. @@ -144,23 +147,21 @@ protected RestHandler() { /** * Creates a REST handler with full CDI injection support. * - * @param agentCard the public agent card containing agent capabilities + * @param agentCardInstance the public agent card instance containing agent capabilities * @param extendedAgentCard optional extended agent card instance * @param cacheMetadata the agent card caching metadata * @param requestHandler the handler for processing A2A requests * @param executor the executor for asynchronous operations */ @Inject - public RestHandler(@PublicAgentCard AgentCard agentCard, @ExtendedAgentCard Instance extendedAgentCard, + public RestHandler(@PublicAgentCard Instance agentCardInstance, + @ExtendedAgentCard Instance extendedAgentCard, AgentCardCacheMetadata cacheMetadata, RequestHandler requestHandler, @Internal Executor executor) { - this.agentCard = agentCard; + this.agentCardInstance = agentCardInstance; this.extendedAgentCard = extendedAgentCard; this.cacheMetadata = cacheMetadata; this.requestHandler = requestHandler; this.executor = executor; - - // Validate transport configuration - AgentCardValidator.validateTransportConfiguration(agentCard); } /** @@ -173,7 +174,7 @@ public RestHandler(@PublicAgentCard AgentCard agentCard, @ExtendedAgentCard Inst */ public RestHandler(AgentCard agentCard, AgentCardCacheMetadata cacheMetadata, RequestHandler requestHandler, Executor executor) { - this.agentCard = agentCard; + this.agentCardInstance = new ResolvedInstance<>(agentCard); this.cacheMetadata = cacheMetadata; this.requestHandler = requestHandler; this.executor = executor; @@ -227,8 +228,8 @@ public RestHandler(AgentCard agentCard, AgentCardCacheMetadata cacheMetadata, */ public HTTPRestResponse sendMessage(ServerCallContext context, String tenant, String body) { try { - A2AVersionValidator.validateProtocolVersion(agentCard, context); - A2AExtensions.validateRequiredExtensions(agentCard, context); + A2AVersionValidator.validateProtocolVersion(agentCard(), context); + A2AExtensions.validateRequiredExtensions(agentCard(), context); org.a2aproject.sdk.grpc.SendMessageRequest.Builder request = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); parseRequestBody(body, request); request.setTenant(tenant); @@ -292,11 +293,11 @@ public HTTPRestResponse sendMessage(ServerCallContext context, String tenant, St */ public HTTPRestResponse sendStreamingMessage(ServerCallContext context, String tenant, String body) { try { - if (!agentCard.capabilities().streaming()) { + if (!agentCard().capabilities().streaming()) { return createErrorResponse(new UnsupportedOperationError(null, "Streaming is not supported by the agent", null)); } - A2AVersionValidator.validateProtocolVersion(agentCard, context); - A2AExtensions.validateRequiredExtensions(agentCard, context); + A2AVersionValidator.validateProtocolVersion(agentCard(), context); + A2AExtensions.validateRequiredExtensions(agentCard(), context); org.a2aproject.sdk.grpc.SendMessageRequest.Builder request = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder(); parseRequestBody(body, request); request.setTenant(tenant); @@ -369,7 +370,7 @@ public HTTPRestResponse cancelTask(ServerCallContext context, String tenant, Str */ public HTTPRestResponse createTaskPushNotificationConfiguration(ServerCallContext context, String tenant, String body, String taskId) { try { - if (!agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { throw new PushNotificationNotSupportedError(); } org.a2aproject.sdk.grpc.TaskPushNotificationConfig.Builder builder = org.a2aproject.sdk.grpc.TaskPushNotificationConfig.newBuilder(); @@ -424,7 +425,7 @@ public HTTPRestResponse createTaskPushNotificationConfiguration(ServerCallContex */ public HTTPRestResponse subscribeToTask(ServerCallContext context, String tenant, String taskId) { try { - if (!agentCard.capabilities().streaming()) { + if (!agentCard().capabilities().streaming()) { return createErrorResponse(new UnsupportedOperationError(null, "Streaming is not supported by the agent", null)); } TaskIdParams params = TaskIdParams.builder().id(taskId).tenant(tenant).build(); @@ -581,7 +582,7 @@ public HTTPRestResponse listTasks(ServerCallContext context, String tenant, */ public HTTPRestResponse getTaskPushNotificationConfiguration(ServerCallContext context, String tenant, String taskId, String configId) { try { - if (!agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { throw new PushNotificationNotSupportedError(); } GetTaskPushNotificationConfigParams params = new GetTaskPushNotificationConfigParams(taskId, configId, tenant); @@ -606,7 +607,7 @@ public HTTPRestResponse getTaskPushNotificationConfiguration(ServerCallContext c */ public HTTPRestResponse listTaskPushNotificationConfigurations(ServerCallContext context, String tenant, String taskId, int pageSize, String pageToken) { try { - if (!agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { throw new PushNotificationNotSupportedError(); } ListTaskPushNotificationConfigsParams params = new ListTaskPushNotificationConfigsParams(taskId, pageSize, pageToken, tenant); @@ -630,7 +631,7 @@ public HTTPRestResponse listTaskPushNotificationConfigurations(ServerCallContext */ public HTTPRestResponse deleteTaskPushNotificationConfiguration(ServerCallContext context, String tenant, String taskId, String configId) { try { - if (!agentCard.capabilities().pushNotifications()) { + if (!agentCard().capabilities().pushNotifications()) { throw new PushNotificationNotSupportedError(); } DeleteTaskPushNotificationConfigParams params = new DeleteTaskPushNotificationConfigParams(taskId, configId, tenant); @@ -797,7 +798,7 @@ private static int mapErrorToHttpStatus(A2AError error) { */ public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, String tenant) { try { - if (!agentCard.capabilities().extendedAgentCard()) { + if (!agentCard().capabilities().extendedAgentCard()) { throw new UnsupportedOperationError(); } if (extendedAgentCard == null || !extendedAgentCard.isResolvable()) { @@ -846,9 +847,22 @@ public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, String t * @see AgentCard * @see #getExtendedAgentCard(ServerCallContext, String) */ + private AgentCard agentCard() { + AgentCard card = agentCardInstance.get(); + if (transportValidated.compareAndSet(false, true)) { + try { + AgentCardValidator.validateTransportConfiguration(card); + } catch (RuntimeException e) { + transportValidated.set(false); + throw e; + } + } + return card; + } + public HTTPRestResponse getAgentCard() { try { - return new HTTPRestResponse(200, APPLICATION_JSON, JsonUtil.toJson(agentCard), + return new HTTPRestResponse(200, APPLICATION_JSON, JsonUtil.toJson(agentCard()), cacheMetadata.getHttpHeadersMap()); } catch (Throwable t) { return createErrorResponse(500, new InternalError(t.getMessage())); diff --git a/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestHandlerTest.java b/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestHandlerTest.java index 890b59e53..68a947e5f 100644 --- a/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestHandlerTest.java +++ b/transport/rest/src/test/java/org/a2aproject/sdk/transport/rest/handler/RestHandlerTest.java @@ -17,6 +17,7 @@ import com.google.gson.JsonParser; import com.google.protobuf.InvalidProtocolBufferException; +import jakarta.enterprise.inject.Instance; import org.a2aproject.sdk.common.MediaType; import org.a2aproject.sdk.server.AgentCardCacheMetadata; import org.a2aproject.sdk.server.ServerCallContext; @@ -31,6 +32,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.mockito.Mockito; @Timeout(value = 1, unit = TimeUnit.MINUTES) public class RestHandlerTest extends AbstractA2ARequestHandlerTest { @@ -554,7 +556,7 @@ public void testExtensionSupportRequiredErrorOnSendMessage() { AgentCard cardWithExtension = AgentCard.builder() .name("test-card") .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -602,7 +604,7 @@ public void testExtensionSupportRequiredErrorOnSendStreamingMessage() { AgentCard cardWithExtension = AgentCard.builder() .name("test-card") .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -697,7 +699,7 @@ public void testRequiredExtensionProvidedSuccess() { AgentCard cardWithExtension = AgentCard.builder() .name("test-card") .description("Test card with required extension") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -760,7 +762,7 @@ public void testVersionNotSupportedErrorOnSendMessage() { AgentCard agentCard = AgentCard.builder() .name("test-card") .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -810,7 +812,7 @@ public void testVersionNotSupportedErrorOnSendStreamingMessage() { AgentCard agentCard = AgentCard.builder() .name("test-card") .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -906,7 +908,7 @@ public void testCompatibleVersionSuccess() { AgentCard agentCard = AgentCard.builder() .name("test-card") .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -961,7 +963,7 @@ public void testNoVersionDefaultsTo0_3_RejectedByV10OnlyServer() { AgentCard agentCard = AgentCard.builder() .name("test-card") .description("Test card with version 1.0") - .supportedInterfaces(Collections.singletonList(new AgentInterface("REST", "http://localhost:9999"))) + .supportedInterfaces(Collections.singletonList(new AgentInterface("HTTP+JSON", "http://localhost:9999"))) .version("1.0.0") .capabilities(AgentCapabilities.builder() .streaming(true) @@ -1077,6 +1079,16 @@ public void testListTasksEmptyResultIncludesAllFields() { "tasks should be empty array"); } + @Test + void constructorDoesNotResolveAgentCardInstance() { + @SuppressWarnings("unchecked") + Instance throwOnGet = Mockito.mock(Instance.class); + Mockito.when(throwOnGet.get()).thenThrow(new AssertionError("Instance.get() must not be called during construction")); + + Assertions.assertDoesNotThrow(() -> new RestHandler(throwOnGet, throwOnGet, + createCacheMetadata(), requestHandler, internalExecutor)); + } + private static void assertProblemDetail(RestHandler.HTTPRestResponse response, int expectedStatus, String expectedReason, String expectedMessage) { Assertions.assertEquals(expectedStatus, response.getStatusCode());