import { AsyncIterableReader } from "./async_iterable_reader.ts";import { contentType, readerFromStreamReader, Status, STATUS_TEXT,} from "./deps.ts";import { DomResponse } from "./http_server_native.ts";import type { ServerResponse } from "./http_server_std.ts";import type { Request } from "./request.ts";import { BODY_TYPES, encodeUrl, isAsyncIterable, isHtml, isReader, isRedirectStatus, readableStreamFromReader, Uint8ArrayTransformStream,} from "./util.ts";
type Body = | string | number | bigint | boolean | symbol | object | undefined | null;type BodyFunction = () => Body | Promise<Body>;
export const REDIRECT_BACK = Symbol("redirect backwards");
const encoder = new TextEncoder();
function toUint8Array(body: Body): Uint8Array { let bodyText: string; if (BODY_TYPES.includes(typeof body)) { bodyText = String(body); } else { bodyText = JSON.stringify(body); } return encoder.encode(bodyText);}
async function convertBodyToBodyInit( body: Body | BodyFunction, type?: string,): Promise<[globalThis.BodyInit | undefined, string | undefined]> { let result: globalThis.BodyInit | undefined; if (BODY_TYPES.includes(typeof body)) { result = String(body); type = type ?? (isHtml(result) ? "html" : "text/plain"); } else if (isReader(body)) { result = readableStreamFromReader(body); } else if ( ArrayBuffer.isView(body) || body instanceof ArrayBuffer || body instanceof Blob || body instanceof URLSearchParams ) { result = body; } else if (body instanceof ReadableStream) { result = body.pipeThrough(new Uint8ArrayTransformStream()); } else if (body instanceof FormData) { result = body; type = "multipart/form-data"; } else if (body && typeof body === "object") { result = JSON.stringify(body); type = type ?? "json"; } else if (typeof body === "function") { const result = body.call(null); return convertBodyToBodyInit(await result, type); } else if (body) { throw new TypeError("Response body was set but could not be converted."); } return [result, type];}
async function convertBodyToStdBody( body: Body | BodyFunction, type?: string,): Promise<[Uint8Array | Deno.Reader | undefined, string | undefined]> { let result: Uint8Array | Deno.Reader | undefined; if (BODY_TYPES.includes(typeof body)) { const bodyText = String(body); result = encoder.encode(bodyText); type = type ?? (isHtml(bodyText) ? "html" : "text/plain"); } else if (body instanceof Uint8Array || isReader(body)) { result = body; } else if (body instanceof ReadableStream) { result = readerFromStreamReader( body.pipeThrough(new Uint8ArrayTransformStream()).getReader(), ); } else if (isAsyncIterable(body)) { result = new AsyncIterableReader(body, toUint8Array); } else if (body && typeof body === "object") { result = encoder.encode(JSON.stringify(body)); type = type ?? "json"; } else if (typeof body === "function") { const result = body.call(null); return convertBodyToStdBody(await result, type); } else if (body) { throw new TypeError("Response body was set but could not be converted."); } return [result, type];}
export class Response { #body?: Body | BodyFunction; #domResponse?: globalThis.Response; #headers = new Headers(); #request: Request; #resources: number[] = []; #serverResponse?: ServerResponse; #status?: Status; #type?: string; #writable = true;
#getBodyInit = async (): Promise<globalThis.BodyInit | undefined> => { const [body, type] = await convertBodyToBodyInit(this.body, this.type); this.type = type; return body; };
#getStdBody = async (): Promise<Uint8Array | Deno.Reader | undefined> => { const [body, type] = await convertBodyToStdBody(this.body, this.type); this.type = type; return body; };
#setContentType = (): void => { if (this.type) { const contentTypeString = contentType(this.type); if (contentTypeString && !this.headers.has("Content-Type")) { this.headers.append("Content-Type", contentTypeString); } } };
get body(): Body | BodyFunction { return this.#body; }
set body(value: Body | BodyFunction) { if (!this.#writable) { throw new Error("The response is not writable."); } this.#body = value; }
get headers(): Headers { return this.#headers; }
set headers(value: Headers) { if (!this.#writable) { throw new Error("The response is not writable."); } this.#headers = value; }
get status(): Status { if (this.#status) { return this.#status; } const typeofbody = typeof this.body; return this.body && (BODY_TYPES.includes(typeofbody) || typeofbody === "object") ? Status.OK : Status.NotFound; }
set status(value: Status) { if (!this.#writable) { throw new Error("The response is not writable."); } this.#status = value; }
get type(): string | undefined { return this.#type; } set type(value: string | undefined) { if (!this.#writable) { throw new Error("The response is not writable."); } this.#type = value; }
get writable(): boolean { return this.#writable; }
constructor(request: Request) { this.#request = request; }
addResource(rid: number): void { this.#resources.push(rid); }
destroy(closeResources = true): void { this.#writable = false; this.#body = undefined; this.#serverResponse = undefined; this.#domResponse = undefined; if (closeResources) { for (const rid of this.#resources) { Deno.close(rid); } } }
redirect(url: string | URL): void; redirect(url: typeof REDIRECT_BACK, alt?: string | URL): void; redirect( url: string | URL | typeof REDIRECT_BACK, alt: string | URL = "/", ): void { if (url === REDIRECT_BACK) { url = this.#request.headers.get("Referrer") ?? String(alt); } else if (typeof url === "object") { url = String(url); } this.headers.set("Location", encodeUrl(url)); if (!this.status || !isRedirectStatus(this.status)) { this.status = Status.Found; }
if (this.#request.accepts("html")) { url = encodeURI(url); this.type = "text/html; charset=utf-8"; this.body = `Redirecting to <a href="${url}">${url}</a>.`; return; } this.type = "text/plain; charset=utf-8"; this.body = `Redirecting to ${url}.`; }
async toDomResponse(): Promise<globalThis.Response> { if (this.#domResponse) { return this.#domResponse; }
const bodyInit = await this.#getBodyInit();
this.#setContentType();
const { headers } = this;
const status = this.#status ?? (bodyInit ? Status.OK : Status.NotFound);
if ( !( bodyInit || headers.has("Content-Type") || headers.has("Content-Length") ) ) { headers.append("Content-Length", "0"); }
this.#writable = false;
const responseInit: ResponseInit = { headers, status, statusText: STATUS_TEXT.get(status), };
return this.#domResponse = new DomResponse(bodyInit, responseInit); }
async toServerResponse(): Promise<ServerResponse> { if (this.#serverResponse) { return this.#serverResponse; } const body = await this.#getStdBody();
this.#setContentType();
const { headers } = this;
if ( !( body || headers.has("Content-Type") || headers.has("Content-Length") ) ) { headers.append("Content-Length", "0"); }
this.#writable = false; return this.#serverResponse = { status: this.#status ?? (body ? Status.OK : Status.NotFound), body, headers, }; }}