Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fetch-Compatible API #680

Merged
merged 1 commit into from
Jan 27, 2025
Merged
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
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 {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,11 +76,66 @@
};
}

/**
* 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.
*/
async request<T = any>(opts: GaxiosOptions = {}): GaxiosPromise<T> {

Check warning on line 138 in src/gaxios.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
let prepared = await this.#prepareRequest(opts);
prepared = await this.#applyRequestInterceptors(prepared);
return this.#applyResponseInterceptors(this._request(prepared));
Expand Down Expand Up @@ -112,7 +177,7 @@
* Internal, retryable version of the `request` method.
* @param opts Set of HTTP options that will be used for this HTTP request.
*/
protected async _request<T = any>(

Check warning on line 180 in src/gaxios.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
opts: GaxiosOptionsPrepared,
): GaxiosPromise<T> {
try {
Expand Down Expand Up @@ -171,7 +236,7 @@
private async getResponseData(
opts: GaxiosOptionsPrepared,
res: Response,
): Promise<any> {

Check warning on line 239 in src/gaxios.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
if (
opts.maxContentLength &&
res.headers.has('content-length') &&
Expand Down Expand Up @@ -512,7 +577,7 @@
*/
private async getResponseDataFromContentType(
response: Response,
): Promise<any> {

Check warning on line 580 in src/gaxios.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
let contentType = response.headers.get('Content-Type');
if (contentType === null) {
// Maintain existing functionality by calling text()
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, {});
});
});
Loading