pnpm/pnpm · error · FetchError
ERR_PNPM_FETCH_${response.status}
ERR_PNPM_FETCH_${response.status}
Error message
GET ${redactUrlCredentials(request.url)}: ${response.statusText} - ${response.status} What it means
A remote tarball request returned a non-200 status; the response is wrapped in a FetchError whose code embeds the HTTP status (ERR_PNPM_FETCH_401, ERR_PNPM_FETCH_404, ERR_PNPM_FETCH_429, ...) and whose message shows the credential-redacted URL, statusText, and status. The downloader retries transient failures twice (factor-10 backoff, 10s min / 60s max) but fails fast on 401/403/404 — those are deterministic.
Source
Thrown at pnpm11/fetching/tarball-fetcher/src/remoteTarballFetcher.ts:138
let data: Buffer
try {
const res = await fetchFromRegistry(url, {
authHeaderValue,
// Tarballs are already compressed; ask the server not to apply an additional
// Content-Encoding so Content-Length matches the body we receive and we don't
// waste CPU on round-trip re-compression. See https://github.com/pnpm/pnpm/issues/11506
headers: { 'accept-encoding': 'identity' },
// The fetch library can retry requests on bad HTTP responses.
// However, it is not enough to retry on bad HTTP responses only.
// Requests should also be retried when the tarball's integrity check fails.
// Hence, we tell fetch to not retry,
// and we perform the retries from this function instead.
retry: { retries: 0 },
timeout: gotOpts.timeout,
})
if (res.status !== 200) {
throw new FetchError({ url, authHeaderValue }, res)
}
// When Content-Encoding is present, Content-Length refers to the encoded form
// of the data, not the decoded bytes that the fetch implementation yields.
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Encoding
const isEncoded = isContentEncoded(res.headers.get('content-encoding'))
const contentLength = !isEncoded && res.headers.has('content-length') && res.headers.get('content-length')
const parsedLength = typeof contentLength === 'string' ? parseInt(contentLength, 10) : NaN
const size = Number.isFinite(parsedLength) && parsedLength >= 0 ? parsedLength : null
if (opts.onStart != null) {
opts.onStart(size, currentAttempt)
}
// In order to reduce the amount of logs, we only report the download progress of big tarballs
const onProgress = (size != null && size >= BIG_TARBALL_SIZE && opts.onProgress)
? throttle(opts.onProgress, 500)
: undefined
const startTime = Date.now()
let downloaded = 0View on GitHub (pinned to 6261b7f388)
Solutions
- Decode the embedded status: 401/403 means fix registry auth (set the token in .npmrc for that registry host)
- 404 means verify the package/version exists (npm view pkg versions) and that the registry URL in .npmrc is correct
- 429/5xx are transient and already retried — check the registry status page or your proxy, then retry after backoff
- If a corporate proxy is in play, bypass it for the registry host or fix its TLS interception
Example fix
# before
$ pnpm install @corp/private-pkg
# ERR_PNPM_FETCH_404 / ERR_PNPM_FETCH_401
# after: point auth at the right registry
$ cat .npmrc
@corp:registry=https://registry.corp.com/
//registry.corp.com/:_authToken=${NPM_TOKEN}
$ pnpm install @corp/private-pkg Defensive patterns
Strategy: retry
Validate before calling
// Probe auth and existence before a full install of a private package
import { execa } from 'execa'
async function assertTarballFetchable (registry: string, pkgName: string, authHeader?: string): Promise<void> {
const res = await fetch(`${registry}/${encodeURIComponent(pkgName).replace('%40', '@')}`, {
headers: authHeader ? { authorization: authHeader } : {},
})
if (res.status === 401 || res.status === 403) throw new Error('registry auth missing/invalid — set the token in .npmrc')
if (res.status === 404) throw new Error('package not found on registry — check name/version/registry URL')
} Try / catch
function parseFetchStatus (code: string): number | null {
const m = /^ERR_PNPM_FETCH_(\d+)$/.exec(code)
return m ? parseInt(m[1], 10) : null
}
try {
await downloader(url, opts)
} catch (err) {
const status = parseFetchStatus((err as any).code ?? '')
if (status != null && (status === 429 || status >= 500)) {
return retryWithBackoff(() => downloader(url, opts), { attempts: 5, factor: 2 })
}
if (status === 401 || status === 403) throw new Error('fix registry credentials', { cause: err })
throw err // 404 and friends are deterministic
} Prevention
- Configure per-registry auth tokens in .npmrc for every private scope before installing
- Note that 401/403/404 already skip pnpm's internal retries — fix the cause instead of rerunning
- Give CI explicit retry-with-backoff around installs to absorb 429/5xx registry episodes
When it happens
Trigger: GET of a tarball URL returns non-200: unpublished package or wrong version (404), missing/invalid registry token (401/403), rate limiting (429), registry or proxy outage (5xx), corporate proxy interception.
Common situations: Missing .npmrc auth token for private registries; package unpublished or version typo; publish-propagation delay (404 right after publish); npm rate limits in CI; proxy returning 502/503.
Related errors
- Fetching ${packageId} failed!
- Redirect location header missing for ${redactUrlCredentials(
- REGISTRY_ERROR
- AUTH_TOKEN_FETCH
- CHANGELOG_TARBALL_FETCH_FAILED
AI-assisted analysis of pnpm/pnpm@6261b7f388 (2026-08-17).
Data as JSON: /api/errors/3f961bb0bbdd7bed.
Report an issue: GitHub.