Skip to content

Commit f693849

Browse files
authored
feat!: Rework error classes for spec 1.0 update (#558)
Test the errors that were introduced since last time This also renames some error classes Fixes #501
1 parent a1d4f8e commit f693849

36 files changed

Lines changed: 2240 additions & 46 deletions

File tree

‎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)