-
Notifications
You must be signed in to change notification settings - Fork 235
/
body.ts
269 lines (247 loc) · 8.64 KB
/
body.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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
// Copyright 2018-2024 the oak authors. All rights reserved. MIT license.
/**
* Contains the oak abstraction to represent a request {@linkcode Body}.
*
* This is not normally used directly by end users.
*
* @module
*/
import { createHttpError, matches, parseFormData, Status } from "./deps.ts";
import type { ServerRequest } from "./types.ts";
type JsonReviver = (key: string, value: unknown) => unknown;
export type BodyType =
| "binary"
| "form"
| "form-data"
| "json"
| "text"
| "unknown";
const KNOWN_BODY_TYPES: [bodyType: BodyType, knownMediaTypes: string[]][] = [
["binary", ["image", "audio", "application/octet-stream"]],
["form", ["urlencoded"]],
["form-data", ["multipart"]],
["json", ["json", "application/*+json", "application/csp-report"]],
["text", ["text"]],
];
async function readBlob(
body?: ReadableStream<Uint8Array> | null,
type?: string | null,
): Promise<Blob> {
if (!body) {
return new Blob(undefined, type ? { type } : undefined);
}
const chunks: Uint8Array[] = [];
for await (const chunk of body) {
chunks.push(chunk);
}
return new Blob(chunks, type ? { type } : undefined);
}
/** An object which encapsulates information around a request body. */
export class Body {
#body?: ReadableStream<Uint8Array> | null;
#memo: Promise<ArrayBuffer | Blob | FormData | string> | null = null;
#memoType: "arrayBuffer" | "blob" | "formData" | "text" | null = null;
#headers?: Headers;
#request?: Request;
#reviver?: JsonReviver;
#type?: BodyType;
#used = false;
constructor(
serverRequest: Pick<ServerRequest, "request" | "headers" | "getBody">,
reviver?: JsonReviver,
) {
if (serverRequest.request) {
this.#request = serverRequest.request;
} else {
this.#headers = serverRequest.headers;
this.#body = serverRequest.getBody();
}
this.#reviver = reviver;
}
/** Is `true` if the request might have a body, otherwise `false`.
*
* **WARNING** this is an unreliable API. In HTTP/2 in many situations you
* cannot determine if a request has a body or not unless you attempt to read
* the body, due to the streaming nature of HTTP/2. As of Deno 1.16.1, for
* HTTP/1.1, Deno also reflects that behavior. The only reliable way to
* determine if a request has a body or not is to attempt to read the body.
*/
get has(): boolean {
return !!(this.#request ? this.#request.body : this.#body);
}
/** Exposes the "raw" `ReadableStream` of the body. */
get stream(): ReadableStream<Uint8Array> | null {
return this.#request ? this.#request.body : this.#body!;
}
/** Returns `true` if the body has been consumed yet, otherwise `false`. */
get used(): boolean {
return this.#request?.bodyUsed ?? !!this.#used;
}
/** Return the body to be reused as BodyInit. */
async init(): Promise<BodyInit | null> {
if (!this.has) {
return null;
}
return await this.#memo ?? this.stream;
}
/** Reads a body to the end and resolves with the value as an
* {@linkcode ArrayBuffer} */
async arrayBuffer(): Promise<ArrayBuffer> {
if (this.#memoType === "arrayBuffer") {
return this.#memo as Promise<ArrayBuffer>;
} else if (this.#memoType) {
throw new TypeError("Body already used as a different type.");
}
this.#memoType = "arrayBuffer";
if (this.#request) {
return this.#memo = this.#request.arrayBuffer();
}
this.#used = true;
return this.#memo = (await readBlob(this.#body)).arrayBuffer();
}
/** Reads a body to the end and resolves with the value as a
* {@linkcode Blob}. */
blob(): Promise<Blob> {
if (this.#memoType === "blob") {
return this.#memo as Promise<Blob>;
} else if (this.#memoType) {
throw new TypeError("Body already used as a different type.");
}
this.#memoType = "blob";
if (this.#request) {
return this.#memo = this.#request.blob();
}
this.#used = true;
return this.#memo = readBlob(
this.#body,
this.#headers?.get("content-type"),
);
}
/** Reads a body as a URL encoded form, resolving the value as
* {@linkcode URLSearchParams}. */
async form(): Promise<URLSearchParams> {
const text = await this.text();
return new URLSearchParams(text);
}
/** Reads a body to the end attempting to parse the body as a set of
* {@linkcode FormData}. */
formData(): Promise<FormData> {
if (this.#memoType === "formData") {
return this.#memo as Promise<FormData>;
} else if (this.#memoType) {
throw new TypeError("Body already used as a different type.");
}
this.#memoType = "formData";
if (this.#request) {
return this.#memo = this.#request.formData();
}
this.#used = true;
if (this.#body && this.#headers) {
const contentType = this.#headers.get("content-type");
if (contentType) {
return this.#memo = parseFormData(contentType, this.#body);
}
}
throw createHttpError(Status.BadRequest, "Missing content type.");
}
/** Reads a body to the end attempting to parse the body as a JSON value.
*
* If a JSON reviver has been assigned, it will be used to parse the body.
*/
// deno-lint-ignore no-explicit-any
async json(): Promise<any> {
try {
return JSON.parse(await this.text(), this.#reviver);
} catch (err) {
if (err instanceof Error) {
throw createHttpError(Status.BadRequest, err.message);
}
throw createHttpError(Status.BadRequest, JSON.stringify(err));
}
}
/** Reads the body to the end resolving with a string. */
async text(): Promise<string> {
if (this.#memoType === "text") {
return this.#memo as Promise<string>;
} else if (this.#memoType) {
throw new TypeError("Body already used as a different type.");
}
this.#memoType = "text";
if (this.#request) {
return this.#memo = this.#request.text();
}
this.#used = true;
return this.#memo = (await readBlob(this.#body)).text();
}
/** Attempts to determine what type of the body is to help determine how best
* to attempt to decode the body. This performs analysis on the supplied
* `Content-Type` header of the request.
*
* **Note** these are not authoritative and should only be used as guidance.
*
* There is the ability to provide custom types when attempting to discern
* the type. Custom types are provided in the format of an object where the
* key is on of {@linkcode BodyType} and the value is an array of media types
* to attempt to match. Values supplied will be additive to known media types.
*
* The returned value is one of the following:
*
* - `"binary"` - The body appears to be binary data and should be consumed as
* an array buffer, readable stream or blob.
* - `"form"` - The value appears to be an URL encoded form and should be
* consumed as a form (`URLSearchParams`).
* - `"form-data"` - The value appears to be multipart form data and should be
* consumed as form data.
* - `"json"` - The value appears to be JSON data and should be consumed as
* decoded JSON.
* - `"text"` - The value appears to be text data and should be consumed as
* text.
* - `"unknown"` - Either there is no body or the body type could not be
* determined.
*/
type(customMediaTypes?: Partial<Record<BodyType, string[]>>): BodyType {
if (this.#type && !customMediaTypes) {
return this.#type;
}
customMediaTypes = customMediaTypes ?? {};
const headers = this.#request?.headers ?? this.#headers;
const contentType = headers?.get("content-type");
if (contentType) {
for (const [bodyType, knownMediaTypes] of KNOWN_BODY_TYPES) {
const customTypes = customMediaTypes[bodyType] ?? [];
if (matches(contentType, [...knownMediaTypes, ...customTypes])) {
this.#type = bodyType;
return this.#type;
}
}
}
return this.#type = "unknown";
}
[Symbol.for("Deno.customInspect")](
inspect: (value: unknown) => string,
): string {
const { has, used } = this;
return `${this.constructor.name} ${inspect({ has, used })}`;
}
[Symbol.for("nodejs.util.inspect.custom")](
depth: number,
// deno-lint-ignore no-explicit-any
options: any,
inspect: (value: unknown, options?: unknown) => string,
// deno-lint-ignore no-explicit-any
): any {
if (depth < 0) {
return options.stylize(`[${this.constructor.name}]`, "special");
}
const newOptions = Object.assign({}, options, {
depth: options.depth === null ? null : options.depth - 1,
});
const { has, used } = this;
return `${options.stylize(this.constructor.name, "special")} ${
inspect(
{ has, used },
newOptions,
)
}`;
}
}