anomalyco / anomalyco/opencode

HTTP 408 is not retried on either path; 409 regressed since #39391

Open
#47,525 0 comments 0 reactions 1 assignee View on GitHub

@nexxeln is already working on this.

Since Sep 5, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

What is the issue?

HTTP 408 Request Timeout is not treated as retryable on either request path, so a turn that hits one ends with an error and the prompt has to be resent by hand. 409 has the same problem on the v2 path.

This looks like a regression: #39391 (fix(ai): retry transient client statuses, merged 2026-07-28) made exactly these two statuses retryable in packages/ai/src/provider-error.ts:

-  if (input.status !== undefined && input.status >= 500)
+  if (input.status === 408 || input.status === 409 || (input.status !== undefined && input.status >= 500))

packages/ai has since become packages/llm, and that behaviour did not come across.

Where it stands on current dev

v2 path — packages/llm/src/route/executor.ts

statusReason() classifies 401, 403, 429, then 400 | 404 | 409 | 413 | 422, then status >= 500 || retryableStatus(status), where:

const retryableStatus = (status: number) => status === 429 || status === 503 || status === 504 || status === 529
  • 408 matches no branch and falls through to UnknownProviderReason, whose retryable getter returns false (packages/llm/src/schema/errors.ts).
  • 409 is now explicitly InvalidRequestReason, also retryable = false — the opposite of what #39391 established.

v1 path — packages/opencode/src/session/retry.ts

retryable() only bypasses the SDK's isRetryable flag for status >= 500, so a 408 the SDK didn't mark retryable is dropped. This path is live: SessionRetry.policy is called from packages/opencode/src/session/processor.ts:675.

The newer RETRYABLE_MESSAGE_PATTERNS don't cover it either. The timeout pattern requires a literal space (request timeout), while the payload reported in #39221 carries request_timeout with an underscore. Running the reported message and responseBody against the current pattern list returns false for both.

Why 408 specifically

408 is the one 4xx that is transient in the same sense as a 5xx — the request never completed, so resending it is the defined behaviour for that status, not a retry of a rejected request. It shows up with OpenAI-compatible proxies that normalise an aborted upstream stream into 408 request_timeout.

Expected behavior

408 classified as retryable on both paths, taking the same backoff and retry-after handling as 5xx. 409 restored to the behaviour #39391 established, unless the reclassification to InvalidRequest was deliberate.

Notes

#39221 reported the v1 half and was closed as addressed by #39391, but the fix landed in the other module and has since been lost.

I have a PR ready for the v1 path and can follow up on the executor.ts half if the maintainers agree with the direction.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.