From 8b6f40cff465f8cd354e03b1ffd61f4e283db3a3 Mon Sep 17 00:00:00 2001 From: meraklbz Date: Mon, 10 Aug 2026 20:33:53 +0800 Subject: [PATCH 1/5] fix: sanitize internal error messages in transport handlers --- .../transport/grpc/handler/GrpcHandler.java | 7 ++- .../grpc/handler/GrpcHandlerTest.java | 24 ++++++++++ .../jsonrpc/handler/JSONRPCHandler.java | 45 +++++++++++++------ .../jsonrpc/handler/JSONRPCHandlerTest.java | 20 +++++++++ .../transport/rest/handler/RestHandler.java | 42 +++++++++++------ .../rest/handler/RestHandlerTest.java | 31 +++++++++++++ 6 files changed, 142 insertions(+), 27 deletions(-) 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 70185f32d..a4a259f9b 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 @@ -13,6 +13,7 @@ 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 jakarta.enterprise.inject.Vetoed; @@ -822,7 +823,11 @@ private void handleSecurityException(StreamObserver responseObserver, Sec } private void handleInternalError(StreamObserver responseObserver, Throwable t) { - handleError(responseObserver, new InternalError(t.getMessage())); + // Log the full exception server-side but send only a generic message to the client + // (BUG-12/46): leaking internal exception messages can expose file paths, library + // names, and other implementation details that aid server fingerprinting (CWE-209). + LOGGER.log(Level.SEVERE, "Internal error while processing gRPC request", t); + handleError(responseObserver, new InternalError("Internal error")); } diff --git a/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandlerTest.java b/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandlerTest.java index 66c620164..111f42ee1 100644 --- a/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandlerTest.java +++ b/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandlerTest.java @@ -651,6 +651,30 @@ public void testOnMessageStreamInternalError() throws Exception { assertGrpcError(streamRecorder, Status.Code.INTERNAL); } + @Test + public void testOnMessageInternalErrorIsSanitized() throws Exception { + // BUG-12/46: a non-A2AError exception must not leak its message to the client + DefaultRequestHandler mocked = Mockito.mock(DefaultRequestHandler.class); + Mockito.doThrow(new RuntimeException("sensitive detail: /var/lib/secret/config.db")) + .when(mocked).onMessageSend(Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); + GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, mocked, internalExecutor); + + org.a2aproject.sdk.grpc.SendMessageRequest request = org.a2aproject.sdk.grpc.SendMessageRequest.newBuilder() + .setMessage(GRPC_MESSAGE) + .build(); + StreamRecorder responseObserver = StreamRecorder.create(); + handler.sendMessage(request, responseObserver); + responseObserver.awaitCompletion(5, TimeUnit.SECONDS); + + Assertions.assertNotNull(responseObserver.getError()); + Assertions.assertInstanceOf(StatusRuntimeException.class, responseObserver.getError()); + StatusRuntimeException sre = (StatusRuntimeException) responseObserver.getError(); + Assertions.assertEquals(Status.Code.INTERNAL, sre.getStatus().getCode()); + Assertions.assertEquals("Internal error", sre.getStatus().getDescription()); + Assertions.assertFalse(sre.getStatus().getDescription().contains("sensitive"), + "Internal exception message must not be leaked to the client"); + } + @Test public void testListPushNotificationConfig() throws Exception { GrpcHandler handler = new TestGrpcHandler(AbstractA2ARequestHandlerTest.CARD, requestHandler, internalExecutor); 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 e6ad92568..29d0a52d2 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,8 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Flow; +import java.util.logging.Level; +import java.util.logging.Logger; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; @@ -133,6 +135,8 @@ @ApplicationScoped public class JSONRPCHandler { + private static final Logger LOGGER = Logger.getLogger(JSONRPCHandler.class.getName()); + // 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 @@ -237,7 +241,7 @@ public SendMessageResponse onMessageSend(SendMessageRequest request, ServerCallC } catch (A2AError e) { return new SendMessageResponse(request.getId(), e); } catch (Throwable t) { - return new SendMessageResponse(request.getId(), new InternalError(t.getMessage())); + return new SendMessageResponse(request.getId(), internalError(t)); } } @@ -297,7 +301,7 @@ public Flow.Publisher onMessageSendStream( } catch (A2AError e) { return ZeroPublisher.fromItems(new SendStreamingMessageResponse(request.getId(), e)); } catch (Throwable throwable) { - return ZeroPublisher.fromItems(new SendStreamingMessageResponse(request.getId(), new InternalError(throwable.getMessage()))); + return ZeroPublisher.fromItems(new SendStreamingMessageResponse(request.getId(), internalError(throwable))); } } @@ -336,7 +340,7 @@ public CancelTaskResponse onCancelTask(CancelTaskRequest request, ServerCallCont } catch (A2AError e) { return new CancelTaskResponse(request.getId(), e); } catch (Throwable t) { - return new CancelTaskResponse(request.getId(), new InternalError(t.getMessage())); + return new CancelTaskResponse(request.getId(), internalError(t)); } } @@ -393,7 +397,7 @@ public Flow.Publisher onSubscribeToTask( // Other A2AError types - wrap inline as part of the stream return ZeroPublisher.fromItems(new SendStreamingMessageResponse(request.getId(), e)); } catch (Throwable throwable) { - return ZeroPublisher.fromItems(new SendStreamingMessageResponse(request.getId(), new InternalError(throwable.getMessage()))); + return ZeroPublisher.fromItems(new SendStreamingMessageResponse(request.getId(), internalError(throwable))); } } @@ -434,7 +438,7 @@ public GetTaskPushNotificationConfigResponse getPushNotificationConfig( } catch (A2AError e) { return new GetTaskPushNotificationConfigResponse(request.getId(), e); } catch (Throwable t) { - return new GetTaskPushNotificationConfigResponse(request.getId(), new InternalError(t.getMessage())); + return new GetTaskPushNotificationConfigResponse(request.getId(), internalError(t)); } } @@ -476,7 +480,7 @@ public CreateTaskPushNotificationConfigResponse setPushNotificationConfig( } catch (A2AError e) { return new CreateTaskPushNotificationConfigResponse(request.getId(), e); } catch (Throwable t) { - return new CreateTaskPushNotificationConfigResponse(request.getId(), new InternalError(t.getMessage())); + return new CreateTaskPushNotificationConfigResponse(request.getId(), internalError(t)); } } @@ -511,7 +515,7 @@ public GetTaskResponse onGetTask(GetTaskRequest request, ServerCallContext conte } catch (A2AError e) { return new GetTaskResponse(request.getId(), e); } catch (Throwable t) { - return new GetTaskResponse(request.getId(), new InternalError(t.getMessage())); + return new GetTaskResponse(request.getId(), internalError(t)); } } @@ -558,7 +562,7 @@ public ListTasksResponse onListTasks(ListTasksRequest request, ServerCallContext } catch (A2AError e) { return new ListTasksResponse(request.getId(), e); } catch (Throwable t) { - return new ListTasksResponse(request.getId(), new InternalError(t.getMessage())); + return new ListTasksResponse(request.getId(), internalError(t)); } } @@ -599,7 +603,7 @@ public ListTaskPushNotificationConfigsResponse listPushNotificationConfigs( } catch (A2AError e) { return new ListTaskPushNotificationConfigsResponse(request.getId(), e); } catch (Throwable t) { - return new ListTaskPushNotificationConfigsResponse(request.getId(), new InternalError(t.getMessage())); + return new ListTaskPushNotificationConfigsResponse(request.getId(), internalError(t)); } } @@ -640,7 +644,7 @@ public DeleteTaskPushNotificationConfigResponse deletePushNotificationConfig( } catch (A2AError e) { return new DeleteTaskPushNotificationConfigResponse(request.getId(), e); } catch (Throwable t) { - return new DeleteTaskPushNotificationConfigResponse(request.getId(), new InternalError(t.getMessage())); + return new DeleteTaskPushNotificationConfigResponse(request.getId(), internalError(t)); } } @@ -682,7 +686,7 @@ public GetExtendedAgentCardResponse onGetExtendedCardRequest( } catch (A2AError e) { return new GetExtendedAgentCardResponse(request.getId(), e); } catch (Throwable t) { - return new GetExtendedAgentCardResponse(request.getId(), new InternalError(t.getMessage())); + return new GetExtendedAgentCardResponse(request.getId(), internalError(t)); } } @@ -729,8 +733,7 @@ public void onError(Throwable throwable) { } else { tube.send( new SendStreamingMessageResponse( - requestId, new - InternalError(throwable.getMessage()))); + requestId, internalError(throwable))); } onComplete(); } @@ -747,4 +750,20 @@ public void onComplete() { public void authorizeTaskAccess(String requestedTaskId, ServerCallContext context, TaskOperation operation) { requestHandler.authorizeTaskAccess(requestedTaskId, context, operation); } + + /** + * Builds a client-safe {@link InternalError} for an unexpected exception (BUG-12/46). + *

+ * The original exception (class, message, stack trace) is logged server-side but the + * client receives only a generic message: leaking internal exception messages can + * expose file paths, library names, and other implementation details that aid server + * fingerprinting (CWE-209). + * + * @param t the unexpected exception + * @return a sanitized internal error with a generic message + */ + private static InternalError internalError(Throwable t) { + LOGGER.log(Level.SEVERE, "Internal error while processing request", t); + return new InternalError("Internal error"); + } } 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 431c089ae..8da2797b7 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,6 +1,7 @@ package org.a2aproject.sdk.transport.jsonrpc.handler; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -1189,6 +1190,25 @@ public void testOnMessageSendInternalError() { assertInstanceOf(InternalError.class, response.getError()); } + @Test + public void testOnMessageSendSanitizesUnexpectedException() { + // BUG-12/46: a non-A2AError exception must not leak its message to the client + DefaultRequestHandler mocked = Mockito.mock(DefaultRequestHandler.class); + Mockito.doThrow(new RuntimeException("sensitive detail: /var/lib/secret/config.db")) + .when(mocked) + .onMessageSend(Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); + + JSONRPCHandler handler = new JSONRPCHandler(CARD, mocked, internalExecutor); + + SendMessageRequest request = new SendMessageRequest("1", new MessageSendParams(MESSAGE, defaultConfiguration(), null)); + SendMessageResponse response = handler.onMessageSend(request, callContext); + + assertInstanceOf(InternalError.class, response.getError()); + assertEquals("Internal error", response.getError().getMessage()); + assertFalse(response.getError().getMessage().contains("sensitive"), + "Internal exception message must not be leaked to the client"); + } + @Test public void testOnMessageStreamInternalError() { DefaultRequestHandler mocked = Mockito.mock(DefaultRequestHandler.class); 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 131d5b9b7..98a661b8c 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 @@ -237,7 +237,7 @@ public HTTPRestResponse sendMessage(ServerCallContext context, String tenant, St } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { - return createErrorResponse(new InternalError(throwable.getMessage())); + return createErrorResponse(internalError(throwable)); } } @@ -311,7 +311,7 @@ public HTTPRestResponse sendStreamingMessage(ServerCallContext context, String t } catch (A2AError e) { return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(e).toJson())); } catch (Throwable throwable) { - return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(new InternalError(throwable.getMessage())).toJson())); + return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(internalError(throwable)).toJson())); } } @@ -354,7 +354,7 @@ public HTTPRestResponse cancelTask(ServerCallContext context, String tenant, Str } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { - return createErrorResponse(new InternalError(throwable.getMessage())); + return createErrorResponse(internalError(throwable)); } } @@ -386,7 +386,7 @@ public HTTPRestResponse createTaskPushNotificationConfiguration(ServerCallContex } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { - return createErrorResponse(new InternalError(throwable.getMessage())); + return createErrorResponse(internalError(throwable)); } } @@ -438,7 +438,7 @@ public HTTPRestResponse subscribeToTask(ServerCallContext context, String tenant } catch (A2AError e) { return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(e).toJson())); } catch (Throwable throwable) { - return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(new InternalError(throwable.getMessage())).toJson())); + return new HTTPRestStreamingResponse(ZeroPublisher.fromItems(new HTTPRestErrorResponse(internalError(throwable)).toJson())); } } @@ -464,7 +464,7 @@ public HTTPRestResponse getTask(ServerCallContext context, String tenant, String } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { - return createErrorResponse(new InternalError(throwable.getMessage())); + return createErrorResponse(internalError(throwable)); } } @@ -566,7 +566,7 @@ public HTTPRestResponse listTasks(ServerCallContext context, String tenant, } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { - return createErrorResponse(new InternalError(throwable.getMessage())); + return createErrorResponse(internalError(throwable)); } } @@ -590,7 +590,7 @@ public HTTPRestResponse getTaskPushNotificationConfiguration(ServerCallContext c } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { - return createErrorResponse(new InternalError(throwable.getMessage())); + return createErrorResponse(internalError(throwable)); } } @@ -615,7 +615,7 @@ public HTTPRestResponse listTaskPushNotificationConfigurations(ServerCallContext } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { - return createErrorResponse(new InternalError(throwable.getMessage())); + return createErrorResponse(internalError(throwable)); } } @@ -639,7 +639,7 @@ public HTTPRestResponse deleteTaskPushNotificationConfiguration(ServerCallContex } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable throwable) { - return createErrorResponse(new InternalError(throwable.getMessage())); + return createErrorResponse(internalError(throwable)); } } @@ -691,6 +691,22 @@ private HTTPRestResponse createErrorResponse(int statusCode, A2AError error) { return new HTTPRestResponse(statusCode, APPLICATION_JSON, jsonBody); } + /** + * Builds a client-safe {@link InternalError} for an unexpected exception (BUG-12/46). + *

+ * The original exception (class, message, stack trace) is logged server-side but the + * client receives only a generic message: leaking internal exception messages can + * expose file paths, library names, and other implementation details that aid server + * fingerprinting (CWE-209). + * + * @param t the unexpected exception + * @return a sanitized internal error with a generic message + */ + private static InternalError internalError(Throwable t) { + log.log(Level.SEVERE, "Internal error while processing request", t); + return new InternalError("Internal error"); + } + private HTTPRestStreamingResponse createStreamingResponse(Flow.Publisher publisher) { return new HTTPRestStreamingResponse(convertToSendStreamingMessageResponse(publisher)); } @@ -744,7 +760,7 @@ public void onError(Throwable throwable) { if (throwable instanceof A2AError jsonrpcError) { tube.send(new HTTPRestErrorResponse(jsonrpcError).toJson()); } else { - tube.send(new HTTPRestErrorResponse(new InternalError(throwable.getMessage())).toJson()); + tube.send(new HTTPRestErrorResponse(internalError(throwable)).toJson()); } onComplete(); } @@ -807,7 +823,7 @@ public HTTPRestResponse getExtendedAgentCard(ServerCallContext context, String t } catch (A2AError e) { return createErrorResponse(e); } catch (Throwable t) { - return createErrorResponse(500, new InternalError(t.getMessage())); + return createErrorResponse(500, internalError(t)); } } @@ -851,7 +867,7 @@ public HTTPRestResponse getAgentCard() { return new HTTPRestResponse(200, APPLICATION_JSON, JsonUtil.toJson(agentCard), cacheMetadata.getHttpHeadersMap()); } catch (Throwable t) { - return createErrorResponse(500, new InternalError(t.getMessage())); + return createErrorResponse(500, internalError(t)); } } 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 8be11c609..f299471f9 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 @@ -23,6 +23,7 @@ import org.a2aproject.sdk.server.auth.UnauthenticatedUser; import org.a2aproject.sdk.server.config.DefaultValuesConfigProvider; import org.a2aproject.sdk.server.requesthandlers.AbstractA2ARequestHandlerTest; +import org.a2aproject.sdk.server.requesthandlers.RequestHandler; import org.a2aproject.sdk.spec.AgentCapabilities; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentExtension; @@ -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 { @@ -1095,4 +1097,33 @@ private static void assertProblemDetail(RestHandler.HTTPRestResponse response, Assertions.assertEquals(expectedReason, detail.get("reason").getAsString(), "reason field mismatch"); Assertions.assertEquals("a2a-protocol.org", detail.get("domain").getAsString(), "domain field mismatch"); } + + @Test + public void testSendMessageSanitizesInternalError() { + // BUG-12/46: a non-A2AError exception must not leak its message to the client + RequestHandler mocked = Mockito.mock(RequestHandler.class); + Mockito.doThrow(new RuntimeException("sensitive detail: /var/lib/secret/config.db")) + .when(mocked).onMessageSend(Mockito.any(), Mockito.any()); + + RestHandler handler = new RestHandler(CARD, createCacheMetadata(), mocked, internalExecutor); + String requestBody = """ + { + "message": { + "messageId": "message-1234", + "contextId": "context-1234", + "role": "ROLE_USER", + "parts": [{"text": "hello"}], + "metadata": {} + } + }"""; + + RestHandler.HTTPRestResponse response = handler.sendMessage(callContext, "", requestBody); + + JsonObject body = JsonParser.parseString(response.getBody()).getAsJsonObject(); + JsonObject error = body.getAsJsonObject("error"); + Assertions.assertEquals(500, error.get("code").getAsInt()); + Assertions.assertEquals("Internal error", error.get("message").getAsString()); + Assertions.assertFalse(error.get("message").getAsString().contains("sensitive"), + "Internal exception message must not be leaked to the client"); + } } From 19fe9eea0c916da820aaa67e97a042b6cce08e60 Mon Sep 17 00:00:00 2001 From: meraklbz Date: Tue, 11 Aug 2026 21:33:43 +0800 Subject: [PATCH 2/5] chore: remove internal tracking ids from comments --- .../a2aproject/sdk/transport/grpc/handler/GrpcHandler.java | 4 ++-- .../sdk/transport/grpc/handler/GrpcHandlerTest.java | 2 +- .../sdk/transport/jsonrpc/handler/JSONRPCHandler.java | 2 +- .../sdk/transport/jsonrpc/handler/JSONRPCHandlerTest.java | 2 +- .../a2aproject/sdk/transport/rest/handler/RestHandler.java | 2 +- .../sdk/transport/rest/handler/RestHandlerTest.java | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) 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 a4a259f9b..231ac087b 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 @@ -823,8 +823,8 @@ private void handleSecurityException(StreamObserver responseObserver, Sec } private void handleInternalError(StreamObserver responseObserver, Throwable t) { - // Log the full exception server-side but send only a generic message to the client - // (BUG-12/46): leaking internal exception messages can expose file paths, library + // Log the full exception server-side but send only a generic message to the client: + // leaking internal exception messages can expose file paths, library // names, and other implementation details that aid server fingerprinting (CWE-209). LOGGER.log(Level.SEVERE, "Internal error while processing gRPC request", t); handleError(responseObserver, new InternalError("Internal error")); diff --git a/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandlerTest.java b/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandlerTest.java index 111f42ee1..10e4cec9a 100644 --- a/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandlerTest.java +++ b/transport/grpc/src/test/java/org/a2aproject/sdk/transport/grpc/handler/GrpcHandlerTest.java @@ -653,7 +653,7 @@ public void testOnMessageStreamInternalError() throws Exception { @Test public void testOnMessageInternalErrorIsSanitized() throws Exception { - // BUG-12/46: a non-A2AError exception must not leak its message to the client + // A non-A2AError exception must not leak its message to the client DefaultRequestHandler mocked = Mockito.mock(DefaultRequestHandler.class); Mockito.doThrow(new RuntimeException("sensitive detail: /var/lib/secret/config.db")) .when(mocked).onMessageSend(Mockito.any(MessageSendParams.class), Mockito.any(ServerCallContext.class)); 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 29d0a52d2..8675fd0bb 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 @@ -752,7 +752,7 @@ public void authorizeTaskAccess(String requestedTaskId, ServerCallContext contex } /** - * Builds a client-safe {@link InternalError} for an unexpected exception (BUG-12/46). + * Builds a client-safe {@link InternalError} for an unexpected exception. *

* The original exception (class, message, stack trace) is logged server-side but the * client receives only a generic message: leaking internal exception messages can 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 8da2797b7..ba1805f1b 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 @@ -1192,7 +1192,7 @@ public void testOnMessageSendInternalError() { @Test public void testOnMessageSendSanitizesUnexpectedException() { - // BUG-12/46: a non-A2AError exception must not leak its message to the client + // A non-A2AError exception must not leak its message to the client DefaultRequestHandler mocked = Mockito.mock(DefaultRequestHandler.class); Mockito.doThrow(new RuntimeException("sensitive detail: /var/lib/secret/config.db")) .when(mocked) 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 98a661b8c..9e5cf4e45 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 @@ -692,7 +692,7 @@ private HTTPRestResponse createErrorResponse(int statusCode, A2AError error) { } /** - * Builds a client-safe {@link InternalError} for an unexpected exception (BUG-12/46). + * Builds a client-safe {@link InternalError} for an unexpected exception. *

* The original exception (class, message, stack trace) is logged server-side but the * client receives only a generic message: leaking internal exception messages can 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 f299471f9..7ac6823d7 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 @@ -1100,7 +1100,7 @@ private static void assertProblemDetail(RestHandler.HTTPRestResponse response, @Test public void testSendMessageSanitizesInternalError() { - // BUG-12/46: a non-A2AError exception must not leak its message to the client + // A non-A2AError exception must not leak its message to the client RequestHandler mocked = Mockito.mock(RequestHandler.class); Mockito.doThrow(new RuntimeException("sensitive detail: /var/lib/secret/config.db")) .when(mocked).onMessageSend(Mockito.any(), Mockito.any()); From e75cb8db4deaacbbbddbd419fab5bd9349481213 Mon Sep 17 00:00:00 2001 From: meraklbz Date: Tue, 11 Aug 2026 23:08:50 +0800 Subject: [PATCH 3/5] fix: sanitize internal error messages in reference server routes The reference JSON-RPC and REST route handlers still propagated the raw exception message to clients via new InternalError(t.getMessage()). Use the generic InternalError message and log the original throwable so server-side observability is preserved. --- .../server/apps/quarkus/A2AServerRoutes.java | 7 ++++- .../server/rest/quarkus/A2AServerRoutes.java | 28 +++++++++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java b/reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java index 44e267cf7..6e68b9cc5 100644 --- a/reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java +++ b/reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java @@ -6,6 +6,8 @@ import static org.a2aproject.sdk.transport.jsonrpc.context.JSONRPCContextKeys.HEADERS_KEY; import static org.a2aproject.sdk.transport.jsonrpc.context.JSONRPCContextKeys.METHOD_NAME_KEY; import static org.a2aproject.sdk.transport.jsonrpc.context.JSONRPCContextKeys.TENANT_KEY; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.List; @@ -170,6 +172,8 @@ @Singleton public class A2AServerRoutes { + private static final Logger LOG = LoggerFactory.getLogger(A2AServerRoutes.class); + @Inject JSONRPCHandler jsonRpcHandler; @@ -337,7 +341,8 @@ public void invokeJSONRPCHandler(String body, RoutingContext rc) { } catch (JsonSyntaxException | JsonProcessingException e) { error = new A2AErrorResponse(new JSONParseError(e.getMessage())); } catch (Throwable t) { - error = new A2AErrorResponse(new InternalError(t.getMessage())); + LOG.error("Failed to process request", t); + error = new A2AErrorResponse(new InternalError("Internal error")); } finally { if (error != null) { rc.response() diff --git a/reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java b/reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java index e68bf055c..d031d479d 100644 --- a/reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java +++ b/reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java @@ -7,6 +7,8 @@ import static org.a2aproject.sdk.transport.rest.context.RestContextKeys.METHOD_NAME_KEY; import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.List; @@ -125,6 +127,8 @@ @Singleton public class A2AServerRoutes { + private static final Logger LOG = LoggerFactory.getLogger(A2AServerRoutes.class); + private static final String HISTORY_LENGTH_PARAM = "historyLength"; private static final String PAGE_SIZE_PARAM = "pageSize"; private static final String PAGE_TOKEN_PARAM = "pageToken"; @@ -306,7 +310,8 @@ public void sendMessage(String body, RoutingContext rc) { try { response = jsonRestHandler.sendMessage(context, extractTenant(rc), body); } catch (Throwable t) { - response = jsonRestHandler.createErrorResponse(new InternalError(t.getMessage())); + LOG.error("Failed to process request", t); + response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); } @@ -424,7 +429,8 @@ public void listTasks(RoutingContext rc) { } catch (IllegalArgumentException e) { response = jsonRestHandler.createErrorResponse(new InvalidParamsError("Invalid parameter value: " + e.getMessage())); } catch (Throwable t) { - response = jsonRestHandler.createErrorResponse(new InternalError(t.getMessage())); + LOG.error("Failed to process request", t); + response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); } @@ -458,7 +464,8 @@ public void getTask(RoutingContext rc) { } catch (NumberFormatException e) { response = jsonRestHandler.createErrorResponse(new InvalidParamsError("bad historyLength")); } catch (Throwable t) { - response = jsonRestHandler.createErrorResponse(new InternalError(t.getMessage())); + LOG.error("Failed to process request", t); + response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); } @@ -492,7 +499,8 @@ public void cancelTask(String body, RoutingContext rc) { if (t instanceof A2AError error) { response = jsonRestHandler.createErrorResponse(error); } else { - response = jsonRestHandler.createErrorResponse(new InternalError(t.getMessage())); + LOG.error("Failed to process request", t); + response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } } finally { sendResponse(rc, response); @@ -602,7 +610,8 @@ public void createTaskPushNotificationConfiguration(String body, RoutingContext response = jsonRestHandler.createTaskPushNotificationConfiguration(context, extractTenant(rc), body, taskId); } } catch (Throwable t) { - response = jsonRestHandler.createErrorResponse(new InternalError(t.getMessage())); + LOG.error("Failed to process request", t); + response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); } @@ -633,7 +642,8 @@ public void getTaskPushNotificationConfiguration(RoutingContext rc) { response = jsonRestHandler.getTaskPushNotificationConfiguration(context, extractTenant(rc), taskId, configId); } } catch (Throwable t) { - response = jsonRestHandler.createErrorResponse(new InternalError(t.getMessage())); + LOG.error("Failed to process request", t); + response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); } @@ -678,7 +688,8 @@ public void listTaskPushNotificationConfigurations(RoutingContext rc) { } catch (NumberFormatException e) { response = jsonRestHandler.createErrorResponse(new InvalidParamsError("bad " + PAGE_SIZE_PARAM)); } catch (Throwable t) { - response = jsonRestHandler.createErrorResponse(new InternalError(t.getMessage())); + LOG.error("Failed to process request", t); + response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); } @@ -711,7 +722,8 @@ public void deleteTaskPushNotificationConfiguration(RoutingContext rc) { response = jsonRestHandler.deleteTaskPushNotificationConfiguration(context, extractTenant(rc), taskId, configId); } } catch (Throwable t) { - response = jsonRestHandler.createErrorResponse(new InternalError(t.getMessage())); + LOG.error("Failed to process request", t); + response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); } From 160179487b6cea9b018749640023c81370629301 Mon Sep 17 00:00:00 2001 From: meraklbz Date: Tue, 11 Aug 2026 23:33:10 +0800 Subject: [PATCH 4/5] ci: retrigger checks (flaky compat-0.3 resubscribe timeout) From 1cda910081956ce830b64fad939ca35004c5230f Mon Sep 17 00:00:00 2001 From: meraklbz Date: Wed, 12 Aug 2026 21:56:22 +0800 Subject: [PATCH 5/5] fix: sanitize remaining serialization error and align reference route messages - RestHandler: the response-serialization failure path leaked the InvalidProtocolBufferException message; sanitize it and log the throwable. - Reference JSON-RPC/REST routes now log the same message as the transport handlers ('Internal error while processing request') and place the slf4j imports after the java imports. --- .../server/apps/quarkus/A2AServerRoutes.java | 6 +++--- .../server/rest/quarkus/A2AServerRoutes.java | 20 +++++++++---------- .../transport/rest/handler/RestHandler.java | 3 ++- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java b/reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java index 6e68b9cc5..f0349d03f 100644 --- a/reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java +++ b/reference/jsonrpc/src/main/java/org/a2aproject/sdk/server/apps/quarkus/A2AServerRoutes.java @@ -6,8 +6,6 @@ import static org.a2aproject.sdk.transport.jsonrpc.context.JSONRPCContextKeys.HEADERS_KEY; import static org.a2aproject.sdk.transport.jsonrpc.context.JSONRPCContextKeys.METHOD_NAME_KEY; import static org.a2aproject.sdk.transport.jsonrpc.context.JSONRPCContextKeys.TENANT_KEY; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.List; @@ -81,6 +79,8 @@ import org.a2aproject.sdk.spec.UnsupportedOperationError; import org.a2aproject.sdk.transport.jsonrpc.handler.JSONRPCHandler; import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Quarkus routing configuration for JSON-RPC A2A protocol requests. @@ -341,7 +341,7 @@ public void invokeJSONRPCHandler(String body, RoutingContext rc) { } catch (JsonSyntaxException | JsonProcessingException e) { error = new A2AErrorResponse(new JSONParseError(e.getMessage())); } catch (Throwable t) { - LOG.error("Failed to process request", t); + LOG.error("Internal error while processing request", t); error = new A2AErrorResponse(new InternalError("Internal error")); } finally { if (error != null) { diff --git a/reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java b/reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java index d031d479d..8cd9f7af3 100644 --- a/reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java +++ b/reference/rest/src/main/java/org/a2aproject/sdk/server/rest/quarkus/A2AServerRoutes.java @@ -7,8 +7,6 @@ import static org.a2aproject.sdk.transport.rest.context.RestContextKeys.METHOD_NAME_KEY; import static io.vertx.core.http.HttpHeaders.CONTENT_TYPE; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.List; @@ -56,6 +54,8 @@ import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.BodyHandler; import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import static org.a2aproject.sdk.spec.A2AMethods.DELETE_TASK_PUSH_NOTIFICATION_CONFIG_METHOD; import static org.a2aproject.sdk.spec.A2AMethods.GET_EXTENDED_AGENT_CARD_METHOD; @@ -310,7 +310,7 @@ public void sendMessage(String body, RoutingContext rc) { try { response = jsonRestHandler.sendMessage(context, extractTenant(rc), body); } catch (Throwable t) { - LOG.error("Failed to process request", t); + LOG.error("Internal error while processing request", t); response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); @@ -429,7 +429,7 @@ public void listTasks(RoutingContext rc) { } catch (IllegalArgumentException e) { response = jsonRestHandler.createErrorResponse(new InvalidParamsError("Invalid parameter value: " + e.getMessage())); } catch (Throwable t) { - LOG.error("Failed to process request", t); + LOG.error("Internal error while processing request", t); response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); @@ -464,7 +464,7 @@ public void getTask(RoutingContext rc) { } catch (NumberFormatException e) { response = jsonRestHandler.createErrorResponse(new InvalidParamsError("bad historyLength")); } catch (Throwable t) { - LOG.error("Failed to process request", t); + LOG.error("Internal error while processing request", t); response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); @@ -499,7 +499,7 @@ public void cancelTask(String body, RoutingContext rc) { if (t instanceof A2AError error) { response = jsonRestHandler.createErrorResponse(error); } else { - LOG.error("Failed to process request", t); + LOG.error("Internal error while processing request", t); response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } } finally { @@ -610,7 +610,7 @@ public void createTaskPushNotificationConfiguration(String body, RoutingContext response = jsonRestHandler.createTaskPushNotificationConfiguration(context, extractTenant(rc), body, taskId); } } catch (Throwable t) { - LOG.error("Failed to process request", t); + LOG.error("Internal error while processing request", t); response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); @@ -642,7 +642,7 @@ public void getTaskPushNotificationConfiguration(RoutingContext rc) { response = jsonRestHandler.getTaskPushNotificationConfiguration(context, extractTenant(rc), taskId, configId); } } catch (Throwable t) { - LOG.error("Failed to process request", t); + LOG.error("Internal error while processing request", t); response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); @@ -688,7 +688,7 @@ public void listTaskPushNotificationConfigurations(RoutingContext rc) { } catch (NumberFormatException e) { response = jsonRestHandler.createErrorResponse(new InvalidParamsError("bad " + PAGE_SIZE_PARAM)); } catch (Throwable t) { - LOG.error("Failed to process request", t); + LOG.error("Internal error while processing request", t); response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); @@ -722,7 +722,7 @@ public void deleteTaskPushNotificationConfiguration(RoutingContext rc) { response = jsonRestHandler.deleteTaskPushNotificationConfiguration(context, extractTenant(rc), taskId, configId); } } catch (Throwable t) { - LOG.error("Failed to process request", t); + LOG.error("Internal error while processing request", t); response = jsonRestHandler.createErrorResponse(new InternalError("Internal error")); } finally { sendResponse(rc, response); 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 9e5cf4e45..445024992 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 @@ -671,7 +671,8 @@ private HTTPRestResponse createSuccessResponse(int statusCode, com.google.protob String jsonBody = ProtoJsonUtils.toJson(JsonFormat.printer().alwaysPrintFieldsWithNoPresence(), builder); return new HTTPRestResponse(statusCode, APPLICATION_JSON, jsonBody); } catch (InvalidProtocolBufferException e) { - return createErrorResponse(new InternalError("Failed to serialize response: " + e.getMessage())); + log.log(Level.SEVERE, "Failed to serialize response", e); + return createErrorResponse(new InternalError("Internal error")); } }