Skip to content

Commit 26d7855

Browse files
committed
feat! Rework error classes for spec 1.0 update
Test the errors that were introduced since last time This also renames some error classes
1 parent a14fd2e commit 26d7855

File tree

36 files changed

+2241
-45
lines changed

36 files changed

+2241
-45
lines changed

client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcErrorMapper.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import io.a2a.common.A2AErrorMessages;
44
import io.a2a.spec.A2AClientException;
55
import io.a2a.spec.ContentTypeNotSupportedError;
6+
import io.a2a.spec.ExtendedCardNotConfiguredError;
7+
import io.a2a.spec.ExtensionSupportRequiredError;
68
import io.a2a.spec.InvalidAgentResponseError;
79
import io.a2a.spec.InvalidParamsError;
810
import io.a2a.spec.InvalidRequestError;
@@ -12,6 +14,7 @@
1214
import io.a2a.spec.TaskNotCancelableError;
1315
import io.a2a.spec.TaskNotFoundError;
1416
import io.a2a.spec.UnsupportedOperationError;
17+
import io.a2a.spec.VersionNotSupportedError;
1518
import io.grpc.Status;
1619

1720
/**
@@ -52,6 +55,12 @@ public static A2AClientException mapGrpcError(Throwable e, String errorPrefix) {
5255
return new A2AClientException(errorPrefix + description, new ContentTypeNotSupportedError(null, description, null));
5356
} else if (description.contains("InvalidAgentResponseError")) {
5457
return new A2AClientException(errorPrefix + description, new InvalidAgentResponseError(null, description, null));
58+
} else if (description.contains("ExtendedCardNotConfiguredError")) {
59+
return new A2AClientException(errorPrefix + description, new ExtendedCardNotConfiguredError(null, description, null));
60+
} else if (description.contains("ExtensionSupportRequiredError")) {
61+
return new A2AClientException(errorPrefix + description, new ExtensionSupportRequiredError(null, description, null));
62+
} else if (description.contains("VersionNotSupportedError")) {
63+
return new A2AClientException(errorPrefix + description, new VersionNotSupportedError(null, description, null));
5564
}
5665
}
5766

client/transport/grpc/src/main/java/io/a2a/client/transport/grpc/GrpcTransport.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ public class GrpcTransport implements ClientTransport {
5959
private static final Metadata.Key<String> EXTENSIONS_KEY = Metadata.Key.of(
6060
A2AHeaders.X_A2A_EXTENSIONS,
6161
Metadata.ASCII_STRING_MARSHALLER);
62+
private static final Metadata.Key<String> VERSION_KEY = Metadata.Key.of(
63+
A2AHeaders.X_A2A_VERSION,
64+
Metadata.ASCII_STRING_MARSHALLER);
6265
private final A2AServiceBlockingV2Stub blockingStub;
6366
private final A2AServiceStub asyncStub;
6467
private final @Nullable List<ClientCallInterceptor> interceptors;
@@ -366,14 +369,19 @@ private Metadata createGrpcMetadata(@Nullable ClientCallContext context, @Nullab
366369
Metadata metadata = new Metadata();
367370

368371
if (context != null && context.getHeaders() != null) {
372+
// Set X-A2A-Version header if present
373+
String versionHeader = context.getHeaders().get(A2AHeaders.X_A2A_VERSION);
374+
if (versionHeader != null) {
375+
metadata.put(VERSION_KEY, versionHeader);
376+
}
377+
369378
// Set X-A2A-Extensions header if present
370379
String extensionsHeader = context.getHeaders().get(A2AHeaders.X_A2A_EXTENSIONS);
371380
if (extensionsHeader != null) {
372381
metadata.put(EXTENSIONS_KEY, extensionsHeader);
373382
}
374383

375384
// Add other headers as needed in the future
376-
// For now, we only handle X-A2A-Extensions
377385
}
378386
if (payloadAndHeaders != null && payloadAndHeaders.getHeaders() != null) {
379387
// Handle all headers from interceptors (including auth headers)
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package io.a2a.client.transport.grpc;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
5+
import static org.junit.jupiter.api.Assertions.assertNotNull;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
8+
import io.a2a.spec.A2AClientException;
9+
import io.a2a.spec.ContentTypeNotSupportedError;
10+
import io.a2a.spec.ExtendedCardNotConfiguredError;
11+
import io.a2a.spec.ExtensionSupportRequiredError;
12+
import io.a2a.spec.InvalidAgentResponseError;
13+
import io.a2a.spec.InvalidParamsError;
14+
import io.a2a.spec.InvalidRequestError;
15+
import io.a2a.spec.JSONParseError;
16+
import io.a2a.spec.MethodNotFoundError;
17+
import io.a2a.spec.PushNotificationNotSupportedError;
18+
import io.a2a.spec.TaskNotCancelableError;
19+
import io.a2a.spec.TaskNotFoundError;
20+
import io.a2a.spec.UnsupportedOperationError;
21+
import io.a2a.spec.VersionNotSupportedError;
22+
import io.grpc.Status;
23+
import io.grpc.StatusRuntimeException;
24+
import org.junit.jupiter.api.Test;
25+
26+
/**
27+
* Tests for GrpcErrorMapper - verifies correct unmarshalling of gRPC errors to A2A error types
28+
*/
29+
public class GrpcErrorMapperTest {
30+
31+
@Test
32+
public void testExtensionSupportRequiredErrorUnmarshalling() {
33+
// Create a gRPC StatusRuntimeException with ExtensionSupportRequiredError in description
34+
String errorMessage = "ExtensionSupportRequiredError: Extension required: https://example.com/test-extension";
35+
StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
36+
.withDescription(errorMessage)
37+
.asRuntimeException();
38+
39+
// Map the gRPC error to A2A error
40+
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
41+
42+
// Verify the result
43+
assertNotNull(result);
44+
assertNotNull(result.getCause());
45+
assertInstanceOf(ExtensionSupportRequiredError.class, result.getCause());
46+
47+
ExtensionSupportRequiredError extensionError = (ExtensionSupportRequiredError) result.getCause();
48+
assertNotNull(extensionError.getMessage());
49+
assertTrue(extensionError.getMessage().contains("https://example.com/test-extension"));
50+
assertTrue(result.getMessage().contains(errorMessage));
51+
}
52+
53+
@Test
54+
public void testVersionNotSupportedErrorUnmarshalling() {
55+
// Create a gRPC StatusRuntimeException with VersionNotSupportedError in description
56+
String errorMessage = "VersionNotSupportedError: Version 2.0 is not supported";
57+
StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
58+
.withDescription(errorMessage)
59+
.asRuntimeException();
60+
61+
// Map the gRPC error to A2A error
62+
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
63+
64+
// Verify the result
65+
assertNotNull(result);
66+
assertNotNull(result.getCause());
67+
assertInstanceOf(VersionNotSupportedError.class, result.getCause());
68+
69+
VersionNotSupportedError versionError = (VersionNotSupportedError) result.getCause();
70+
assertNotNull(versionError.getMessage());
71+
assertTrue(versionError.getMessage().contains("Version 2.0 is not supported"));
72+
}
73+
74+
@Test
75+
public void testExtendedCardNotConfiguredErrorUnmarshalling() {
76+
// Create a gRPC StatusRuntimeException with ExtendedCardNotConfiguredError in description
77+
String errorMessage = "ExtendedCardNotConfiguredError: Extended card not configured for this agent";
78+
StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
79+
.withDescription(errorMessage)
80+
.asRuntimeException();
81+
82+
// Map the gRPC error to A2A error
83+
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
84+
85+
// Verify the result
86+
assertNotNull(result);
87+
assertNotNull(result.getCause());
88+
assertInstanceOf(ExtendedCardNotConfiguredError.class, result.getCause());
89+
90+
ExtendedCardNotConfiguredError extendedCardError = (ExtendedCardNotConfiguredError) result.getCause();
91+
assertNotNull(extendedCardError.getMessage());
92+
assertTrue(extendedCardError.getMessage().contains("Extended card not configured"));
93+
}
94+
95+
@Test
96+
public void testTaskNotFoundErrorUnmarshalling() {
97+
// Create a gRPC StatusRuntimeException with TaskNotFoundError in description
98+
String errorMessage = "TaskNotFoundError: Task task-123 not found";
99+
StatusRuntimeException grpcException = Status.NOT_FOUND
100+
.withDescription(errorMessage)
101+
.asRuntimeException();
102+
103+
// Map the gRPC error to A2A error
104+
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
105+
106+
// Verify the result
107+
assertNotNull(result);
108+
assertNotNull(result.getCause());
109+
assertInstanceOf(TaskNotFoundError.class, result.getCause());
110+
}
111+
112+
@Test
113+
public void testUnsupportedOperationErrorUnmarshalling() {
114+
// Create a gRPC StatusRuntimeException with UnsupportedOperationError in description
115+
String errorMessage = "UnsupportedOperationError: Operation not supported";
116+
StatusRuntimeException grpcException = Status.UNIMPLEMENTED
117+
.withDescription(errorMessage)
118+
.asRuntimeException();
119+
120+
// Map the gRPC error to A2A error
121+
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
122+
123+
// Verify the result
124+
assertNotNull(result);
125+
assertNotNull(result.getCause());
126+
assertInstanceOf(UnsupportedOperationError.class, result.getCause());
127+
}
128+
129+
@Test
130+
public void testInvalidParamsErrorUnmarshalling() {
131+
// Create a gRPC StatusRuntimeException with InvalidParamsError in description
132+
String errorMessage = "InvalidParamsError: Invalid parameters provided";
133+
StatusRuntimeException grpcException = Status.INVALID_ARGUMENT
134+
.withDescription(errorMessage)
135+
.asRuntimeException();
136+
137+
// Map the gRPC error to A2A error
138+
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
139+
140+
// Verify the result
141+
assertNotNull(result);
142+
assertNotNull(result.getCause());
143+
assertInstanceOf(InvalidParamsError.class, result.getCause());
144+
}
145+
146+
@Test
147+
public void testContentTypeNotSupportedErrorUnmarshalling() {
148+
// Create a gRPC StatusRuntimeException with ContentTypeNotSupportedError in description
149+
String errorMessage = "ContentTypeNotSupportedError: Content type application/xml not supported";
150+
StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
151+
.withDescription(errorMessage)
152+
.asRuntimeException();
153+
154+
// Map the gRPC error to A2A error
155+
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
156+
157+
// Verify the result
158+
assertNotNull(result);
159+
assertNotNull(result.getCause());
160+
assertInstanceOf(ContentTypeNotSupportedError.class, result.getCause());
161+
162+
ContentTypeNotSupportedError contentTypeError = (ContentTypeNotSupportedError) result.getCause();
163+
assertNotNull(contentTypeError.getMessage());
164+
assertTrue(contentTypeError.getMessage().contains("Content type application/xml not supported"));
165+
}
166+
167+
@Test
168+
public void testFallbackToStatusCodeMapping() {
169+
// Create a gRPC StatusRuntimeException without specific error type in description
170+
StatusRuntimeException grpcException = Status.NOT_FOUND
171+
.withDescription("Generic not found error")
172+
.asRuntimeException();
173+
174+
// Map the gRPC error to A2A error
175+
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException);
176+
177+
// Verify fallback to status code mapping
178+
assertNotNull(result);
179+
assertNotNull(result.getCause());
180+
assertInstanceOf(TaskNotFoundError.class, result.getCause());
181+
}
182+
183+
@Test
184+
public void testCustomErrorPrefix() {
185+
// Create a gRPC StatusRuntimeException
186+
String errorMessage = "ExtensionSupportRequiredError: Extension required: https://example.com/ext";
187+
StatusRuntimeException grpcException = Status.FAILED_PRECONDITION
188+
.withDescription(errorMessage)
189+
.asRuntimeException();
190+
191+
// Map with custom error prefix
192+
String customPrefix = "Custom Error: ";
193+
A2AClientException result = GrpcErrorMapper.mapGrpcError(grpcException, customPrefix);
194+
195+
// Verify custom prefix is used
196+
assertNotNull(result);
197+
assertTrue(result.getMessage().startsWith(customPrefix));
198+
assertInstanceOf(ExtensionSupportRequiredError.class, result.getCause());
199+
}
200+
}

