-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.ts
More file actions
130 lines (106 loc) · 3.47 KB
/
Copy pathindex.ts
File metadata and controls
130 lines (106 loc) · 3.47 KB
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
import { finished } from "node:stream";
import { createInflate, createGunzip, createBrotliDecompress } from "node:zlib";
import {
BindenError,
Middleware,
IMiddlewareParams,
Context,
ct_text,
ct_json,
ct_form,
} from "binden";
import type { Duplex, Readable } from "stream";
export type IBodyContentType = typeof ct_form | typeof ct_json | typeof ct_text;
export type IParse<T = unknown> = (input: string) => T | undefined;
export interface IBodyParserOptions<T = unknown> extends IMiddlewareParams {
parse?: IParse<T>;
}
export class BodyParser<T = unknown> extends Middleware {
readonly #parse: IParse<T>;
public constructor({ parse, ...rest }: IBodyParserOptions = {}) {
super(rest);
if (typeof parse !== "undefined" && typeof parse !== "function") {
throw new TypeError("`parse` is not a function");
}
this.#parse = parse ?? JSON.parse;
}
public async run(context: Context): Promise<void> {
const { name: middleware } = BodyParser;
const { request, log: logger } = context;
const log = logger.child({ middleware });
const { destroyed, method = "GET" } = request;
if (BodyParser.#methods.has(method)) {
log.debug("Unsupported method", { method });
return;
}
if (destroyed) {
log.debug("Skip parsing", { destroyed });
return;
}
const { content_type, content_encoding } = request;
const type = content_type?.type ?? null;
if (type !== ct_json && type !== ct_text && type !== ct_form) {
log.debug("Unsupported Content-Type", { content_type: type });
return;
}
const streams: [string, Duplex][] = [];
for (const { encoding } of content_encoding) {
if (encoding === "gzip" || encoding === "x-gzip") {
streams.push([encoding, createGunzip()]);
} else if (encoding === "deflate") {
streams.push([encoding, createInflate()]);
} else if (encoding === "br") {
streams.push([encoding, createBrotliDecompress()]);
} else {
log.debug("Unsupported encoding", { encoding });
throw new BindenError(415);
}
}
const body = await new Promise<string>((resolve, reject) => {
let { request: stream } = context as { request: Readable };
for (const [encoding, decompresser] of streams) {
decompresser.once("error", (error) => {
log.debug("Decoding failed", { error, encoding });
reject(new BindenError(415, { cause: error }));
});
stream.pipe(decompresser);
stream = decompresser;
}
const chunks: Buffer[] = [];
stream.on("data", (data: Buffer) => chunks.push(data));
finished(stream, (error) => {
if (error) {
reject(error);
} else {
resolve(Buffer.concat(chunks).toString());
}
});
});
try {
request.body =
type === ct_json
? this.#parse(body)
: type === ct_form
? new URLSearchParams(body)
: body;
} catch (error: unknown) {
log.debug("Request body does not match provided Content-Type", {
error,
type,
body,
});
throw new BindenError(415, { cause: error as Error });
}
}
/** Set of methods with no request body */
public static get unsupported_methods(): Set<string> {
return new Set(BodyParser.#methods);
}
static readonly #methods: ReadonlySet<string> = new Set([
"GET",
"HEAD",
"OPTIONS",
"TRACE",
]);
}
export default new BodyParser();