@typespec/http-server-js
Version:
TypeSpec HTTP server code generator for JavaScript
257 lines (248 loc) • 9.48 kB
text/typescript
// Copyright (c) Microsoft Corporation
// Licensed under the MIT license.
import { Module } from "../../src/ctx.js";
export let module: Module = undefined as any;
// prettier-ignore
const lines = [
"// Copyright (c) Microsoft Corporation",
"// Licensed under the MIT license.",
"",
"import type * as http from \"node:http\";",
"",
"export interface HttpPart {",
" headers: { [k: string]: string | undefined };",
" body: ReadableStream<Buffer>;",
"}",
"",
"/**",
" * Consumes a stream of incoming data and splits it into individual streams for each part of a multipart request, using",
" * the provided `boundary` value.",
" */",
"function MultipartBoundaryTransformStream(",
" boundary: string,",
"): ReadableWritablePair<ReadableStream<Buffer>, Buffer> {",
" let buffer: Buffer = Buffer.alloc(0);",
" // Initialize subcontroller to an object that does nothing. Multipart bodies may contain a preamble before the first",
" // boundary, so this dummy controller will discard it.",
" let subController: { enqueue(chunk: Buffer): void; close(): void } | null = {",
" enqueue() {},",
" close() {},",
" };",
"",
" let boundarySplit = Buffer.from(`--${boundary}`);",
" let initialized = false;",
"",
" // We need to keep at least the length of the boundary split plus room for CRLFCRLF in the buffer to detect the boundaries.",
" // We subtract one from this length because if the whole thing were in the buffer, we would detect it and move past it.",
" const bufferKeepLength = boundarySplit.length + BUF_CRLFCRLF.length - 1;",
" let _readableController: ReadableStreamDefaultController<ReadableStream<Buffer>> = null as any;",
"",
" const readable = new ReadableStream<ReadableStream<Buffer>>({",
" start(controller) {",
" _readableController = controller;",
" },",
" });",
"",
" const readableController = _readableController;",
"",
" const writable = new WritableStream<Buffer>({",
" write: async (chunk) => {",
" buffer = Buffer.concat([buffer, chunk]);",
"",
" let index: number;",
"",
" while ((index = buffer.indexOf(boundarySplit)) !== -1) {",
" // We found a boundary, emit everything before it and initialize a new stream for the next part.",
"",
" // We are initialized if we have found the boundary at least once.",
" //",
" // Cases",
" // 1. If the index is zero and we aren't initialized, there was no preamble.",
" // 2. If the index is zero and we are initialized, then we had to have found \\r\\n--boundary, nothing special to do.",
" // 3. If the index is not zero, and we are initialized, then we found \\r\\n--boundary somewhere in the middle,",
" // nothing special to do.",
" // 4. If the index is not zero and we aren't initialized, then we need to check that boundarySplit was preceded",
" // by \\r\\n for validity, because the preamble must end with \\r\\n.",
"",
" if (index > 0) {",
" if (!initialized) {",
" if (!buffer.subarray(index - 2, index).equals(Buffer.from(\"\\r\\n\"))) {",
" readableController.error(new Error(\"Invalid preamble in multipart body.\"));",
" } else {",
" await enqueueSub(buffer.subarray(0, index - 2));",
" }",
" } else {",
" await enqueueSub(buffer.subarray(0, index));",
" }",
" }",
"",
" // We enqueued everything before the boundary, so we clear the buffer past the boundary",
" buffer = buffer.subarray(index + boundarySplit.length);",
"",
" // We're done with the current part, so close the stream. If this is the opening boundary, there won't be a",
" // subcontroller yet.",
" subController?.close();",
" subController = null;",
"",
" if (!initialized) {",
" initialized = true;",
" boundarySplit = Buffer.from(`\\r\\n${boundarySplit}`);",
" }",
" }",
"",
" if (buffer.length > bufferKeepLength) {",
" await enqueueSub(buffer.subarray(0, -bufferKeepLength));",
" buffer = buffer.subarray(-bufferKeepLength);",
" }",
" },",
" close() {",
" if (!/--(\\r\\n)?/.test(buffer.toString(\"utf-8\"))) {",
" readableController.error(new Error(\"Unexpected characters after final boundary.\"));",
" }",
"",
" subController?.close();",
"",
" readableController.close();",
" },",
" });",
"",
" async function enqueueSub(s: Buffer) {",
" subController ??= await new Promise<ReadableStreamDefaultController>((resolve) => {",
" readableController.enqueue(",
" new ReadableStream<Buffer>({",
" start: (controller) => resolve(controller),",
" }),",
" );",
" });",
"",
" subController.enqueue(s);",
" }",
"",
" return { readable, writable };",
"}",
"",
"const BUF_CRLFCRLF = Buffer.from(\"\\r\\n\\r\\n\");",
"",
"/**",
" * Consumes a stream of the contents of a single part of a multipart request and emits an `HttpPart` object for each part.",
" * This consumes just enough of the stream to read the headers, and then forwards the rest of the stream as the body.",
" */",
"class HttpPartTransform extends TransformStream<ReadableStream<Buffer>, HttpPart> {",
" constructor() {",
" super({",
" transform: async (partRaw, controller) => {",
" const reader = partRaw.getReader();",
"",
" let buf = Buffer.alloc(0);",
" let idx;",
"",
" while ((idx = buf.indexOf(BUF_CRLFCRLF)) === -1) {",
" const { done, value } = await reader.read();",
" if (done) {",
" throw new Error(\"Unexpected end of part.\");",
" }",
" buf = Buffer.concat([buf, value]);",
" }",
"",
" const headerText = buf.subarray(0, idx).toString(\"utf-8\").trim();",
"",
" const headers = Object.fromEntries(",
" headerText.split(\"\\r\\n\").map((line) => {",
" const [name, value] = line.split(\": \", 2);",
"",
" return [name.toLowerCase(), value];",
" }),",
" ) as { [k: string]: string };",
"",
" const body = new ReadableStream<Buffer>({",
" start(controller) {",
" controller.enqueue(buf.subarray(idx + BUF_CRLFCRLF.length));",
" },",
" async pull(controller) {",
" const { done, value } = await reader.read();",
"",
" if (done) {",
" controller.close();",
" } else {",
" controller.enqueue(value);",
" }",
" },",
" });",
"",
" controller.enqueue({ headers, body });",
" },",
" });",
" }",
"}",
"",
"/**",
" * Processes a request as a multipart request, returning a stream of `HttpPart` objects, each representing an individual",
" * part in the multipart request.",
" *",
" * Only call this function if you have already validated the content type of the request and confirmed that it is a",
" * multipart request.",
" *",
" * @throws Error if the content-type header is missing or does not contain a boundary field.",
" *",
" * @param request - the incoming request to parse as multipart",
" * @returns a stream of HttpPart objects, each representing an individual part in the multipart request",
" */",
"export function createMultipartReadable(request: http.IncomingMessage): ReadableStream<HttpPart> {",
" const boundary = request.headers[\"content-type\"]",
" ?.split(\";\")",
" .find((s) => s.includes(\"boundary=\"))",
" ?.split(\"=\", 2)[1];",
" if (!boundary) {",
" throw new Error(\"Invalid request: missing boundary in content-type.\");",
" }",
"",
" const bodyStream = new ReadableStream<Uint8Array>({",
" start(controller) {",
" request.on(\"data\", (chunk: Buffer) => {",
" controller.enqueue(chunk);",
" });",
" request.on(\"end\", () => controller.close());",
" },",
" });",
"",
" return bodyStream",
" .pipeThrough(MultipartBoundaryTransformStream(boundary))",
" .pipeThrough(new HttpPartTransform());",
"}",
"",
"// Gross polyfill because Safari doesn't support this yet.",
"//",
"// https://bugs.webkit.org/show_bug.cgi?id=194379",
"// https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream#browser_compatibility",
"(ReadableStream.prototype as any)[Symbol.asyncIterator] ??= async function* () {",
" const reader = this.getReader();",
" try {",
" while (true) {",
" const { done, value } = await reader.read();",
" if (done) return value;",
" yield value;",
" }",
" } finally {",
" reader.releaseLock();",
" }",
"};",
"",
"declare global {",
" interface ReadableStream<R> {",
" [Symbol.asyncIterator](): AsyncIterableIterator<R>;",
" }",
"}",
"",
];
export async function createModule(parent: Module): Promise<Module> {
if (module) return module;
module = {
name: "multipart",
cursor: parent.cursor.enter("multipart"),
imports: [],
declarations: [],
};
module.declarations.push(lines);
parent.declarations.push(module);
return module;
}