nextjs - 💡(How to fix) Fix Add AbortSignal.timeout() to unbounded fetch() calls in install/build tooling

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Several fetch() calls used during installation and build tooling have no timeout or AbortSignal configured, so they can hang indefinitely if a remote endpoint is slow or unresponsive. Affected paths: the SWC binary download at next install, the mkcert binary download under --experimental-https, the npm registry lookup in patch-incorrect-lockfile (which runs at next dev/next build start), and the build trace upload that runs after a successful build. In all four cases the calling code already awaits the fetch result and handles errors — adding AbortSignal.timeout(ms) is a small, targeted change that makes the hang case fail fast with a clear error instead of blocking the terminal silently.

Error Message

Several fetch() calls used during installation and build tooling have no timeout or AbortSignal configured, so they can hang indefinitely if a remote endpoint is slow or unresponsive. Affected paths: the SWC binary download at next install, the mkcert binary download under --experimental-https, the npm registry lookup in patch-incorrect-lockfile (which runs at next dev/next build start), and the build trace upload that runs after a successful build. In all four cases the calling code already awaits the fetch result and handles errors — adding AbortSignal.timeout(ms) is a small, targeted change that makes the hang case fail fast with a clear error instead of blocking the terminal silently. throw new Error(request failed with status ${response.status}) Timeout values are suggestions; the telemetry upload in particular could use a shorter budget since its failure is non-fatal. Each site already has error handling, so a timed-out AbortError would surface as the normal catch / if (!res.ok) path.

Root Cause

Several fetch() calls used during installation and build tooling have no timeout or AbortSignal configured, so they can hang indefinitely if a remote endpoint is slow or unresponsive. Affected paths: the SWC binary download at next install, the mkcert binary download under --experimental-https, the npm registry lookup in patch-incorrect-lockfile (which runs at next dev/next build start), and the build trace upload that runs after a successful build. In all four cases the calling code already awaits the fetch result and handles errors — adding AbortSignal.timeout(ms) is a small, targeted change that makes the hang case fail fast with a clear error instead of blocking the terminal silently.

Fix Action

Fix / Workaround

Several fetch() calls used during installation and build tooling have no timeout or AbortSignal configured, so they can hang indefinitely if a remote endpoint is slow or unresponsive. Affected paths: the SWC binary download at next install, the mkcert binary download under --experimental-https, the npm registry lookup in patch-incorrect-lockfile (which runs at next dev/next build start), and the build trace upload that runs after a successful build. In all four cases the calling code already awaits the fetch result and handles errors — adding AbortSignal.timeout(ms) is a small, targeted change that makes the hang case fail fast with a clear error instead of blocking the terminal silently.

packages/next/src/lib/patch-incorrect-lockfile.ts — line 15