client/transport/jsonrpc/src/test/java/io/a2a/client/transport/jsonrpc/JSONRPCTransportTest.java

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@
4141

4242
import io.a2a.spec.A2AClientException;
4343
import io.a2a.spec.AgentCard;
44+
import io.a2a.spec.ExtensionSupportRequiredError;
45+
import io.a2a.spec.VersionNotSupportedError;
4446
import io.a2a.spec.AgentInterface;
4547
import io.a2a.spec.AgentSkill;
4648
import io.a2a.spec.Artifact;
@@ -680,4 +682,117 @@ public void testA2AClientSendMessageWithMixedParts() throws Exception {
680682
assertEquals("Analyzed chart image and data: Bar chart showing quarterly data with values [10, 20, 30, 40].", ((TextPart) part).text());
681683
assertTrue(task.metadata().isEmpty());
682684
}
685+
686+
/**
687+
* Test that ExtensionSupportRequiredError is properly unmarshalled from JSON-RPC error response.
688+
*/
689+
@Test
690+
public void testExtensionSupportRequiredErrorUnmarshalling() throws Exception {
691+
// Mock server returns JSON-RPC error with code -32008 (EXTENSION_SUPPORT_REQUIRED_ERROR)
692+
String errorResponseBody = """
693+
{
694+
"jsonrpc": "2.0",
695+
"id": 1,
696+
"error": {
697+
"code": -32008,
698+
"message": "Extension required: https://example.com/test-extension"
699+
}
700+
}
701+
""";
702+
703+
this.server.when(
704+
request()
705+
.withMethod("POST")
706+
.withPath("/")
707+
)
708+
.respond(
709+
response()
710+
.withStatusCode(200)
711+
.withBody(errorResponseBody)
712+
);
713+
714+
JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
715+
Message message = Message.builder()
716+
.role(Message.Role.USER)
717+
.parts(Collections.singletonList(new TextPart("test message")))
718+
.contextId("context-test")
719+
.messageId("message-test")
720+
.build();
721+
MessageSendConfiguration configuration = MessageSendConfiguration.builder()
722+
.acceptedOutputModes(List.of("text"))
723+
.blocking(true)
724+
.build();
725+
MessageSendParams params = MessageSendParams.builder()
726+
.message(message)
727+
.configuration(configuration)
728+
.build();
729+
730+
// Should throw A2AClientException with ExtensionSupportRequiredError as cause
731+
try {
732+
client.sendMessage(params, null);
733+
fail("Expected A2AClientException to be thrown");
734+
} catch (A2AClientException e) {
735+
// Verify the cause is ExtensionSupportRequiredError
736+
assertInstanceOf(ExtensionSupportRequiredError.class, e.getCause());
737+
ExtensionSupportRequiredError extensionError = (ExtensionSupportRequiredError) e.getCause();
738+
assertTrue(extensionError.getMessage().contains("https://example.com/test-extension"));
739+
}
740+
}
741+
742+
/**
743+
* Test that VersionNotSupportedError is properly unmarshalled from JSON-RPC error response.
744+
*/
745+
@Test
746+
public void testVersionNotSupportedErrorUnmarshalling() throws Exception {
747+
// Mock server returns JSON-RPC error with code -32009 (VERSION_NOT_SUPPORTED_ERROR)
748+
String errorResponseBody = """
749+
{
750+
"jsonrpc": "2.0",
751+
"id": 1,
752+
"error": {
753+
"code": -32009,
754+
"message": "Protocol version 2.0 is not supported. This agent supports version 1.0"
755+
}
756+
}
757+
""";
758+
759+
this.server.when(
760+
request()
761+
.withMethod("POST")
762+
.withPath("/")
763+
)
764+
.respond(
765+
response()
766+
.withStatusCode(200)
767+
.withBody(errorResponseBody)
768+
);
769+
770+
JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001");
771+
Message message = Message.builder()
772+
.role(Message.Role.USER)
773+
.parts(Collections.singletonList(new TextPart("test message")))
774+
.contextId("context-test")
775+
.messageId("message-test")
776+
.build();
777+
MessageSendConfiguration configuration = MessageSendConfiguration.builder()
778+
.acceptedOutputModes(List.of("text"))
779+
.blocking(true)
780+
.build();
781+
MessageSendParams params = MessageSendParams.builder()
782+
.message(message)
783+
.configuration(configuration)
784+
.build();
785+
786+
// Should throw A2AClientException with VersionNotSupportedError as cause
787+
try {
788+
client.sendMessage(params, null);
789+
fail("Expected A2AClientException to be thrown");
790+
} catch (A2AClientException e) {
791+
// Verify the cause is VersionNotSupportedError
792+
assertInstanceOf(VersionNotSupportedError.class, e.getCause());
793+
VersionNotSupportedError versionError = (VersionNotSupportedError) e.getCause();
794+
assertTrue(versionError.getMessage().contains("2.0"));
795+
assertTrue(versionError.getMessage().contains("1.0"));
796+
}
797+
}
683798
}

0 commit comments

Comments
 (0)