Skip to content

Commit

Permalink
feat: fetch-Compatible API
Browse files Browse the repository at this point in the history
  • Loading branch information
d-goog committed Jan 25, 2025
1 parent 3a70575 commit 9c86666
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 8 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 0 additions & 4 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
71 changes: 68 additions & 3 deletions src/gaxios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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<T = unknown>(
...args: Parameters<typeof fetch> | Parameters<Gaxios['request']>
): GaxiosPromise<T> {
// 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.
Expand Down
59 changes: 58 additions & 1 deletion test/test.getch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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, {});
});
});

0 comments on commit 9c86666

Please sign in to comment.