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
- Lingua principale
- Java
- Stelle
- 5.6k
- Fork
- 1.3k
- Merge medio
- 4g 12h
- PR unite (30g)
- 77
Descrizione
**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.
Guida per i contributori
Apri la guida per i contributori
Valutazione
Questa issue non è ancora stata valutata.