From 84faea8e65d0a80fdcc574390f27839d4be93133 Mon Sep 17 00:00:00 2001 From: Daniel Bankhead Date: Thu, 16 May 2024 13:02:51 -0700 Subject: [PATCH] feat: Introduce `GaxiosOptionsPrepared` for improved type guarantees --- src/common.ts | 14 ++++++++++---- src/gaxios.ts | 34 ++++++++++++++-------------------- src/index.ts | 1 + src/interceptor.ts | 8 +++++--- test/test.getch.ts | 36 ++++++++++++++++++++---------------- 5 files changed, 50 insertions(+), 43 deletions(-) diff --git a/src/common.ts b/src/common.ts index 5235b9ec..0eb2c4a9 100644 --- a/src/common.ts +++ b/src/common.ts @@ -128,7 +128,7 @@ export interface Headers { export type GaxiosPromise = Promise>; export interface GaxiosResponse extends Response { - config: GaxiosOptions; + config: GaxiosOptionsPrepared; data: T; } @@ -148,8 +148,8 @@ export interface GaxiosOptions extends Omit { * @deprecated Use {@link GaxiosOptions.fetchImplementation} instead. */ adapter?: ( - options: GaxiosOptions, - defaultAdapter: (options: GaxiosOptions) => GaxiosPromise + options: GaxiosOptionsPrepared, + defaultAdapter: (options: GaxiosOptionsPrepared) => GaxiosPromise ) => GaxiosPromise; url?: string | URL; /** @@ -330,13 +330,19 @@ export interface GaxiosOptions extends Omit { */ errorRedactor?: typeof defaultErrorRedactor | false; } + +export interface GaxiosOptionsPrepared extends GaxiosOptions { + headers: globalThis.Headers; + url: NonNullable; +} + /** * A partial object of `GaxiosOptions` with only redactable keys * * @experimental */ export type RedactableGaxiosOptions = Pick< - GaxiosOptions, + GaxiosOptions | GaxiosOptionsPrepared, 'body' | 'data' | 'headers' | 'url' >; /** diff --git a/src/gaxios.ts b/src/gaxios.ts index 8da69ddb..9e17a8dd 100644 --- a/src/gaxios.ts +++ b/src/gaxios.ts @@ -21,6 +21,7 @@ import { GaxiosMultipartOptions, GaxiosError, GaxiosOptions, + GaxiosOptionsPrepared, GaxiosPromise, GaxiosResponse, Headers, @@ -50,7 +51,7 @@ export class Gaxios { * Interceptors */ interceptors: { - request: GaxiosInterceptorManager; + request: GaxiosInterceptorManager; response: GaxiosInterceptorManager; }; @@ -77,7 +78,7 @@ export class Gaxios { } private async _defaultAdapter( - config: GaxiosOptions + config: GaxiosOptionsPrepared ): Promise> { const fetchImpl = config.fetchImplementation || @@ -89,7 +90,7 @@ export class Gaxios { const preparedOpts = {...config}; delete preparedOpts.data; - const res = (await fetchImpl(config.url!, preparedOpts as {})) as Response; + const res = (await fetchImpl(config.url, preparedOpts as {})) as Response; let data = await this.getResponseData(config, res); // `node-fetch`'s data isn't writable. Native `fetch`'s is. @@ -123,7 +124,7 @@ export class Gaxios { * @param opts Set of HTTP options that will be used for this HTTP request. */ protected async _request( - opts: GaxiosOptions = {} + opts: GaxiosOptionsPrepared ): GaxiosPromise { try { let translatedResponse: GaxiosResponse; @@ -175,7 +176,7 @@ export class Gaxios { } private async getResponseData( - opts: GaxiosOptions, + opts: GaxiosOptionsPrepared, res: Response ): Promise { if ( @@ -262,8 +263,8 @@ export class Gaxios { * @returns {Promise} Promise that resolves to the set of options or response after interceptors are applied. */ async #applyRequestInterceptors( - options: GaxiosOptions - ): Promise { + options: GaxiosOptionsPrepared + ): Promise { let promiseChain = Promise.resolve(options); for (const interceptor of this.interceptors.request.values()) { @@ -271,7 +272,7 @@ export class Gaxios { promiseChain = promiseChain.then( interceptor.resolved, interceptor.rejected - ) as Promise; + ) as Promise; } } @@ -309,7 +310,9 @@ export class Gaxios { * @param options The original options passed from the client. * @returns Prepared options, ready to make a request */ - async #prepareRequest(options: GaxiosOptions): Promise { + async #prepareRequest( + options: GaxiosOptions + ): Promise { const opts: GaxiosOptions = extend(true, {}, this.defaults, options); if (!opts.url) { throw new Error('URL is required.'); @@ -476,18 +479,9 @@ export class Gaxios { (opts as {duplex: string}).duplex = 'half'; } - // preserve the original type for auditing later - if (opts.headers instanceof Headers) { - opts.headers = preparedHeaders; - } else { - const headers: Headers = {}; - preparedHeaders.forEach((value, key) => { - headers[key] = value; - }); - opts.headers = headers; - } + opts.headers = preparedHeaders; - return opts; + return opts as GaxiosOptionsPrepared; } /** diff --git a/src/index.ts b/src/index.ts index a18ddef3..7ec77637 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,7 @@ export { GaxiosError, GaxiosPromise, GaxiosResponse, + GaxiosOptionsPrepared as PreparedGaxiosOptions, Headers, RetryConfig, } from './common'; diff --git a/src/interceptor.ts b/src/interceptor.ts index d52aacbb..9ccfad86 100644 --- a/src/interceptor.ts +++ b/src/interceptor.ts @@ -11,12 +11,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import {GaxiosError, GaxiosOptions, GaxiosResponse} from './common'; +import {GaxiosError, GaxiosOptionsPrepared, GaxiosResponse} from './common'; /** * Interceptors that can be run for requests or responses. These interceptors run asynchronously. */ -export interface GaxiosInterceptor { +export interface GaxiosInterceptor< + T extends GaxiosOptionsPrepared | GaxiosResponse, +> { /** * Function to be run when applying an interceptor. * @@ -37,5 +39,5 @@ export interface GaxiosInterceptor { * Class to manage collections of GaxiosInterceptors for both requests and responses. */ export class GaxiosInterceptorManager< - T extends GaxiosOptions | GaxiosResponse, + T extends GaxiosOptionsPrepared | GaxiosResponse, > extends Set | null> {} diff --git a/test/test.getch.ts b/test/test.getch.ts index 01e3b7fb..f9afcb38 100644 --- a/test/test.getch.ts +++ b/test/test.getch.ts @@ -25,7 +25,11 @@ import { GaxiosResponse, GaxiosPromise, } from '../src'; -import {GAXIOS_ERROR_SYMBOL, Headers} from '../src/common'; +import { + GAXIOS_ERROR_SYMBOL, + GaxiosOptionsPrepared, + Headers, +} from '../src/common'; import {pkg} from '../src/util'; import fs from 'fs'; @@ -155,8 +159,8 @@ describe('🥁 configuration options', () => { const inst = new Gaxios({headers: {apple: 'juice'}}); const res = await inst.request({url, headers: {figgy: 'pudding'}}); scope.done(); - assert.strictEqual(res.config.headers!.apple, 'juice'); - assert.strictEqual(res.config.headers!.figgy, 'pudding'); + assert.strictEqual(res.config.headers.get('apple'), 'juice'); + assert.strictEqual(res.config.headers.get('figgy'), 'pudding'); }); it('should allow setting a base url in the options', async () => { @@ -1125,7 +1129,7 @@ describe('🍂 defaults & instances', () => { } // eslint-disable-next-line @typescript-eslint/no-explicit-any protected async _request( - opts: GaxiosOptions = {} + opts: GaxiosOptionsPrepared ): GaxiosPromise { assert(opts.agent); return super._request(opts); @@ -1141,8 +1145,8 @@ describe('🍂 defaults & instances', () => { }); const res = await inst.request({url, headers: {figgy: 'pudding'}}); scope.done(); - assert.strictEqual(res.config.headers!.apple, 'juice'); - assert.strictEqual(res.config.headers!.figgy, 'pudding'); + assert.strictEqual(res.config.headers.get('apple'), 'juice'); + assert.strictEqual(res.config.headers.get('figgy'), 'pudding'); const agentCache = inst.getAgentCache(); assert(agentCache.get(key)); }); @@ -1173,7 +1177,7 @@ describe('interceptors', () => { const instance = new Gaxios(); instance.interceptors.request.add({ resolved: config => { - config.headers = {hello: 'world'}; + config.headers.set('hello', 'world'); return Promise.resolve(config); }, }); @@ -1190,7 +1194,7 @@ describe('interceptors', () => { validateStatus: () => { return true; }, - }) as unknown as Promise + }) as unknown as Promise ); const instance = new Gaxios(); const interceptor = {resolved: spyFunc}; @@ -1212,22 +1216,22 @@ describe('interceptors', () => { const instance = new Gaxios(); instance.interceptors.request.add({ resolved: config => { - config.headers!['foo'] = 'bar'; + config.headers.set('foo', 'bar'); return Promise.resolve(config); }, }); instance.interceptors.request.add({ resolved: config => { - assert.strictEqual(config.headers!['foo'], 'bar'); - config.headers!['bar'] = 'baz'; + assert.strictEqual(config.headers.get('foo'), 'bar'); + config.headers.set('bar', 'baz'); return Promise.resolve(config); }, }); instance.interceptors.request.add({ resolved: config => { - assert.strictEqual(config.headers!['foo'], 'bar'); - assert.strictEqual(config.headers!['bar'], 'baz'); - config.headers!['baz'] = 'buzz'; + assert.strictEqual(config.headers.get('foo'), 'bar'); + assert.strictEqual(config.headers.get('bar'), 'baz'); + config.headers.set('baz', 'buzz'); return Promise.resolve(config); }, }); @@ -1244,7 +1248,7 @@ describe('interceptors', () => { validateStatus: () => { return true; }, - }) as unknown as Promise + }) as unknown as Promise ); const instance = new Gaxios(); instance.interceptors.request.add({ @@ -1272,7 +1276,7 @@ describe('interceptors', () => { }); instance.interceptors.request.add({ resolved: config => { - config.headers = {hello: 'world'}; + config.headers.set('hello', 'world'); return Promise.resolve(config); }, rejected: err => {