diff --git a/core/packages/gaxios/src/gaxios.ts b/core/packages/gaxios/src/gaxios.ts index c2854de06661..6e9c7d004411 100644 --- a/core/packages/gaxios/src/gaxios.ts +++ b/core/packages/gaxios/src/gaxios.ts @@ -147,6 +147,8 @@ export class Gaxios implements FetchCompliance { private async _defaultAdapter( config: GaxiosOptionsPrepared, ): Promise> { + const usingInternalFetch = + !config.fetchImplementation && !this.defaults.fetchImplementation; const fetchImpl = config.fetchImplementation || this.defaults.fetchImplementation || @@ -158,7 +160,7 @@ export class Gaxios implements FetchCompliance { delete preparedOpts.data; const res = (await fetchImpl(config.url, preparedOpts as {})) as Response; - const data = await this.getResponseData(config, res); + const data = await this.getResponseData(config, res, usingInternalFetch); if (!Object.getOwnPropertyDescriptor(res, 'data')?.configurable) { // Work-around for `node-fetch` v3 as accessing `data` would otherwise throw @@ -227,6 +229,10 @@ export class Gaxios implements FetchCompliance { err = e; } else if (e instanceof Error) { err = new GaxiosError(e.message, opts, undefined, e); + } else if (typeof e === 'string') { + // Native `fetch` rejects with the `AbortSignal`'s reason as-is, which + // is not necessarily an `Error`. + err = new GaxiosError(e, opts, undefined, e); } else { err = new GaxiosError('Unexpected Gaxios Error', opts, undefined, e); } @@ -257,6 +263,7 @@ export class Gaxios implements FetchCompliance { private async getResponseData( opts: GaxiosOptionsPrepared, res: Response, + usingInternalFetch = false, ): Promise> { if (res.status === HTTP_STATUS_NO_CONTENT) { return ''; @@ -276,8 +283,29 @@ export class Gaxios implements FetchCompliance { } switch (opts.responseType) { - case 'stream': + case 'stream': { + // Native `fetch` resolves a `ReadableStream`, so convert it to retain + // the `stream.Readable` contract. Browser bundles stub or polyfill + // `stream` without `fromWeb` and have always resolved a + // `ReadableStream`, so the conversion is skipped there. A + // caller-provided `fetchImplementation` keeps its own body type. + const body = res.body as + | (ReadableStream & {getReader?: unknown}) + | null; + const isWebStream = typeof body?.getReader === 'function'; + + if ( + usingInternalFetch && + isWebStream && + typeof Readable?.fromWeb === 'function' + ) { + return Readable.fromWeb( + body as unknown as import('stream/web').ReadableStream, + ); + } + return res.body; + } case 'json': { const data = await res.text(); try { @@ -671,10 +699,16 @@ export class Gaxios implements FetchCompliance { static async #getFetch() { const hasWindow = typeof window !== 'undefined' && !!window; + const hasGlobalFetch = typeof globalThis.fetch === 'function'; + // Prefer native `fetch`, available since Node 18 - this package's minimum + // supported version. `node-fetch` can intermittently fail requests with + // `Premature close` errors and remains only as a fallback. this.#fetch ||= hasWindow ? window.fetch - : (await import('node-fetch')).default; + : hasGlobalFetch + ? globalThis.fetch + : (await import('node-fetch')).default; return this.#fetch; } diff --git a/core/packages/gaxios/test/test.getch.ts b/core/packages/gaxios/test/test.getch.ts index 62072b7897c8..bcd60b59e7e6 100644 --- a/core/packages/gaxios/test/test.getch.ts +++ b/core/packages/gaxios/test/test.getch.ts @@ -863,10 +863,7 @@ describe('🥁 configuration options', () => { await assert.rejects( () => gaxios.request({url, timeout, signal}), - // `node-fetch` always rejects with the generic 'abort' error: - /abort/, - // native `fetch` matches the error properly: - // new RegExp(message) + new RegExp(message), ); }); });