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 */
12import type * as Cause from "../Cause.ts"
13import * as Context from "../Context.ts"
14import * as Effect from "../Effect.ts"
15import * as Exit from "../Exit.ts"
16import * as Fiber from "../Fiber.ts"
17import { constVoid, dual } from "../Function.ts"
18import { contA, contE } from "../internal/core.ts"
19import { effectIsExit, fiberEnterUninterruptibleUnsafe, reportCauseUnsafe } from "../internal/effect.ts"
20import * as Layer from "../Layer.ts"
21import * as Scope from "../Scope.ts"
22import * as Stream from "../Stream.ts"
23import { nativeTracer } from "../Tracer.ts"
24import * as HttpBody from "./HttpBody.ts"
25import { type HttpMiddleware, isTracerDisabledUnsafe, tracer } from "./HttpMiddleware.ts"
26import { causeResponse, ClientAbort, HttpServerError, InternalError } from "./HttpServerError.ts"
27import { HttpServerRequest } from "./HttpServerRequest.ts"
28import * as Request from "./HttpServerRequest.ts"
29import type { HttpServerResponse } from "./HttpServerResponse.ts"
30import * as Response from "./HttpServerResponse.ts"
31import * 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 */
41export 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
190const 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 */
204export 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 */
215export 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
234const 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 */
243export 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 */
255export 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 */
268export 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 */
280export 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 */
301export 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 */
349export 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 */
370export 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 */
441export 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 */
469export 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 })