Skip to content

Commit d4409cb

Browse files
committed
Refactor header handling code into its own header-utils
1 parent dd97882 commit d4409cb

File tree

6 files changed

+166
-131
lines changed

6 files changed

+166
-131
lines changed

src/client/mockttp-admin-request-builder.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,9 @@ import gql from 'graphql-tag';
33

44
import { MockedEndpoint, MockedEndpointData } from "../types";
55

6-
import {
7-
buildBodyReader,
8-
objectHeadersToRaw,
9-
rawHeadersToObject
10-
} from '../util/request-utils';
6+
import { buildBodyReader } from '../util/request-utils';
7+
import { objectHeadersToRaw, rawHeadersToObject } from '../util/header-utils';
8+
119
import type { Serialized } from '../serialization/serialization';
1210

1311
import { AdminQuery } from './admin-query';

src/rules/requests/request-handlers.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,18 @@ import {
2828
shouldKeepAlive,
2929
dropDefaultHeaders,
3030
isHttp2,
31+
isAbsoluteUrl,
32+
writeHead
33+
} from '../../util/request-utils';
34+
import {
3135
h1HeadersToH2,
3236
h2HeadersToH1,
33-
isAbsoluteUrl,
3437
objectHeadersToRaw,
3538
rawHeadersToObject,
3639
flattenPairedRawHeaders,
3740
findRawHeader,
38-
writeHead,
3941
pairFlatRawHeaders
40-
} from '../../util/request-utils';
42+
} from '../../util/header-utils';
4143
import { streamToBuffer, asBuffer } from '../../util/buffer-utils';
4244
import { isLocalhostAddress, isLocalPortActive, isSocketLoop } from '../../util/socket-util';
4345
import {

src/rules/websockets/websocket-handlers.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ import {
1717
CloseConnectionHandler,
1818
TimeoutHandler
1919
} from '../requests/request-handlers';
20+
import {
21+
isHttp2
22+
} from '../../util/request-utils';
2023
import {
2124
findRawHeader,
22-
isHttp2,
2325
objectHeadersToRaw,
2426
pairFlatRawHeaders,
2527
rawHeadersToObject
26-
} from '../../util/request-utils';
28+
} from '../../util/header-utils';
2729
import { streamToBuffer } from '../../util/buffer-utils';
2830
import { isLocalhostAddress } from '../../util/socket-util';
2931
import { MaybePromise } from '../../util/type-utils';

src/server/mockttp-server.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,12 @@ import {
4242
buildInitiatedRequest,
4343
tryToParseHttp,
4444
buildBodyReader,
45-
getPathFromAbsoluteUrl,
45+
getPathFromAbsoluteUrl
46+
} from "../util/request-utils";
47+
import {
4648
pairFlatRawHeaders,
4749
rawHeadersToObject
48-
} from "../util/request-utils";
50+
} from "../util/header-utils";
4951
import { AbortError } from "../rules/requests/request-handlers";
5052
import { WebSocketRuleData, WebSocketRule } from "../rules/websockets/websocket-rule";
5153
import { RejectWebSocketHandler, WebSocketHandler } from "../rules/websockets/websocket-handlers";

