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: Support HeadersInit for GaxiosRequest#headers #686

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
18 changes: 0 additions & 18 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,24 +170,6 @@ export interface GaxiosOptions extends RequestInit {
defaultAdapter: (options: GaxiosOptionsPrepared) => GaxiosPromise<T>,
) => GaxiosPromise<T>;
url?: string | URL;
/**
* Headers to add to the request.
*
* @remarks
*
* Using the proper Headers type has the following benefits:
* - creates consistency throughout the libraries; no need to check properties for different casing
* - see {@link https://github.com/googleapis/gaxios/issues/262 #262}
* - Alignment with {@link https://developer.mozilla.org/en-US/docs/Web/API/Request/headers `Request#headers`}
* - Can easily append an existing header or create it ('upsert') if it does exist, like so:
* ```ts
* const headers = new Headers();
*
* // creates, if not exist, or appends to an existing header
* headers.append('x-goog-api-client', 'abc');
* ```
*/
headers?: Headers;
baseURL?: string | URL;
/**
* The data to send in the {@link RequestInit.body} of the request. Objects will be
Expand Down
56 changes: 36 additions & 20 deletions src/gaxios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,10 @@

// prepare headers
if (input && typeof input === 'object' && 'headers' in input) {
this.#mergeHeaders(headers, input.headers);
Gaxios.mergeHeaders(headers, input.headers);
}
if (init) {
this.#mergeHeaders(headers, new Headers(init.headers));
Gaxios.mergeHeaders(headers, new Headers(init.headers));
}

// prepare request
Expand All @@ -135,7 +135,7 @@
* 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 @@ -177,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 @@ -236,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 @@ -362,23 +362,6 @@
return promiseChain;
}

/**
* Merges headers.
*
* @param base headers to append/overwrite to
* @param append headers to append/overwrite with
* @returns the base headers instance with merged `Headers`
*/
#mergeHeaders(base: Headers, append?: Headers) {
append?.forEach((value, key) => {
// set-cookie is the only header that would repeat.
// A bit of background: https://developer.mozilla.org/en-US/docs/Web/API/Headers/getSetCookie
key === 'set-cookie' ? base.append(key, value) : base.set(key, value);
});

return base;
}

/**
* Validates the options, merges them with defaults, and prepare request.
*
Expand All @@ -390,7 +373,7 @@
): Promise<GaxiosOptionsPrepared> {
// Prepare Headers - copy in order to not mutate the original objects
const preparedHeaders = new Headers(this.defaults.headers);
this.#mergeHeaders(preparedHeaders, options.headers);
Gaxios.mergeHeaders(preparedHeaders, options.headers);

// Merge options
const opts = extend(true, {}, this.defaults, options);
Expand Down Expand Up @@ -577,7 +560,7 @@
*/
private async getResponseDataFromContentType(
response: Response,
): Promise<any> {

Check warning on line 563 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 Expand Up @@ -664,4 +647,37 @@

return this.#fetch;
}

/**
* Merges headers.
* If the base headers do not exist a new `Headers` object will be returned.
*
* @remarks
*
* Using this utility can be helpful when the headers are not known to exist:
* - if they exist as `Headers`, that instance will be used
* - if they exist in another form, they will be used as a new Headers object
* - if they do not exist
*
* @param base headers to append/overwrite to
* @param append headers to append/overwrite with
* @returns the base headers instance with merged `Headers`
*/
static mergeHeaders(base?: HeadersInit, ...append: HeadersInit[]): Headers {
base = base instanceof Headers ? base : new Headers(base);

for (const headers of append) {
const add = headers instanceof Headers ? headers : new Headers(headers);

add.forEach((value, key) => {
// set-cookie is the only header that would repeat.
// A bit of background: https://developer.mozilla.org/en-US/docs/Web/API/Headers/getSetCookie
key === 'set-cookie' ? base.append(key, value) : base.set(key, value);
});
}

return base;
}
}

type HeadersInit = ConstructorParameters<typeof Headers>[0];
49 changes: 48 additions & 1 deletion test/test.getch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ describe('🥁 configuration options', () => {
const inst = new Gaxios({headers: new Headers({apple: 'juice'})});
const res = await inst.request({
url,
headers: new Headers({figgy: 'pudding'}),
headers: {figgy: 'pudding'},
});
scope.done();
assert.strictEqual(res.config.headers.get('apple'), 'juice');
Expand Down Expand Up @@ -1511,3 +1511,50 @@ describe('fetch-compatible API', () => {
assert.deepStrictEqual(res.data, {});
});
});

describe('merge headers', () => {
it('should merge Headers', () => {
const base = {a: 'a'};
const append = {b: 'b'};
const expected = new Headers({...base, ...append});

const matrixBase = [{...base}, Object.entries(base), new Headers(base)];
const matrixAppend = [
{...append},
Object.entries(append),
new Headers(append),
];

for (const base of matrixBase) {
for (const append of matrixAppend) {
const headers = Gaxios.mergeHeaders(base, append);

assert.deepStrictEqual(headers, expected);
}
}
});

it('should merge multiple Headers', () => {
const base = {a: 'a'};
const append = {b: 'b'};
const appendMore = {c: 'c'};
const expected = new Headers({...base, ...append, ...appendMore});

const headers = Gaxios.mergeHeaders(base, append, appendMore);

assert.deepStrictEqual(headers, expected);
});

it('should merge Set-Cookie Headers', () => {
const base = {'set-cookie': 'a=a'};
const append = {'set-cookie': 'b=b'};
const expected = new Headers([
['set-cookie', 'a=a'],
['set-cookie', 'b=b'],
]);

const headers = Gaxios.mergeHeaders(base, append);

assert.deepStrictEqual(headers.getSetCookie(), expected.getSetCookie());
});
});