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

Abierto
#3,057 2 comentarios 0 reacciones 0 asignados Ver en GitHub
area/core/model bug
Lenguaje dominante
Java
Estrellas
5.6k
Forks
1.3k
Merge medio
4 d 12 h
PR fusionados (30 d)
77

Descripción

**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.

Guía de contribución

Abrir la guía de contribución

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.