agentscope-ai / agentscope-ai/agentscope-java

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

Offen
#3,057 2 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
area/core/model bug
Vorherrschende Sprache
Java
Sterne
5.6k
Forks
1.3k
Ø Merge
4 T. 12 Std.
Gemergte PRs (30 T.)
77

Beschreibung

**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):

```java
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:

```java
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.

Beitragsleitfaden

Beitragsleitfaden öffnen

Bewertung

Dieses Issue wurde noch nicht bewertet.

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.