nodejs/node · error · RequestRetryError
Request failed
Error message
Request failed
What it means
Thrown by RetryHandler.onResponseStart in the else branch: a new response is starting, the handler already knows an end boundary (this.end != null, set from a prior 206/Content-Length) but headers have not been sent yet, and the code path fell through to the else. This indicates an internal state inconsistency in the retry/resume flow rather than a normal user error; the handler surfaces it as a RequestRetryError carrying the status code so the caller sees the failure instead of a silent stall.
Source
Thrown at deps/undici/src/lib/handler/retry-handler.js:371
// for instance not safe to assume if the response is byte-per-byte
// equal
if (
this.etag != null &&
this.etag[0] === 'W' &&
this.etag[1] === '/'
) {
this.etag = null
}
this.headersSent = true
this.handler.onResponseStart?.(
this.controllerProxy,
statusCode,
headers,
statusMessage
)
} else {
throw new RequestRetryError('Request failed', statusCode, {
headers,
data: { count: this.retryCount }
})
}
}
onResponseData (_controller, chunk) {
if (this.error) {
return
}
this.start += chunk.length
this.handler.onResponseData?.(this.controllerProxy, chunk)
}
onResponseEnd (_controller, trailers) {
if (this.error && this.retryOpts.throwOnError) {View on GitHub (pinned to 1b2de5e052)
Solutions
- Capture statusCode and headers from the error's data and verify the server returns consistent statuses across retries.
- Reduce retry aggressiveness for the affected route (lower maxRetries, narrow statusCodes/errorCodes).
- If reproducible, report it with the response sequence (status codes + headers) so the retry contract can be reconciled with the origin.
- For endpoints that cannot be safely resumed, fetch atomically without the retry interceptor.
Example fix
// before
client = client.compose(interceptors.retry({ maxRetries: 5 }))
// after (constrain retry to idempotent, range-honoring responses)
client = client.compose(interceptors.retry({
maxRetries: 2,
statusCodes: [500, 502, 503, 504],
errorCodes: ['ECONNRESET', 'EPIPE']
})) Defensive patterns
Strategy: try-catch
Try / catch
try { await retryClient.request(opts) } catch (e) { if (e.code === 'UND_ERR_REQ_RETRY' && e.message === 'Request failed') { const status = e.data?.statusCode ?? e.statusCode; /* narrow retry policy or fetch atomically */ await baseClient.request(opts) } else throw e } Prevention
- Constrain retryOpts (statusCodes/errorCodes/maxRetries) to deterministic cases.
- Watch for origins returning inconsistent statuses across attempts.
- Capture statusCode + headers from the error data to diagnose.
When it happens
Trigger: A retry attempt produces a fresh 2xx response after an end boundary was already computed from a previous attempt, in a combination of statuses the handler did not expect (e.g. an initial 206 set this.end, then a retry returned a 200 without going through the resume branch).
Common situations: Servers whose retry behavior is non-deterministic across attempts (206 then 200, or vice versa); intercepted/proxied responses that alter status codes; edge cases in CDN failover. Usually surfaces as 'Request failed' with the offending status code.
Related errors
- Content-Length mismatch
- server does not support the range header and the payload was
- Content-Range mismatch
- ETag mismatch
- UND_ERR_INVALID_ARG
AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13).
Data as JSON: /api/errors/b6fee12420c44bf2.
Report an issue: GitHub.