This repository was archived by the owner on Mar 20, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 535
/
Copy pathparseBody.ts
132 lines (115 loc) · 3.79 KB
/
parseBody.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import type { IncomingMessage } from 'http';
import type { Inflate, Gunzip } from 'zlib';
import zlib from 'zlib';
import querystring from 'querystring';
import getBody from 'raw-body';
import httpError from 'http-errors';
import contentType from 'content-type';
import type { ParsedMediaType } from 'content-type';
type Request = IncomingMessage & { body?: unknown };
/**
* Provided a "Request" provided by express or connect (typically a node style
* HTTPClientRequest), Promise the body data contained.
*/
export async function parseBody(
req: Request,
): Promise<{ [param: string]: unknown }> {
const { body } = req;
// If express has already parsed a body as a keyed object, use it.
if (typeof body === 'object' && !(body instanceof Buffer)) {
return body as { [param: string]: unknown };
}
// Skip requests without content types.
if (req.headers['content-type'] === undefined) {
return {};
}
const typeInfo = contentType.parse(req);
// If express has already parsed a body as a string, and the content-type
// was application/graphql, parse the string body.
if (typeof body === 'string' && typeInfo.type === 'application/graphql') {
return { query: body };
}
// Already parsed body we didn't recognise? Parse nothing.
if (body != null) {
return {};
}
const rawBody = await readBody(req, typeInfo);
// Use the correct body parser based on Content-Type header.
switch (typeInfo.type) {
case 'application/graphql':
return { query: rawBody };
case 'application/json':
if (jsonObjRegex.test(rawBody)) {
try {
return JSON.parse(rawBody);
} catch {
// Do nothing
}
}
throw httpError(400, 'POST body sent invalid JSON.');
case 'application/x-www-form-urlencoded':
return querystring.parse(rawBody);
}
// If no Content-Type header matches, parse nothing.
return {};
}
/**
* RegExp to match an Object-opening brace "{" as the first non-space
* in a string. Allowed whitespace is defined in RFC 7159:
*
* ' ' Space
* '\t' Horizontal tab
* '\n' Line feed or New line
* '\r' Carriage return
*/
const jsonObjRegex = /^[ \t\n\r]*\{/;
// Read and parse a request body.
async function readBody(
req: Request,
typeInfo: ParsedMediaType,
): Promise<string> {
const charset = typeInfo.parameters.charset?.toLowerCase() ?? 'utf-8';
// Assert charset encoding per JSON RFC 7159 sec 8.1
if (!charset.startsWith('utf-')) {
throw httpError(415, `Unsupported charset "${charset.toUpperCase()}".`);
}
// Get content-encoding (e.g. gzip)
const contentEncoding = req.headers['content-encoding'];
const encoding =
typeof contentEncoding === 'string'
? contentEncoding.toLowerCase()
: 'identity';
const length = encoding === 'identity' ? req.headers['content-length'] : null;
const limit = 100 * 1024; // 100kb
const stream = decompressed(req, encoding);
// Read body from stream.
try {
return await getBody(stream, { encoding: charset, length, limit });
} catch (rawError: unknown) {
const error = httpError(
400,
/* istanbul ignore next: Thrown by underlying library. */
rawError instanceof Error ? rawError : String(rawError),
);
error.message =
error.type === 'encoding.unsupported'
? `Unsupported charset "${charset.toUpperCase()}".`
: `Invalid body: ${error.message}.`;
throw error;
}
}
// Return a decompressed stream, given an encoding.
function decompressed(
req: Request,
encoding: string,
): Request | Inflate | Gunzip {
switch (encoding) {
case 'identity':
return req;
case 'deflate':
return req.pipe(zlib.createInflate());
case 'gzip':
return req.pipe(zlib.createGunzip());
}
throw httpError(415, `Unsupported content-encoding "${encoding}".`);
}