async function fetchPkgInfo(pkg: string) {
  if (!registry) registry = getRegistry()
  // no signal / timeout
  const res = await fetch(`${registry}${pkg}`)
  if (!res.ok) {

// patch-incorrect-lockfile.ts L15 — registry metadata lookup -const res = await fetch(${registry}${pkg}) +const res = await fetch(${registry}${pkg}, { signal: AbortSignal.timeout(30_000) })

Code Example

// no signal / timeout
await fetch(downloadUrl).then((res) => {
  const { ok, body } = res
  if (!ok || !body) {

---

// no signal / timeout
const response = await fetch(downloadUrl)

if (!response.ok || !response.body) {
  throw new Error(`request failed with status ${response.status}`)

---

async function fetchPkgInfo(pkg: string) {
  if (!registry) registry = getRegistry()
  // no signal / timeout
  const res = await fetch(`${registry}${pkg}`)
  if (!res.ok) {

---

// no signal / timeout
let res = await fetch(traceUploadUrl, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-trace-transfer-mode': shouldUploadFullTrace ? 'full' : 'default',
  },
  body: JSON.stringify(body),
})

---

// download-swc.ts L41 — binary download, allow generous time
-await fetch(downloadUrl).then((res) => {
+await fetch(downloadUrl, { signal: AbortSignal.timeout(120_000) }).then((res) => {

// mkcert.ts L48 — binary download
-const response = await fetch(downloadUrl)
+const response = await fetch(downloadUrl, { signal: AbortSignal.timeout(60_000) })

// patch-incorrect-lockfile.ts L15 — registry metadata lookup
-const res = await fetch(`${registry}${pkg}`)
+const res = await fetch(`${registry}${pkg}`, { signal: AbortSignal.timeout(30_000) })

// trace-uploader.ts L225 — post-build telemetry upload
 let res = await fetch(traceUploadUrl, {
   method: 'POST',
   headers: { ... },
   body: JSON.stringify(body),
+  signal: AbortSignal.timeout(15_000),
 })
RAW_BUFFERClick to expand / collapse

Summary

Several fetch() calls used during installation and build tooling have no timeout or AbortSignal configured, so they can hang indefinitely if a remote endpoint is slow or unresponsive. Affected paths: the SWC binary download at next install, the mkcert binary download under --experimental-https, the npm registry lookup in patch-incorrect-lockfile (which runs at next dev/next build start), and the build trace upload that runs after a successful build. In all four cases the calling code already awaits the fetch result and handles errors — adding AbortSignal.timeout(ms) is a small, targeted change that makes the hang case fail fast with a clear error instead of blocking the terminal silently.

What I observed

packages/next/src/lib/download-swc.ts — line 41

// no signal / timeout
await fetch(downloadUrl).then((res) => {
  const { ok, body } = res
  if (!ok || !body) {

packages/next/src/lib/mkcert.ts — line 48

// no signal / timeout
const response = await fetch(downloadUrl)

if (!response.ok || !response.body) {
  throw new Error(`request failed with status ${response.status}`)

packages/next/src/lib/patch-incorrect-lockfile.ts — line 15

async function fetchPkgInfo(pkg: string) {
  if (!registry) registry = getRegistry()
  // no signal / timeout
  const res = await fetch(`${registry}${pkg}`)
  if (!res.ok) {

packages/next/src/trace/trace-uploader.ts — line 225

// no signal / timeout
let res = await fetch(traceUploadUrl, {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-trace-transfer-mode': shouldUploadFullTrace ? 'full' : 'default',
  },
  body: JSON.stringify(body),
})

Suggested fix

Wrap each call with AbortSignal.timeout(ms) (Node 17.3+ / undici built-in, already available in the Node versions Next.js supports):

// download-swc.ts L41 — binary download, allow generous time
-await fetch(downloadUrl).then((res) => {
+await fetch(downloadUrl, { signal: AbortSignal.timeout(120_000) }).then((res) => {

// mkcert.ts L48 — binary download
-const response = await fetch(downloadUrl)
+const response = await fetch(downloadUrl, { signal: AbortSignal.timeout(60_000) })

// patch-incorrect-lockfile.ts L15 — registry metadata lookup
-const res = await fetch(`${registry}${pkg}`)
+const res = await fetch(`${registry}${pkg}`, { signal: AbortSignal.timeout(30_000) })

// trace-uploader.ts L225 — post-build telemetry upload
 let res = await fetch(traceUploadUrl, {
   method: 'POST',
   headers: { ... },
   body: JSON.stringify(body),
+  signal: AbortSignal.timeout(15_000),
 })

Timeout values are suggestions; the telemetry upload in particular could use a shorter budget since its failure is non-fatal. Each site already has error handling, so a timed-out AbortError would surface as the normal catch / if (!res.ok) path.

Notes

AbortSignal.timeout() is a static factory (no manual clearTimeout needed) and has been available since Node 17.3 / undici 5.x. For older Node targets a setTimeout + AbortController combo is equivalent. The font-fetching path already has retry logic added in #51890 — these four tooling paths have not yet received similar treatment.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING