import type { IncomingMessage, ServerResponse } from "node:http" import type { ApiCoreHeaders, ApiCoreRequestFormData, ApiCoreStream } from "../api/api-core.ts" import { ApiCore } from "../api/api-core.ts" export class NodeHttpApiCoreStream implements ApiCoreStream { protected readonly response: ServerResponse constructor(response: ServerResponse) { this.response = response } async write(chunk: Buffer | string, encoding: BufferEncoding): Promise { await new Promise((resolve, reject) => { this.response.write(chunk, encoding, (error) => { if (error !== undefined) { reject(error instanceof Error ? error : new Error(String(error))) return } resolve() }) }) } async end(): Promise { await new Promise((resolve) => { this.response.end(() => { resolve() }) }) } } export interface ApiCoreNodeHttpOptions { request: IncomingMessage response: ServerResponse } export class ApiCoreNodeHttp extends ApiCore { protected readonly request: IncomingMessage protected readonly response: ServerResponse protected requestUrl: URL | undefined protected bodyBufferTask: Promise | undefined protected bodyTextTask: Promise | undefined protected bodyFormDataTask: Promise | undefined constructor(options: ApiCoreNodeHttpOptions) { super() this.request = options.request this.response = options.response this.bodyBufferTask = undefined this.bodyTextTask = undefined this.bodyFormDataTask = undefined this.requestUrl = undefined } async getRequestHeaders(): Promise { const headers: ApiCoreHeaders = {} for (const [name, rawValue] of Object.entries(this.request.headers)) { if (Array.isArray(rawValue)) { const [firstValue, ...restValues] = rawValue if (firstValue === undefined) { headers[name] = undefined continue } headers[name] = [firstValue, ...restValues] continue } headers[name] = rawValue === undefined ? undefined : [rawValue] } return await Promise.resolve(headers) } getRequestUrl(): string { let requestUrl = this.requestUrl if (requestUrl === undefined) { const partialUrl = this.request.url ?? "/" const host = this.request.headers.host if (host === undefined || host.length === 0) { requestUrl = new URL(partialUrl, "http://127.0.0.1") } else { requestUrl = new URL(partialUrl, `http://${host}`) } this.requestUrl = requestUrl } return requestUrl.href } async getRequestMethod(): Promise { const method = this.request.method ?? "GET" return await Promise.resolve(method) } protected async getRequestBodyBuffer(): Promise { if (this.bodyBufferTask !== undefined) { return await this.bodyBufferTask } this.bodyBufferTask = new Promise((resolve, reject) => { const chunks: Buffer[] = [] this.request.on("data", (chunk: Buffer | string) => { if (typeof chunk === "string") { chunks.push(Buffer.from(chunk)) return } chunks.push(chunk) }) this.request.once("aborted", () => { reject(new Error("Request body was aborted.")) }) this.request.once("error", (error) => { reject(error) }) this.request.once("end", () => { resolve(Buffer.concat(chunks)) }) }) return await this.bodyBufferTask } async getRequestBodyText(): Promise { if (this.bodyTextTask !== undefined) { return await this.bodyTextTask } this.bodyTextTask = this.getRequestBodyBuffer().then((bodyBuffer) => { return bodyBuffer.toString("utf8") }) return await this.bodyTextTask } async getRequestBodyFormData(): Promise { if (this.bodyFormDataTask !== undefined) { return await this.bodyFormDataTask } this.bodyFormDataTask = (async () => { const contentType = this.request.headers["content-type"] if ( contentType === undefined || /^(application\/x-www-form-urlencoded|multipart\/form-data)\b/i.test(contentType) === false ) { return [] } const bodyBuffer = await this.getRequestBodyBuffer() const requestHeaders = Object.entries(await this.getRequestHeaders()).flatMap( ([name, value]) => { if (value === undefined) { return [] } return value.map((item) => { return [name, item] as [string, string] }) }, ) const request = new Request(this.getRequestUrl(), { method: await this.getRequestMethod(), headers: requestHeaders, body: new Uint8Array(bodyBuffer), }) const formData = await request.formData() return Array.from(formData.entries()).map(([name, value]) => { return { name, value, } }) })() return await this.bodyFormDataTask } protected prepareResponse(contentType: string, statusCode: number = 200): void { if (this.response.writableEnded === true) { return } if (this.response.headersSent === false) { this.response.statusCode = statusCode this.response.setHeader("Content-Type", contentType) } } async text(data: unknown): Promise { this.prepareResponse("text/plain; charset=utf-8") await new Promise((resolve) => { this.response.end(String(data), () => { resolve() }) }) } async json(data: unknown): Promise { this.prepareResponse("application/json; charset=utf-8") await new Promise((resolve) => { this.response.end(JSON.stringify(data), () => { resolve() }) }) } async stream(): Promise { this.prepareResponse("text/event-stream; charset=utf-8") this.response.setHeader("Cache-Control", "no-cache") this.response.setHeader("Connection", "keep-alive") this.response.flushHeaders() const stream = new NodeHttpApiCoreStream(this.response) return await Promise.resolve(stream) } async error(): Promise { if (this.response.writableEnded === true) { return await Promise.resolve() } this.prepareResponse("application/json; charset=utf-8", 500) await new Promise((resolve) => { this.response.end( JSON.stringify({ status: "error", data: { reason: "internal_server_error", }, }), () => { resolve() }, ) }) } } export const createApiCoreNodeHttp = (options: ApiCoreNodeHttpOptions): ApiCoreNodeHttp => { return new ApiCoreNodeHttp(options) }