diff --git a/src/common.ts b/src/common.ts index eae37f2..9db4988 100644 --- a/src/common.ts +++ b/src/common.ts @@ -77,7 +77,7 @@ export class GaxiosError extends Error { constructor( message: string, - public config: GaxiosOptions, + public config: GaxiosOptionsPrepared, public response?: GaxiosResponse, public error?: Error | NodeJS.ErrnoException ) { @@ -111,7 +111,7 @@ export class GaxiosError extends Error { } if (config.errorRedactor) { - config.errorRedactor({ + config.errorRedactor({ config: this.config, response: this.response, }); @@ -119,37 +119,35 @@ export class GaxiosError extends Error { } } -/** - * @deprecated use native {@link globalThis.Headers}. - */ -export interface Headers { - [index: string]: any; -} -export type GaxiosPromise = Promise>; +type GaxiosResponseData = + | ReturnType + | GaxiosOptionsPrepared['data']; + +export type GaxiosPromise = Promise>; -export interface GaxiosResponse extends Response { - config: GaxiosOptions; +export interface GaxiosResponse extends Response { + config: GaxiosOptionsPrepared; data: T; } export interface GaxiosMultipartOptions { - headers: Headers; + headers: HeadersInit; content: string | Readable; } /** * Request options that are used to form the request. */ -export interface GaxiosOptions extends Omit { +export interface GaxiosOptions extends RequestInit { /** * Optional method to override making the actual HTTP request. Useful * for writing tests. * * @deprecated Use {@link GaxiosOptions.fetchImplementation} instead. */ - adapter?: ( - options: GaxiosOptions, - defaultAdapter: (options: GaxiosOptions) => GaxiosPromise + adapter?: ( + options: GaxiosOptionsPrepared, + defaultAdapter: (options: GaxiosOptionsPrepared) => GaxiosPromise ) => GaxiosPromise; url?: string | URL; /** @@ -167,16 +165,6 @@ export interface GaxiosOptions extends Omit { | 'OPTIONS' | 'TRACE' | 'PATCH'; - /** - * Recommended: Provide a native {@link globalThis.Headers Headers} object. - * - * @privateRemarks - * - * This type does not have the native {@link globalThis.Headers Headers} in - * its signature as it would break customers looking to modify headers before - * providing to this library (new, unnecessary type checks/guards). - */ - headers?: Headers; /** * The data to send in the {@link RequestInit.body} of the request. Objects will be * serialized as JSON, except for: @@ -308,24 +296,11 @@ export interface GaxiosOptions extends Omit { */ errorRedactor?: typeof defaultErrorRedactor | false; } -/** - * A partial object of `GaxiosOptions` with only redactable keys - * - * @experimental - */ -export type RedactableGaxiosOptions = Pick< - GaxiosOptions, - 'body' | 'data' | 'headers' | 'url' ->; -/** - * A partial object of `GaxiosResponse` with only redactable keys - * - * @experimental - */ -export type RedactableGaxiosResponse = Pick< - GaxiosResponse, - 'config' | 'data' | 'headers' ->; + +export interface GaxiosOptionsPrepared extends GaxiosOptions { + headers: globalThis.Headers; + url: URL; +} /** * Configuration for the Gaxios `request` method. @@ -381,7 +356,10 @@ export interface RetryConfig { retryBackoff?: (err: GaxiosError, defaultBackoffMs: number) => Promise; } -function translateData(responseType: string | undefined, data: any) { +function translateData( + responseType: string | undefined, + data: GaxiosResponseData +) { switch (responseType) { case 'stream': return data; @@ -404,51 +382,30 @@ function translateData(responseType: string | undefined, data: any) { * * @experimental */ -export function defaultErrorRedactor(data: { - config?: RedactableGaxiosOptions; - response?: RedactableGaxiosResponse; -}) { +export function defaultErrorRedactor< + O extends GaxiosOptionsPrepared, + R extends GaxiosResponse, +>(data: {config?: O; response?: R}) { const REDACT = '< - See `errorRedactor` option in `gaxios` for configuration>.'; - function redactHeaders(headers?: Headers | globalThis.Headers) { + function redactHeaders(headers?: Headers) { if (!headers) return; - function check(key: string) { + headers.forEach((_, key) => { // any casing of `Authentication` // any casing of `Authorization` // anything containing secret, such as 'client secret' - return ( + if ( /^authentication$/i.test(key) || /^authorization$/i.test(key) || /secret/i.test(key) - ); - } - - function redactHeadersObject(headers: Headers) { - for (const key of Object.keys(headers)) { - if (check(key)) headers[key] = REDACT; - } - } - - function redactHeadersHeaders(headers: globalThis.Headers) { - headers.forEach((value, key) => { - if (check(key)) headers.set(key, REDACT); - }); - } - - // support `node-fetch` Headers and other third-parties - if (headers instanceof Headers || 'set' in headers) { - redactHeadersHeaders(headers as globalThis.Headers); - } else { - redactHeadersObject(headers); - } + ) + headers.set(key, REDACT); + }); } - function redactString( - obj: T, - key: keyof T - ) { + function redactString(obj: T, key: keyof T) { if ( typeof obj === 'object' && obj !== null && @@ -466,9 +423,7 @@ export function defaultErrorRedactor(data: { } } - function redactObject( - obj: T | null - ) { + function redactObject(obj: T | null) { if (!obj) { return; } else if ( @@ -507,7 +462,7 @@ export function defaultErrorRedactor(data: { redactObject(data.config.body); try { - const url = new URL('', data.config.url); + const url = data.config.url; if (url.searchParams.has('token')) { url.searchParams.set('token', REDACT); @@ -517,7 +472,7 @@ export function defaultErrorRedactor(data: { url.searchParams.set('client_secret', REDACT); } - data.config.url = url.toString(); + data.config.url = url; } catch { // ignore error - no need to parse an invalid URL } diff --git a/src/gaxios.ts b/src/gaxios.ts index 0e19c29..e857d1b 100644 --- a/src/gaxios.ts +++ b/src/gaxios.ts @@ -20,9 +20,9 @@ import { GaxiosMultipartOptions, GaxiosError, GaxiosOptions, + GaxiosOptionsPrepared, GaxiosPromise, GaxiosResponse, - Headers, defaultErrorRedactor, } from './common'; import {getRetryConfig} from './retry'; @@ -49,7 +49,7 @@ export class Gaxios { * Interceptors */ interceptors: { - request: GaxiosInterceptorManager; + request: GaxiosInterceptorManager; response: GaxiosInterceptorManager; }; @@ -76,11 +76,11 @@ export class Gaxios { } private async _defaultAdapter( - config: GaxiosOptions + config: GaxiosOptionsPrepared ): Promise> { const fetchImpl = config.fetchImplementation || fetch; - const res = await fetchImpl(config.url!, config); + const res = await fetchImpl(config.url, config); const data = await this.getResponseData(config, res); return Object.assign(res, {config, data}) as GaxiosResponse; @@ -91,7 +91,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; @@ -143,7 +143,7 @@ export class Gaxios { } private async getResponseData( - opts: GaxiosOptions, + opts: GaxiosOptionsPrepared, res: Response ): Promise { if ( @@ -177,7 +177,7 @@ export class Gaxios { #urlMayUseProxy( url: string | URL, - noProxy: GaxiosOptions['noProxy'] = [] + noProxy: GaxiosOptionsPrepared['noProxy'] = [] ): boolean { const candidate = new URL(url); const noProxyList = [...noProxy]; @@ -225,13 +225,13 @@ export class Gaxios { * Applies the request interceptors. The request interceptors are applied after the * call to prepareRequest is completed. * - * @param {GaxiosOptions} options The current set of options. + * @param {GaxiosOptionsPrepared} options The current set of options. * - * @returns {Promise} Promise that resolves to the set of options or response after interceptors are applied. + * @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()) { @@ -239,7 +239,7 @@ export class Gaxios { promiseChain = promiseChain.then( interceptor.resolved, interceptor.rejected - ) as Promise; + ) as Promise; } } @@ -250,9 +250,9 @@ export class Gaxios { * Applies the response interceptors. The response interceptors are applied after the * call to request is made. * - * @param {GaxiosOptions} options The current set of options. + * @param {GaxiosOptionsPrepared} options The current set of options. * - * @returns {Promise} Promise that resolves to the set of options or response after interceptors are applied. + * @returns {Promise} Promise that resolves to the set of options or response after interceptors are applied. */ async #applyResponseInterceptors( response: GaxiosResponse | Promise @@ -277,8 +277,10 @@ 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 { - const opts: GaxiosOptions = extend(true, {}, this.defaults, options); + async #prepareRequest( + options: GaxiosOptions + ): Promise { + const opts = extend(true, {}, this.defaults, options); if (!opts.url) { throw new Error('URL is required.'); } @@ -432,18 +434,10 @@ 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; - } - - return opts; + return Object.assign(opts, { + headers: preparedHeaders, + url: opts.url instanceof URL ? opts.url : new URL(opts.url), + }); } /** @@ -498,8 +492,12 @@ export class Gaxios { ) { const finale = `--${boundary}--`; for (const currentPart of multipartOptions) { + const headers = + currentPart.headers instanceof Headers + ? currentPart.headers + : new Headers(currentPart.headers); const partContentType = - currentPart.headers['Content-Type'] || 'application/octet-stream'; + headers.get('Content-Type') || 'application/octet-stream'; const preamble = `--${boundary}\r\nContent-Type: ${partContentType}\r\n\r\n`; yield preamble; if (typeof currentPart.content === 'string') { diff --git a/src/index.ts b/src/index.ts index a18ddef..c563eac 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,7 +18,7 @@ export { GaxiosError, GaxiosPromise, GaxiosResponse, - Headers, + GaxiosOptionsPrepared, RetryConfig, } from './common'; export {Gaxios, GaxiosOptions}; diff --git a/src/interceptor.ts b/src/interceptor.ts index d52aacb..9ccfad8 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 bdd0e53..0203204 100644 --- a/test/test.getch.ts +++ b/test/test.getch.ts @@ -25,7 +25,7 @@ import { GaxiosResponse, GaxiosPromise, } from '../src'; -import {GAXIOS_ERROR_SYMBOL, Headers} from '../src/common'; +import {GAXIOS_ERROR_SYMBOL, GaxiosOptionsPrepared} from '../src/common'; import {pkg} from '../src/util'; import fs from 'fs'; @@ -113,7 +113,11 @@ describe('🚙 error handling', () => { headers: {}, } as GaxiosResponse; - const error = new GaxiosError('translation test', {}, response); + const error = new GaxiosError( + 'translation test', + {} as GaxiosOptionsPrepared, + response + ); assert(error.response); assert.equal(error.response.data, notJSON); @@ -124,7 +128,7 @@ describe('🚙 error handling', () => { const wrongVersion = {[GAXIOS_ERROR_SYMBOL]: '0.0.0'}; const correctVersion = {[GAXIOS_ERROR_SYMBOL]: pkg.version}; - const child = new A('', {}); + const child = new A('', {} as GaxiosOptionsPrepared); assert.equal(wrongVersion instanceof GaxiosError, false); assert.equal(correctVersion instanceof GaxiosError, true); @@ -153,8 +157,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 () => { @@ -1093,7 +1097,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); @@ -1109,8 +1113,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)); }); @@ -1141,7 +1145,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); }, }); @@ -1158,7 +1162,7 @@ describe('interceptors', () => { validateStatus: () => { return true; }, - }) as unknown as Promise + }) as unknown as Promise ); const instance = new Gaxios(); const interceptor = {resolved: spyFunc}; @@ -1180,22 +1184,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); }, }); @@ -1212,7 +1216,7 @@ describe('interceptors', () => { validateStatus: () => { return true; }, - }) as unknown as Promise + }) as unknown as Promise ); const instance = new Gaxios(); instance.interceptors.request.add({ @@ -1240,7 +1244,7 @@ describe('interceptors', () => { }); instance.interceptors.request.add({ resolved: config => { - config.headers = {hello: 'world'}; + config.headers.set('hello', 'world'); return Promise.resolve(config); }, rejected: err => {