Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4ea159f

Browse files
authoredJan 4, 2024
fix(headers): always send lowercase headers and strip undefined (BREAKING in rare cases) (#608)
BREAKING: If you previously sent `My-Header: foo` and `my-header: bar`, both would get sent. Now, only one will. If you previously sent `My-Header: undefined`, it would send as such. Now, the header will not be sent.
1 parent 045ee74 commit 4ea159f

File tree

2 files changed

+72
-20
lines changed

2 files changed

+72
-20
lines changed
 

‎src/core.ts

+50-13
Original file line numberDiff line numberDiff line change
@@ -301,18 +301,7 @@ export abstract class APIClient {
301301
headers[this.idempotencyHeader] = options.idempotencyKey;
302302
}
303303

304-
const reqHeaders: Record<string, string> = {
305-
...(contentLength && { 'Content-Length': contentLength }),
306-
...this.defaultHeaders(options),
307-
...headers,
308-
};
309-
// let builtin fetch set the Content-Type for multipart bodies
310-
if (isMultipartBody(options.body) && shimsKind !== 'node') {
311-
delete reqHeaders['Content-Type'];
312-
}
313-
314-
// Strip any headers being explicitly omitted with null
315-
Object.keys(reqHeaders).forEach((key) => reqHeaders[key] === null && delete reqHeaders[key]);
304+
const reqHeaders = this.buildHeaders({ options, headers, contentLength });
316305

317306
const req: RequestInit = {
318307
method,
@@ -324,9 +313,35 @@ export abstract class APIClient {
324313
signal: options.signal ?? null,
325314
};
326315

316+
return { req, url, timeout };
317+
}
318+
319+
private buildHeaders({
320+
options,
321+
headers,
322+
contentLength,
323+
}: {
324+
options: FinalRequestOptions;
325+
headers: Record<string, string | null | undefined>;
326+
contentLength: string | null | undefined;
327+
}): Record<string, string> {
328+
const reqHeaders: Record<string, string> = {};
329+
if (contentLength) {
330+
reqHeaders['content-length'] = contentLength;
331+
}
332+
333+
const defaultHeaders = this.defaultHeaders(options);
334+
applyHeadersMut(reqHeaders, defaultHeaders);
335+
applyHeadersMut(reqHeaders, headers);
336+
337+
// let builtin fetch set the Content-Type for multipart bodies
338+
if (isMultipartBody(options.body) && shimsKind !== 'node') {
339+
delete reqHeaders['content-type'];
340+
}
341+
327342
this.validateHeaders(reqHeaders, headers);
328343

329-
return { req, url, timeout };
344+
return reqHeaders;
330345
}
331346

332347
/**
@@ -1013,6 +1028,28 @@ export function hasOwn(obj: Object, key: string): boolean {
10131028
return Object.prototype.hasOwnProperty.call(obj, key);
10141029
}
10151030

1031+
/**
1032+
* Copies headers from "newHeaders" onto "targetHeaders",
1033+
* using lower-case for all properties,
1034+
* ignoring any keys with undefined values,
1035+
* and deleting any keys with null values.
1036+
*/
1037+
function applyHeadersMut(targetHeaders: Headers, newHeaders: Headers): void {
1038+
for (const k in newHeaders) {
1039+
if (!hasOwn(newHeaders, k)) continue;
1040+
const lowerKey = k.toLowerCase();
1041+
if (!lowerKey) continue;
1042+
1043+
const val = newHeaders[k];
1044+
1045+
if (val === null) {
1046+
delete targetHeaders[lowerKey];
1047+
} else if (val !== undefined) {
1048+
targetHeaders[lowerKey] = val;
1049+
}
1050+
}
1051+
}
1052+
10161053
export function debug(action: string, ...args: any[]) {
10171054
if (typeof process !== 'undefined' && process.env['DEBUG'] === 'true') {
10181055
console.log(`OpenAI:DEBUG:${action}`, ...args);

‎tests/index.test.ts

+22-7
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,25 @@ describe('instantiate client', () => {
2828

2929
test('they are used in the request', () => {
3030
const { req } = client.buildRequest({ path: '/foo', method: 'post' });
31-
expect((req.headers as Headers)['X-My-Default-Header']).toEqual('2');
31+
expect((req.headers as Headers)['x-my-default-header']).toEqual('2');
3232
});
3333

34-
test('can be overriden with `undefined`', () => {
34+
test('can ignore `undefined` and leave the default', () => {
3535
const { req } = client.buildRequest({
3636
path: '/foo',
3737
method: 'post',
3838
headers: { 'X-My-Default-Header': undefined },
3939
});
40-
expect((req.headers as Headers)['X-My-Default-Header']).toBeUndefined();
40+
expect((req.headers as Headers)['x-my-default-header']).toEqual('2');
4141
});
4242

43-
test('can be overriden with `null`', () => {
43+
test('can be removed with `null`', () => {
4444
const { req } = client.buildRequest({
4545
path: '/foo',
4646
method: 'post',
4747
headers: { 'X-My-Default-Header': null },
4848
});
49-
expect((req.headers as Headers)['X-My-Default-Header']).toBeUndefined();
49+
expect(req.headers as Headers).not.toHaveProperty('x-my-default-header');
5050
});
5151
});
5252

@@ -179,12 +179,27 @@ describe('request building', () => {
179179
describe('Content-Length', () => {
180180
test('handles multi-byte characters', () => {
181181
const { req } = client.buildRequest({ path: '/foo', method: 'post', body: { value: '—' } });
182-
expect((req.headers as Record<string, string>)['Content-Length']).toEqual('20');
182+
expect((req.headers as Record<string, string>)['content-length']).toEqual('20');
183183
});
184184

185185
test('handles standard characters', () => {
186186
const { req } = client.buildRequest({ path: '/foo', method: 'post', body: { value: 'hello' } });
187-
expect((req.headers as Record<string, string>)['Content-Length']).toEqual('22');
187+
expect((req.headers as Record<string, string>)['content-length']).toEqual('22');
188+
});
189+
});
190+
191+
describe('custom headers', () => {
192+
test('handles undefined', () => {
193+
const { req } = client.buildRequest({
194+
path: '/foo',
195+
method: 'post',
196+
body: { value: 'hello' },
197+
headers: { 'X-Foo': 'baz', 'x-foo': 'bar', 'x-Foo': undefined, 'x-baz': 'bam', 'X-Baz': null },
198+
});
199+
expect((req.headers as Record<string, string>)['x-foo']).toEqual('bar');
200+
expect((req.headers as Record<string, string>)['x-Foo']).toEqual(undefined);
201+
expect((req.headers as Record<string, string>)['X-Foo']).toEqual(undefined);
202+
expect((req.headers as Record<string, string>)['x-baz']).toEqual(undefined);
188203
});
189204
});
190205
});

0 commit comments

Comments
 (0)
Please sign in to comment.