Skip to content

[Bug]: Model retry misclassifies streaming transport errors (Connection reset) as non-retryable when wrapped by OpenAIException with null status code #3057

Description

@wahllllll

Describe the bug

Model calls that fail with a streaming transport error (e.g. Connection reset on a stale pooled connection) are not retried, even though the agent is configured with ExecutionConfig.MODEL_DEFAULTS (maxAttempts=3) and the log confirms the retry config was applied:

DEBUG io.agentscope.core.model.ModelUtils - Applied retry config: maxAttempts=3, initialBackoff=PT2S for model: qwen3.5-flash
... (1 ms later, no retry happened)
INFO  ...AgentTraceMiddleware - ERROR | OpenAIException: HTTP transport error during streaming: SSE/NDJSON stream failed: java.net.SocketException: Connection reset

The error propagates to the caller immediately. A manual retry seconds later succeeds on a fresh connection, so the error is transient and clearly retryable.

To Reproduce

  1. Build a HarnessAgent (or ReActAgent) with an OpenAI-compatible model (OpenAIChatModel via agentscope-extensions-model-openai, default JdkHttpTransport) and .modelExecutionConfig(ExecutionConfig.MODEL_DEFAULTS).
  2. Make a model call so the HTTP connection pool holds a connection to the endpoint, then keep it idle long enough (e.g. 1–2 hours) for the server / intermediate device to close it.
  3. Make another model call on the same JVM. The request is written to the stale pooled connection and receives a TCP RST within ~150 ms.

Expected behavior

A connection error during streaming (Connection reset / broken pipe on a stale pooled connection) should be retried according to the ExecutionConfig retry policy — the Javadoc of ExecutionConfig states that "Network/IO errors" are retryable.

Error messages

io.agentscope.extensions.model.openai.exception.OpenAIException:
    HTTP transport error during streaming: SSE/NDJSON stream failed: java.net.SocketException: Connection reset
  Caused by: io.agentscope.core.model.transport.HttpTransportException:
    SSE/NDJSON stream failed: java.net.SocketException: Connection reset      (statusCode = null)
  Caused by: java.util.concurrent.CompletionException: java.net.SocketException: Connection reset
  Caused by: java.net.SocketException: Connection reset

Environment

  • AgentScope-Java Version: 2.0.3
  • Java Version: 17
  • OS: Windows 11 (server side), model endpoint: DashScope OpenAI-compatible mode
  • Modules: agentscope-extensions-model-openai, default transport JdkHttpTransport via HttpTransportFactory.getDefault()

Additional context

Root cause — for streaming transport failures, OpenAIClient.stream maps the exception via new OpenAIException(msg, ex), i.e. no HTTP status code. OpenAIException implements ModelHttpException, so in ExecutionConfig.isRetryableError (v2.0.3):

if (error instanceof HttpTransportException hte) {
    return hte.isRetryable();            // (1) never reached — outer exception is OpenAIException
}
if (error instanceof ModelHttpException mhe) {
    return mhe.isRetryableHttpStatus();  // (2) OpenAIException matches here;
                                         //     statusCode == null → returns false immediately
}
if (error instanceof TimeoutException) { return true; }
if (error instanceof IOException) { return true; }   // (3) never reached
// cause-chain recursion                        // (4) never reached

Branch (2) returns false without consulting the cause chain, which bypasses:

  • HttpTransportException.isRetryable() — which correctly returns true for statusCode == null ("Connection errors are usually retryable"), and
  • the IOException rule (SocketException is an IOException).

So any streaming transport failure raised through the OpenAI extension is misclassified as a permanent client error and is never retried under MODEL_DEFAULTS.

Suggested fix (either works):

  1. In isRetryableError, when a ModelHttpException has getStatusCode() == null, fall through to cause-chain inspection instead of returning false immediately.
  2. Or make OpenAIException not report itself as a ModelHttpException when constructed without a status code.

Workaround (currently in use) — a custom retryOn predicate that first applies ExecutionConfig.RETRYABLE_ERRORS, then walks the cause chain for an HttpTransportException with a null status code:

private static boolean isModelErrorRetryable(Throwable error) {
    if (ExecutionConfig.RETRYABLE_ERRORS.test(error)) {
        return true;
    }
    for (Throwable cause = error.getCause(); cause != null && cause != error; cause = cause.getCause()) {
        if (cause instanceof HttpTransportException transport && transport.getStatusCode() == null) {
            return true;
        }
    }
    return false;
}

Happy to submit a PR for fix option 1 if the approach is agreed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/core/modelModel providers and formattersbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions