Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,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.
Expand Down Expand Up @@ -170,6 +172,8 @@
@Singleton
public class A2AServerRoutes {

private static final Logger LOG = LoggerFactory.getLogger(A2AServerRoutes.class);

@Inject
JSONRPCHandler jsonRpcHandler;

Expand Down Expand Up @@ -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("Internal error while processing request", t);
error = new A2AErrorResponse(new InternalError("Internal error"));
} finally {
if (error != null) {
rc.response()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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("Internal error while processing request", t);
response = jsonRestHandler.createErrorResponse(new InternalError("Internal error"));
} finally {
sendResponse(rc, response);
}
Expand Down Expand Up @@ -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("Internal error while processing request", t);
response = jsonRestHandler.createErrorResponse(new InternalError("Internal error"));
} finally {
sendResponse(rc, response);
}
Expand Down Expand Up @@ -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("Internal error while processing request", t);
response = jsonRestHandler.createErrorResponse(new InternalError("Internal error"));
} finally {
sendResponse(rc, response);
}
Expand Down Expand Up @@ -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("Internal error while processing request", t);
response = jsonRestHandler.createErrorResponse(new InternalError("Internal error"));
}
} finally {
sendResponse(rc, response);
Expand Down Expand Up @@ -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("Internal error while processing request", t);
response = jsonRestHandler.createErrorResponse(new InternalError("Internal error"));
} finally {
sendResponse(rc, response);
}
Expand Down Expand Up @@ -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("Internal error while processing request", t);
response = jsonRestHandler.createErrorResponse(new InternalError("Internal error"));
} finally {
sendResponse(rc, response);
}
Expand Down Expand Up @@ -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("Internal error while processing request", t);
response = jsonRestHandler.createErrorResponse(new InternalError("Internal error"));
} finally {
sendResponse(rc, response);
}
Expand Down Expand Up @@ -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("Internal error while processing request", t);
response = jsonRestHandler.createErrorResponse(new InternalError("Internal error"));
} finally {
sendResponse(rc, response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -826,7 +827,11 @@ private <V> void handleSecurityException(StreamObserver<V> responseObserver, Sec
}

private <V> void handleInternalError(StreamObserver<V> responseObserver, Throwable t) {
handleError(responseObserver, new InternalError(t.getMessage()));
// 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"));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,30 @@ public void testOnMessageStreamInternalError() throws Exception {
assertGrpcError(streamRecorder, Status.Code.INTERNAL);
}

@Test
public void testOnMessageInternalErrorIsSanitized() throws Exception {
// 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<org.a2aproject.sdk.grpc.SendMessageResponse> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -132,6 +134,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
Expand Down Expand Up @@ -236,7 +240,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));
}
}

Expand Down Expand Up @@ -296,7 +300,7 @@ public Flow.Publisher<SendStreamingMessageResponse> 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)));
}
}

Expand Down Expand Up @@ -335,7 +339,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));
}
}

Expand Down Expand Up @@ -392,7 +396,7 @@ public Flow.Publisher<SendStreamingMessageResponse> 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)));
}
}

Expand Down Expand Up @@ -433,7 +437,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));
}
}

Expand Down Expand Up @@ -475,7 +479,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));
}
}

Expand Down Expand Up @@ -510,7 +514,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));
}
}

Expand Down Expand Up @@ -557,7 +561,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));
}
}

Expand Down Expand Up @@ -598,7 +602,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));
}
}

Expand Down Expand Up @@ -639,7 +643,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));
}
}

Expand Down Expand Up @@ -681,7 +685,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));
}
}

Expand Down Expand Up @@ -728,8 +732,7 @@ public void onError(Throwable throwable) {
} else {
tube.send(
new SendStreamingMessageResponse(
requestId, new
InternalError(throwable.getMessage())));
requestId, internalError(throwable)));
}
onComplete();
}
Expand All @@ -746,4 +749,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.
* <p>
* 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");
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -1188,6 +1189,25 @@ public void testOnMessageSendInternalError() {
assertInstanceOf(InternalError.class, response.getError());
}

@Test
public void testOnMessageSendSanitizesUnexpectedException() {
// 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);
Expand Down
Loading
Loading