diff --git a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransport.java b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransport.java index 070f564e5..bc7844084 100644 --- a/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransport.java +++ b/client/transport/jsonrpc/src/main/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransport.java @@ -46,6 +46,7 @@ import org.a2aproject.sdk.jsonrpc.common.wrappers.CreateTaskPushNotificationConfigResponse; import org.a2aproject.sdk.spec.A2AClientError; import org.a2aproject.sdk.spec.A2AClientException; +import org.a2aproject.sdk.spec.A2AClientHTTPError; import org.a2aproject.sdk.spec.A2AError; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.AgentInterface; @@ -321,10 +322,12 @@ private PayloadAndHeaders applyInterceptors(String methodName, @Nullable Object } private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders, String method) throws IOException, InterruptedException, JsonProcessingException { - A2AHttpClient.PostBuilder builder = createPostBuilder(url, payloadAndHeaders,method); + A2AHttpClient.PostBuilder builder = createPostBuilder(url, payloadAndHeaders, method); A2AHttpResponse response = builder.post(); if (!response.success()) { - throw new IOException("Request failed " + response.status()); + int status = response.status(); + String message = "Request failed with HTTP " + status; + throw new A2AClientException(message, new A2AClientHTTPError(status, message, response.body())); } return response.body(); } diff --git a/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportTest.java b/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportTest.java index 93c391aa2..5c1ab098d 100644 --- a/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportTest.java +++ b/client/transport/jsonrpc/src/test/java/org/a2aproject/sdk/client/transport/jsonrpc/JSONRPCTransportTest.java @@ -38,6 +38,7 @@ import java.util.Map; import org.a2aproject.sdk.spec.A2AClientException; +import org.a2aproject.sdk.spec.A2AClientHTTPError; import org.a2aproject.sdk.spec.AgentCard; import org.a2aproject.sdk.spec.ExtensionSupportRequiredError; import org.a2aproject.sdk.spec.VersionNotSupportedError; @@ -644,6 +645,48 @@ public void testExtensionSupportRequiredErrorUnmarshalling() throws Exception { } } + /** + * Test that HTTP error responses expose the status code via A2AClientHTTPError cause, + * while remaining backward compatible (A2AClientException is still thrown). + */ + @Test + public void testHttpErrorExposeStatusCode() throws Exception { + this.server.when( + request() + .withMethod("POST") + .withPath("/") + ) + .respond( + response() + .withStatusCode(503) + .withBody("{\"error\": \"Service Unavailable\"}") + ); + + JSONRPCTransport client = new JSONRPCTransport("http://localhost:4001"); + MessageSendParams params = MessageSendParams.builder() + .message(Message.builder() + .role(Message.Role.ROLE_USER) + .parts(Collections.singletonList(new TextPart("hello"))) + .contextId("ctx") + .messageId("msg") + .build()) + .build(); + + try { + client.sendMessage(params, null); + fail("Expected A2AClientException to be thrown"); + } catch (A2AClientException e) { + // Backward compatible: still throws A2AClientException + assertTrue(e.getMessage().contains("503"), "Expected message to contain '503' but was: " + e.getMessage()); + // New: cause carries structured HTTP status + assertInstanceOf(A2AClientHTTPError.class, e.getCause()); + A2AClientHTTPError httpError = (A2AClientHTTPError) e.getCause(); + assertEquals(503, httpError.getCode()); + assertNotNull(httpError.getResponseBody()); + assertTrue(httpError.getResponseBody().contains("Service Unavailable"), "Expected response body to contain 'Service Unavailable' but was: " + httpError.getResponseBody()); + } + } + /** * Test that VersionNotSupportedError is properly unmarshalled from JSON-RPC error response. */ diff --git a/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientHTTPError.java b/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientHTTPError.java index b840286be..b90ddb09c 100644 --- a/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientHTTPError.java +++ b/spec/src/main/java/org/a2aproject/sdk/spec/A2AClientHTTPError.java @@ -1,6 +1,7 @@ package org.a2aproject.sdk.spec; import org.a2aproject.sdk.util.Assert; +import org.jspecify.annotations.Nullable; /** * Client exception indicating an HTTP transport error with a specific status code. @@ -15,14 +16,14 @@ *
- * Usage example: + * This exception is set as the cause of {@link A2AClientException} so that callers + * can inspect the HTTP status code while remaining backward compatible: *
{@code
- * if (response.statusCode() >= 400) {
- * throw new A2AClientHTTPError(
- * response.statusCode(),
- * "HTTP error: " + response.statusMessage(),
- * response.body()
- * );
+ * } catch (A2AClientException e) {
+ * if (e.getCause() instanceof A2AClientHTTPError httpError) {
+ * int status = httpError.getCode(); // e.g. 401, 503
+ * String body = httpError.getResponseBody(); // raw response body, may be null
+ * }
* }
* }
*
@@ -40,6 +41,12 @@ public class A2AClientHTTPError extends A2AClientError {
*/
private final String message;
+ /**
+ * The raw HTTP response body, may be {@code null}.
+ */
+ @Nullable
+ private final String responseBody;
+
/**
* Creates a new HTTP client error with the specified status code and message.
*
@@ -47,25 +54,42 @@ public class A2AClientHTTPError extends A2AClientError {
* @param message the error message
* @param data additional error data (may be the response body)
* @throws IllegalArgumentException if code or message is null
+ * @deprecated Use {@link #A2AClientHTTPError(int, String, String)} instead to preserve the response body.
*/
+ @Deprecated(since = "1.0.0.Beta1", forRemoval = true)
public A2AClientHTTPError(int code, String message, Object data) {
Assert.checkNotNullParam("code", code);
Assert.checkNotNullParam("message", message);
this.code = code;
this.message = message;
+ this.responseBody = data instanceof String s ? s : null;
}
/**
- * Gets the error code
+ * Creates a new HTTP client error with the specified status code, message, and response body.
*
- * @return the error code
+ * @param code the HTTP status code (e.g. 401, 503)
+ * @param message the error message
+ * @param responseBody the raw HTTP response body, may be {@code null}
+ */
+ public A2AClientHTTPError(int code, String message, @Nullable String responseBody) {
+ Assert.checkNotNullParam("message", message);
+ this.code = code;
+ this.message = message;
+ this.responseBody = responseBody;
+ }
+
+ /**
+ * Gets the HTTP status code.
+ *
+ * @return the HTTP status code (e.g. 401, 404, 500, 503)
*/
public int getCode() {
return code;
}
/**
- * Gets the error message
+ * Gets the error message.
*
* @return the error message
*/
@@ -73,4 +97,13 @@ public int getCode() {
public String getMessage() {
return message;
}
+
+ /**
+ * Returns the raw HTTP response body, if available.
+ *
+ * @return the response body, or {@code null} if not available
+ */
+ public @Nullable String getResponseBody() {
+ return responseBody;
+ }
}