Skip to content

feat(fetch): use native global fetch instead of cross-fetch for workers - #777

Open
omercnet wants to merge 3 commits into
mainfrom
shuni/14233-user-request-1-problem-i9mh945fk81wuipxs82pf
Open

feat(fetch): use native global fetch instead of cross-fetch for workers#777
omercnet wants to merge 3 commits into
mainfrom
shuni/14233-user-request-1-problem-i9mh945fk81wuipxs82pf

Conversation

@omercnet

@omercnet omercnet commented Aug 1, 2026

Copy link
Copy Markdown
Member

Fixes descope/etc#14233

View Shuni run

Done — committed as 2170384.

What changed

  • lib/fetch-polyfill.ts — collapsed to a one-line indirection over globalThis.fetch; removed the cross-fetch import, the globalThis.Headers assignment, and the 30MB highWaterMark patch (node-fetch-only). Module path kept, so the authz/fga test mocks are untouched.
  • lib/index.ts:55globalThis.process?.versions?.node (bare process is a ReferenceError in a Worker).
  • package.json — dropped cross-fetch, engines.node>= 18.0.0; lockfile edited surgically (48 lines: cross-fetch, node-fetch, whatwg-url, tr46, webidl-conversions — nothing else referenced them).
  • .github/workflows/ci.yml — dropped 16 from the three node matrices; the new test spies on globalThis.fetch, which doesn't exist there.
  • lib/fetch-polyfill.test.ts — delegation test + a dist assertion (cross-fetch/node-fetch/node:http/node:https absent from dist/index.esm.js).
  • README.md — Node 18+, runs on Workers/edge, no Node built-ins.

Two things to know

npm install and npm run build are broken on main, independently of this: commit b7b6e0e bumped typescript to ^7 but left package-lock.json at ^4.6.4 (so npm ci is out of sync), and rollup-plugin-dts@4 peer-requires typescript@^4.1. @rollup/plugin-typescript also crashes on TS 7. I built and tested with typescript@5.9.3 installed locally with --no-save --no-package-lock — nothing committed. That needs its own fix.

I did not run the wrangler dev repro (§5.1) — no Workers runtime in this sandbox. It's worth running against this build to close the loop, but the failure mode is removed either way: there is no longer any path to node-fetch, regardless of whether the customer was pre-2.8.0 or hit the global-timing case.

Not committed: feat: was chosen over fix: so release-please cuts a minor, per §6's recommendation for the engines bump — switch to fix: if a patch is preferred.


Created by Shuni 🐕

cross-fetch resolves to node-fetch, which requires node's http/https at
import time. In Cloudflare Workers (wrangler nodejs_compat / Nitro) unenv
stubs those modules with functions that throw, so any request through the
SDK failed with "[unenv] https.request is not implemented yet!".

Native fetch is available on every runtime the SDK now supports, so the
polyfill collapses to a thin indirection over globalThis.fetch and no
bundler can pull node built-ins into an edge build. Dropped along with it:
the globalThis.Headers assignment and the 30MB highWaterMark patch, both
node-fetch-only workarounds.

Also reads process via globalThis, since a bare `process` is a
ReferenceError in a Worker without nodejs_compat.

Requires node 18+ (node 16 has been EOL since Sept 2023).
@shuni-bot

shuni-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

🐕 Uh oh! I ran into an error.

View failed run

Please check the logs for details.

@shuni-bot

shuni-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

🐕 Suggested Reviewers

The review assignment covers primary areas impacted by the change: fetch polyfill logic and main index file modifications. Selected reviewers have recent contributions in these core files, ensuring relevant expertise and broad coverage of the affected code.

Reviewer Reason
orius123 oris123 has contributed to 'lib/fetch-polyfill.ts' and 'lib/index.ts', which are directly affected by this change, especially in the fetch polyfill logic.
nirgur nirgur's recent commits also involve 'lib/fetch-polyfill.ts' and 'lib/index.ts', making them a relevant reviewer for understanding the impact on the fetch implementation.
gaokevin1 gaokevin1 has commits in 'lib/index.ts', which is part of the change scope, and can provide insights on overall integration.
nmacianx nmacianx has contributed specifically to 'lib/fetch-polyfill.ts', focusing on the polyfill logic.
mattolson mattolson's commit in 'lib/fetch-polyfill.ts' indicates relevant familiarity with fetch polyfill adjustments.

Suggested by Shuni based on git history and PR context. Names are not @-mentioned to avoid notifying anyone — request a review from whoever fits best.

@shuni-bot shuni-bot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐕 Shuni's Review

Drops cross-fetch and collapses fetch-polyfill.ts to a thin wrapper over native fetch, bumps engines to Node 18+, and fixes the bare process reference that ReferenceErrors on Workers.

Verified: the removed globalThis.Headers ??= Headers is safe (Node ≥18 exposes Headers/fetch as writable globals, and jest-environment-node copies them, so index.test.ts:669 and the new jest.spyOn(globalThis, 'fetch') both hold). Hand-edited lockfile is consistent — no residual cross-fetch/node-fetch/whatwg-url/tr46/webidl-conversions refs, lockfileVersion 3 so no legacy section to sync.

