UNPKG

7.24 kBPlain TextView Raw
1// deno-lint-ignore-file no-explicit-any
2import type { Awaitable } from "./utils/common-types.js";
3import { combine, Context } from "./context.js";
4import { accepts, Accepted } from './content-negotiation.js'
5import { payloadTooLarge } from '@worker-tools/response-creators'
6
7export const JSON = 'application/json';
8export const FORM = 'application/x-www-form-urlencoded'
9export const FORM_DATA = 'multipart/form-data';
10export const TEXT_HTML = 'text/html'
11export const TEXT_PLAIN = 'text/plain'
12/** Standard MIME type for binary data */
13export const BINARY = 'application/octet-stream';
14/** Non-standard MIME type for binary data. Sometimes used, so included anyway. */
15export const X_BINARY = 'application/x-binary';
16
17export interface BodyParserOptions<J> {
18 defaultJSON?: J,
19 maxSize?: number
20}
21
22export type BodyParsable =
23 | typeof FORM
24 | typeof FORM_DATA
25 | typeof JSON
26 | typeof BINARY
27 | typeof X_BINARY
28 | `application/${string}+json`
29 | `text/${string}`
30
31const defaultBody: BodyParsable[] = [
32 FORM,
33 FORM_DATA,
34 JSON,
35 BINARY,
36 X_BINARY,
37 TEXT_HTML,
38 TEXT_PLAIN,
39]
40
41export interface BodyJSONContext<J = any> {
42 accepted: typeof JSON,
43 body: J
44 json: J
45}
46
47export interface BodyVendorJSONContext<J = any> {
48 accepted: `application/${string}+json`,
49 body: J
50 json: J
51}
52
53export interface BodyFormContext {
54 accepted: typeof FORM,
55 body: URLSearchParams,
56 form: URLSearchParams,
57 // form: { [key: string]: string }
58}
59
60export interface BodyFormDataContext {
61 accepted: typeof FORM_DATA,
62 body: FormData
63 formData: FormData
64 // form: { [key: string]: string }
65 // files: { [key: string]: File }
66}
67
68export interface BodyBinaryContext {
69 accepted: typeof BINARY | typeof X_BINARY
70 body: ArrayBuffer,
71 arrayBuffer: ArrayBuffer,
72 blob: Blob,
73}
74
75// export interface BodyVendorBinaryContext {
76// accepted: `application/vnd.${string}`,
77// body: ArrayBuffer,
78// buffer: ArrayBuffer,
79// blob: Blob,
80// }
81
82export interface BodyTextContext {
83 accepted: `text/${string}`,
84 body: string,
85 text: string,
86}
87
88// export interface BodyGenericBinaryContext {
89// accepted: `application/${string}` ,
90// text: string
91// }
92// NOT working because TS lacks a "any string except 'json', 'x-www-form-urlencoded' and 'octet-stream'" type,
93// which we need to make this work. Progress here: https://github.com/microsoft/TypeScript/pull/29317
94
95export type BodyContext<J> =
96 | BodyJSONContext<J>
97 | BodyBinaryContext
98 | BodyFormContext
99 | BodyFormDataContext
100 | BodyTextContext
101 | BodyVendorJSONContext<J>
102// Not possible to provide a fallback rn: https://github.com/microsoft/TypeScript/issues/48073
103
104const _isString = (x: [string, FormDataEntryValue]): x is [string, string] => !(x[1] instanceof File)
105const _isFile = (x: [string, FormDataEntryValue]): x is [string, File] => x[1] instanceof File
106
107export type BodyParserDeps = Context & Accepted<BodyParsable>
108
109const isBodyTextContext = <J = any>(nx: BodyContext<J>): nx is BodyTextContext =>
110 nx.accepted?.startsWith('text/')
111
112const isBodyVendorJSONContext = <J = any>(nx: BodyContext<J>): nx is BodyVendorJSONContext<J> =>
113 nx.accepted?.startsWith('application/') && nx.accepted.endsWith('+json')
114
115// const isBodyVendorBinaryContext = <J = any>(nx: BodyContext<J>): nx is BodyVendorBinaryContext =>
116// nx.accepted?.startsWith('application/vnd.')
117
118const MB = 1024**2
119
120async function checkSize(req: Request, maxSize: number) {
121 let size = 0;
122 await req.clone().body!.pipeTo(
123 new WritableStream({
124 write(chunk, ctrl) {
125 size += chunk.byteLength
126 if (size > maxSize) {
127 ctrl.error(new Error('Payload too large'))
128 }
129 }
130 }))
131 return size <= maxSize
132}
133
134export const bodyParser = <J = any>(
135 opts: BodyParserOptions<J> = {},
136) => async <X extends BodyParserDeps>(
137 ax: Awaitable<X>
138): Promise<X & BodyContext<J>> => {
139 const x = await ax;
140 const nx = x as X & BodyContext<J>;
141
142 const ok = await checkSize(x.request, opts.maxSize ?? 1 * MB)
143 if (!ok) throw payloadTooLarge()
144
145 switch (nx.accepted) {
146 case JSON: {
147 nx.body = nx.json = await x.request.json()
148 return nx;
149 }
150 case FORM: {
151 nx.body = nx.form = new URLSearchParams(await x.request.text())
152 // FIXME: Multiple values per key??
153 // nx.form = Object.fromEntries(form);
154 return nx;
155 }
156 case FORM_DATA: {
157 nx.body = nx.formData = await x.request.formData();
158 // FIXME: Multiple values per key??
159 // const tuples = [...formData];
160 // nx.form = Object.fromEntries(tuples.filter(isString));
161 // nx.files = Object.fromEntries(tuples.filter(isFile));
162 return nx;
163 }
164 case BINARY:
165 case X_BINARY: {
166 nx.body = nx.arrayBuffer = await x.request.arrayBuffer();
167 nx.blob = new Blob([nx.arrayBuffer]) // TODO: does this copy??
168 return nx;
169 }
170 default: {
171 if (isBodyTextContext(nx)) {
172 nx.body = nx.text = await x.request.text();
173 } else if (isBodyVendorJSONContext(nx)) {
174 nx.body = nx.json = await x.request.json()
175 return nx;
176 // } else if (isBodyVendorBinaryContext(nx)) {
177 // nx.body = nx.buffer = await x.request.arrayBuffer();
178 // nx.blob = new Blob([nx.buffer]) // TODO: does this copy??
179 // return nx;
180 } else {
181 // Anything else gets the binary treatment (outside of scope of type system)
182 (<any>nx).body = (<any>nx).buffer = await x.request.arrayBuffer();
183 (<any>nx).blob = new Blob([(<any>nx).buffer])
184 return nx;
185 }
186 return nx;
187 }
188 }
189 }
190
191export const defaultBodyParser = <J = any>(options?: BodyParserOptions<J>) =>
192 combine(accepts(defaultBody), bodyParser(options));
193
194// type ErrorOf<T> = T extends { error?: infer E } ? E : never
195
196// (async () => {
197// const ctx: Context = { request: new Request('/'), effects: [], waitUntil: (_f: any) => {}, handled: Promise.resolve(null as any) }
198// const z = provides([])(accepts([])(ctx))
199
200
201// const x = await parseBody()(accepts(['text/x-foo', 'application/vnd.github.v3+json', FORM, FORM_DATA])(ctx))
202// if (x.accepted === 'application/vnd.github.v3+json') {
203// x.body
204// } else if (x.accepted === 'text/x-foo') {
205// x.body
206// } else if (x.accepted === 'application/x-www-form-urlencoded') {
207// x.body
208// }
209
210// const y = await bodyParser()(ctx)
211// if (y.accepted === 'application/x-www-form-urlencoded') {
212// y.bodyParams
213// y.body
214// }
215// if (y.accepted === 'multipart/form-data') {
216// y.formData
217// y.body
218// }
219// if (y.accepted === 'application/foobar+json') {
220// y.json
221// y.body
222// }
223// // if (x.accepted === 'application/x-www-form-urlencoded') {
224// // x.body
225// // x.bodyParams
226// // x.form
227// // }
228// // else if (x.accepted === 'multipart/form-data') {
229// // x.formData
230// // x.form
231// // x.files
232// // } else if (x.accepted === 'application/octet-stream' || x.accepted === 'application/x-binary') {
233// // x.buffer
234// // x.blob
235// // } else if (x.accepted === 'application/vnd.github.v3+json') {
236
237// // } else if (x.accepted === 'text/foo') {
238// // x.text
239// // }
240// })
241