nodejs/node · error · RequestRetryError

Content-Range mismatch

Error message

Content-Range mismatch

What it means

Thrown by RetryHandler.onResponseStart during a retry resume when the resumed response is expected to carry a Content-Range header (it is a 206 or a 200 resuming from start>0) but parseRangeHeader returns null, i.e. the header is missing or unparseable. Without a Content-Range the handler cannot verify it is resuming from the correct byte offset.

Source

Thrown at deps/undici/src/lib/handler/retry-handler.js:283

    // Checkpoint for resume from where we left it
    if (this.headersSent) {
      // Only Partial Content 206 supposed to provide Content-Range,
      // any other status code that partially consumed the payload
      // should not be retried because it would result in downstream
      // wrongly concatenate multiple responses.
      if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) {
        throw new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, {
          headers,
          data: { count: this.retryCount }
        })
      }

      const contentRange = parseRangeHeader(headers['content-range'])
      // If no content range
      if (!contentRange) {
        // We always throw here as we want to indicate that we entred unexpected path
        throw new RequestRetryError('Content-Range mismatch', statusCode, {
          headers,
          data: { count: this.retryCount }
        })
      }

      // Let's start with a weak etag check
      if (this.etag != null && this.etag !== headers.etag) {
        // We always throw here as we want to indicate that we entred unexpected path
        throw new RequestRetryError('ETag mismatch', statusCode, {
          headers,
          data: { count: this.retryCount }
        })
      }

      validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount)

      const { start, size, end = size ? size - 1 : null } = contentRange

View on GitHub (pinned to 1b2de5e052)

Solutions

  1. Confirm the server returns a well-formed Content-Range header (e.g. 'bytes 1000-1999/5000') on ranged responses.
  2. Disable transparent retry-resume for origins that do not send Content-Range.
  3. Validate the header with curl -I -H 'Range: bytes=0-0' before relying on resume.

Example fix

// before (relying on retry resume against an origin that strips Content-Range)
client = client.compose(interceptors.retry())

// after
// verify with: curl -r 0-0 -I https://example.com/file  (look for content-range)
// if missing, drop the retry interceptor and fetch atomically:
client = baseClient
Defensive patterns

Strategy: retry

Try / catch

try { await retryClient.request(opts) } catch (e) { if (e.code === 'UND_ERR_REQ_RETRY' && /Content-Range mismatch/.test(e.message)) { await baseClient.request(opts) } else throw e }

Prevention

When it happens

Trigger: A resume request gets a 2xx response that lacks the Content-Range header, or carries a malformed Content-Range that parseRangeHeader cannot interpret.

Common situations: Servers that answer 200 to Range requests without Content-Range; intermediary proxies that strip Content-Range; misconfigured compression that removes the header; buggy origin returning partial content without metadata.

Related errors


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