Skip to content

server_is_overloaded stream errors wrapped in Error are not retried #26129

Closed as not planned

Description

@Dandi007

Summary

This is a follow-up to #25888.

#25888 added server_is_overloaded handling in ProviderError.parseStreamError(), which fixes the path where the stream error payload reaches that parser.

However, there is still another real failure path where the same provider payload is wrapped in an Error instance first, for example:

{
  "type": "error",
  "sequence_number": 2,
  "error": {
    "type": "service_unavailable_error",
    "code": "server_is_overloaded",
    "message": "Our servers are currently overloaded. Please try again later.",
    "param": null
  }
}

When this payload is surfaced as new Error(JSON.stringify(payload)), it can still be persisted as an UnknownError and the session stops instead of retrying.

Problem observed

We observed failed assistant messages where OpenAI returned a stream error with:

  • error.type = "service_unavailable_error"
  • error.code = "server_is_overloaded"

This is a transient provider overload condition and should be retryable.

But in the wrapped Error(JSON.stringify(...)) path, MessageV2.fromError() currently classifies it before ProviderError.parseStreamError() can inspect the payload.

Current flow:

case e instanceof Error:
  return new NamedError.Unknown({ message: errorMessage(e) }, { cause: e }).toObject()

default:
  const parsed = ProviderError.parseStreamError(e)

As a result:

  1. ProviderError.parseStreamError() is never reached.
  2. The stream error becomes NamedError.Unknown.
  3. SessionRetry.retryable() does not classify the nested error.code = "server_is_overloaded" shape as retryable.
  4. The session stops instead of retrying.

Why the latest code is still insufficient

The latest code already includes #25888, which added this in ProviderError.parseStreamError():

case "server_is_overloaded":
case "server_error":
  return {
    type: "api_error",
    isRetryable: true,
    ...
  }

That fix works only if the payload reaches parseStreamError().

It does not cover the Error(JSON.stringify(...)) path because the e instanceof Error branch in MessageV2.fromError() returns UnknownError before the parser is attempted.

There is also a second gap in SessionRetry.retryable(): the JSON fallback recognizes some top-level overload-like codes, but it does not inspect the nested OpenAI/Codex stream shape:

{
  "type": "error",
  "error": {
    "type": "service_unavailable_error",
    "code": "server_is_overloaded"
  }
}

So even if such an error has already been persisted as an UnknownError, it is still not retried.

Expected behavior

OpenAI/Codex stream errors with nested overload codes should be retried when they are surfaced either as:

  • plain stream error objects / { message: JSON.stringify(payload) }
  • Error(JSON.stringify(payload))
  • already-persisted UnknownError messages containing the same JSON payload

Suggested fix

A narrow fix would be:

  1. In MessageV2.fromError(), attempt ProviderError.parseStreamError(e) before converting generic Error instances into UnknownError.
  2. In SessionRetry.retryable(), recognize the nested OpenAI/Codex overload shape:
    • error.code = "server_is_overloaded"
    • error.type = "service_unavailable_error"
  3. Keep the matching narrow to avoid retrying unrelated JSON error messages.
  4. Add regression tests for the wrapped Error(JSON.stringify(...)) stream error shape.

Local verification

I tested a local patch with regression coverage for the wrapped stream error path:

bun test --timeout 30000 test/session/retry.test.ts
bun run typecheck

Both passed locally.

Activity

  1. github-actions commented on May 7, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    The analysis in this issue is detailed and appreciated — if you believe the wrapped Error(JSON.stringify(...)) path represents a distinct unaddressed gap not covered by those issues, please add a comment clarifying that distinction so maintainers can triage accordingly.

  2. Dandi007 commented on May 7, 2026

    @Dandi007
    Author

    Thanks for the duplicate check.

    I agree this is related to #25884 / #25730 / #25886 / #25728, but I believe this issue tracks one additional path that those fixes do not cover.

    The existing fixes handle the stream payload when it reaches ProviderError.parseStreamError(), for example as { message: JSON.stringify(payload) }.

    The gap here is specifically when the same stream payload is wrapped as an actual Error instance:

    new Error(JSON.stringify({
      type: "error",
      error: {
        type: "service_unavailable_error",
        code: "server_is_overloaded",
      },
    }))

    In the current MessageV2.fromError() flow, this hits the e instanceof Error branch before the default branch can call ProviderError.parseStreamError():

    case e instanceof Error:
      return new NamedError.Unknown({ message: errorMessage(e) }, { cause: e }).toObject()
    
    default:
      const parsed = ProviderError.parseStreamError(e)

    So even after #25888 / #25886-style fixes, this wrapped Error(JSON.stringify(...)) path can still become UnknownError and bypass retry.

    I think the remaining fix is to attempt ProviderError.parseStreamError(e) before converting generic Error instances to UnknownError, plus a regression test for the wrapped Error(JSON.stringify(...)) case.

  3. github-actions commented on Jul 8, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions