Skip to content

ChatCompletionsHttpClient should expose non-success HTTP status through a typed exception#1451

Description

@akbarkanso

馃敶 Required Information

Describe the Bug:

ChatCompletionsHttpClient converts every non-successful HTTP response into a plain IOException whose message contains the OkHttp Response.toString() value and response body:

if (!response.isSuccessful()) {
    String bodyStr = body != null ? body.string() : "";
    emitter.tryOnError(
        new IOException(
            "HTTP request failed with status: "
                + response
                + " - body: "
                + bodyStr));
}

As a result, callers cannot inspect the HTTP status programmatically.

For example, implementing a bounded retry for HTTP 429 currently requires parsing text such as:

HTTP request failed with status: Response{protocol=h2, code=429, message=, url=...} - body: ...

This couples callers to:

  • the exception message;
  • OkHttp's Response.toString() format;
  • field ordering inside that format;
  • URL and response body content embedded in the message.

The same issue applies to both streaming and non streaming requests.

Steps to Reproduce:

  1. Start an HTTP server whose /v1/chat/completions endpoint returns HTTP 429.
  2. Construct a ChatCompletionsHttpClient pointing to that server.
  3. Call complete(...).
  4. Inspect the emitted failure.

Example:

HttpOptions options = HttpOptions.builder()
        .baseUrl("http://localhost:8080/v1")
        .build();

ChatCompletionsClient client =
        new ChatCompletionsHttpClient(options);

client.complete(request, false)
        .test()
        .assertError(IOException.class);

The resulting exception has no status accessor. The only representation of 429 is inside getMessage().

Expected Behavior:

Non successful HTTP responses should be emitted through a typed exception that exposes the HTTP status separately from its presentation message.

For example:

public final class ChatCompletionsHttpException extends IOException {
    private final int statusCode;
    private final String responseBody;

    public int statusCode() {
        return statusCode;
    }

    public String responseBody() {
        return responseBody;
    }
}

The client could then emit an exception constructed from the HTTP response, for example:

new ChatCompletionsHttpException(
        response.code(),
        response.message(),
        bodyStr);

Alternatively, ADK could use an existing exception type that exposes a stable numeric HTTP status code.

The important requirement is that callers can inspect the HTTP status without parsing Throwable.getMessage().

Observed Behavior:

All non success statuses are emitted as plain IOException instances:

IOException

A caller cannot safely distinguish between:

  • HTTP 401;
  • HTTP 429;
  • HTTP 500;
  • a connection level IOException;
  • an exception whose response body happens to contain "code":429.

The only way to determine the HTTP status is currently to parse the exception message.

Environment Details:

  • ADK Library Version (see Maven dependency): com.google.adk:google-adk:1.7.1
  • OS: N/A
  • TS Version (tsc --version): N/A (Java ADK)

Model Information:

  • Which model is being used: N/A (the issue concerns HTTP error handling in ChatCompletionsHttpClient, independent of the model)

馃煛 Optional Information

Regression:

N/A. The current ChatCompletionsHttpClient source appears to retain the same behavior.

Logs:

N/A. A representative failure message is:

HTTP request failed with status: Response{protocol=h2, code=429, message=, url=...} - body: ...

Screenshots / Video:

N/A.

Additional Context:

OpenAI compatible gateways commonly use HTTP 429 for temporary rate limiting. Applications may need to apply bounded retry/backoff specifically to that status while immediately surfacing authentication and request errors.

With a typed exception, retry code becomes stable and explicit:

if (failure instanceof ChatCompletionsHttpException exception
        && exception.statusCode() == 429) {
    // Apply bounded backoff.
}

Without a typed exception, callers must use brittle message parsing:

Pattern.compile(
    "^HTTP request failed with status: Response\\{...code=429...");

Message parsing can break after an OkHttp or ADK formatting change and can accidentally classify response body text as transport metadata.

Security consideration

The current exception message includes the full response representation and response body. Depending on endpoint configuration, these may contain URL details or provider specific information that applications unintentionally write to logs.

A typed exception would allow applications to log only safe metadata, such as:

statusCode=429

while retaining response details as explicitly accessed fields when needed.

Scope

This request does not require ADK to implement a retry policy.

It only asks ChatCompletionsHttpClient to preserve machine readable HTTP response metadata so callers, or a future OpenAiCompatibleLlm, can implement their own retry policy safely.

Minimal Reproduction Code:

HttpOptions options = HttpOptions.builder()
        .baseUrl("http://localhost:8080/v1")
        .build();

ChatCompletionsClient client =
        new ChatCompletionsHttpClient(options);

client.complete(request, false)
        .test()
        .assertError(IOException.class);

The test server should return HTTP 429 from /v1/chat/completions.

Suggested Tests:

Add streaming and non streaming tests asserting that:

  1. HTTP 429 emits the typed exception with statusCode() == 429.
  2. HTTP 401 emits the same type with statusCode() == 401.
  3. Connection failures remain ordinary IOException.
  4. Response body text containing "code":429 does not affect the exposed transport status.
  5. Existing error body information remains available without being required in the exception message.

How often has this issue occurred?:

  • Always (100%)

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions