Skip to content
Open
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 @@ -3,6 +3,8 @@
import io.a2a.common.A2AErrorMessages;
import io.a2a.spec.A2AClientException;
import io.a2a.spec.ContentTypeNotSupportedError;
import io.a2a.spec.ExtendedCardNotConfiguredError;
import io.a2a.spec.ExtensionSupportRequiredError;
import io.a2a.spec.InvalidAgentResponseError;
import io.a2a.spec.InvalidParamsError;
import io.a2a.spec.InvalidRequestError;
Expand All @@ -12,6 +14,7 @@
import io.a2a.spec.TaskNotCancelableError;
import io.a2a.spec.TaskNotFoundError;
import io.a2a.spec.UnsupportedOperationError;
import io.a2a.spec.VersionNotSupportedError;
import io.grpc.Status;

/**
Expand Down Expand Up @@ -52,6 +55,12 @@ public static A2AClientException mapGrpcError(Throwable e, String errorPrefix) {
return new A2AClientException(errorPrefix + description, new ContentTypeNotSupportedError(null, description, null));
} else if (description.contains("InvalidAgentResponseError")) {
return new A2AClientException(errorPrefix + description, new InvalidAgentResponseError(null, description, null));
} else if (description.contains("ExtendedCardNotConfiguredError")) {
return new A2AClientException(errorPrefix + description, new ExtendedCardNotConfiguredError(null, description, null));
} else if (description.contains("ExtensionSupportRequiredError")) {
return new A2AClientException(errorPrefix + description, new ExtensionSupportRequiredError(null, description, null));
} else if (description.contains("VersionNotSupportedError")) {
return new A2AClientException(errorPrefix + description, new VersionNotSupportedError(null, description, null));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ public class GrpcTransport implements ClientTransport {
private static final Metadata.Key<String> EXTENSIONS_KEY = Metadata.Key.of(
A2AHeaders.X_A2A_EXTENSIONS,
Metadata.ASCII_STRING_MARSHALLER);
private static final Metadata.Key<String> VERSION_KEY = Metadata.Key.of(
A2AHeaders.X_A2A_VERSION,
Metadata.ASCII_STRING_MARSHALLER);
private final A2AServiceBlockingV2Stub blockingStub;
private final A2AServiceStub asyncStub;
private final @Nullable List<ClientCallInterceptor> interceptors;
Expand Down Expand Up @@ -366,14 +369,19 @@ private Metadata createGrpcMetadata(@Nullable ClientCallContext context, @Nullab
Metadata metadata = new Metadata();

if (context != null && context.getHeaders() != null) {
// Set X-A2A-Version header if present
String versionHeader = context.getHeaders().get(A2AHeaders.X_A2A_VERSION);
if (versionHeader != null) {
metadata.put(VERSION_KEY, versionHeader);
}

// Set X-A2A-Extensions header if present
String extensionsHeader = context.getHeaders().get(A2AHeaders.X_A2A_EXTENSIONS);
if (extensionsHeader != null) {
metadata.put(EXTENSIONS_KEY, extensionsHeader);
}

// Add other headers as needed in the future
// For now, we only handle X-A2A-Extensions
}
if (payloadAndHeaders != null && payloadAndHeaders.getHeaders() != null) {
// Handle all headers from interceptors (including auth headers)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
package io.a2a.client.transport.grpc;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.a2a.spec.A2AClientException;
import io.a2a.spec.ContentTypeNotSupportedError;
import io.a2a.spec.ExtendedCardNotConfiguredError;
import io.a2a.spec.ExtensionSupportRequiredError;
import io.a2a.spec.InvalidAgentResponseError;
import io.a2a.spec.InvalidParamsError;
import io.a2a.spec.InvalidRequestError;
import io.a2a.spec.JSONParseError;
import io.a2a.spec.MethodNotFoundError;
import io.a2a.spec.PushNotificationNotSupportedError;
import io.a2a.spec.TaskNotCancelableError;
import io.a2a.spec.TaskNotFoundError;
import io.a2a.spec.UnsupportedOperationError;
import io.a2a.spec.VersionNotSupportedError;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import org.junit.jupiter.api.Test;

/**
* Tests for GrpcErrorMapper - verifies correct unmarshalling of gRPC errors to A2A error types
*/
public class GrpcErrorMapperTest {

@Test
public void testExtensionSupportRequiredErrorUnmarshalling() {
// Create a gRPC StatusRuntimeException with ExtensionSupportRequiredError in description
String errorMessage = "ExtensionSupportRequiredError: Extension required: https://example.com/test-extension";
StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
.withDescription(errorMessage)
.asRuntimeException();

// Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);

// Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(ExtensionSupportRequiredError.class, result.getCause());

ExtensionSupportRequiredError extensionError = (ExtensionSupportRequiredError) result.getCause();
assertNotNull(extensionError.getMessage());
assertTrue(extensionError.getMessage().contains("https://example.com/test-extension"));
assertTrue(result.getMessage().contains(errorMessage));
}

@Test
public void testVersionNotSupportedErrorUnmarshalling() {
// Create a gRPC StatusRuntimeException with VersionNotSupportedError in description
String errorMessage = "VersionNotSupportedError: Version 2.0 is not supported";
StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
.withDescription(errorMessage)
.asRuntimeException();

// Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);

// Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(VersionNotSupportedError.class, result.getCause());

VersionNotSupportedError versionError = (VersionNotSupportedError) result.getCause();
assertNotNull(versionError.getMessage());
assertTrue(versionError.getMessage().contains("Version 2.0 is not supported"));
}

@Test
public void testExtendedCardNotConfiguredErrorUnmarshalling() {
// Create a gRPC StatusRuntimeException with ExtendedCardNotConfiguredError in description
String errorMessage = "ExtendedCardNotConfiguredError: Extended card not configured for this agent";
StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
.withDescription(errorMessage)
.asRuntimeException();

// Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);

// Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(ExtendedCardNotConfiguredError.class, result.getCause());

ExtendedCardNotConfiguredError extendedCardError = (ExtendedCardNotConfiguredError) result.getCause();
assertNotNull(extendedCardError.getMessage());
assertTrue(extendedCardError.getMessage().contains("Extended card not configured"));
}

@Test
public void testTaskNotFoundErrorUnmarshalling() {
// Create a gRPC StatusRuntimeException with TaskNotFoundError in description
String errorMessage = "TaskNotFoundError: Task task-123 not found";
StatusRuntimeException grpcException = Status.NOT_FOUND
.withDescription(errorMessage)
.asRuntimeException();

// Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);

// Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(TaskNotFoundError.class, result.getCause());
}

@Test
public void testUnsupportedOperationErrorUnmarshalling() {
// Create a gRPC StatusRuntimeException with UnsupportedOperationError in description
String errorMessage = "UnsupportedOperationError: Operation not supported";
StatusRuntimeException grpcException = Status.UNIMPLEMENTED
.withDescription(errorMessage)
.asRuntimeException();

// Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);

// Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(UnsupportedOperationError.class, result.getCause());
}

@Test
public void testInvalidParamsErrorUnmarshalling() {
// Create a gRPC StatusRuntimeException with InvalidParamsError in description
String errorMessage = "InvalidParamsError: Invalid parameters provided";
StatusRuntimeException grpcException = Status.INVALID_ARGUMENT
.withDescription(errorMessage)
.asRuntimeException();

// Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);

// Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(InvalidParamsError.class, result.getCause());
}

@Test
public void testContentTypeNotSupportedErrorUnmarshalling() {
// Create a gRPC StatusRuntimeException with ContentTypeNotSupportedError in description
String errorMessage = "ContentTypeNotSupportedError: Content type application/xml not supported";
StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
.withDescription(errorMessage)
.asRuntimeException();

// Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);

// Verify the result
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(ContentTypeNotSupportedError.class, result.getCause());

ContentTypeNotSupportedError contentTypeError = (ContentTypeNotSupportedError) result.getCause();
assertNotNull(contentTypeError.getMessage());
assertTrue(contentTypeError.getMessage().contains("Content type application/xml not supported"));
}

@Test
public void testFallbackToStatusCodeMapping() {
// Create a gRPC StatusRuntimeException without specific error type in description
StatusRuntimeException grpcException = Status.NOT_FOUND
.withDescription("Generic not found error")
.asRuntimeException();

// Map the gRPC error to A2A error
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);

// Verify fallback to status code mapping
assertNotNull(result);
assertNotNull(result.getCause());
assertInstanceOf(TaskNotFoundError.class, result.getCause());
}

