Next.js

Fix: Cannot set property Request of #<Object> getter

Reproduced line by line with Next.js's own polyfill file and abortcontroller-polyfill's published source across Node 16, 17 and 18, plus which of two independent fixes actually applies.

Mahdi Ben RhoumaMahdi Ben Rhouma
9 min read

TL;DR#

Next.js's own Node.js-side fetch polyfill defines a lazy, getter-only Request on the global object whenever global.fetch is missing — true for Node.js below 18. Any dependency that still tries to patch the fetch globals itself, such as abortcontroller-polyfill, then does a plain Request = … assignment against that getter-only property and throws, because an accessor with no setter cannot be overwritten. The fix is to either stop that dependency from loading abortcontroller-polyfill (for web3, upgrade to 1.8.2 or later, which guards the load) or move the build to Node.js 18 or newer, where Next.js's own polyfill never installs in the first place.

text
TypeError: Cannot set property Request of #<Object> which has only a getter

That is the exact message from a Stack Overflow question: a TypeScript Next.js app that built and ran fine locally, then failed this way only when deployed to Vercel. The trace underneath it runs straight through node_modules/abortcontroller-polyfill/dist/polyfill-patch-fetch.js, which is the actual file this clash lives in, regardless of which project dependency pulled it in.

Why it happens#

Next.js 13.0.x shipped its own fallback so that server-side code could call fetch() without the caller worrying about which Node.js version would run it. That fallback is one real file, node-polyfill-fetch.js. Here is the relevant part of the source for next@13.0.2 (the version named in the GitHub thread below). The logic is identical in the package this article's own reproduction actually installed from npm: diffing this source against the real node_modules/next/dist/server/node-polyfill-fetch.js it produced shows the same if (!global.fetch) guard and the same three getter-only properties, apart from formatting and an appended source-map comment:

js
// excerpt — apart from formatting, identical in the published package (see text above)
if (!global.fetch) {
  // … global.fetch is assigned a plain, reassignable function here …
 
  Object.defineProperties(global, {
    Headers: { get() { return getFetchImpl().Headers } },
    Request: { get() { return getRequestImpl() } },
    Response: { get() { return getFetchImpl().Response } },
  })
}

Two things matter in that snippet. First, the whole block is skipped when global.fetch already exists. Second, Headers, Request and Response are installed with Object.defineProperties and only a get() — no set, and configurable left at its default of false. That is the textbook shape of a property JavaScript refuses to let you overwrite: an accessor with a getter and no setter, non-configurable. Reassigning it from strict-mode code throws exactly TypeError: Cannot set property X of #<Object> which has only a getter, with X being whatever you tried to reassign — and the code that does the reassigning below opens with a 'use strict' directive of its own. A related Node.js-18-only failure on this site comes from the same transition, where the new native fetch() turned out to be stricter, not more lenient, than whatever shipped before it.

abortcontroller-polyfill exists to add AbortController/AbortSignal support to whatever fetch() it finds. The published 1.7.8 release wraps the whole file in a UMD header, and the function that does the actual patching (lines 588-614 of that same file) opens with its own early exit:

js
// abortcontroller-polyfill@1.7.8, lines 588-614
(function (self) {
  if (!polyfillNeeded(self)) {
    return
  }
  // … the patch runs here …
})(typeof self !== 'undefined' ? self : global)

Node.js has no browser self, so that IIFE always runs against global there. Inside that function, after the early exit, it does this:

js
// abortcontroller-polyfill@1.7.8 — dist/polyfill-patch-fetch.js, lines 600-601
self.fetch = fetch
self.Request = Request

self.fetch = fetch is harmless — Next's global.fetch is a plain function, happy to be replaced. self.Request = Request is the direct, unconditional assignment that hits Next's getter-only accessor and throws. The library is clearly aware that getter-only properties exist in this space — a comment earlier in the same file already warns about a near-identical TypeError on a Request instance's own signal property — but that awareness never extended to guarding this particular global assignment, at least not in this file.

