Skip to content

Commit

Permalink
feat: add request and response interceptors
Browse files Browse the repository at this point in the history
  • Loading branch information
ddelgrosso1 committed May 2, 2024
1 parent 1d31567 commit c21f3b3
Show file tree
Hide file tree
Showing 5 changed files with 405 additions and 1 deletion.
67 changes: 66 additions & 1 deletion src/gaxios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
import {getRetryConfig} from './retry';
import {PassThrough, Stream, pipeline} from 'stream';
import {v4} from 'uuid';
import {GaxiosInterceptorManager} from './interceptor';

/* eslint-disable @typescript-eslint/no-explicit-any */

Expand Down Expand Up @@ -63,6 +64,11 @@ function getHeader(options: GaxiosOptions, header: string): string | undefined {
return undefined;
}

enum GaxiosInterceptorType {
Request = 1,
Response,
}

export class Gaxios {
protected agentCache = new Map<
string | URL,
Expand All @@ -74,12 +80,24 @@ export class Gaxios {
*/
defaults: GaxiosOptions;

/**
* Interceptors
*/
interceptors: {
request: GaxiosInterceptorManager<GaxiosOptions>;
response: GaxiosInterceptorManager<GaxiosResponse>;
};

/**
* The Gaxios class is responsible for making HTTP requests.
* @param defaults The default set of options to be used for this instance.
*/
constructor(defaults?: GaxiosOptions) {
this.defaults = defaults || {};
this.interceptors = {
request: new GaxiosInterceptorManager(),
response: new GaxiosInterceptorManager(),
};
}

/**
Expand All @@ -88,7 +106,11 @@ export class Gaxios {
*/
async request<T = any>(opts: GaxiosOptions = {}): GaxiosPromise<T> {
opts = await this.#prepareRequest(opts);
return this._request(opts);
opts = await this.#applyInterceptors(opts);
return this.#applyInterceptors(
this._request(opts),
GaxiosInterceptorType.Response
);
}

private async _defaultAdapter<T>(
Expand Down Expand Up @@ -230,6 +252,49 @@ export class Gaxios {
return true;
}

/**
* Applies the interceptors. The request interceptors are applied after the
* call to prepareRequest is completed. The response interceptors are applied after the call
* to translateResponse.
*
* @param {T} optionsOrResponse The current set of options or the translated response.
*
* @returns {Promise<T>} Promise that resolves to the set of options or response after interceptors are applied.
*/
async #applyInterceptors<
T extends
| GaxiosOptions
| GaxiosResponse
| Promise<GaxiosOptions | GaxiosResponse>,
>(
optionsOrResponse: T,
type: GaxiosInterceptorType = GaxiosInterceptorType.Request
): Promise<T> {
let promiseChain = Promise.resolve(optionsOrResponse) as Promise<T>;

if (type === GaxiosInterceptorType.Request) {
for (const interceptor of this.interceptors.request) {
if (interceptor) {
promiseChain = promiseChain.then(
interceptor.resolved as unknown as (opts: T) => Promise<T>,
interceptor.rejected
) as Promise<T>;
}
}
} else {
for (const interceptor of this.interceptors.response) {
if (interceptor) {
promiseChain = promiseChain.then(
interceptor.resolved as unknown as (resp: T) => Promise<T>,
interceptor.rejected
) as Promise<T>;
}
}
}

return promiseChain;
}

/**
* Validates the options, merges them with defaults, and prepare request.
*
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export {
RetryConfig,
} from './common';
export {Gaxios, GaxiosOptions};
export * from './interceptor';

/**
* The default instance used when the `request` method is directly
Expand Down
104 changes: 104 additions & 0 deletions src/interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Copyright 2024 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import {GaxiosError, GaxiosOptions, GaxiosResponse} from './common';

/**
* Interceptors that can be run for requests or responses. These interceptors run asynchronously.
*/
export interface GaxiosInterceptor<T extends GaxiosOptions | GaxiosResponse> {
/**
* Function to be run when applying an interceptor.
*
* @param {T} configOrResponse The current configuration or response.
* @returns {Promise<T>} Promise that resolves to the modified set of options or response.
*/
resolved?: (configOrResponse: T) => Promise<T>;
/**
* Function to be run if the previous call to resolved throws / rejects or the request results in an invalid status
* as determined by the call to validateStatus.
*
* @param {GaxiosError} err The error thrown from the previously called resolved function.
*/
rejected?: (err: GaxiosError) => void;
}

/**
* Class to manage collections of GaxiosInterceptors for both requests and responses.
*/
export class GaxiosInterceptorManager<T extends GaxiosOptions | GaxiosResponse>
implements
Iterator<GaxiosInterceptor<T> | null>,
Iterable<GaxiosInterceptor<T> | null>
{
#interceptorQueue: Array<GaxiosInterceptor<T> | null>;
#index: number;

constructor() {
this.#interceptorQueue = [];
this.#index = 0;
}

[Symbol.iterator](): Iterator<GaxiosInterceptor<T> | null> {
return this;
}

next(): IteratorResult<
GaxiosInterceptor<T> | null,
GaxiosInterceptor<T> | null
> {
const value =
this.#index < this.#interceptorQueue.length
? this.#interceptorQueue[this.#index]
: undefined;

return this.#index++ >= this.#interceptorQueue.length
? ({
done: true,
value,
} as IteratorReturnResult<GaxiosInterceptor<T> | null>)
: ({
done: false,
value,
} as IteratorYieldResult<GaxiosInterceptor<T> | null>);
}

/**
* Adds an interceptor to the queue.
*
* @param {GaxiosInterceptor} interceptor the interceptor to be added.
*
* @returns {number} an identifier that can be used to remove the interceptor.
*/
addInterceptor(interceptor: GaxiosInterceptor<T>): number {
return this.#interceptorQueue.push(interceptor) - 1;
}

/**
* Removes an interceptor from the queue.
*
* @param {number} id the previously id of the interceptor to remove.
*/
removeInterceptor(id: number) {
if (this.#interceptorQueue[id]) {
this.#interceptorQueue[id] = null;
}
}

/**
* Removes all interceptors from the queue.
*/
removeAll() {
this.#interceptorQueue = [];
}
}

0 comments on commit c21f3b3

Please sign in to comment.