Skip to content

[Feat]: A2AHttpResponse should expose response headers #920

@wdemis

Description

@wdemis

Is your feature request related to a problem? Please describe.

According to the A2A Spec:

Rate Limiting and Abuse Prevention:

- Agents SHOULD implement rate limiting on all operations
- Agents SHOULD return appropriate error responses when rate limits are exceeded
- Agents MAY implement different rate limits for different operations or user tiers

We noticed that the org.a2aproject.sdk.client.http.A2AHttpResponse does not expose response headers in the interface. This matters because if the server returns a 429 with a Retry-After header, it would be nice to access that so the client consumer can know how long to wait for a retry.

Currently the org.a2aproject.sdk.client.http.JdkA2AHttpClient.JdkPostBuilder does not check for a 429, which is one limiting factor:

            if (response.statusCode() == HTTP_UNAUTHORIZED) {
                throw new IOException(A2AErrorMessages.AUTHENTICATION_FAILED);
            } else if (response.statusCode() == HTTP_FORBIDDEN) {
                throw new IOException(A2AErrorMessages.AUTHORIZATION_FAILED);
            }

...But we have our own implementation of org.a2aproject.sdk.client.http.A2AHttpClient with a custom PostBuilder where we could catch the 429. Nevertheless- the A2AHttpResponse doesn't expose headers for us to read the value from. Our only option is to short-circuit throwing our own exception with the header info captured.

Ideally, the client SDK would expose response headers — at minimum for error responses. Retry-After (429/503) and WWW-Authenticate (401) are both examples where the header carries information that a client consumer needs to act on programmatically.

Describe the solution you'd like

Extend org.a2aproject.sdk.client.http.A2AHttpResponse to expose response headers and ensure they are passed on to the org.a2aproject.sdk.spec.A2AClientHTTPError

Describe alternatives you've considered

Our alternative today is to short-circuit the client SDK exception handling by grabbing the response header ourselves in the 429 case:

public class CustomA2AHttpClient implements A2AHttpClient {

    private class CustomPostBuilder implements PostBuilder {
        // ...

        @Override
        public A2AHttpResponse post() throws IOException, InterruptedException {
            HttpResponse<byte[]> raw = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());

            // Workaround: intercept 429 before the SDK wraps it as A2AClientHTTPError,
            // because A2AHttpResponse doesn't expose headers and the Retry-After value is lost.
            if (raw.statusCode() == 429) {
                String retryAfter = raw.headers().firstValue("Retry-After").orElse(null);
                throw new IOException("Rate limited (Retry-After: " + retryAfter + ")");
                // We lose the structured Retry-After value here — our only option is to
                // encode it in the exception message or throw a custom exception type.
            }

            return toA2AHttpResponse(raw);
        }
    }
}

Additional context

Note on our current workaround:
We intercept the 429 in our A2AHttpClient.PostBuilder.post() implementation by throwing a RuntimeException subclass before returning A2AHttpResponse. This works today because the SDK's JSONRPCTransport.sendPostRequest() only catches IOException and InterruptedException — unchecked exceptions propagate through unmodified.
However, this is fragile. If a future SDK version adds a broader catch (e.g. catch (Exception e) or catch (RuntimeException e)) around the builder.post() call — perhaps to wrap transport errors in A2AClientException — our exception would be swallowed or re-wrapped, silently breaking the Retry-After propagation.

We have an integration test that checks for this potential future case, but the underlying issue remains: we're relying on an implicit contract (unchecked exceptions pass through) rather than an explicit API.

A proper A2AHttpResponse.headers() method would eliminate this workaround entirely.

Code of Conduct

  • I agree to follow this project's Code of Conduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels
    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions