Skip to content

Commit 49a8080

Browse files
Foolproof request headers casing (#1157)
1 parent f88d836 commit 49a8080

File tree

7 files changed

+59
-42
lines changed

7 files changed

+59
-42
lines changed

‎src/cache/axios.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import type {
22
AxiosInstance,
33
AxiosInterceptorManager,
44
AxiosRequestConfig,
5+
AxiosRequestHeaders,
56
AxiosResponse,
6-
AxiosResponseHeaders,
77
InternalAxiosRequestConfig
88
} from 'axios';
99
import type { CacheInstance, CacheProperties } from './cache.js';
@@ -94,7 +94,7 @@ export interface CacheRequestConfig<R = any, D = any> extends AxiosRequestConfig
9494

9595
/** Cached version of type {@link InternalAxiosRequestConfig} */
9696
export interface InternalCacheRequestConfig<R = any, D = any> extends CacheRequestConfig<R, D> {
97-
headers: AxiosResponseHeaders;
97+
headers: AxiosRequestHeaders;
9898
}
9999

100100
/**

‎src/header/extract.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { AxiosRequestHeaders, AxiosResponseHeaders } from 'axios';
2+
13
/**
24
* Extracts specified header values from request headers.
35
* Generic utility for extracting a subset of headers.
@@ -7,13 +9,13 @@
79
* @returns Object with extracted header values
810
*/
911
export function extractHeaders(
10-
requestHeaders: Record<string, any>,
12+
requestHeaders: AxiosRequestHeaders | AxiosResponseHeaders,
1113
headerNames: string[]
12-
): Record<string, string> {
13-
const result: Record<string, string> = {};
14+
): Record<string, string | undefined> {
15+
const result: Record<string, string | undefined> = {};
1416

1517
for (const name of headerNames) {
16-
result[name] = String(requestHeaders[name.toLowerCase()] || '');
18+
result[name] = requestHeaders.get(name)?.toString();
1719
}
1820

1921
return result;

‎src/interceptors/request.ts‎

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,7 @@ import { Header } from '../header/headers.js';
66
import type { CachedResponse, LoadingStorageValue } from '../storage/types.js';
77
import { regexOrStringMatch } from '../util/cache-predicate.js';
88
import type { RequestInterceptor } from './build.js';
9-
import {
10-
type ConfigWithCache,
11-
createValidateStatus,
12-
isMethodIn,
13-
updateStaleRequest
14-
} from './util.js';
9+
import { createValidateStatus, isMethodIn, updateStaleRequest } from './util.js';
1510

1611
export function defaultRequestInterceptor(axios: AxiosCacheInstance): RequestInterceptor {
1712
const onFulfilled: RequestInterceptor['onFulfilled'] = async (config) => {
@@ -110,9 +105,13 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance): RequestInt
110105
// shouldn't be cached an therefore neither in the browser.
111106
// https://stackoverflow.com/a/2068407
112107
if (config.cache.cacheTakeover) {
113-
config.headers[Header.CacheControl] ??= 'no-cache, no-store, must-revalidate, max-age=0';
114-
config.headers[Header.Pragma] ??= 'no-cache';
115-
config.headers[Header.Expires] ??= '0';
108+
config.headers.set(
109+
Header.CacheControl,
110+
'no-cache, no-store, must-revalidate, max-age=0',
111+
false
112+
);
113+
config.headers.set(Header.Pragma, 'no-cache', false);
114+
config.headers.set(Header.Expires, '0', false);
116115
}
117116

118117
if (!isMethodIn(config.method, config.cache.methods)) {
@@ -232,7 +231,7 @@ export function defaultRequestInterceptor(axios: AxiosCacheInstance): RequestInt
232231
// The override option is meant to bypass cache and get fresh data, not revalidate existing cache.
233232
// Adding conditional headers would cause the server to return 304 Not Modified instead of fresh data.
234233
if ((cache.state === 'stale' || cache.state === 'must-revalidate') && !overrideCache) {
235-
updateStaleRequest(cache, config as ConfigWithCache<unknown>);
234+
updateStaleRequest(cache, { ...config, cache: config.cache });
236235

237236
if (__ACI_DEV__) {
238237
axios.debug({

‎src/interceptors/util.ts‎

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import type { Method } from 'axios';
2-
import type { CacheAxiosResponse, CacheRequestConfig } from '../cache/axios.js';
2+
import type {
3+
CacheAxiosResponse,
4+
CacheRequestConfig,
5+
InternalCacheRequestConfig
6+
} from '../cache/axios.js';
37
import type { CacheProperties } from '../cache/cache.js';
48
import { Header } from '../header/headers.js';
59
import type {
@@ -29,7 +33,7 @@ export function isMethodIn(
2933
return methodList.some((method) => method === requestMethod);
3034
}
3135

32-
export interface ConfigWithCache<D> extends CacheRequestConfig<unknown, D> {
36+
export interface ConfigWithCache<D> extends InternalCacheRequestConfig<unknown, D> {
3337
cache: Partial<CacheProperties<unknown, D>>;
3438
}
3539

@@ -41,25 +45,24 @@ export function updateStaleRequest<D>(
4145
cache: StaleStorageValue | MustRevalidateStorageValue,
4246
config: ConfigWithCache<D>
4347
): void {
44-
config.headers ||= {};
45-
4648
const { etag, modifiedSince } = config.cache;
4749

4850
if (etag) {
49-
const etagValue = etag === true ? (cache.data?.headers[Header.ETag] as unknown) : etag;
51+
const etagValue = etag === true ? cache.data?.headers[Header.ETag] : etag;
5052

5153
if (etagValue) {
52-
config.headers[Header.IfNoneMatch] = etagValue;
54+
config.headers.set(Header.IfNoneMatch, etagValue);
5355
}
5456
}
5557

5658
if (modifiedSince) {
57-
config.headers[Header.IfModifiedSince] =
59+
config.headers.set(
60+
Header.IfModifiedSince,
61+
// If last-modified is not present, use the createdAt timestamp
5862
modifiedSince === true
59-
? // If last-modified is not present, use the createdAt timestamp
60-
(cache.data.headers[Header.LastModified] as unknown) ||
61-
new Date(cache.createdAt).toUTCString()
62-
: modifiedSince.toUTCString();
63+
? cache.data.headers[Header.LastModified] || new Date(cache.createdAt).toUTCString()
64+
: modifiedSince.toUTCString()
65+
);
6366
}
6467
}
6568

‎src/storage/types.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface CachedResponseMeta {
1717
* vary: { authorization: 'Bearer X' }
1818
* }
1919
*/
20-
vary?: Record<string, string>;
20+
vary?: Record<string, string | undefined>;
2121
}
2222

2323
export interface CachedResponse {

‎test/interceptors/request.test.ts‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -377,10 +377,23 @@ describe('Request Interceptor', () => {
377377
cache: { cacheTakeover: false }
378378
});
379379

380-
const headers2 = req2.request.config.headers as Record<string, string>;
381-
assert.equal(headers2[Header.CacheControl], undefined);
382-
assert.equal(headers2[Header.Pragma], undefined);
383-
assert.equal(headers2[Header.Expires], undefined);
380+
const headers2 = req2.request.config.headers;
381+
assert.equal(headers2.get(Header.CacheControl), undefined);
382+
assert.equal(headers2.get(Header.Pragma), undefined);
383+
assert.equal(headers2.get(Header.Expires), undefined);
384+
385+
const req3 = await axios.get('url3', {
386+
cache: { cacheTakeover: true },
387+
headers: { PRAGma: 'my-custom-value' }
388+
});
389+
390+
const headers3 = req3.request.config.headers;
391+
assert.equal(
392+
headers3.get(Header.CacheControl),
393+
'no-cache, no-store, must-revalidate, max-age=0'
394+
);
395+
assert.equal(headers3.get(Header.Pragma), 'my-custom-value');
396+
assert.equal(headers3.get(Header.Expires), '0');
384397
});
385398

386399
it('ensure cached data is not transformed', async () => {

‎test/interceptors/vary.test.ts‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,11 @@ describe('Vary Header Support', () => {
3636
});
3737

3838
const resp2 = await axios.get('url', {
39-
headers: { authorization: 'Bearer A', 'accept-language': 'en' }
39+
headers: { authorization: 'Bearer A', 'Accept-Language': 'en' }
4040
});
4141

4242
const resp3 = await axios.get('url', {
43-
headers: { authorization: 'Bearer A', 'accept-language': 'fr' }
43+
headers: { authorization: 'Bearer A', 'ACCEPT-LANGUAGE': 'fr' }
4444
});
4545

4646
assert.equal(resp1.cached, false);
@@ -59,22 +59,22 @@ describe('Vary Header Support', () => {
5959
return { user: Math.random(), call: networkCallCount, timestamp: Date.now() };
6060
});
6161

62-
// 9 concurrent requests: 3 variations, 3 requests each
62+
// 9 concurrent requests: 3 variations, 3 requests each (in different casing)
6363
const requests = [
6464
// 3 with Bearer A
6565
axios.get('url', { headers: { authorization: 'Bearer A' } }),
66-
axios.get('url', { headers: { authorization: 'Bearer A' } }),
66+
axios.get('url', { headers: { auTHORIZAtion: 'Bearer A' } }),
6767
axios.get('url', { headers: { authorization: 'Bearer A' } }),
6868

6969
// 3 with Bearer B
70-
axios.get('url', { headers: { authorization: 'Bearer B' } }),
71-
axios.get('url', { headers: { authorization: 'Bearer B' } }),
72-
axios.get('url', { headers: { authorization: 'Bearer B' } }),
70+
axios.get('url', { headers: { Authorization: 'Bearer B' } }),
71+
axios.get('url', { headers: { AUTHORIZATION: 'Bearer B' } }),
72+
axios.get('url', { headers: { auTHOrization: 'Bearer B' } }),
7373

7474
// 3 with Bearer C
75-
axios.get('url', { headers: { authorization: 'Bearer C' } }),
76-
axios.get('url', { headers: { authorization: 'Bearer C' } }),
77-
axios.get('url', { headers: { authorization: 'Bearer C' } })
75+
axios.get('url', { headers: { authOrization: 'Bearer C' } }),
76+
axios.get('url', { headers: { authorIZAtion: 'Bearer C' } }),
77+
axios.get('url', { headers: { authorizATion: 'Bearer C' } })
7878
];
7979

8080
const responses = await Promise.all(requests);

0 commit comments

Comments
 (0)