fix(gaxios): prefer globalThis.fetch over node-fetch in Node.js 18+ environments#8107
fix(gaxios): prefer globalThis.fetch over node-fetch in Node.js 18+ environments#8107bmbferreira wants to merge 2 commits intogoogleapis:mainfrom
Conversation
…nvironments
In Node.js 24.15 (ships with undici 7.24.4), dynamically importing
node-fetch v3 via import('node-fetch').default no longer reliably
returns a callable function, causing all requests to fail with:
TypeError: fetchImpl is not a function
at Gaxios._request (gaxios.ts:227:15)
This regression breaks all Google Cloud Node.js clients using gaxios
for authentication (e.g. @google-cloud/bigquery via google-auth-library).
The JWT token fetch fails before any request reaches Google APIs.
Node.js 18+ ships a stable global fetch API (globalThis.fetch).
We now prefer it over the dynamic node-fetch import in non-browser
environments, falling back to node-fetch only on older Node.js versions.
Fixes #ISSUE
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request updates the fetch implementation to prefer native globalThis.fetch in Node.js 18+ environments, addressing potential node-fetch compatibility issues in newer runtimes. It also modifies multipart form-data generation by replacing explicit CRLF characters with literal newlines. Feedback indicates that this change is a regression, as RFC 7578 requires CRLF (\r\n) and template literals normalize newlines to LF, which could lead to compatibility issues with strict servers.
…rals The previous commit inadvertently embedded literal CRLF bytes inside template literals instead of using \r\n escape sequences. While both produce the same runtime output, literal embedded bytes are susceptible to git line-ending normalization and are stylistically incorrect. Restores the original \r\n escape sequence style in: - const preamble template literal (RFC 7578 multipart boundary) - yield '\r\n' terminator
bmbferreira
left a comment
There was a problem hiding this comment.
Thank you for catching those! The embedded literal CRLF bytes in the template literals were an unintended artifact of the API-based commit process (base64 round-trip converted \r\n escape sequences to raw bytes). Fixed in 5667474 — the multipart preamble and yield '\r\n' now use proper escape sequences again, restoring RFC 7578 compliance. The only intended change in this PR remains the #getFetch() method.
Also: the CI matrix doesn't cover Node.js 24This bug went undetected because the presubmit workflow only tests against Node 18, 20, and 22: matrix:
node-version: [18, 20, 22]Node.js 24 (currently LTS-eligible and shipping with undici 7.24.4) is not included. Adding it to the matrix would have caught this regression automatically. Would you be open to a follow-up PR that adds Node 24 to |
Problem
#getFetch()in gaxios has a latent bug: it checkstypeof window !== 'undefined'to detect browser environments, then useswindow.fetchas its HTTP implementation. If any code in the process setsglobalThis.windowto an object that does not implementfetch(e.g. a jsdom window for server-side HTML rendering), gaxios findswindowtruthy butwindow.fetch === undefined, and every request fails with:Real-world trigger
In our codebase,
packages/theydo-editor-schema/src/dom-setup.ts(a server-side Tiptap helper) setsglobalThis.window = jsdomInstance.windowso thatwindow.DOMParseris available. jsdom does not implementfetch, sowindow.fetch === undefined. This caused all@google-cloud/bigqueryauth requests to fail with the above error — 185 production errors over April 21–24, 2026.The root cause is the gaxios
hasWindowheuristic: any library that setsglobalThis.windowfor DOM-polyfill purposes will inadvertently break all Google Cloud auth in the same process.Why this matters beyond our case
This trap is easy to fall into:
window.fetchfetchby defaultglobalThis.window = someObjectassignment without explicitfetchwill silently break Google Cloud auth for the entire processFix
Prefer
globalThis.fetchdirectly in Node.js environments rather than going throughwindow.fetch.globalThis.fetchis stable since Node.js 18 and is not polluted by jsdom or other DOM shims.Note: we worked around the issue in our own code by assigning
dom.window.fetch = globalThis.fetchafter setting up jsdom, but the root fragility remains in gaxios — any downstream user of@google-cloud/*libraries that setsglobalThis.windowwithoutfetchwill hit the same silent breakage.Related
Closes #7602