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!: Headers should be Headers #653

Merged
merged 26 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
58c7dc2
feat!: Upgrade to `node-fetch` v3
d-goog Apr 16, 2024
f8570d9
Merge branch 'main' into upgrade-node-fetch
danielbankhead Apr 16, 2024
7fd2a95
refactor: Use native `FormData` in testing
d-goog Apr 17, 2024
0cc7856
Merge branch 'upgrade-node-fetch' of github.com:googleapis/gaxios int…
d-goog Apr 17, 2024
a61cfa5
feat!: Improve spec compliance
d-goog Apr 19, 2024
35a73b1
test(temp): Run 18+
d-goog Apr 19, 2024
e7addeb
🦉 Updates from OwlBot post-processor
gcf-owl-bot[bot] Apr 19, 2024
9816229
feat: Improve types, streamline, and standardize
d-goog May 14, 2024
93bf4a8
Merge branch 'main' of github.com:googleapis/gaxios into upgrade-node…
d-goog May 15, 2024
9164b87
test: Adopt `Headers` type from merge
d-goog May 15, 2024
84faea8
feat: Introduce `GaxiosOptionsPrepared` for improved type guarantees
d-goog May 16, 2024
4e6de5f
feat: Ensure `GaxiosOptionsPrepared.url` is always a `URL`
d-goog May 16, 2024
e6c2371
refactor: Clean-up Types & Resolve lint warnings
d-goog May 16, 2024
98f7009
refactor: streamline `.data` for `Response`
d-goog May 20, 2024
4894e39
docs: Simplify example
d-goog May 22, 2024
a7e2e83
refactor: streamline, no error case
d-goog May 22, 2024
8413f68
feat: Basic `GET` Support for `Request` objects
d-goog Sep 11, 2024
04e264d
test: remove `.only`
d-goog Sep 11, 2024
d163a90
refactor: simplify `httpMethodsToRetry`
d-goog Sep 11, 2024
705fad3
chore: update `nock`
d-goog Sep 11, 2024
9e52755
test: cleanup
d-goog Sep 11, 2024
33dccb7
feat!: Headers should be `Headers`
d-goog Oct 9, 2024
74d6d23
Merge branch 'main' of github.com:googleapis/gaxios into headers-as-h…
d-goog Oct 25, 2024
a38bcca
fix: Multipart Headers
d-goog Oct 25, 2024
192baf0
refactor: clean-up and optimize
d-goog Oct 25, 2024
500d713
Merge branch 'main' into headers-as-headers
danielbankhead Oct 29, 2024
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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ Gaxios supports setting default properties both on the default instance, and on
const gaxios = require('gaxios');
gaxios.instance.defaults = {
baseURL: 'https://example.com'
headers: {
headers: new Headers({
Authorization: 'SOME_TOKEN'
}
})
}
gaxios.request({url: '/data'}).then(...);
```
Expand All @@ -51,7 +51,7 @@ interface GaxiosOptions = {
baseURL: 'https://example.com';

// The HTTP methods to be sent with the request.
headers: { 'some': 'header' } || new Headers(),
headers: new Headers(),

// The data to send in the body of the request. Objects will be serialized as JSON
// except for:
Expand Down
4 changes: 2 additions & 2 deletions browser-test/test.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ describe('💻 browser tests', () => {
});

it('should support multipart post from the browser', async () => {
const headers: {[key: string]: string} = {};
const headers = new Headers();
const multipart = [
{
'Content-Type': 'application/json',
Expand All @@ -55,7 +55,7 @@ describe('💻 browser tests', () => {
const boundary =
globalThis?.crypto.randomUUID() || (await import('crypto')).randomUUID();
const finale = `--${boundary}--`;
headers['Content-Type'] = `multipart/related; boundary=${boundary}`;
headers.set('Content-Type', `multipart/related; boundary=${boundary}`);

let content = '';
for (const part of multipart) {
Expand Down
25 changes: 20 additions & 5 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,6 @@ import {Readable} from 'stream';
type _BodyInit = typeof globalThis extends {BodyInit: infer T}
? T
: import('undici-types').BodyInit;
type _HeadersInit = typeof globalThis extends {HeadersInit: infer T}
? T
: import('undici-types').HeadersInit;

/**
* Support `instanceof` operator for `GaxiosError`s in different versions of this library.
Expand Down Expand Up @@ -145,7 +142,7 @@ export interface GaxiosResponse<T = GaxiosResponseData> extends Response {
}

export interface GaxiosMultipartOptions {
headers: _HeadersInit;
headers: Headers;
content: string | Readable;
}

Expand All @@ -164,6 +161,24 @@ 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;
/**
* @deprecated
*/
Expand Down Expand Up @@ -323,7 +338,7 @@ export interface GaxiosOptions extends RequestInit {
}

export interface GaxiosOptionsPrepared extends GaxiosOptions {
headers: globalThis.Headers;
headers: Headers;
url: URL;
}

Expand Down
34 changes: 24 additions & 10 deletions src/gaxios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,23 @@ export class Gaxios {
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 @@ -302,7 +319,13 @@ export class Gaxios {
async #prepareRequest(
options: GaxiosOptions,
): 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);

// Merge options
const opts = extend(true, {}, this.defaults, options);

if (!opts.url) {
throw new Error('URL is required.');
}
Expand Down Expand Up @@ -344,11 +367,6 @@ export class Gaxios {
opts.follow = options.maxRedirects;
}

const preparedHeaders =
opts.headers instanceof Headers
? opts.headers
: new Headers(opts.headers);

const shouldDirectlyPassData =
typeof opts.data === 'string' ||
opts.data instanceof ArrayBuffer ||
Expand Down Expand Up @@ -526,12 +544,8 @@ export class Gaxios {
) {
const finale = `--${boundary}--`;
for (const currentPart of multipartOptions) {
const headers =
currentPart.headers instanceof Headers
? currentPart.headers
: new Headers(currentPart.headers as HeadersInit);
const partContentType =
headers.get('Content-Type') || 'application/octet-stream';
currentPart.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') {
Expand Down
46 changes: 27 additions & 19 deletions test/test.getch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('🚙 error handling', () => {
data: notJSON,
status: 500,
statusText: '',
headers: {},
headers: new Headers(),
// workaround for `node-fetch`'s `.data` deprecation...
bodyUsed: true,
} as GaxiosResponse;
Expand Down Expand Up @@ -163,8 +163,11 @@ describe('🥁 configuration options', () => {

it('should handle nested options passed into the constructor', async () => {
const scope = nock(url).get('/').reply(200);
const inst = new Gaxios({headers: {apple: 'juice'}});
const res = await inst.request({url, headers: {figgy: 'pudding'}});
const inst = new Gaxios({headers: new Headers({apple: 'juice'})});
const res = await inst.request({
url,
headers: new Headers({figgy: 'pudding'}),
});
scope.done();
assert.strictEqual(res.config.headers.get('apple'), 'juice');
assert.strictEqual(res.config.headers.get('figgy'), 'pudding');
Expand Down Expand Up @@ -714,7 +717,9 @@ describe('🎏 data handling', () => {
url,
method: 'POST',
data: encoded,
headers: {'content-type': 'application/x-www-form-urlencoded'},
headers: new Headers({
'content-type': 'application/x-www-form-urlencoded',
}),
});
scope.done();
assert.deepStrictEqual(res.data, {});
Expand Down Expand Up @@ -745,9 +750,9 @@ describe('🎏 data handling', () => {
url,
method: 'POST',
data: body,
headers: {
headers: new Headers({
'Content-Type': 'application/json-patch+json',
},
}),
});
scope.done();
assert.deepStrictEqual(res.data, {});
Expand All @@ -763,9 +768,9 @@ describe('🎏 data handling', () => {
url,
method: 'POST',
data: body,
headers: {
headers: new Headers({
'Content-Type': 'application/x-www-form-urlencoded',
},
}),
});
scope.done();
assert.deepStrictEqual(res.data, {});
Expand Down Expand Up @@ -909,7 +914,7 @@ describe('🎏 data handling', () => {
method: 'POST',
multipart: [
{
headers: {'Content-Type': 'application/json'},
headers: new Headers({'Content-Type': 'application/json'}),
content: body,
},
],
Expand Down Expand Up @@ -939,11 +944,11 @@ describe('🎏 data handling', () => {
method: 'POST',
multipart: [
{
headers: {'Content-Type': 'application/json'},
headers: new Headers({'Content-Type': 'application/json'}),
content: body,
},
{
headers: {'Content-Type': 'text/plain'},
headers: new Headers({'Content-Type': 'text/plain'}),
content: textContent,
},
],
Expand Down Expand Up @@ -1106,7 +1111,7 @@ describe('🍂 defaults & instances', () => {
url,
method: 'POST',
data: pkg,
headers: {'content-type': 'application/dicom'},
headers: new Headers({'content-type': 'application/dicom'}),
});
scope.done();
assert.deepStrictEqual(res.data, {});
Expand Down Expand Up @@ -1145,11 +1150,14 @@ describe('🍂 defaults & instances', () => {
const key = fs.readFileSync('./test/fixtures/fake.key', 'utf8');
const scope = nock(url).get('/').reply(200);
const inst = new GaxiosAssertAgentCache({
headers: {apple: 'juice'},
headers: new Headers({apple: 'juice'}),
cert: fs.readFileSync('./test/fixtures/fake.cert', 'utf8'),
key,
});
const res = await inst.request({url, headers: {figgy: 'pudding'}});
const res = await inst.request({
url,
headers: new Headers({figgy: 'pudding'}),
});
scope.done();
assert.strictEqual(res.config.headers.get('apple'), 'juice');
assert.strictEqual(res.config.headers.get('figgy'), 'pudding');
Expand All @@ -1160,12 +1168,12 @@ describe('🍂 defaults & instances', () => {
const key = fs.readFileSync('./test/fixtures/fake.key', 'utf8');
const scope = nock(url).get('/').reply(200).get('/').reply(200);
const inst = new GaxiosAssertAgentCache({
headers: {apple: 'juice'},
headers: new Headers({apple: 'juice'}),
cert: fs.readFileSync('./test/fixtures/fake.cert', 'utf8'),
key,
});
await inst.request({url, headers: {figgy: 'pudding'}});
await inst.request({url, headers: {figgy: 'pudding'}});
await inst.request({url, headers: new Headers({figgy: 'pudding'})});
await inst.request({url, headers: new Headers({figgy: 'pudding'})});
scope.done();
const agentCache = inst.getAgentCache();
assert(agentCache.get(key));
Expand Down Expand Up @@ -1241,7 +1249,7 @@ describe('interceptors', () => {
return Promise.resolve(config);
},
});
await instance.request({url, headers: {}});
await instance.request({url});
scope.done();
});

Expand Down Expand Up @@ -1354,7 +1362,7 @@ describe('interceptors', () => {
return Promise.resolve(response);
},
});
const resp = await instance.request({url, headers: {}});
const resp = await instance.request({url});
scope.done();
assert.strictEqual(resp.headers.get('foo'), 'bar');
assert.strictEqual(resp.headers.get('bar'), 'baz');
Expand Down