| 1 | export class FormBodyError extends Error { |
| 2 | constructor( |
| 3 | readonly status: 400 | 413 | 415, |
| 4 | message: string |
| 5 | ) { |
| 6 | super(message); |
| 7 | this.name = "FormBodyError"; |
| 8 | } |
| 9 | } |
| 10 | |
| 11 | export async function readBoundedUrlEncodedForm( |
| 12 | request: Request, |
| 13 | maxBytes: number |
| 14 | ): Promise<URLSearchParams> { |
| 15 | const mediaType = request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase(); |
| 16 | if (mediaType !== "application/x-www-form-urlencoded") { |
| 17 | throw new FormBodyError(415, "expected application/x-www-form-urlencoded"); |
| 18 | } |
| 19 | |
| 20 | const rawLength = request.headers.get("content-length"); |
| 21 | if (rawLength !== null) { |
| 22 | if (!/^\d+$/.test(rawLength)) throw new FormBodyError(400, "invalid Content-Length"); |
| 23 | if (Number(rawLength) > maxBytes) throw new FormBodyError(413, "payload too large"); |
| 24 | } |
| 25 | |
| 26 | if (!request.body) return new URLSearchParams(); |
| 27 | |
| 28 | const reader = request.body.getReader(); |
| 29 | const chunks: Uint8Array[] = []; |
| 30 | let total = 0; |
| 31 | while (true) { |
| 32 | const { done, value } = await reader.read(); |
| 33 | if (done) break; |
| 34 | total += value.byteLength; |
| 35 | if (total > maxBytes) { |
| 36 | try { |
| 37 | await reader.cancel("payload too large"); |
| 38 | } catch { |
| 39 | // A source cancellation error must not obscure the enforced size limit. |
| 40 | } |
| 41 | throw new FormBodyError(413, "payload too large"); |
| 42 | } |
| 43 | chunks.push(value); |
| 44 | } |
| 45 | |
| 46 | const bytes = new Uint8Array(total); |
| 47 | let offset = 0; |
| 48 | for (const chunk of chunks) { |
| 49 | bytes.set(chunk, offset); |
| 50 | offset += chunk.byteLength; |
| 51 | } |
| 52 | return new URLSearchParams(new TextDecoder().decode(bytes)); |
| 53 | } |
| 54 |