// deno-lint-ignore-file no-explicit-any import type { Awaitable } from "./utils/common-types.ts"; import { combine, Context } from "./context.ts"; import { accepts, Accepted } from './content-negotiation.ts' import { payloadTooLarge } from 'https://ghuc.cc/worker-tools/response-creators/index.ts' export const JSON = 'application/json'; export const FORM = 'application/x-www-form-urlencoded' export const FORM_DATA = 'multipart/form-data'; export const TEXT_HTML = 'text/html' export const TEXT_PLAIN = 'text/plain' /** Standard MIME type for binary data */ export const BINARY = 'application/octet-stream'; /** Non-standard MIME type for binary data. Sometimes used, so included anyway. */ export const X_BINARY = 'application/x-binary'; export interface BodyParserOptions { defaultJSON?: J, maxSize?: number } export type BodyParsable = | typeof FORM | typeof FORM_DATA | typeof JSON | typeof BINARY | typeof X_BINARY | `application/${string}+json` | `text/${string}` const defaultBody: BodyParsable[] = [ FORM, FORM_DATA, JSON, BINARY, X_BINARY, TEXT_HTML, TEXT_PLAIN, ] export interface BodyJSONContext { accepted: typeof JSON, body: J json: J } export interface BodyVendorJSONContext { accepted: `application/${string}+json`, body: J json: J } export interface BodyFormContext { accepted: typeof FORM, body: URLSearchParams, form: URLSearchParams, // form: { [key: string]: string } } export interface BodyFormDataContext { accepted: typeof FORM_DATA, body: FormData formData: FormData // form: { [key: string]: string } // files: { [key: string]: File } } export interface BodyBinaryContext { accepted: typeof BINARY | typeof X_BINARY body: ArrayBuffer, arrayBuffer: ArrayBuffer, blob: Blob, } // export interface BodyVendorBinaryContext { // accepted: `application/vnd.${string}`, // body: ArrayBuffer, // buffer: ArrayBuffer, // blob: Blob, // } export interface BodyTextContext { accepted: `text/${string}`, body: string, text: string, } // export interface BodyGenericBinaryContext { // accepted: `application/${string}` , // text: string // } // NOT working because TS lacks a "any string except 'json', 'x-www-form-urlencoded' and 'octet-stream'" type, // which we need to make this work. Progress here: https://github.com/microsoft/TypeScript/pull/29317 export type BodyContext = | BodyJSONContext | BodyBinaryContext | BodyFormContext | BodyFormDataContext | BodyTextContext | BodyVendorJSONContext // Not possible to provide a fallback rn: https://github.com/microsoft/TypeScript/issues/48073 const _isString = (x: [string, FormDataEntryValue]): x is [string, string] => !(x[1] instanceof File) const _isFile = (x: [string, FormDataEntryValue]): x is [string, File] => x[1] instanceof File export type BodyParserDeps = Context & Accepted const isBodyTextContext = (nx: BodyContext): nx is BodyTextContext => nx.accepted?.startsWith('text/') const isBodyVendorJSONContext = (nx: BodyContext): nx is BodyVendorJSONContext => nx.accepted?.startsWith('application/') && nx.accepted.endsWith('+json') // const isBodyVendorBinaryContext = (nx: BodyContext): nx is BodyVendorBinaryContext => // nx.accepted?.startsWith('application/vnd.') const MB = 1024**2 async function checkSize(req: Request, maxSize: number) { let size = 0; await req.clone().body!.pipeTo( new WritableStream({ write(chunk, ctrl) { size += chunk.byteLength if (size > maxSize) { ctrl.error(new Error('Payload too large')) } } })) return size <= maxSize } export const bodyParser = ( opts: BodyParserOptions = {}, ) => async ( ax: Awaitable ): Promise> => { const x = await ax; const nx = x as X & BodyContext; const ok = await checkSize(x.request, opts.maxSize ?? 1 * MB) if (!ok) throw payloadTooLarge() switch (nx.accepted) { case JSON: { nx.body = nx.json = await x.request.json() return nx; } case FORM: { nx.body = nx.form = new URLSearchParams(await x.request.text()) // FIXME: Multiple values per key?? // nx.form = Object.fromEntries(form); return nx; } case FORM_DATA: { nx.body = nx.formData = await x.request.formData(); // FIXME: Multiple values per key?? // const tuples = [...formData]; // nx.form = Object.fromEntries(tuples.filter(isString)); // nx.files = Object.fromEntries(tuples.filter(isFile)); return nx; } case BINARY: case X_BINARY: { nx.body = nx.arrayBuffer = await x.request.arrayBuffer(); nx.blob = new Blob([nx.arrayBuffer]) // TODO: does this copy?? return nx; } default: { if (isBodyTextContext(nx)) { nx.body = nx.text = await x.request.text(); } else if (isBodyVendorJSONContext(nx)) { nx.body = nx.json = await x.request.json() return nx; // } else if (isBodyVendorBinaryContext(nx)) { // nx.body = nx.buffer = await x.request.arrayBuffer(); // nx.blob = new Blob([nx.buffer]) // TODO: does this copy?? // return nx; } else { // Anything else gets the binary treatment (outside of scope of type system) (nx).body = (nx).buffer = await x.request.arrayBuffer(); (nx).blob = new Blob([(nx).buffer]) return nx; } return nx; } } } export const defaultBodyParser = (options?: BodyParserOptions) => combine(accepts(defaultBody), bodyParser(options)); // type ErrorOf = T extends { error?: infer E } ? E : never // (async () => { // const ctx: Context = { request: new Request('/'), effects: [], waitUntil: (_f: any) => {}, handled: Promise.resolve(null as any) } // const z = provides([])(accepts([])(ctx)) // const x = await parseBody()(accepts(['text/x-foo', 'application/vnd.github.v3+json', FORM, FORM_DATA])(ctx)) // if (x.accepted === 'application/vnd.github.v3+json') { // x.body // } else if (x.accepted === 'text/x-foo') { // x.body // } else if (x.accepted === 'application/x-www-form-urlencoded') { // x.body // } // const y = await bodyParser()(ctx) // if (y.accepted === 'application/x-www-form-urlencoded') { // y.bodyParams // y.body // } // if (y.accepted === 'multipart/form-data') { // y.formData // y.body // } // if (y.accepted === 'application/foobar+json') { // y.json // y.body // } // // if (x.accepted === 'application/x-www-form-urlencoded') { // // x.body // // x.bodyParams // // x.form // // } // // else if (x.accepted === 'multipart/form-data') { // // x.formData // // x.form // // x.files // // } else if (x.accepted === 'application/octet-stream' || x.accepted === 'application/x-binary') { // // x.buffer // // x.blob // // } else if (x.accepted === 'application/vnd.github.v3+json') { // // } else if (x.accepted === 'text/foo') { // // x.text // // } // })