Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions core/packages/gaxios/src/gaxios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ export class Gaxios implements FetchCompliance {
private async _defaultAdapter<T>(
config: GaxiosOptionsPrepared,
): Promise<GaxiosResponse<T>> {
const usingInternalFetch =
!config.fetchImplementation && !this.defaults.fetchImplementation;
const fetchImpl =
config.fetchImplementation ||
this.defaults.fetchImplementation ||
Expand All @@ -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
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -257,6 +263,7 @@ export class Gaxios implements FetchCompliance {
private async getResponseData(
opts: GaxiosOptionsPrepared,
res: Response,
usingInternalFetch = false,
): Promise<ReturnType<JSON['parse']>> {
if (res.status === HTTP_STATUS_NO_CONTENT) {
return '';
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
}
Expand Down
5 changes: 1 addition & 4 deletions core/packages/gaxios/test/test.getch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
);
});
});
Expand Down
Loading