A minimal reproduction, run on 25 October 2026 with a throwaway npm install of exactly these two published packages (next@13.0.2 and abortcontroller-polyfill@1.7.8), nothing else — no Vercel, no TypeScript, no web3 — confirms it: requiring next/dist/server/node-polyfill-fetch.js and then abortcontroller-polyfill/dist/polyfill-patch-fetch.js throws the exact error above on node:16-alpine (v16.20.2, the same major named in the GitHub issue below) and on node:17.4.0-alpine. Object.getOwnPropertyDescriptor(global, 'Request') on those two images reports {"hasGet":true,"hasSet":false,"configurable":false} right before the throw. On that same Node.js 16.20.2, AbortController is already a native function, but Request.prototype.hasOwnProperty('signal') is false — abortcontroller-polyfill's own internal check looks at both, and only the second one still says a patch is needed.

The same two npm installs on node:18-alpine (v18.20.8) require cleanly, but not because abortcontroller-polyfill successfully reassigns anything. There, Request.prototype.hasOwnProperty('signal') is true — Node's own docs mark fetch, Request, Response and Headers as on by default from Node.js 18 — so abortcontroller-polyfill's own pre-check decides no patch is needed at all and returns before touching Request, confirmed by comparing global.Request's own reference before and after the require(), which comes back unchanged. Next's polyfill file never installs its getter here either, for the separate reason already given: global.fetch already exists when it runs, so its if (!global.fetch) guard skips the whole block.

The original GitHub issue, filed against web3.js because that was the dependency the first reporters had in common, was narrowed by another commenter to the same two variables: web3@1.8.0 plus Next.js 13.0.2 breaks on Node.js 16 and works on Node.js 18; web3@1.7.4 plus the same Next.js works on either. Requiring web3 itself — not just the two isolated files above — confirms the same split, and narrows the version further: web3@1.8.0 and web3@1.8.1 both throw on Node.js 16; web3@1.8.2 and the current 1.10.4 (the highest 1.x release, from February 2024) do not. Diffing web3-providers-http's own published source between those two releases shows exactly what changed, in the one line that matters:

js
// web3-providers-http@1.8.1 — unconditional
require('abortcontroller-polyfill/dist/polyfill-patch-fetch')
js
// web3-providers-http@1.8.2 — guarded
if (typeof global !== 'undefined' && !global.AbortController) {
  require('abortcontroller-polyfill/dist/polyfill-patch-fetch')
}

1.8.2 added its own guard in front of the require, checking only global.AbortController — not the Request.prototype.signal check abortcontroller-polyfill makes internally. AbortController is already native on Node.js 16.20.2, confirmed above, so this guard is satisfied on the exact Node.js version these reports used, not only on Node.js 18: across every Node.js version this reproduction tested (16.20.2 through 18.20.8), the fix does not depend on upgrading Node.js at all. next@13.0.2's own floor goes lower still, to Node.js >=14.6.0; this reproduction did not test that range, so whether the same guard is satisfied there too stays open. One commenter in that thread said, on 14 November 2022, that they had opened a pull request; replying to a different developer hitting the same error on a newer next release six weeks later, on 27 December, that same commenter said the fix would be "available in the next version update" rather than confirming it had already shipped. web3.js's own changelog records the result under 1.8.2 (released 30 January 2023, per the npm registry): "Fixed Next.js builds failing on Node.js v16, Abortcontroller added if it doesn't exist globally (#5601)" — naming this exact issue.

Fix#

1. Find what pulls in the old polyfill#

bash
npm ls abortcontroller-polyfill

A project on web3 1.8.0 to 1.8.2 (the releases this article checked) reports a chain like this, whether or not that particular version actually triggers the crash — abortcontroller-polyfill stays a dependency of web3-providers-http even after 1.8.2 guards the require that loads it; 1.7.4 has no such dependency at all:

