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 @@ -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;
Expand Down Expand Up @@ -321,10 +322,12 @@ private PayloadAndHeaders applyInterceptors(String methodName, @Nullable Object
}

private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders, String method) throws IOException, InterruptedException, JsonProcessingException {
Comment thread
pratik3558 marked this conversation as resolved.
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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*/
Expand Down
53 changes: 43 additions & 10 deletions spec/src/main/java/org/a2aproject/sdk/spec/A2AClientHTTPError.java
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -15,14 +16,14 @@
* <li>5xx - Server errors (500 Internal Server Error, 503 Service Unavailable, etc.)</li>
* </ul>
* <p>
* 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:
* <pre>{@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
* }
* }
* }</pre>
*
Expand All @@ -40,37 +41,69 @@ 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.
*
* @param code the HTTP status code
* @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
*/
@Override
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;
}
}
Loading