Skip to content

fix(gaxios): prefer globalThis.fetch over node-fetch in Node.js 18+ environments#8107

Open
bmbferreira wants to merge 2 commits intogoogleapis:mainfrom
bmbferreira:fix/gaxios-prefer-native-fetch-node-24
Open

fix(gaxios): prefer globalThis.fetch over node-fetch in Node.js 18+ environments#8107
bmbferreira wants to merge 2 commits intogoogleapis:mainfrom
bmbferreira:fix/gaxios-prefer-native-fetch-node-24

Conversation

@bmbferreira
Copy link
Copy Markdown

@bmbferreira bmbferreira commented Apr 24, 2026

Problem

#getFetch() in gaxios has a latent bug: it checks typeof window !== 'undefined' to detect browser environments, then uses window.fetch as its HTTP implementation. If any code in the process sets globalThis.window to an object that does not implement fetch (e.g. a jsdom window for server-side HTML rendering), gaxios finds window truthy but window.fetch === undefined, and every request fails with:

TypeError: fetchImpl is not a function
    at Gaxios._request (gaxios.ts:227:15)
    at async getToken (.../google-auth-library/build/src/gtoken/getToken.js:52:26)
    at async JWT.refreshTokenNoCache (...)
    at async GoogleAuth.authorizeRequest (...)

Real-world trigger

In our codebase, packages/theydo-editor-schema/src/dom-setup.ts (a server-side Tiptap helper) sets globalThis.window = jsdomInstance.window so that window.DOMParser is available. jsdom does not implement fetch, so window.fetch === undefined. This caused all @google-cloud/bigquery auth requests to fail with the above error — 185 production errors over April 21–24, 2026.

The root cause is the gaxios hasWindow heuristic: any library that sets globalThis.window for 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:

  • jsdom is commonly used in server-side rendering, testing, and HTML parsing — none of these set window.fetch
  • happy-dom similarly lacks fetch by default
  • Any globalThis.window = someObject assignment without explicit fetch will silently break Google Cloud auth for the entire process

Fix

Prefer globalThis.fetch directly in Node.js environments rather than going through window.fetch. globalThis.fetch is stable since Node.js 18 and is not polluted by jsdom or other DOM shims.

static async #getFetch() {
  const hasWindow = typeof window !== 'undefined' && !!window;

  if (!this.#fetch) {
    if (hasWindow) {
      this.#fetch = window.fetch;
    } else if (typeof globalThis.fetch === 'function') {
      // Prefer native fetch when available (Node.js 18+).
      //
      // Checking hasWindow is not sufficient — any code that sets
      // globalThis.window (e.g. jsdom for server-side DOM polyfilling)
      // without implementing fetch will cause window.fetch === undefined,
      // making fetchImpl non-callable. globalThis.fetch is not affected
      // by DOM shims and is always the native Node.js implementation.
      this.#fetch = globalThis.fetch.bind(globalThis);
    } else {
      // Fallback: older Node.js versions without a global fetch.
      this.#fetch = (await import('node-fetch')).default;
    }
  }

  return this.#fetch;
}

Note: we worked around the issue in our own code by assigning dom.window.fetch = globalThis.fetch after setting up jsdom, but the root fragility remains in gaxios — any downstream user of @google-cloud/* libraries that sets globalThis.window without fetch will hit the same silent breakage.

Related

Closes #7602

…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
@bmbferreira bmbferreira requested a review from a team as a code owner April 24, 2026 15:18
@google-cla
Copy link
Copy Markdown

google-cla Bot commented Apr 24, 2026

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.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment thread core/packages/gaxios/src/gaxios.ts Outdated
Comment thread core/packages/gaxios/src/gaxios.ts Outdated
…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
Copy link
Copy Markdown
Author

@bmbferreira bmbferreira left a comment

Choose a reason for hiding this comment

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

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.

@bmbferreira
Copy link
Copy Markdown
Author

Also: the CI matrix doesn't cover Node.js 24

This 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 .github/workflows/presubmit.yaml? Happy to open that separately.

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.

We should be supporting node 24 tests

1 participant