text
`-- web3@1.8.2
  `-- web3-core@1.8.2
    `-- web3-core-requestmanager@1.8.2
      `-- web3-providers-http@1.8.2
        `-- abortcontroller-polyfill@1.7.8

So presence alone does not say whether the require actually runs. If npm ls abortcontroller-polyfill prints nothing at all, no package in your tree can trigger this specific clash. If it does print, check the version of the package one level above it instead — for web3, that is web3-providers-http, and 1.8.2 or later is the one that matters.

2. Change the dependency, if you can#

For web3 specifically, the simplest fix is an upgrade within the same 1.x major: web3@^1.8.2 resolves to the current 1.10.4 today, which this article's own reproduction loaded cleanly:

bash
npm install web3@^1.8.2

Pinning back to 1.7.4 — what the original reports actually did, before 1.8.2 existed — works too, for the same reason: neither version loads abortcontroller-polyfill at all. The current web3 major, 4.16.0, has an empty abortcontroller-polyfill tree as well, but 4.x is a new major version, which may break 1.x code outright rather than merely fixing this one error; treat it as a separate migration, not this fix.

3. Or change the Node.js version, independent of the dependency#

This is the fix that works regardless of which package patches fetch, because it removes Next's getter-only Request instead of working around it. Make the Node.js version your editor, your CI and your deployment target all agree on at least 18:

json
{
  "engines": {
    "node": ">=18.17.0"
  }
}

On Vercel, the same version can be set without touching package.json, from the project's Settings → Build and Deployment → Node.js Version; an engines.node range in package.json takes priority over that dashboard setting. Either way, confirm it by printing process.version in the build itself rather than trusting what your editor's terminal reports — one developer in the same GitHub thread spent ten hours on this before realising their Netlify deploy needed its own Node.js version pin; adding a .node-version file set to 19.1.0 in the project root fixed it for them.

Verify the fix#

Re-run the build (next build locally, or trigger a fresh Vercel deployment) and confirm the TypeError is gone. If you changed the dependency, do not stop at npm ls abortcontroller-polyfill — it still lists the package for web3@1.8.2 and later, as shown above. Check the version of whichever package actually requires it instead (npm ls web3-providers-http, for web3) against the version you confirmed is guarded. If you changed the Node.js version instead, node -v in the same environment that runs the build should report 18 or higher; Next's own polyfill file skips entirely once global.fetch already exists, so there is nothing left for any dependency's assignment to collide with.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

If it's not web3: finding the real culprit#

web3 is only the dependency the earliest reporters in that thread happened to share; the mechanism has nothing to do with Ethereum libraries specifically. Anything that loads abortcontroller-polyfill's dist/polyfill-patch-fetch entry point unconditionally — directly, or several layers down a dependency tree, the way web3 below 1.8.2 did — trips the same getter on an older Next.js running on Node.js below 18. npm ls abortcontroller-polyfill (step 1 above) is the fastest way to find the actual chain in your own project rather than assuming it is whichever package this article happened to use as its example.

Switching to Node.js 18 does not always finish the job on the first try, either: in the same GitHub thread, a developer who moved to Node.js 18 while on web3@1.8.0 hit a separate crash, TypeError: callback is not a function inside web3-providers-http. He attributed it to @loopring-web/web3-provider, a wrapper he suspected carried "a very old version of web3"; using web3@1.8.1 directly on Node.js 18, instead of through that wrapper, resolved it for him. If your error changes shape after a fix, read the new trace before assuming it is a continuation of the same bug — it may not be.

It is also worth knowing which installs this clash cannot reach. node-polyfill-fetch.js is confirmed absent from the published next@14.0.0 package, whose own engines.node field already requires >=18.17.0 — a version that always has native fetch. This error belongs to Next.js 13.x installs (the ones that still ship the file) actually running on a Node.js below the versions this reproduction confirmed safe (18.20.8 and below, down to 16.20.2); it is not a current-Next.js problem, and not a Next.js 12.x one either — 12.x's own node-polyfill-fetch.js assigns global.Request directly, without the getter-only Object.defineProperties 13.x uses, so it never produces this clash. Two related build-only failures worth ruling out before assuming this is your cause: a native SWC binary missing for the platform that actually runs the build, and a Docker Compose service name that resolves in development but not inside the production build — both produce a Next.js build that fails in one build environment but not another, for reasons that have nothing to do with fetch polyfills.

Further reading: CORS Not Working on Vercel? Fix the vercel.json Headers.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote