diff --git a/README.md b/README.md index 9513e596..42914906 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,15 @@ import {request} from 'gaxios'; const res = await request({url: 'https://google.com/'}); ``` +## `fetch`-Compatible API Example + +We offer a drop-in `fetch`-compatible API as well. + +```js +import {instance} from 'gaxios'; +const res = await instance.fetch('https://google.com/'); +``` + ## Setting Defaults Gaxios supports setting default properties both on the default instance, and on additional instances. This is often useful when making many requests to the same domain with the same base settings. For example: diff --git a/src/common.ts b/src/common.ts index a0590a46..57aad5ff 100644 --- a/src/common.ts +++ b/src/common.ts @@ -277,10 +277,6 @@ export interface GaxiosOptions extends RequestInit { validateStatus?: (status: number) => boolean; retryConfig?: RetryConfig; retry?: boolean; - /** - * Enables aborting via {@link AbortController}. - */ - signal?: AbortSignal; /** * @deprecated non-spec. https://github.com/node-fetch/node-fetch/issues/1438 */ diff --git a/src/gaxios.ts b/src/gaxios.ts index 4ba7f800..51ef2f1a 100644 --- a/src/gaxios.ts +++ b/src/gaxios.ts @@ -30,12 +30,22 @@ import {getRetryConfig} from './retry.js'; import {Readable} from 'stream'; import {GaxiosInterceptorManager} from './interceptor.js'; -/* eslint-disable @typescript-eslint/no-explicit-any */ - const randomUUID = async () => globalThis.crypto?.randomUUID() || (await import('crypto')).randomUUID(); -export class Gaxios { +/** + * An interface for enforcing `fetch`-type compliance. + * + * @remarks + * + * This provides type guarantees during build-time, ensuring the `fetch` method is 1:1 + * compatible with the `fetch` API. + */ +interface FetchCompliance { + fetch: typeof fetch; +} + +export class Gaxios implements FetchCompliance { protected agentCache = new Map< string | URL, Agent | ((parsedUrl: URL) => Agent) @@ -66,6 +76,61 @@ export class Gaxios { }; } + /** + * A {@link fetch `fetch`} compliant API for {@link Gaxios}. + * + * @remarks + * + * This is useful as a drop-in replacement for `fetch` API usage. + * + * @example + * + * ```ts + * const gaxios = new Gaxios(); + * const myFetch: typeof fetch = (...args) => gaxios.fetch(...args); + * await myFetch('https://example.com'); + * ``` + * + * @param args `fetch` API or `Gaxios#request` parameters + * @returns the {@link Response} with Gaxios-added properties + */ + fetch( + ...args: Parameters | Parameters + ): GaxiosPromise { + // Up to 2 parameters in either overload + const input = args[0]; + const init = args[1]; + + let url: URL | undefined = undefined; + const headers = new Headers(); + + // prepare URL + if (typeof input === 'string') { + url = new URL(input); + } else if (input instanceof URL) { + url = input; + } else if (input && input.url) { + url = new URL(input.url); + } + + // prepare headers + if (input && typeof input === 'object' && 'headers' in input) { + this.#mergeHeaders(headers, input.headers); + } + if (init) { + this.#mergeHeaders(headers, new Headers(init.headers)); + } + + // prepare request + if (typeof input === 'object' && !(input instanceof URL)) { + // input must have been a non-URL object + return this.request({...init, ...input, headers, url}); + } else { + // input must have been a string or URL + return this.request({...init, headers, url}); + } + } + /** * Perform an HTTP request with the given options. * @param opts Set of HTTP options that will be used for this HTTP request. diff --git a/test/test.getch.ts b/test/test.getch.ts index 77387fdc..debef585 100644 --- a/test/test.getch.ts +++ b/test/test.getch.ts @@ -740,7 +740,7 @@ describe('🥁 configuration options', () => { }); describe('🎏 data handling', () => { - it('should accpet a ReadableStream as request data', async () => { + it('should accept a ReadableStream as request data', async () => { const scope = nock(url).post('/', 'test').reply(200, {}); const res = await request({ url, @@ -1454,3 +1454,60 @@ describe('interceptors', () => { }); }); }); + +/** + * Fetch-compliant API testing. + * + * Documentation: + * - https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API + * - https://nodejs.org/docs/latest/api/globals.html#fetch + */ +describe('fetch-compatible API', () => { + it('should accept a `string`', async () => { + const scope = nock(url).get('/').reply(200, {}); + + const gaxios = new Gaxios(); + const res = await gaxios.fetch(url); + + scope.done(); + assert(typeof url === 'string'); + assert.deepStrictEqual(res.data, {}); + }); + + it('should accept a `URL`', async () => { + const scope = nock(url).get('/').reply(200, {}); + + const gaxios = new Gaxios(); + const res = await gaxios.fetch(new URL(url)); + + scope.done(); + assert.deepStrictEqual(res.data, {}); + }); + + it('should accept an input with initialization', async () => { + const scope = nock(url).post('/', 'abc').reply(200, {}); + + const gaxios = new Gaxios(); + const res = await gaxios.fetch(url, { + body: Buffer.from('abc'), + method: 'POST', + }); + + scope.done(); + assert.deepStrictEqual(res.data, {}); + }); + + it('should accept `GaxiosOptions`', async () => { + const scope = nock(url).post('/', 'abc').reply(200, {}); + + const gaxios = new Gaxios(); + const options: GaxiosOptions = { + body: Buffer.from('abc'), + method: 'POST', + }; + const res = await gaxios.fetch(url, options); + + scope.done(); + assert.deepStrictEqual(res.data, {}); + }); +});