Skip to content

Commit bfbc98f

Browse files
committed
Exclude provider error prefix from mid-stream abort retries
Prevent classified provider error payloads carrying streamed 5xx messages from falsely matching transport socket abort phrases. Refs #973
1 parent 33a6d28 commit bfbc98f

2 files changed

Lines changed: 35 additions & 0 deletions

File tree

‎internal/agent/midstream_retry_test.go‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,7 @@ func TestIsMidStreamTransportAbort(t *testing.T) {
347347
"provider stream error: read: connection closed",
348348
"write: broken pipe",
349349
"provider stream error: server closed the connection",
350+
"provider stream error: software caused connection abort",
350351
}
351352
for _, m := range aborts {
352353
if !isMidStreamTransportAbort(m) {
@@ -361,9 +362,16 @@ func TestIsMidStreamTransportAbort(t *testing.T) {
361362
"net/http: timeout awaiting response headers",
362363
"dial tcp 10.0.0.1:443: connect: connection refused",
363364
"i/o timeout",
365+
"500 Internal Server Error",
366+
"502 Bad Gateway",
367+
"503 Service Unavailable",
364368
"504 Gateway Timeout",
365369
"provider request error: request does not satisfy oneOf schema",
366370
"provider request error: connection closed is not a supported finish reason",
371+
"provider error: connection closed is not a supported finish reason",
372+
"provider error: upstream connection reset by peer",
373+
"provider error: unexpected EOF while reading upstream response",
374+
"provider error: 502 Bad Gateway",
367375
"auth error: connection reset by peer",
368376
"rate limit error: server closed",
369377
"schema error: property oneOf is invalid",
@@ -400,6 +408,31 @@ func TestRunDoesNotRetryClassifiedProviderErrorWithSocketPhrase(t *testing.T) {
400408
}
401409
}
402410

411+
func TestRunDoesNotRetryProviderErrorWithSocketPhrase(t *testing.T) {
412+
p := &midStreamAbortProvider{
413+
abortBefore: 1,
414+
abortError: "provider error: unexpected EOF while reading upstream response",
415+
}
416+
var notices string
417+
opts := Options{
418+
Registry: tools.NewRegistry(),
419+
OnReasoning: func(s string) { notices += s },
420+
}
421+
_, err := Run(context.Background(), "go", p, opts)
422+
if err == nil {
423+
t.Fatal("expected application error, got nil")
424+
}
425+
if got := atomic.LoadInt32(&p.calls); got != 1 {
426+
t.Fatalf("provider error with 'unexpected EOF' must not retry (got %d calls, want 1)", got)
427+
}
428+
if notices != "" {
429+
t.Fatalf("expected no reconnect notices, got %q", notices)
430+
}
431+
if !strings.Contains(err.Error(), "unexpected EOF") {
432+
t.Fatalf("expected original error preserved, got %v", err)
433+
}
434+
}
435+
403436
func TestRunDoesNotRetryApplicationErrorContainingEOFSubstring(t *testing.T) {
404437
p := &midStreamAbortProvider{
405438
abortBefore: 1,

‎internal/agent/reconnect.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,11 +172,13 @@ var midStreamAbortNeedles = []string{
172172
"server closed",
173173
"wsarecv",
174174
"connection was aborted",
175+
"software caused connection abort",
175176
"forcibly closed",
176177
}
177178

178179
var classifiedNonTransportPrefixes = []string{
179180
"provider request error:",
181+
"provider error:",
180182
"auth error:",
181183
"rate limit error:",
182184
}

0 commit comments

Comments
 (0)