From 8e91dcaa95cc042d876ca72c23e07a3bf0e81be5 Mon Sep 17 00:00:00 2001 From: Bruno Ferreira Date: Fri, 24 Apr 2026 16:18:27 +0100 Subject: [PATCH 1/2] fix(gaxios): prefer globalThis.fetch over node-fetch in Node.js 18+ environments 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 --- core/packages/gaxios/src/gaxios.ts | 32 +++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/core/packages/gaxios/src/gaxios.ts b/core/packages/gaxios/src/gaxios.ts index 2df23b4c07b6..49c4a3737bbe 100644 --- a/core/packages/gaxios/src/gaxios.ts +++ b/core/packages/gaxios/src/gaxios.ts @@ -628,14 +628,18 @@ export class Gaxios implements FetchCompliance { for (const currentPart of multipartOptions) { const partContentType = currentPart.headers.get('Content-Type') || 'application/octet-stream'; - const preamble = `--${boundary}\r\nContent-Type: ${partContentType}\r\n\r\n`; + const preamble = `--${boundary} +Content-Type: ${partContentType} + +`; yield preamble; if (typeof currentPart.content === 'string') { yield currentPart.content; } else { yield* currentPart.content; } - yield '\r\n'; + yield ' +'; } yield finale; } @@ -670,9 +674,27 @@ export class Gaxios implements FetchCompliance { static async #getFetch() { const hasWindow = typeof window !== 'undefined' && !!window; - this.#fetch ||= hasWindow - ? window.fetch - : (await import('node-fetch')).default; + if (!this.#fetch) { + if (hasWindow) { + this.#fetch = window.fetch; + } else if (typeof globalThis.fetch === 'function') { + // Prefer native fetch when available (Node.js 18+). + // + // Dynamically importing `node-fetch` can fail in newer Node.js + // runtimes (e.g. 24.15+ with undici >=7.24.4) because internal changes + // to the native fetch implementation cause `import("node-fetch").default` + // to return a non-callable value, resulting in: + // TypeError: fetchImpl is not a function + // + // Node.js 18+ ships a stable global `fetch` that is fully compatible + // with the Fetch API, so we prefer it and avoid the `node-fetch` + // import entirely when it is available. + 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; } From 56674744cce9799651bd4208c16220f78bc2c8bf Mon Sep 17 00:00:00 2001 From: Bruno Ferreira Date: Fri, 24 Apr 2026 16:28:03 +0100 Subject: [PATCH 2/2] fix(gaxios): restore \r\n escape sequences in multipart template literals 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 --- core/packages/gaxios/src/gaxios.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/core/packages/gaxios/src/gaxios.ts b/core/packages/gaxios/src/gaxios.ts index 49c4a3737bbe..36636144996f 100644 --- a/core/packages/gaxios/src/gaxios.ts +++ b/core/packages/gaxios/src/gaxios.ts @@ -628,18 +628,14 @@ export class Gaxios implements FetchCompliance { for (const currentPart of multipartOptions) { const partContentType = currentPart.headers.get('Content-Type') || 'application/octet-stream'; - const preamble = `--${boundary} -Content-Type: ${partContentType} - -`; + const preamble = `--${boundary}\r\nContent-Type: ${partContentType}\r\n\r\n`; yield preamble; if (typeof currentPart.content === 'string') { yield currentPart.content; } else { yield* currentPart.content; } - yield ' -'; + yield '\r\n'; } yield finale; }