src/util/header-utils.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
import {
2+
Headers,
3+
RawHeaders
4+
} from "../types";
5+
6+
/*
7+
8+
These utils support conversion between the various header representations that we deal
9+
with. Those are:
10+
11+
- Flat arrays of [key, value, key, value, key, ...]. This is the raw header format
12+
generally used by Node.js's APIs throughout.
13+
- Raw header tuple arrays like [[key, value], [key, value]]. This is our own raw header
14+
format, aiming to be fairly easy to use and to preserve header order, header dupes &
15+
header casing throughout.
16+
- Formatted header objects of { key: value, key: value }. These are returned as the most
17+
convenient and consistent header format: keys are lowercased, and values are either
18+
strings or arrays of strings (for duplicate headers). This is returned by Node's APIs,
19+
but with some unclear normalization rules, so in practice we build raw headers and
20+
reconstruct this ourselves everyhere, by lowercasing & building arrays of values.
21+
22+
*/
23+
24+
export const findRawHeader = (rawHeaders: RawHeaders, targetKey: string) =>
25+
rawHeaders.find(([key]) => key.toLowerCase() === targetKey);
26+
27+
const findRawHeaders = (rawHeaders: RawHeaders, targetKey: string) =>
28+
rawHeaders.filter(([key]) => key.toLowerCase() === targetKey);
29+
30+
/**
31+
* Return node's _very_ raw headers ([k, v, k, v, ...]) into our slightly more convenient
32+
* pairwise tuples [[k, v], [k, v], ...] RawHeaders structure.
33+
*/
34+
export function pairFlatRawHeaders(flatRawHeaders: string[]): RawHeaders {
35+
const result: RawHeaders = [];
36+
for (let i = 0; i < flatRawHeaders.length; i += 2 /* Move two at a time */) {
37+
result[i/2] = [flatRawHeaders[i], flatRawHeaders[i+1]];
38+
}
39+
return result;
40+
}
41+
42+
export function flattenPairedRawHeaders(rawHeaders: RawHeaders): string[] {
43+
return rawHeaders.flat();
44+
}
45+
46+
/**
47+
* Take a raw headers, and turn them into headers, but without some of Node's concessions
48+
* to ease of use, i.e. keeping multiple values as arrays.
49+
*/
50+
export function rawHeadersToObject(rawHeaders: RawHeaders): Headers {
51+
return rawHeaders.reduce<Headers>((headers, [key, value]) => {
52+
key = key.toLowerCase();
53+
54+
const existingValue = headers[key];
55+
56+
if (Array.isArray(existingValue)) {
57+
existingValue.push(value);
58+
} else if (existingValue) {
59+
headers[key] = [existingValue, value];
60+
} else {
61+
headers[key] = value;
62+
}
63+
64+
return headers;
65+
}, {});
66+
}
67+
68+
export function objectHeadersToRaw(headers: Headers): RawHeaders {
69+
const rawHeaders: RawHeaders = [];
70+
71+
for (let key in headers) {
72+
const value = headers[key];
73+
74+
if (value === undefined) continue; // Drop undefined header values
75+
76+
if (Array.isArray(value)) {
77+
value.forEach((v) => rawHeaders.push([key, v]));
78+
} else {
79+
rawHeaders.push([key, value]);
80+
}
81+
}
82+
83+
return rawHeaders;
84+
}
85+
86+
export function objectHeadersToFlat(headers: Headers): string[] {
87+
const flatHeaders: string[] = [];
88+
89+
for (let key in headers) {
90+
const value = headers[key];
91+
92+
if (value === undefined) continue; // Drop undefined header values
93+
94+
if (Array.isArray(value)) {
95+
value.forEach((v) => {
96+
flatHeaders.push(key);
97+
flatHeaders.push(v);
98+
});
99+
} else {
100+
flatHeaders.push(key);
101+
flatHeaders.push(value);
102+
}
103+
}
104+
105+
return flatHeaders;
106+
}
107+
108+
// See https://httptoolkit.tech/blog/translating-http-2-into-http-1/ for details on the
109+
// transformations required between H2 & H1 when proxying.
110+
export function h2HeadersToH1(h2Headers: RawHeaders): RawHeaders {
111+
let h1Headers = h2Headers.filter(([key]) => key[0] !== ':');
112+
113+
if (!findRawHeader(h1Headers, 'host') && findRawHeader(h2Headers, ':authority')) {
114+
h1Headers.unshift(['Host', findRawHeader(h2Headers, ':authority')![1]]);
115+
}
116+
117+
// In HTTP/1 you MUST only send one cookie header - in HTTP/2 sending multiple is fine,
118+
// so we have to concatenate them:
119+
const cookieHeaders = findRawHeaders(h1Headers, 'cookie')
120+
if (cookieHeaders.length > 1) {
121+
h1Headers = h1Headers.filter(([key]) => key.toLowerCase() !== 'cookie');
122+
h1Headers.push(['Cookie', cookieHeaders.join('; ')]);
123+
}
124+
125+
return h1Headers;
126+
}
127+
128+
// Take from http2/util.js in Node itself
129+
const HTTP2_ILLEGAL_HEADERS = [
130+
'connection',
131+
'upgrade',
132+
'host',
133+
'http2-settings',
134+
'keep-alive',
135+
'proxy-connection',
136+
'transfer-encoding'
137+
];
138+
139+
export function h1HeadersToH2(headers: RawHeaders): RawHeaders {
140+
return headers.filter(([key]) =>
141+
!HTTP2_ILLEGAL_HEADERS.includes(key.toLowerCase())
142+
);
143+
}

src/util/request-utils.ts

Lines changed: 7 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ import {
3131
streamToBuffer,
3232
asBuffer
3333
} from './buffer-utils';
34+
import {
35+
flattenPairedRawHeaders,
36+
objectHeadersToFlat,
37+
objectHeadersToRaw,
38+
pairFlatRawHeaders,
39+
rawHeadersToObject
40+
} from './header-utils';
3441

3542
// Is this URL fully qualified?
3643
// Note that this supports only HTTP - no websockets or anything else.
@@ -116,41 +123,6 @@ export function isHttp2(
116123
('stream' in message && 'createPushResponse' in message); // H2 response
117124
}
118125

