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

  1. Disable range-based resume for that endpoint (avoid the retry interceptor, or set retryOptions that do not resume).
  2. Verify the server returns correct Content-Length for Range requests (test with curl -r).
  3. 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

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


AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13). Data as JSON: /api/errors/c59ad3b187c52113. Report an issue: GitHub.