nodejs/node · error · RequestRetryError
ETag mismatch
Error message
ETag mismatch
What it means
Thrown by RetryHandler.onResponseStart during a retry resume when the original response had a strong ETag recorded but the resumed response carries a different ETag. The retry handler checkpoints the body by byte offset and uses If-Match to guarantee the same resource; an ETag change means the resource changed under us, so resuming would corrupt the downloaded file. This is a safety guard, not a bug.
Source
Thrown at deps/undici/src/lib/handler/retry-handler.js:292
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
assert(this.start === start, 'content-range mismatch')
assert(this.end == null || this.end === end, 'content-range mismatch')
return
}
if (this.end == null) {
if (statusCode === 206) {
// First time we receive 206View on GitHub (pinned to 1b2de5e052)
Solutions
- Restart the download from byte 0 when the resource is mutable (do not resume).
- Pin the version via a strong ETag/If-Match that the server honors, or via an immutable content-addressed URL.
- Disable transparent retry-resume for endpoints whose content can change between requests.
Example fix
// before (resume across a version change)
client = client.compose(interceptors.retry())
await client.request({ path: '/mutable-file' })
// after
// fetch atomically; on ETag mismatch restart from the beginning
async function stableDownload(path) {
let etag, buf = []
while (true) {
const res = await agent.request({ path, headers: etag ? { 'if-match': etag } : {} })
if (res.statusCode === 412) { buf = []; continue }
etag = res.headers.etag
buf.push(await res.body.arrayBuffer())
return Buffer.concat(buf.map(Buffer.from))
}
} Defensive patterns
Strategy: retry
Try / catch
try { await retryClient.request({ path }) } catch (e) { if (e.code === 'UND_ERR_REQ_RETRY' && /ETag mismatch/.test(e.message)) { /* resource changed: restart from 0 */ await baseClient.request({ path }) } else throw e } Prevention
- Use immutable/content-addressed URLs for mutable resources.
- Pin versions with a strong ETag + If-Match honored by the origin.
- Restart downloads on ETag change rather than resuming.
When it happens
Trigger: A ranged retry resume request where the origin object was overwritten between the original and the resume response, producing a new ETag; the If-Match check is bypassed or the server ignores If-Match and returns the new representation anyway.
Common situations: Mutable objects in object storage overwritten mid-download; CDN cache misses returning a newer version; deployments happening during a long download; servers that do not honor If-Match.
Related errors
- Content-Length mismatch
- server does not support the range header and the payload was
- Content-Range mismatch
- Request failed
- UND_ERR_INVALID_ARG
AI-assisted analysis of nodejs/node@1b2de5e052 (2026-08-13).
Data as JSON: /api/errors/3638e1383883c0eb.
Report an issue: GitHub.