Sniffed out 1 issue:

  • 1 🟡 MEDIUM: Node 16 drop ships as a minor with no fetch fallback

Heads-up: CI will be red here for an unrelated pre-existing reason — HEAD's lockfile pins typescript@4.7.4 while package.json wants ^7.0.0, so npm ci fails in all four jobs. Good bones otherwise. Woof!

Comment thread package.json
},
"engines": {
"node": ">= 16.0.0"
"node": ">= 18.0.0"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 MEDIUM: This is a breaking change for Node 16 consumers, but the PR is titled feat: so release-please will cut a minor (2.14.0).

engines is only a warning by default (not engine-strict), so a Node 16 user on ^2.x installs 2.14.0 successfully and then every SDK request throws TypeError: globalThis.fetch is not a function — the cross-fetch fallback that used to cover them is gone, and there's no guard or actionable error.

Node 16 is EOL so dropping it is reasonable, but consider a major (or at minimum an explicit release-note callout).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid, and @asafshen agreed — this is now a major. 11b8ad5 adds a BREAKING CHANGE: footer; the PR title also needs feat(fetch)!: since the repo squash-merges (details in the thread above).

That closes the escape hatch you identified: on a major, a Node 16 consumer on ^2.x never receives this release at all. Reaching the TypeError now requires explicitly installing @latest on an EOL runtime past an EBADENGINE warning, so I did not add a runtime guard — it'd be code for a runtime we're deliberately dropping. Cheap to add later (three lines in fetch-polyfill.ts) if a real ticket shows up.

@shuni-bot

shuni-bot Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

🐕 Shuni hit a problem and couldn't finish.

Code changes detected in review-only mode. This violates strict read-only enforcement.

View workflow logs

Comment thread package.json
},
"engines": {
"node": ">= 16.0.0"
"node": ">= 18.0.0"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this implies we need to bump major
who uses node16/17?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — going major. Pushed 11b8ad5, a marker commit with a BREAKING CHANGE: footer.

One thing you need to do, though: this repo squash-merges (every commit on main ends in (#NNN)), so release-please reads the PR title, not the commit bodies. The title needs the !:

feat(fetch)!: use native global fetch instead of cross-fetch for workers

The pr-title-check job accepts ! fine. The marker commit is only a fallback for the case where the squash body keeps the default commit list — the title is the authoritative lever, and I can't set it from here.

Who's on node 16/17: Node 16 EOL'd Sept 2023, Node 17 June 2022. CI was still matrixing 16 (dropped in this PR), but that was the only thing keeping it alive. With a major, ^2.x consumers never auto-upgrade, so the failure mode @shuni-bot flagged in the sibling thread needs someone to explicitly npm i @descope/node-sdk@latest on Node 16 and click past an EBADENGINE warning — which is what engines + a major bump is for. I skipped adding a runtime guard for that; worth adding only if support actually sees such a ticket.

Comment thread package.json
},
"dependencies": {
"@descope/core-js-sdk": "^2.66.0",
"cross-fetch": "^4.0.0",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that cross-fetch should support CF worker - github.com/lquixada/cross-fetch/issues/69

is it not?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Investigated — issue #69 is right for wrangler's own bundler and wrong for everything else. cross-fetch's Worker-safety is a bundler-config coin flip, and it's the flip that lost here.

cross-fetch@4.1.0's package.json:

"main":    "dist/node-ponyfill.js",      // -> node-fetch -> require('http'/'https')
"browser": "dist/browser-ponyfill.js",   // -> safe
"react-native": "dist/react-native-ponyfill.js"

There is no exports map. That's the crux: without exports, no worker/workerd export condition can be honored, so the only thing that steers a bundler away from node-ponyfill is the legacy browser mainField. I ran the resolution matrix through esbuild against a real cross-fetch@4.1.0 install:

bundler config resolves to pulls http/https
platform: node (mainFields ['main','module']) node-ponyfillnode-fetch yes
platform: browser browser-ponyfill no
platform: neutral + conditions: [workerd, worker, browser] ERROR: Could not resolve "cross-fetch"
mainFields: ['browser','module','main'] browser-ponyfill no

Wrangler puts browser first in mainFields, so plain wrangler dev/deploy gets the good path — that's what #69 is reporting. But a toolchain that resolves with node mainFields and enables nodejs_compat (Nitro/Nuxt cloudflare preset, next-on-pages, and anything else driving unenv) gets node-ponyfill, and unenv then stubs https with a throwing mock. That is literally the reported error: [unenv] https.request is not implemented yet!. Note row 3 too — being explicit about worker conditions doesn't rescue it, it makes it worse.

And the kicker: even on the good path cross-fetch buys us nothing. browser-ponyfill.js ends with

var ctx = __global__.fetch ? __global__ : __globalThis__;
exports.fetch = ctx.fetch

i.e. when a global fetch exists (always, on any Worker) it hands back the native one — the same thing our 5-line file now does directly, minus ~600 lines of dead XHR-based whatwg-fetch shim (and XMLHttpRequest doesn't exist in Workers anyway, so that fallback could never have worked there). It also hands it back unbound, which is the illegal-invocation footgun our wrapper exists to avoid.

So: keep it removed. Depending on cross-fetch means depending on every downstream consumer's bundler being configured the way wrangler configures it.

Comment thread lib/fetch-polyfill.ts
// we want to make sure we are setting the "highWaterMark" so we are doing it on both args
args.forEach((arg) => {
// Updated to only apply highWaterMark to objects, as it can't be applied to strings (it breaks it)
if (arg && typeof arg === 'object') {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this removes some specific features we added to customize highWaterMarkMb , are we ok not supporting it anymore?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch to check, but it was already dead — this PR is deleting a corpse, not a feature.

Three things:

  1. highWaterMark is a node-fetch@2-only init option. Node's built-in fetch (undici) has no equivalent and silently ignores it. So it only ever did anything on the node-fetch code path.

  2. It has been unreachable on every supported runtime since v2.10.0. fix(fetch): prefer native fetch to avoid node-fetch premature close #742 / 5c85f1f (2026-06-18) changed the export to typeof globalThis.fetch === 'function' ? nativeWrapper : patchedFetch. On Node 18+ that's always the native wrapper, so patchedFetch — and with it the highWaterMark assignment — has only run on Node <18 for the last two releases. Nobody has been getting a 30MB buffer since June.

  3. It was never customizable. grep -rn highWaterMark across the repo returns zero hits outside the lines this PR deletes — no config option, no README mention, no test. It was a hardcoded 1024 * 1024 * 30 constant. The ??= meant a caller who happened to stuff highWaterMark onto a request init would have it preserved, but that was undocumented and untyped.

The bug it originally patched (#149, node-fetch hanging on large responses) is a node-fetch stream bug; undici doesn't have it, and if it did we'd have heard since v2.10.0. Nothing to port forward.

@omercnet

omercnet commented Aug 2, 2026

Copy link
Copy Markdown
Member Author

@shuni see review and investigate

@shuni-bot

shuni-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

🐕 Done! Updated PR: #777

View Shuni run

📋 Actions Taken

View AI response

Investigated all four threads and replied to each. One commit, 11b8ad5 — the only thing the review actually asked me to change.

Thread 3 — "cross-fetch should support CF worker, is it not?" (the interesting one)

cross-fetch@4.1.0 has no exports map, only main (→ node-ponyfill → node-fetch → http/https) and the legacy browser field. I ran the resolution matrix through esbuild against a real install:

config resolves to pulls http/https
platform: node node-ponyfillnode-fetch yes
platform: browser browser-ponyfill no
platform: neutral + conditions: [workerd, worker, browser] cannot resolve
mainFields: ['browser','module','main'] browser-ponyfill no

Wrangler puts browser first, so wrangler dev/deploy gets the good path — that's what issue #69 reports, and it's true. But anything resolving with node mainFields plus nodejs_compat (Nitro/Nuxt cloudflare preset, next-on-pages) gets node-ponyfill, and unenv stubs https with a throwing mock — exactly the reported [unenv] https.request is not implemented yet!. Row 3 is the tell: being explicit about worker conditions makes it worse, not better, because there's no exports map to honor them.

And on the good path cross-fetch just hands back globalThis.fetch anyway (var ctx = __global__.fetch ? __global__ : __globalThis__) — unbound, plus ~600 lines of dead XHR shim that couldn't run in a Worker regardless. It buys nothing there.

Thread 4 — highWaterMarkMb. Already dead code, not a feature being dropped: it's a node-fetch@2-only init option (undici ignores it), it's been unreachable on Node 18+ since #742/5c85f1f made native fetch preferred two releases ago, and grep -rn highWaterMark returns zero hits outside the deleted lines — hardcoded 30MB, no config option, no docs, no test.

Threads 1 + 2 — major bump. Agreed, added 11b8ad5 with a BREAKING CHANGE: footer. You need to retitle the PR to feat(fetch)!: ... — the repo squash-merges, so release-please reads the title, not commit bodies; the marker commit is only a fallback. I skipped the Node-16 runtime guard: on a major, ^2.x users never receive this, so hitting the TypeError requires installing @latest on an EOL runtime past an EBADENGINE warning.

Also: the PR body's caveat about npm install/npm run build being broken is stale — the TS v7 revert (3e46dd3) is merged in. Build, format, lint and all 453 tests green on real deps, nothing pinned locally.


View session on Shuni Portal 🐾

BREAKING CHANGE: the SDK now requires Node 18+ and uses the runtime native
fetch. Node 16 and 17 (EOL since Sept 2023 and June 2022) no longer get the
bundled cross-fetch/node-fetch fallback.

Marker commit so release-please cuts a major. Under squash-merge the PR title
is authoritative, so it also needs the `!`: `feat(fetch)!: ...`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants