nodejs/node · error · RequestRetryError
Content-Length mismatch
Error message
Content-Length mismatch
What it means
Thrown by validatePartialResponseContentLength inside the RetryHandler when resuming a partially consumed response. After a mid-stream failure the retry handler re-requests a byte range; if the server's 206 response carries a Content-Length that does not equal range.end - range.start + 1, the resume would produce corrupt data, so the handler refuses it. This protects callers from silently truncated or overlapping bodies.
Source
Thrown at deps/undici/src/lib/handler/retry-handler.js:30
function calculateRetryAfterHeader (retryAfter) {
const retryTime = new Date(retryAfter).getTime()
return isNaN(retryTime) ? null : retryTime - Date.now()
}
function validatePartialResponseContentLength (headers, range, statusCode, retryCount) {
const contentLength = headers['content-length']
if (contentLength == null) {
return
}
if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) {
return
}
const length = Number(contentLength)
const expectedLength = range.end - range.start + 1
if (!Number.isFinite(length) || length !== expectedLength) {
throw new RequestRetryError('Content-Length mismatch', statusCode, {
headers,
data: { count: retryCount }
})
}
}
// A stable controller handed to the downstream handler for the lifetime of the
// request. Each transparent retry/resume is a *separate* dispatch with its
// *own* connection controller. Without a stable proxy the downstream body keeps
// flow-controlling the original (now-dead) controller while data flows on the
// new one: backpressure pauses the new connection's controller, but the
// consumer's resume() targets the old one, so the resumed body stalls forever.
// The proxy always forwards to the controller of the currently active connection.
class RetryController {
constructor () {
this.target = null
}
View on GitHub (pinned to 1b2de5e052)
Solutions
- Disable range-based resume for that endpoint (avoid the retry interceptor, or set retryOptions that do not resume).
- Verify the server returns correct Content-Length for Range requests (test with curl -r).
- If the resource is mutable, fetch it atomically without partial retries, or use a strong ETag/If-Match guard at the application layer.
Example fix
// before
client = client.compose(interceptors.redirect({ maxRedirections: 5 }), interceptors.retry())
// after (server does not honor Range correctly -> do not retry-resume)
client = client.compose(interceptors.redirect({ maxRedirections: 5 }))
// or fetch the whole body without transparent retry on partial reads Defensive patterns
Strategy: retry
Try / catch
try { await client.request({ path, headers: { range: `bytes=${start}-` } }) } catch (e) { if (e.code === 'UND_ERR_REQ_RETRY' && /Content-Length mismatch/.test(e.message)) { start = 0; await client.request({ path }) /* restart */ } else throw e } Prevention
- Test origin Range support with curl -r before relying on resume.
- Avoid the retry interceptor for origins known to mishandle Content-Length.
- Restart from byte 0 on integrity failures.
When it happens
Trigger: Using the retry() interceptor (or Agent with retryOptions) against a server that returns a 206 with a Content-Length inconsistent with the requested Range, or a server/proxy that rewrites Content-Length on ranged responses.
Common situations: CDNs/proxies that mangle Content-Length on ranged requests; servers without proper Range support that still answer 206; concurrent writes changing the resource mid-download so the range window shifts.
Related errors
- server does not support the range header and the payload was
- Content-Range mismatch
- ETag mismatch
- Request failed
- UND_ERR_INVALID_ARG
AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13).
Data as JSON: /api/errors/c59ad3b187c52113.
Report an issue: GitHub.