UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

128 lines (105 loc) 3.6 kB
import { ReadableStream as WebStream } from "node:stream/web"; import { $hook, $inject, Alepha, AlephaError } from "alepha"; import { $logger } from "alepha/logger"; import { routeMethods, type ServerHandler, type ServerRequest, ServerRouterProvider, } from "alepha/server"; import { $proxy, type ProxyPrimitiveOptions } from "../primitives/$proxy.ts"; export class ServerProxyProvider { protected readonly log = $logger(); protected readonly routerProvider = $inject(ServerRouterProvider); protected readonly alepha = $inject(Alepha); protected readonly configure = $hook({ on: "configure", handler: () => { for (const proxy of this.alepha.primitives($proxy)) { this.createProxy(proxy.options); } }, }); public createProxy(options: ProxyPrimitiveOptions): void { if (options.disabled) { return; } const path = options.path; const target = typeof options.target === "function" ? options.target() : options.target; if (!path.endsWith("/*")) { throw new AlephaError("Proxy path should end with '/*'"); } // Extract base path without /* const handler = this.createProxyHandler(target, options); for (const method of routeMethods) { this.routerProvider.createRoute({ method, path, handler, }); } this.log.info("Proxying", { path, target }); } public createProxyHandler( target: string, options: Omit<ProxyPrimitiveOptions, "path">, ): ServerHandler { return async (request) => { const url = new URL(target + request.url.pathname); if (request.url.search) { url.search = request.url.search; } options.rewrite?.(url); const requestInit = { url: url.toString(), method: request.method, headers: { ...request.headers, "accept-encoding": "identity", // ignore compression }, body: this.getRawRequestBody(request), }; if (requestInit.body) { (requestInit as any).duplex = "half"; } if (options.beforeRequest) { await options.beforeRequest(request, requestInit); } this.log.debug("Proxying request", { url: url.toString(), method: request.method, headers: request.headers, }); // Note: `beforeRequest` may set `requestInit.fetcher` to a Cloudflare // service binding — a NATIVE workerd RequestInit field that routes the // subrequest through that Worker (same-zone subrequests to a host // served by another Worker's route would bypass it and 522). Node's // fetch ignores the unknown field. const response = await fetch(requestInit.url, requestInit); request.reply.status = response.status; request.reply.headers = Object.fromEntries(response.headers.entries()); request.reply.body = response.body; this.log.debug("Received response", { status: request.reply.status, headers: request.reply.headers, }); if (options.afterResponse) { await options.afterResponse(request, response); } }; } protected getRawRequestBody(req: ServerRequest): ReadableStream | undefined { const { method } = req; if (method === "GET" || method === "HEAD" || method === "OPTIONS") { return; } if (req.raw?.web?.req) { return req.raw.web.req.body as ReadableStream; } if (req.raw?.node?.req) { const nodeReq = req.raw.node.req; return WebStream.from(nodeReq) as unknown as ReadableStream; } } }