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

Đang mở
#3,057 2 bình luận 0 reaction 0 người được giao Xem trên GitHub
area/core/model bug
Ngôn ngữ chính
Java
Star
5.6k
Fork
1.3k
Merge trung bình
4 ngày 12 giờ
Pull request đã merge (30 ngày)
77

Mô tả

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

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.