119-
export function h2HeadersToH1(h2Headers: RawHeaders): RawHeaders {
120-
let h1Headers = h2Headers.filter(([key]) => key[0] !== ':');
121-
122-
if (!findRawHeader(h1Headers, 'host') && findRawHeader(h2Headers, ':authority')) {
123-
h1Headers.unshift(['Host', findRawHeader(h2Headers, ':authority')![1]]);
124-
}
125-
126-
// In HTTP/1 you MUST only send one cookie header - in HTTP/2 sending multiple is fine,
127-
// so we have to concatenate them:
128-
const cookieHeaders = findRawHeaders(h1Headers, 'cookie')
129-
if (cookieHeaders.length > 1) {
130-
h1Headers = h1Headers.filter(([key]) => key.toLowerCase() !== 'cookie');
131-
h1Headers.push(['Cookie', cookieHeaders.join('; ')]);
132-
}
133-
134-
return h1Headers;
135-
}
136-
137-
// Take from http2/util.js in Node itself
138-
const HTTP2_ILLEGAL_HEADERS = [
139-
'connection',
140-
'upgrade',
141-
'host',
142-
'http2-settings',
143-
'keep-alive',
144-
'proxy-connection',
145-
'transfer-encoding'
146-
];
147-
148-
export function h1HeadersToH2(headers: RawHeaders): RawHeaders {
149-
return headers.filter(([key]) =>
150-
!HTTP2_ILLEGAL_HEADERS.includes(key.toLowerCase())
151-
);
152-
}
153-
154126
// Parse an in-progress request or response stream, i.e. where the body or possibly even the headers have
155127
// not been fully received/sent yet.
156128
const parseBodyStream = (bodyStream: stream.Readable, maxSize: number, getHeaders: () => Headers): OngoingBody => {
@@ -252,90 +224,6 @@ export const parseRequestBody = (
252224
transformedRequest.body = parseBodyStream(req, options.maxSize, () => req.headers);
253225
};
254226

255-
export const findRawHeader = (rawHeaders: RawHeaders, targetKey: string) =>
256-
rawHeaders.find(([key]) => key.toLowerCase() === targetKey);
257-
258-
export const findRawHeaders = (rawHeaders: RawHeaders, targetKey: string) =>
259-
rawHeaders.filter(([key]) => key.toLowerCase() === targetKey);
260-
261-
/**
262-
* Return node's _very_ raw headers ([k, v, k, v, ...]) into our slightly more convenient
263-
* pairwise tuples [[k, v], [k, v], ...] RawHeaders structure.
264-
*/
265-
export function pairFlatRawHeaders(flatRawHeaders: string[]): RawHeaders {
266-
const result: RawHeaders = [];
267-
for (let i = 0; i < flatRawHeaders.length; i += 2 /* Move two at a time */) {
268-
result[i/2] = [flatRawHeaders[i], flatRawHeaders[i+1]];
269-
}
270-
return result;
271-
}
272-
273-
export function flattenPairedRawHeaders(rawHeaders: RawHeaders): string[] {
274-
return rawHeaders.flat();
275-
}
276-
277-
/**
278-
* Take a raw headers, and turn them into headers, but without some of Node's concessions
279-
* to ease of use, i.e. keeping multiple values as arrays.
280-
*/
281-
export function rawHeadersToObject(rawHeaders: RawHeaders): Headers {
282-
return rawHeaders.reduce<Headers>((headers, [key, value]) => {
283-
key = key.toLowerCase();
284-
285-
const existingValue = headers[key];
286-
287-
if (Array.isArray(existingValue)) {
288-
existingValue.push(value);
289-
} else if (existingValue) {
290-
headers[key] = [existingValue, value];
291-
} else {
292-
headers[key] = value;
293-
}
294-
295-
return headers;
296-
}, {});
297-
}
298-
299-
export function objectHeadersToRaw(headers: Headers): RawHeaders {
300-
const rawHeaders: RawHeaders = [];
301-
302-
for (let key in headers) {
303-
const value = headers[key];
304-
305-
if (value === undefined) continue; // Drop undefined header values
306-
307-
if (Array.isArray(value)) {
308-
value.forEach((v) => rawHeaders.push([key, v]));
309-
} else {
310-
rawHeaders.push([key, value]);
311-
}
312-
}
313-
314-
return rawHeaders;
315-
}
316-
317-
export function objectHeadersToFlat(headers: Headers): string[] {
318-
const flatHeaders: string[] = [];
319-
320-
for (let key in headers) {
321-
const value = headers[key];
322-
323-
if (value === undefined) continue; // Drop undefined header values
324-
325-
if (Array.isArray(value)) {
326-
value.forEach((v) => {
327-
flatHeaders.push(key);
328-
flatHeaders.push(v);
329-
});
330-
} else {
331-
flatHeaders.push(key);
332-
flatHeaders.push(value);
333-
}
334-
}
335-
336-
return flatHeaders;
337-
}
338-
339227
/**
340228
* Build an initiated request: the external representation of a request
341229
* that's just started.

0 commit comments

Comments
 (0)