/** * Runs Effect HTTP server handlers at platform boundaries. * * This module turns effects that produce `HttpServerResponse` values into Web * `Request` handlers and other server adapters. It also applies middleware, * converts failures into responses, runs hooks before a response is sent, and * manages request scopes for streamed responses. * * @stability unstable * @since 4.0.0 */ import type * as Cause from "../Cause.ts" import * as Context from "../Context.ts" import * as Effect from "../Effect.ts" import * as Exit from "../Exit.ts" import * as Fiber from "../Fiber.ts" import { constVoid, dual } from "../Function.ts" import { contA, contE } from "../internal/core.ts" import { effectIsExit, fiberEnterUninterruptibleUnsafe, reportCauseUnsafe } from "../internal/effect.ts" import * as Layer from "../Layer.ts" import * as Scope from "../Scope.ts" import * as Stream from "../Stream.ts" import { nativeTracer } from "../Tracer.ts" import * as HttpBody from "./HttpBody.ts" import { type HttpMiddleware, isTracerDisabledUnsafe, tracer } from "./HttpMiddleware.ts" import { causeResponse, ClientAbort, HttpServerError, InternalError } from "./HttpServerError.ts" import { HttpServerRequest } from "./HttpServerRequest.ts" import * as Request from "./HttpServerRequest.ts" import type { HttpServerResponse } from "./HttpServerResponse.ts" import * as Response from "./HttpServerResponse.ts" import * as requestState from "./internal/requestState.ts" /** * Runs an HTTP server effect, sends the produced response with the supplied handler, and converts failures into HTTP responses. * On handler failure, the request scope closes with the original cause. * If the send handler returns an `HttpServerResponse`, such as the `101` * response of an upgraded request, middleware observes it in place of the * handler's response. * * @stability unstable * @category combinators * @since 4.0.0 */ export const toHandled = ( self: Effect.Effect, handleResponse: ( request: HttpServerRequest, response: HttpServerResponse ) => Effect.Effect, middleware?: HttpMiddleware | undefined ): Effect.Effect> => { const handleCause = (frame: RequestFrame, cause: Cause.Cause) => { frame.cause = cause const request = frame.request return Effect.flatMapEager(causeResponse(cause), ([response, cause]) => { const fiber = Fiber.getCurrent()! reportCauseUnsafe(fiber, cause) const handler = frame.preResponseHandler const cont = cause.reasons.length === 0 ? Effect.succeed(response) : Effect.failCause(cause) if (handler === undefined) { ;(request as any)[handledSymbol] = true return Effect.flatMapEager( handleResponse(request, response), () => cont ) } return Effect.flatMapEager( Effect.flatMapEager(handler(request, response), (response) => { ;(request as any)[handledSymbol] = true return handleResponse(request, response) }), () => cont ) }) } // Writes the response, applying any registered pre-response handler first. // Returns an `Exit` when the write completes synchronously. const sendResponse = ( frame: RequestFrame, response: HttpServerResponse ): Effect.Effect => { const request = frame.request const handler = frame.preResponseHandler if (handler === undefined) { ;(request as any)[handledSymbol] = true return Effect.mapEager( handleResponse(request, response), (sent) => Response.isHttpServerResponse(sent) ? sent : response ) } return Effect.flatMapEager(handler(request, response), (sentResponse) => { ;(request as any)[handledSymbol] = true return Effect.mapEager( handleResponse(request, sentResponse), (sent) => Response.isHttpServerResponse(sent) ? sent : response ) }) } const withMiddleware = (frame: RequestFrame): Effect.Effect< unknown, E | EH | HttpServerError, HttpServerRequest | R | RH > => { const request = frame.request const responded = Effect.matchCauseEffect(self, { onSuccess: (response) => sendResponse(frame, response), onFailure: (cause) => handleCause(frame, cause) }) return middleware === undefined ? responded : Effect.matchCauseEffect(tracer(middleware(responded)), { onFailure(cause): Effect.Effect { reportCauseUnsafe(Fiber.getCurrent()!, cause) if (handledSymbol in request) return Effect.void return Effect.matchCauseEffectEager(causeResponse(cause), { onFailure(_) { return handleResponse(request, Response.empty({ status: 500 })) }, onSuccess([response]) { return handleResponse(request, response) } }) }, onSuccess(response): Effect.Effect { return handledSymbol in request ? Effect.void : handleResponse(request, response) } }) } // Apply tracing lazily so disabled requests can use the single-frame path. const traced = (frame: RequestFrame) => tracer( withMiddleware(frame) as Effect.Effect< HttpServerResponse, E | EH | HttpServerError, HttpServerRequest | R | RH > ) // Handle untraced requests without middleware in one continuation frame. // Effectful writes use `finishAfter` to restore state and close the scope. const finishAfter = ( frame: RequestFrame, fiber: Fiber.Fiber, effect: Effect.Effect ) => Effect.onExitPrimitive(effect, (exit) => { fiber.setContext(frame.prev) if (scopeEjected in frame.scope) return undefined return Scope.closeUnsafe(frame.scope, frame.cause === undefined ? exit : Exit.failCause(frame.cause)) }, true) class RequestFrame extends requestState.RequestState { readonly scope: Scope.Closeable readonly prev: Context.Context readonly request: HttpServerRequest cause: Cause.Cause | undefined constructor(scope: Scope.Closeable, prev: Context.Context, request: HttpServerRequest) { super() this.scope = scope this.prev = prev this.request = request } [contA](response: HttpServerResponse, fiber: Fiber.Fiber) { const sent = sendResponse(this, response) // Most response writes complete synchronously. if (effectIsExit(sent) && sent._tag === "Success") { fiber.setContext(this.prev) if (scopeEjected in this.scope) return sent const closed = Scope.closeUnsafe(this.scope, sent) return closed === undefined ? sent : Effect.flatMap(closed, () => sent) } return finishAfter(this, fiber, sent) } [contE](cause: Cause.Cause, fiber: Fiber.Fiber) { return finishAfter(this, fiber, handleCause(this, cause)) } } return Effect.withFiber((fiber) => { fiberEnterUninterruptibleUnsafe(fiber) const scope = Scope.makeUnsafe() const frame = new RequestFrame(scope, fiber.context, Context.getUnsafe(fiber.context, HttpServerRequest)) fiber.setContext(Context.add(Context.add(frame.prev, Scope.Scope, scope), requestState.CurrentRequestState, frame)) // The native tracer has no backend, so its spans are unobservable. const currentTracer = fiber.cache.tracer if ( middleware === undefined && (currentTracer === undefined || currentTracer === nativeTracer || isTracerDisabledUnsafe(fiber, frame.request)) ) { ;(fiber as any)._stack.push(frame) return self } return finishAfter(frame, fiber, middleware === undefined ? traced(frame) : withMiddleware(frame)) }) as any } const handledSymbol = Symbol.for("effect/http/HttpEffect/handled") /** * Disables automatic closing for an HTTP request scope. * * **Gotchas** * * Use only when another owner will close the scope; otherwise resources attached * to the request scope can leak. * * @stability unstable * @category resource management * @since 4.0.0 */ export const scopeDisableClose = (scope: Scope.Scope): void => { ;(scope as any)[scopeEjected] = true } /** * Returns a streaming server response that closes the request scope when the body stream exits. * * @stability unstable * @category resource management * @since 4.0.0 */ export const scopeTransferToStream = ( response: HttpServerResponse ): HttpServerResponse => { if (response.body._tag !== "Stream") { return response } const fiber = Fiber.getCurrent()! const scope = Context.getUnsafe(fiber.context, Scope.Scope) as Scope.Closeable scopeDisableClose(scope) return Response.setBodyKeepHeaders( response, HttpBody.stream( Stream.onExit(response.body.stream, (exit) => Scope.close(scope, exit)), response.body.contentType, response.body.contentLength ) ) } const scopeEjected = Symbol.for("effect/http/HttpEffect/scopeEjected") /** * Function run with the current request and response just before the response is sent, allowing the response to be replaced or failing with `HttpServerError`. * * @stability unstable * @category handlers * @since 4.0.0 */ export type PreResponseHandler = ( request: HttpServerRequest, response: HttpServerResponse ) => Effect.Effect /** * Registers an additional pre-response handler for the current HTTP server request. * * @stability unstable * @category handlers * @since 4.0.0 */ export const appendPreResponseHandler = (handler: PreResponseHandler): Effect.Effect => withPreResponseHandler(Effect.void, handler) /** * Registers a pre-response handler for the HTTP server request that owns the * supplied fiber context. Does nothing outside a server request. * * @stability unstable * @category unsafe * @since 4.0.0 */ export const appendPreResponseHandlerUnsafe: ( context: Context.Context, handler: PreResponseHandler ) => void = requestState.appendPreResponseHandlerUnsafe /** * Runs an effect after registering a pre-response handler for the current HTTP server request. * * @stability unstable * @category handlers * @since 4.0.0 */ export const withPreResponseHandler: { /** * Runs an effect after registering a pre-response handler for the current HTTP server request. * * @stability unstable * @category handlers * @since 4.0.0 */ (handler: PreResponseHandler): (self: Effect.Effect) => Effect.Effect /** * Runs an effect after registering a pre-response handler for the current HTTP server request. * * @stability unstable * @category handlers * @since 4.0.0 */ (self: Effect.Effect, handler: PreResponseHandler): Effect.Effect } = dual< ( handler: PreResponseHandler ) => (self: Effect.Effect) => Effect.Effect, (self: Effect.Effect, handler: PreResponseHandler) => Effect.Effect >(2, (self, handler) => Effect.withFiber((fiber) => { appendPreResponseHandlerUnsafe(fiber.context, handler) return self })) /** * Converts an HTTP server effect into a Web `Request` handler using the supplied base context and optional middleware. * * @stability unstable * @category converting * @since 4.0.0 */ export const toWebHandlerWith = >( context: Context.Context ) => ( self: Effect.Effect, middleware?: HttpMiddleware | undefined ): [ReqR] extends [never] ? (request: Request, context?: Context.Context | undefined) => Promise : (request: Request, context: Context.Context) => Promise => { const resolveSymbol = Symbol.for("@effect/platform/HttpApp/resolve") const httpApp = toHandled(self, (request, response) => { const withoutBody = request.method === "HEAD" if (!Response.omitsBody(response, withoutBody)) { // The transferred request scope closes when an abort interrupts the body, even if it is no longer read. response = scopeTransferToStream(response) } ;(request as any)[resolveSymbol]( Response.toWeb(response, { withoutBody, context, signal: (request.source as globalThis.Request).signal }) ) return Effect.void }, middleware) return (request: Request, _context?: Context.Context | undefined): Promise => new Promise((resolve) => { let reqContext = context if (Context.isContext(_context)) { for (const [key, value] of _context.mapUnsafe) { reqContext = Context.addUnsafe(reqContext, key, value) } } const httpServerRequest = Request.fromWeb(request) reqContext = Context.add(reqContext, HttpServerRequest, httpServerRequest) ;(httpServerRequest as any)[resolveSymbol] = resolve const fiber = Effect.runForkWith(reqContext)(httpApp as any) const signal = request.signal if (fiber.pollUnsafe() === undefined && signal) { const onAbort = () => fiber.interruptUnsafe(undefined, ClientAbort.annotation) if (signal.aborted) { onAbort() } else { signal.addEventListener("abort", onAbort, { once: true }) fiber.addObserver(() => signal.removeEventListener("abort", onAbort)) } } }) } /** * Converts an HTTP server effect into a Web `Request` handler using an empty base context. * * @stability unstable * @category converting * @since 4.0.0 */ export const toWebHandler: ( self: Effect.Effect, middleware?: HttpMiddleware | undefined ) => (request: Request, context?: Context.Context | undefined) => Promise = toWebHandlerWith(Context.empty()) /** * Builds a Web `Request` handler from a layer and handler factory, returning the handler with a `dispose` function for the layer scope. * * **Details** * * Every call to `dispose` returns the same promise, which resolves once the * layer resources have been released. * * The layer is built immediately, so the cost is paid when the handler is * created rather than on the first request. A layer that performs asynchronous * work while building may still be in progress when the first request arrives, * in which case that request waits for the build to finish. If the build fails, * every request rejects with the build error. * * @stability unstable * @category converting * @since 4.0.0 */ export const toWebHandlerLayerWith = < E, Provided, LE, R, ReqR = Exclude >( layer: Layer.Layer, options: { readonly toHandler: ( context: Context.Context ) => Effect.Effect, LE> readonly middleware?: HttpMiddleware | undefined readonly memoMap?: Layer.MemoMap | undefined } ): { readonly dispose: () => Promise readonly handler: [ReqR] extends [never] ? ( request: Request, context?: Context.Context | undefined ) => Promise : ( request: Request, context: Context.Context ) => Promise } => { const scope = Scope.makeUnsafe() let disposing: Promise | undefined const dispose = () => disposing ??= Effect.runPromise(Scope.close(scope, Exit.void)) let handlerCache: | ((request: Request, context?: Context.Context | undefined) => Promise) | undefined const handlerPromise = Effect.runPromise(Effect.gen(function*() { const context = yield* (options.memoMap ? Layer.buildWithMemoMap(layer, options.memoMap, scope) : Layer.buildWithScope(layer, scope)) return handlerCache = toWebHandlerWith(context)( yield* options.toHandler(context), options.middleware ) as any })) // A failed build must not become an unhandled rejection at startup. Every // request still rejects with the build error below. handlerPromise.catch(constVoid) function handler( request: Request, context?: Context.Context | undefined ): Promise { if (handlerCache) { return handlerCache(request, context) } return handlerPromise.then((f) => f(request, context)) } return { dispose, handler: handler as any } as const } /** * Builds a Web `Request` handler for an HTTP server effect using a layer to provide its services, returning the handler with a `dispose` function. * * **Details** * * The layer is built immediately, so the cost is paid when the handler is * created rather than on the first request. A layer that performs asynchronous * work while building may still be in progress when the first request arrives, * in which case that request waits for the build to finish. If the build fails, * every request rejects with the build error. * * @stability unstable * @category converting * @since 4.0.0 */ export const toWebHandlerLayer = >( self: Effect.Effect, layer: Layer.Layer, options?: { readonly middleware?: HttpMiddleware | undefined readonly memoMap?: Layer.MemoMap | undefined } | undefined ): { readonly dispose: () => Promise readonly handler: [ReqR] extends [never] ? (request: Request, context?: Context.Context | undefined) => Promise : ( request: Request, context: Context.Context ) => Promise } => toWebHandlerLayerWith(layer, { ...options, toHandler: () => Effect.succeed(self) }) /** * Adapts a Web `Request` handler into an HTTP server effect for the current `HttpServerRequest`. * * @stability unstable * @category converting * @since 4.0.0 */ export const fromWebHandler = ( handler: (request: Request) => Promise ): Effect.Effect => Effect.callback((resume, signal) => { const fiber = Fiber.getCurrent()! const request = Context.getUnsafe(fiber.context, HttpServerRequest) const requestResult = Request.toWebResult(request, { signal, context: fiber.context }) if (requestResult._tag === "Failure") { return resume(Effect.fail(new HttpServerError({ reason: requestResult.failure }))) } handler(requestResult.success).then( (response) => resume(Effect.succeed(Response.fromWeb(response))), (cause) => resume(Effect.fail( new HttpServerError({ reason: new InternalError({ cause, request, description: "HttpApp.fromWebHandler: Error in handler" }) }) )) ) })