import { ServerRequest } from "./deps.ts";import { httpErrors } from "./httpError.ts";import { isMediaType } from "./isMediaType.ts";import { FormDataReader } from "./multipart.ts";import { HTTPMethods } from "./types.d.ts";import { preferredCharsets } from "./negotiation/charset.ts";import { preferredEncodings } from "./negotiation/encoding.ts";import { preferredLanguages } from "./negotiation/language.ts";import { preferredMediaTypes } from "./negotiation/mediaType.ts";
export type BodyType = | "json" | "form" | "form-data" | "text" | "raw" | "undefined" | "reader";
export type BodyJson = { type: "json"; value: any };export type BodyForm = { type: "form"; value: URLSearchParams };export type BodyFormData = { type: "form-data"; value: FormDataReader };export type BodyText = { type: "text"; value: string };export type BodyRaw = { type: "raw"; value: Uint8Array };export type BodyUndefined = { type: "undefined"; value: undefined };
export type BodyReader = { type: "reader"; value: Deno.Reader };
export type Body = | BodyJson | BodyForm | BodyFormData | BodyText | BodyRaw | BodyUndefined;
export interface BodyOptions { asReader?: boolean;
contentTypes?: { raw?: string[]; json?: string[]; form?: string[]; formData?: string[]; text?: string[]; };}
export interface BodyOptionsAsReader extends BodyOptions { asReader: true;}
const decoder = new TextDecoder();
export interface BodyContentTypes { json?: string[]; form?: string[]; text?: string[];}
const defaultBodyContentTypes = { json: ["json", "application/*+json", "application/csp-report"], form: ["urlencoded"], formData: ["multipart"], text: ["text"],};
export class Request { #body?: Body | BodyReader; #rawBodyPromise?: Promise<Uint8Array>; #serverRequest: ServerRequest; #url?: URL;
get hasBody(): boolean { return ( this.headers.get("transfer-encoding") !== null || !!parseInt(this.headers.get("content-length") ?? "") ); }
get headers(): Headers { return this.#serverRequest.headers; }
get method(): HTTPMethods { return this.#serverRequest.method as HTTPMethods; }
get secure(): boolean { return this.url.protocol === "https:"; }
get serverRequest(): ServerRequest { return this.#serverRequest; }
get url(): URL { if (!this.#url) { const serverRequest = this.#serverRequest; const proto = serverRequest.proto.split("/")[0].toLowerCase(); this.#url = new URL( `${proto}://${serverRequest.headers.get("host")}${serverRequest.url}`, ); } return this.#url; }
constructor( serverRequest: ServerRequest, ) { this.#serverRequest = serverRequest; }
accepts(): string[] | undefined; accepts(...types: string[]): string | undefined; accepts(...types: string[]): string | string[] | undefined { const acceptValue = this.#serverRequest.headers.get("Accept"); if (!acceptValue) { return; } if (types.length) { return preferredMediaTypes(acceptValue, types)[0]; } return preferredMediaTypes(acceptValue); }
acceptsCharsets(): string[] | undefined; acceptsCharsets(...charsets: string[]): string | undefined; acceptsCharsets(...charsets: string[]): string[] | string | undefined { const acceptCharsetValue = this.#serverRequest.headers.get( "Accept-Charset", ); if (!acceptCharsetValue) { return; } if (charsets.length) { return preferredCharsets(acceptCharsetValue, charsets)[0]; } return preferredCharsets(acceptCharsetValue); }
acceptsEncodings(): string[] | undefined; acceptsEncodings(...encodings: string[]): string | undefined; acceptsEncodings(...encodings: string[]): string[] | string | undefined { const acceptEncodingValue = this.#serverRequest.headers.get( "Accept-Encoding", ); if (!acceptEncodingValue) { return; } if (encodings.length) { return preferredEncodings(acceptEncodingValue, encodings)[0]; } return preferredEncodings(acceptEncodingValue); }
acceptsLanguages(): string[] | undefined; acceptsLanguages(...langs: string[]): string | undefined; acceptsLanguages(...langs: string[]): string[] | string | undefined { const acceptLanguageValue = this.#serverRequest.headers.get( "Accept-Language", ); if (!acceptLanguageValue) { return; } if (langs.length) { return preferredLanguages(acceptLanguageValue, langs)[0]; } return preferredLanguages(acceptLanguageValue); }
async body(options: BodyOptionsAsReader): Promise<BodyReader>; async body(options?: BodyOptions): Promise<Body>; async body( { asReader, contentTypes = {} }: BodyOptions = {}, ): Promise<Body | BodyReader> { if (this.#body) { if (asReader && this.#body.type !== "reader") { return Promise.reject( new TypeError(`Body already consumed as type: "${this.#body.type}".`), ); } else if (this.#body.type === "reader") { return Promise.reject( new TypeError(`Body already consumed as type: "reader".`), ); } return this.#body; } const encoding = this.headers.get("content-encoding") || "identity"; if (encoding !== "identity") { throw new httpErrors.UnsupportedMediaType( `Unsupported content-encoding: ${encoding}`, ); } if (!this.hasBody) { return (this.#body = { type: "undefined", value: undefined }); } const contentType = this.headers.get("content-type"); if (contentType) { if (asReader) { return (this.#body = { type: "reader", value: this.#serverRequest.body, }); } const contentTypesFormData = [ ...defaultBodyContentTypes.formData, ...(contentTypes.formData ?? []), ]; if (isMediaType(contentType, contentTypesFormData)) { return (this.#body = { type: "form-data", value: new FormDataReader(contentType, this.#serverRequest.body), }); } const rawBody = await (this.#rawBodyPromise ?? (this.#rawBodyPromise = Deno.readAll(this.#serverRequest.body))); const value = decoder.decode(rawBody); const contentTypesRaw = contentTypes.raw; const contentTypesJson = [ ...defaultBodyContentTypes.json, ...(contentTypes.json ?? []), ]; const contentTypesForm = [ ...defaultBodyContentTypes.form, ...(contentTypes.form ?? []), ]; const contentTypesText = [ ...defaultBodyContentTypes.text, ...(contentTypes.text ?? []), ]; console.log("contentType", contentType); if (contentTypesRaw && isMediaType(contentType, contentTypesRaw)) { return (this.#body = { type: "raw", value: rawBody }); } else if (isMediaType(contentType, contentTypesJson)) { return (this.#body = { type: "json", value: JSON.parse(value) }); } else if (isMediaType(contentType, contentTypesForm)) { return (this.#body = { type: "form", value: new URLSearchParams(value.replace(/\+/g, " ")), }); } else if (isMediaType(contentType, contentTypesText)) { return (this.#body = { type: "text", value }); } else { return (this.#body = { type: "raw", value: rawBody }); } } throw new httpErrors.UnsupportedMediaType( contentType ? `Unsupported content-type: ${contentType}` : "Missing content-type", ); }}