@Test
public void testCustomErrorPrefix() {
// Create a gRPC StatusRuntimeException
String errorMessage = "ExtensionSupportRequiredError: Extension required: https://example.com/ext";
StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
.withDescription(errorMessage)
.asRuntimeException();

// Map with custom error prefix
String customPrefix = "Custom Error: ";
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException, customPrefix);

// Verify custom prefix is used
assertNotNull(result);
assertTrue(result.getMessage().startsWith(customPrefix));
assertInstanceOf(ExtensionSupportRequiredError.class, result.getCause());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@

import io.a2a.spec.A2AClientException;
import io.a2a.spec.AgentCard;
import io.a2a.spec.ExtensionSupportRequiredError;
import io.a2a.spec.VersionNotSupportedError;
import io.a2a.spec.AgentInterface;
import io.a2a.spec.AgentSkill;
import io.a2a.spec.Artifact;
Expand Down Expand Up @@ -680,4 +682,117 @@ public void testA2AClientSendMessageWithMixedParts() throws Exception {
assertEquals("Analyzed chart image and data: Bar chart showing quarterly data with values [10, 20, 30, 40].", ((TextPart) part).text());
assertTrue(task.metadata().isEmpty());
}

/**
* Test that ExtensionSupportRequiredError is properly unmarshalled from JSON-RPC error response.
*/
@Test
public void testExtensionSupportRequiredErrorUnmarshalling() throws Exception {
// Mock server returns JSON-RPC error with code -32008 (EXTENSION_SUPPORT_REQUIRED_ERROR)
String errorResponseBody = """
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32008,
"message": "Extension required: https://example.com/test-extension"
}
}
""";

this.server.when(
request()
.withMethod("POST")
.withPath("/")
)
.respond(
response()
.withStatusCode(200)
.withBody(errorResponseBody)
);

JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
Message message = Message.builder()
.role(Message.Role.USER)
.parts(Collections.singletonList(new TextPart("test message")))
.contextId("context-test")
.messageId("message-test")
.build();
MessageSendConfiguration configuration = MessageSendConfiguration.builder()
.acceptedOutputModes(List.of("text"))
.blocking(true)
.build();
MessageSendParams params = MessageSendParams.builder()
.message(message)
.configuration(configuration)
.build();

// Should throw A2AClientException with ExtensionSupportRequiredError as cause
try {
client.sendMessage(params, null);
fail("Expected A2AClientException to be thrown");
} catch (A2AClientException e) {
// Verify the cause is ExtensionSupportRequiredError
assertInstanceOf(ExtensionSupportRequiredError.class, e.getCause());
ExtensionSupportRequiredError extensionError = (ExtensionSupportRequiredError) e.getCause();
assertTrue(extensionError.getMessage().contains("https://example.com/test-extension"));
}
}

/**
* Test that VersionNotSupportedError is properly unmarshalled from JSON-RPC error response.
*/
@Test
public void testVersionNotSupportedErrorUnmarshalling() throws Exception {
// Mock server returns JSON-RPC error with code -32009 (VERSION_NOT_SUPPORTED_ERROR)
String errorResponseBody = """
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32009,
"message": "Protocol version 2.0 is not supported. This agent supports version 1.0"
}
}
""";

this.server.when(
request()
.withMethod("POST")
.withPath("/")
)
.respond(
response()
.withStatusCode(200)
.withBody(errorResponseBody)
);

JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
Message message = Message.builder()
.role(Message.Role.USER)
.parts(Collections.singletonList(new TextPart("test message")))
.contextId("context-test")
.messageId("message-test")
.build();
MessageSendConfiguration configuration = MessageSendConfiguration.builder()
.acceptedOutputModes(List.of("text"))
.blocking(true)
.build();
MessageSendParams params = MessageSendParams.builder()
.message(message)
.configuration(configuration)
.build();

// Should throw A2AClientException with VersionNotSupportedError as cause
try {
client.sendMessage(params, null);
fail("Expected A2AClientException to be thrown");
} catch (A2AClientException e) {
// Verify the cause is VersionNotSupportedError
assertInstanceOf(VersionNotSupportedError.class, e.getCause());
VersionNotSupportedError versionError = (VersionNotSupportedError) e.getCause();
assertTrue(versionError.getMessage().contains("2.0"));
assertTrue(versionError.getMessage().contains("1.0"));
}
}
}
Loading