| 1 | /** |
| 2 | * Runs Effect HTTP server handlers at platform boundaries. |
| 3 | * |
| 4 | * This module turns effects that produce `HttpServerResponse` values into Web |
| 5 | * `Request` handlers and other server adapters. It also applies middleware, |
| 6 | * converts failures into responses, runs hooks before a response is sent, and |
| 7 | * manages request scopes for streamed responses. |
| 8 | * |
| 9 | * @stability unstable |
| 10 | * @since 4.0.0 |
| 11 | */ |
| 12 | import type * as Cause from "../Cause.ts" |
| 13 | import * as Context from "../Context.ts" |
| 14 | import * as Effect from "../Effect.ts" |
| 15 | import * as Exit from "../Exit.ts" |
| 16 | import * as Fiber from "../Fiber.ts" |
| 17 | import { constVoid, dual } from "../Function.ts" |
| 18 | import { contA, contE } from "../internal/core.ts" |
| 19 | import { effectIsExit, fiberEnterUninterruptibleUnsafe, reportCauseUnsafe } from "../internal/effect.ts" |
| 20 | import * as Layer from "../Layer.ts" |
| 21 | import * as Scope from "../Scope.ts" |
| 22 | import * as Stream from "../Stream.ts" |
| 23 | import { nativeTracer } from "../Tracer.ts" |
| 24 | import * as HttpBody from "./HttpBody.ts" |
| 25 | import { type HttpMiddleware, isTracerDisabledUnsafe, tracer } from "./HttpMiddleware.ts" |
| 26 | import { causeResponse, ClientAbort, HttpServerError, InternalError } from "./HttpServerError.ts" |
| 27 | import { HttpServerRequest } from "./HttpServerRequest.ts" |
| 28 | import * as Request from "./HttpServerRequest.ts" |
| 29 | import type { HttpServerResponse } from "./HttpServerResponse.ts" |
| 30 | import * as Response from "./HttpServerResponse.ts" |
| 31 | import * as preResponseHandler from "./internal/preResponseHandler.ts" |
| 32 | |
| 33 | /** |
| 34 | * Runs an HTTP server effect, sends the produced response with the supplied handler, and converts failures into HTTP responses. |
| 35 | * On handler failure, the request scope closes with the original cause. |
| 36 | * |
| 37 | * @stability unstable |
| 38 | * @category combinators |
| 39 | * @since 4.0.0 |
| 40 | */ |
| 41 | export const toHandled = <E, R, EH, RH>( |
| 42 | self: Effect.Effect<HttpServerResponse, E, R>, |
| 43 | handleResponse: ( |
| 44 | request: HttpServerRequest, |
| 45 | response: HttpServerResponse |
| 46 | ) => Effect.Effect<unknown, EH, RH>, |
| 47 | middleware?: HttpMiddleware | undefined |
| 48 | ): Effect.Effect<void, never, Exclude<R | RH | HttpServerRequest, Scope.Scope>> => { |
| 49 | const handleCause = (frame: RequestFrame, cause: Cause.Cause<E | EH | HttpServerError>) => { |
| 50 | frame.cause = cause |
| 51 | const request = frame.request |
| 52 | return Effect.flatMapEager(causeResponse(cause), ([response, cause]) => { |
| 53 | const fiber = Fiber.getCurrent()! |
| 54 | reportCauseUnsafe(fiber, cause) |
| 55 | const handler = preResponseHandler.requestPreResponseHandlers.get(request.source) |
| 56 | const cont = cause.reasons.length === 0 ? Effect.succeed(response) : Effect.failCause(cause) |
| 57 | if (handler === undefined) { |
| 58 | ;(request as any)[handledSymbol] = true |
| 59 | return Effect.flatMapEager( |
| 60 | handleResponse(request, response), |
| 61 | () => cont |
| 62 | ) |
| 63 | } |
| 64 | |
| 65 | return Effect.flatMapEager( |
| 66 | Effect.flatMapEager(handler(request, response), (response) => { |
| 67 | ;(request as any)[handledSymbol] = true |
| 68 | return handleResponse(request, response) |
| 69 | }), |
| 70 | () => cont |
| 71 | ) |
| 72 | }) |
| 73 | } |
| 74 | |
| 75 | // Writes the response, applying any registered pre-response handler first. |
| 76 | // Returns an `Exit` when the write completes synchronously. |
| 77 | const sendResponse = ( |
| 78 | request: HttpServerRequest, |
| 79 | response: HttpServerResponse |
| 80 | ): Effect.Effect<HttpServerResponse, EH | HttpServerError, RH> => { |
| 81 | const handler = preResponseHandler.requestPreResponseHandlers.get(request.source) |
| 82 | if (handler === undefined) { |
| 83 | ;(request as any)[handledSymbol] = true |
| 84 | return Effect.mapEager(handleResponse(request, response), () => response) |
| 85 | } |
| 86 | return Effect.flatMapEager(handler(request, response), (sentResponse) => { |
| 87 | ;(request as any)[handledSymbol] = true |
| 88 | return Effect.mapEager(handleResponse(request, sentResponse), () => response) |
| 89 | }) |
| 90 | } |
| 91 | |
| 92 | const withMiddleware = (frame: RequestFrame): Effect.Effect< |
| 93 | unknown, |
| 94 | E | EH | HttpServerError, |
| 95 | HttpServerRequest | R | RH |
| 96 | > => { |
| 97 | const request = frame.request |
| 98 | const responded = Effect.matchCauseEffect(self, { |
| 99 | onSuccess: (response) => sendResponse(request, response), |
| 100 | onFailure: (cause) => handleCause(frame, cause) |
| 101 | }) |
| 102 | return middleware === undefined ? |
| 103 | responded : |
| 104 | Effect.matchCauseEffect(tracer(middleware(responded)), { |
| 105 | onFailure(cause): Effect.Effect<void, EH, RH> { |
| 106 | reportCauseUnsafe(Fiber.getCurrent()!, cause) |
| 107 | if (handledSymbol in request) return Effect.void |
| 108 | return Effect.matchCauseEffectEager(causeResponse(cause), { |
| 109 | onFailure(_) { |
| 110 | return handleResponse(request, Response.empty({ status: 500 })) |
| 111 | }, |
| 112 | onSuccess([response]) { |
| 113 | return handleResponse(request, response) |
| 114 | } |
| 115 | }) |
| 116 | }, |
| 117 | onSuccess(response): Effect.Effect<void, EH, RH> { |
| 118 | return handledSymbol in request ? Effect.void : handleResponse(request, response) |
| 119 | } |
| 120 | }) |
| 121 | } |
| 122 | |
| 123 | // Apply tracing lazily so disabled requests can use the single-frame path. |
| 124 | const traced = (frame: RequestFrame) => |
| 125 | tracer( |
| 126 | withMiddleware(frame) as Effect.Effect< |
| 127 | HttpServerResponse, |
| 128 | E | EH | HttpServerError, |
| 129 | HttpServerRequest | R | RH |
| 130 | > |
| 131 | ) |
| 132 | |
| 133 | // Handle untraced requests without middleware in one continuation frame. |
| 134 | // Effectful writes use `finishAfter` to restore state and close the scope. |
| 135 | const finishAfter = ( |
| 136 | frame: RequestFrame, |
| 137 | fiber: Fiber.Fiber<unknown, unknown>, |
| 138 | effect: Effect.Effect<unknown, unknown, unknown> |
| 139 | ) => |
| 140 | Effect.onExitPrimitive(effect, (exit) => { |
| 141 | fiber.setContext(frame.prev) |
| 142 | if (scopeEjected in frame.scope) return undefined |
| 143 | return Scope.closeUnsafe(frame.scope, frame.cause === undefined ? exit : Exit.failCause(frame.cause)) |
| 144 | }, true) |
| 145 | |
| 146 | class RequestFrame { |
| 147 | readonly scope: Scope.Closeable |
| 148 | readonly prev: Context.Context<never> |
| 149 | readonly request: HttpServerRequest |
| 150 | cause: Cause.Cause<E | EH | HttpServerError> | undefined |
| 151 | constructor(scope: Scope.Closeable, prev: Context.Context<never>, request: HttpServerRequest) { |
| 152 | this.scope = scope |
| 153 | this.prev = prev |
| 154 | this.request = request |
| 155 | } |
| 156 | [contA](response: HttpServerResponse, fiber: Fiber.Fiber<unknown, unknown>) { |
| 157 | const sent = sendResponse(this.request, response) |
| 158 | // Most response writes complete synchronously. |
| 159 | if (effectIsExit(sent) && sent._tag === "Success") { |
| 160 | fiber.setContext(this.prev) |
| 161 | if (scopeEjected in this.scope) return sent |
| 162 | const closed = Scope.closeUnsafe(this.scope, sent) |
| 163 | return closed === undefined ? sent : Effect.flatMap(closed, () => sent) |
| 164 | } |
| 165 | return finishAfter(this, fiber, sent) |
| 166 | } |
| 167 | [contE](cause: Cause.Cause<E | EH | HttpServerError>, fiber: Fiber.Fiber<unknown, unknown>) { |
| 168 | return finishAfter(this, fiber, handleCause(this, cause)) |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | return Effect.withFiber((fiber) => { |
| 173 | fiberEnterUninterruptibleUnsafe(fiber) |
| 174 | const scope = Scope.makeUnsafe() |
| 175 | const frame = new RequestFrame(scope, fiber.context, Context.getUnsafe(fiber.context, HttpServerRequest)) |
| 176 | fiber.setContext(Context.add(frame.prev, Scope.Scope, scope)) |
| 177 | // The native tracer has no backend, so its spans are unobservable. |
| 178 | const currentTracer = fiber.cache.tracer |
| 179 | if ( |
| 180 | middleware === undefined && |
| 181 | (currentTracer === undefined || currentTracer === nativeTracer || isTracerDisabledUnsafe(fiber, frame.request)) |
| 182 | ) { |
| 183 | ;(fiber as any)._stack.push(frame) |
| 184 | return self |
| 185 | } |
| 186 | return finishAfter(frame, fiber, middleware === undefined ? traced(frame) : withMiddleware(frame)) |
| 187 | }) as any |
| 188 | } |
| 189 | |
| 190 | const handledSymbol = Symbol.for("effect/http/HttpEffect/handled") |
| 191 | |
| 192 | /** |
| 193 | * Disables automatic closing for an HTTP request scope. |
| 194 | * |
| 195 | * **Gotchas** |
| 196 | * |
| 197 | * Use only when another owner will close the scope; otherwise resources attached |
| 198 | * to the request scope can leak. |
| 199 | * |
| 200 | * @stability unstable |
| 201 | * @category resource management |
| 202 | * @since 4.0.0 |
| 203 | */ |
| 204 | export const scopeDisableClose = (scope: Scope.Scope): void => { |
| 205 | ;(scope as any)[scopeEjected] = true |
| 206 | } |
| 207 | |
| 208 | /** |
| 209 | * Returns a streaming server response that closes the request scope when the body stream exits. |
| 210 | * |
| 211 | * @stability unstable |
| 212 | * @category resource management |
| 213 | * @since 4.0.0 |
| 214 | */ |
| 215 | export const scopeTransferToStream = ( |
| 216 | response: HttpServerResponse |
| 217 | ): HttpServerResponse => { |
| 218 | if (response.body._tag !== "Stream") { |
| 219 | return response |
| 220 | } |
| 221 | const fiber = Fiber.getCurrent()! |
| 222 | const scope = Context.getUnsafe(fiber.context, Scope.Scope) as Scope.Closeable |
| 223 | scopeDisableClose(scope) |
| 224 | return Response.setBodyKeepHeaders( |
| 225 | response, |
| 226 | HttpBody.stream( |
| 227 | Stream.onExit(response.body.stream, (exit) => Scope.close(scope, exit)), |
| 228 | response.body.contentType, |
| 229 | response.body.contentLength |
| 230 | ) |
| 231 | ) |
| 232 | } |
| 233 | |
| 234 | const scopeEjected = Symbol.for("effect/http/HttpEffect/scopeEjected") |
| 235 | |
| 236 | /** |
| 237 | * Function run with the current request and response just before the response is sent, allowing the response to be replaced or failing with `HttpServerError`. |
| 238 | * |
| 239 | * @stability unstable |
| 240 | * @category handlers |
| 241 | * @since 4.0.0 |
| 242 | */ |
| 243 | export type PreResponseHandler = ( |
| 244 | request: HttpServerRequest, |
| 245 | response: HttpServerResponse |
| 246 | ) => Effect.Effect<HttpServerResponse, HttpServerError> |
| 247 | |
| 248 | /** |
| 249 | * Registers an additional pre-response handler for the current HTTP server request. |
| 250 | * |
| 251 | * @stability unstable |
| 252 | * @category handlers |
| 253 | * @since 4.0.0 |
| 254 | */ |
| 255 | export const appendPreResponseHandler = (handler: PreResponseHandler): Effect.Effect<void, never, HttpServerRequest> => |
| 256 | HttpServerRequest.use((request) => { |
| 257 | appendPreResponseHandlerUnsafe(request, handler) |
| 258 | return Effect.void |
| 259 | }) |
| 260 | |
| 261 | /** |
| 262 | * Registers a pre-response handler for the supplied HTTP server request. |
| 263 | * |
| 264 | * @stability unstable |
| 265 | * @category unsafe |
| 266 | * @since 4.0.0 |
| 267 | */ |
| 268 | export const appendPreResponseHandlerUnsafe: ( |
| 269 | request: HttpServerRequest, |
| 270 | handler: PreResponseHandler |
| 271 | ) => void = preResponseHandler.appendPreResponseHandlerUnsafe |
| 272 | |
| 273 | /** |
| 274 | * Runs an effect after registering a pre-response handler for the current HTTP server request. |
| 275 | * |
| 276 | * @stability unstable |
| 277 | * @category handlers |
| 278 | * @since 4.0.0 |
| 279 | */ |
| 280 | export const withPreResponseHandler: { |
| 281 | (handler: PreResponseHandler): <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R | HttpServerRequest> |
| 282 | <A, E, R>(self: Effect.Effect<A, E, R>, handler: PreResponseHandler): Effect.Effect<A, E, R | HttpServerRequest> |
| 283 | } = dual< |
| 284 | ( |
| 285 | handler: PreResponseHandler |
| 286 | ) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R | HttpServerRequest>, |
| 287 | <A, E, R>(self: Effect.Effect<A, E, R>, handler: PreResponseHandler) => Effect.Effect<A, E, R | HttpServerRequest> |
| 288 | >(2, (self, handler) => |
| 289 | HttpServerRequest.use((request) => { |
| 290 | appendPreResponseHandlerUnsafe(request, handler) |
| 291 | return self |
| 292 | })) |
| 293 | |
| 294 | /** |
| 295 | * Converts an HTTP server effect into a Web `Request` handler using the supplied base context and optional middleware. |
| 296 | * |
| 297 | * @stability unstable |
| 298 | * @category converting |
| 299 | * @since 4.0.0 |
| 300 | */ |
| 301 | export const toWebHandlerWith = <Provided, R = never, ReqR = Exclude<R, Provided | Scope.Scope | HttpServerRequest>>( |
| 302 | context: Context.Context<Provided> |
| 303 | ) => |
| 304 | <E>( |
| 305 | self: Effect.Effect<HttpServerResponse, E, R>, |
| 306 | middleware?: HttpMiddleware | undefined |
| 307 | ): [ReqR] extends [never] ? |
| 308 | (request: Request, context?: Context.Context<never> | undefined) => Promise<globalThis.Response> |
| 309 | : (request: Request, context: Context.Context<ReqR>) => Promise<globalThis.Response> => |
| 310 | { |
| 311 | const resolveSymbol = Symbol.for("@effect/platform/HttpApp/resolve") |
| 312 | const httpApp = toHandled(self, (request, response) => { |
| 313 | const withoutBody = request.method === "HEAD" |
| 314 | if (!Response.omitsBody(response, withoutBody)) { |
| 315 | response = scopeTransferToStream(response) |
| 316 | } |
| 317 | ;(request as any)[resolveSymbol]( |
| 318 | Response.toWeb(response, { withoutBody, context }) |
| 319 | ) |
| 320 | return Effect.void |
| 321 | }, middleware) |
| 322 | return (request: Request, _context?: Context.Context<never> | undefined): Promise<globalThis.Response> => |
| 323 | new Promise((resolve) => { |
| 324 | let reqContext = context |
| 325 | if (Context.isContext(_context)) { |
| 326 | for (const [key, value] of _context.mapUnsafe) { |
| 327 | reqContext = Context.addUnsafe(reqContext, key, value) |
| 328 | } |
| 329 | } |
| 330 | const httpServerRequest = Request.fromWeb(request) |
| 331 | reqContext = Context.add(reqContext, HttpServerRequest, httpServerRequest) |
| 332 | ;(httpServerRequest as any)[resolveSymbol] = resolve |
| 333 | const fiber = Effect.runForkWith(reqContext)(httpApp as any) |
| 334 | if (fiber.pollUnsafe() === undefined) { |
| 335 | request.signal?.addEventListener("abort", () => { |
| 336 | fiber.interruptUnsafe(undefined, ClientAbort.annotation) |
| 337 | }, { once: true }) |
| 338 | } |
| 339 | }) |
| 340 | } |
| 341 | |
| 342 | /** |
| 343 | * Converts an HTTP server effect into a Web `Request` handler using an empty base context. |
| 344 | * |
| 345 | * @stability unstable |
| 346 | * @category converting |
| 347 | * @since 4.0.0 |
| 348 | */ |
| 349 | export const toWebHandler: <E>( |
| 350 | self: Effect.Effect<HttpServerResponse, E, HttpServerRequest | Scope.Scope>, |
| 351 | middleware?: HttpMiddleware | undefined |
| 352 | ) => (request: Request, context?: Context.Context<never> | undefined) => Promise<globalThis.Response> = |
| 353 | toWebHandlerWith(Context.empty()) |
| 354 | |
| 355 | /** |
| 356 | * Builds a Web `Request` handler from a layer and handler factory, returning the handler with a `dispose` function for the layer scope. |
| 357 | * |
| 358 | * **Details** |
| 359 | * |
| 360 | * The layer is built immediately, so the cost is paid when the handler is |
| 361 | * created rather than on the first request. A layer that performs asynchronous |
| 362 | * work while building may still be in progress when the first request arrives, |
| 363 | * in which case that request waits for the build to finish. If the build fails, |
| 364 | * every request rejects with the build error. |
| 365 | * |
| 366 | * @stability unstable |
| 367 | * @category converting |
| 368 | * @since 4.0.0 |
| 369 | */ |
| 370 | export const toWebHandlerLayerWith = < |
| 371 | E, |
| 372 | Provided, |
| 373 | LE, |
| 374 | R, |
| 375 | ReqR = Exclude<R, Provided | Scope.Scope | HttpServerRequest> |
| 376 | >( |
| 377 | layer: Layer.Layer<Provided, LE>, |
| 378 | options: { |
| 379 | readonly toHandler: ( |
| 380 | context: Context.Context<Provided> |
| 381 | ) => Effect.Effect<Effect.Effect<HttpServerResponse, E, R>, LE> |
| 382 | readonly middleware?: HttpMiddleware | undefined |
| 383 | readonly memoMap?: Layer.MemoMap | undefined |
| 384 | } |
| 385 | ): { |
| 386 | readonly dispose: () => Promise<void> |
| 387 | readonly handler: [ReqR] extends [never] ? ( |
| 388 | request: Request, |
| 389 | context?: Context.Context<never> | undefined |
| 390 | ) => Promise<globalThis.Response> |
| 391 | : ( |
| 392 | request: Request, |
| 393 | context: Context.Context<ReqR> |
| 394 | ) => Promise<globalThis.Response> |
| 395 | } => { |
| 396 | const scope = Scope.makeUnsafe() |
| 397 | const dispose = () => Effect.runPromise(Scope.close(scope, Exit.void)) |
| 398 | |
| 399 | let handlerCache: |
| 400 | | ((request: Request, context?: Context.Context<ReqR> | undefined) => Promise<globalThis.Response>) |
| 401 | | undefined |
| 402 | const handlerPromise = Effect.runPromise(Effect.gen(function*() { |
| 403 | const context = yield* (options.memoMap |
| 404 | ? Layer.buildWithMemoMap(layer, options.memoMap, scope) |
| 405 | : Layer.buildWithScope(layer, scope)) |
| 406 | return handlerCache = toWebHandlerWith<Provided, R>(context)( |
| 407 | yield* options.toHandler(context), |
| 408 | options.middleware |
| 409 | ) as any |
| 410 | })) |
| 411 | // A failed build must not become an unhandled rejection at startup. Every |
| 412 | // request still rejects with the build error below. |
| 413 | handlerPromise.catch(constVoid) |
| 414 | function handler( |
| 415 | request: Request, |
| 416 | context?: Context.Context<ReqR> | undefined |
| 417 | ): Promise<globalThis.Response> { |
| 418 | if (handlerCache) { |
| 419 | return handlerCache(request, context) |
| 420 | } |
| 421 | return handlerPromise.then((f) => f(request, context)) |
| 422 | } |
| 423 | return { dispose, handler: handler as any } as const |
| 424 | } |
| 425 | |
| 426 | /** |
| 427 | * Builds a Web `Request` handler for an HTTP server effect using a layer to provide its services, returning the handler with a `dispose` function. |
| 428 | * |
| 429 | * **Details** |
| 430 | * |
| 431 | * The layer is built immediately, so the cost is paid when the handler is |
| 432 | * created rather than on the first request. A layer that performs asynchronous |
| 433 | * work while building may still be in progress when the first request arrives, |
| 434 | * in which case that request waits for the build to finish. If the build fails, |
| 435 | * every request rejects with the build error. |
| 436 | * |
| 437 | * @stability unstable |
| 438 | * @category converting |
| 439 | * @since 4.0.0 |
| 440 | */ |
| 441 | export const toWebHandlerLayer = <E, R, Provided, LE, ReqR = Exclude<R, Provided | Scope.Scope | HttpServerRequest>>( |
| 442 | self: Effect.Effect<HttpServerResponse, E, R>, |
| 443 | layer: Layer.Layer<Provided, LE>, |
| 444 | options?: { |
| 445 | readonly middleware?: HttpMiddleware | undefined |
| 446 | readonly memoMap?: Layer.MemoMap | undefined |
| 447 | } | undefined |
| 448 | ): { |
| 449 | readonly dispose: () => Promise<void> |
| 450 | readonly handler: [ReqR] extends [never] |
| 451 | ? (request: Request, context?: Context.Context<never> | undefined) => Promise<globalThis.Response> |
| 452 | : ( |
| 453 | request: Request, |
| 454 | context: Context.Context<ReqR> |
| 455 | ) => Promise<globalThis.Response> |
| 456 | } => |
| 457 | toWebHandlerLayerWith(layer, { |
| 458 | ...options, |
| 459 | toHandler: () => Effect.succeed(self) |
| 460 | }) |
| 461 | |
| 462 | /** |
| 463 | * Adapts a Web `Request` handler into an HTTP server effect for the current `HttpServerRequest`. |
| 464 | * |
| 465 | * @stability unstable |
| 466 | * @category converting |
| 467 | * @since 4.0.0 |
| 468 | */ |
| 469 | export const fromWebHandler = ( |
| 470 | handler: (request: Request) => Promise<Response> |
| 471 | ): Effect.Effect<HttpServerResponse, HttpServerError, HttpServerRequest> => |
| 472 | Effect.callback((resume, signal) => { |
| 473 | const fiber = Fiber.getCurrent()! |
| 474 | const request = Context.getUnsafe(fiber.context, HttpServerRequest) |
| 475 | const requestResult = Request.toWebResult(request, { |
| 476 | signal, |
| 477 | context: fiber.context |
| 478 | }) |
| 479 | if (requestResult._tag === "Failure") { |
| 480 | return resume(Effect.fail(new HttpServerError({ reason: requestResult.failure }))) |
| 481 | } |
| 482 | handler(requestResult.success).then( |
| 483 | (response) => resume(Effect.succeed(Response.fromWeb(response))), |
| 484 | (cause) => |
| 485 | resume(Effect.fail( |
| 486 | new HttpServerError({ |
| 487 | reason: new InternalError({ |
| 488 | cause, |
| 489 | request, |
| 490 | description: "HttpApp.fromWebHandler: Error in handler" |
| 491 | }) |
| 492 | }) |
| 493 | )) |
| 494 | ) |
| 495 | }) |