1/**
2 * Builds server routes from declarative `HttpApi` contracts.
3 *
4 * This module turns an `HttpApi` description plus group handlers into
5 * `HttpRouter` routes. At runtime it decodes request parts with schemas, runs
6 * middleware and security handlers, invokes the registered endpoint handler, and
7 * encodes successes or declared errors into `HttpServerResponse` values.
8 *
9 * @stability unstable
10 * @since 4.0.0
11 */
12import type { NonEmptyReadonlyArray } from "../Array.ts"
13import type * as Cause from "../Cause.ts"
14import * as Context from "../Context.ts"
15import * as Effect from "../Effect.ts"
16import * as Base64 from "../encoding/Base64.ts"
17import * as Sse from "../encoding/Sse.ts"
18import * as Fiber from "../Fiber.ts"
19import type { FileSystem } from "../FileSystem.ts"
20import * as Function from "../Function.ts"
21import type { Cookie } from "../http/Cookies.ts"
22import type * as Etag from "../http/Etag.ts"
23import * as HttpEffect from "../http/HttpEffect.ts"
24import * as HttpMethod from "../http/HttpMethod.ts"
25import type { HttpPlatform } from "../http/HttpPlatform.ts"
26import * as HttpRouter from "../http/HttpRouter.ts"
27import * as Request from "../http/HttpServerRequest.ts"
28import { HttpServerRequest } from "../http/HttpServerRequest.ts"
29import * as Response from "../http/HttpServerResponse.ts"
30import type { HttpServerResponse } from "../http/HttpServerResponse.ts"
31import * as Multipart from "../http/Multipart.ts"
32import * as UrlParams from "../http/UrlParams.ts"
33import { stringOrRedacted } from "../internal/redacted.ts"
34import * as Layer from "../Layer.ts"
35import type { Path } from "../Path.ts"
36import { type Pipeable, pipeArguments } from "../Pipeable.ts"
37import { hasProperty } from "../Predicate.ts"
38import * as Redacted from "../Redacted.ts"
39import * as Result from "../Result.ts"
40import * as Schema from "../Schema.ts"
41import * as SchemaAST from "../SchemaAST.ts"
42import * as SchemaIssue from "../SchemaIssue.ts"
43import * as SchemaTransformation from "../SchemaTransformation.ts"
44import * as Scope from "../Scope.ts"
45import * as Stream from "../Stream.ts"
46import type { NoInfer } from "../Types.ts"
47import * as UndefinedOr from "../UndefinedOr.ts"
48import * as HttpApi from "./HttpApi.ts"
49import * as HttpApiEndpoint from "./HttpApiEndpoint.ts"
50import { HttpApiSchemaError } from "./HttpApiError.ts"
51import type * as HttpApiGroup from "./HttpApiGroup.ts"
52import * as HttpApiMiddleware from "./HttpApiMiddleware.ts"
53import * as HttpApiSchema from "./HttpApiSchema.ts"
54import type * as HttpApiSecurity from "./HttpApiSecurity.ts"
55import * as MediaType from "./internal/mediaType.ts"
56import * as HttpApiPath from "./internal/path.ts"
57import * as OpenApi from "./OpenApi.ts"
58
59/**
60 * Registers an `HttpApi` with a `HttpRouter`.
61 *
62 * @stability unstable
63 * @category layers
64 * @since 4.0.0
65 */
66export const layer = <Id extends string, Groups extends HttpApiGroup.Constraint>(
67 api: HttpApi.HttpApi<Id, Groups>,
68 options?: {
69 readonly openapiPath?: `/${string}` | undefined
70 }
71): Layer.Layer<
72 never,
73 never,
74 | Etag.Generator
75 | HttpRouter.HttpRouter
76 | FileSystem
77 | HttpPlatform
78 | Path
79 | HttpApiGroup.ToService<Id, Groups>
80> =>
81 HttpRouter.use(Effect.fnUntraced(function*(router) {
82 const services = yield* Effect.context<
83 | Etag.Generator
84 | HttpRouter.HttpRouter
85 | FileSystem
86 | HttpPlatform
87 | Path
88 >()
89 const routes: Array<HttpRouter.Route<any, any>> = []
90 const availableGroups = Array.from(services.mapUnsafe.keys()).filter((key) =>
91 key.startsWith("effect/http-api/HttpApiGroup/")
92 )
93 const groups = Object.values(api.groups) as ReadonlyArray<HttpApiGroup.Top>
94 for (const group of groups) {
95 const groupRoutes = services.mapUnsafe.get(group.key)?.routes as Array<HttpRouter.Route<any, any>>
96 if (groupRoutes === undefined) {
97 const available = availableGroups.length === 0 ? "none" : availableGroups.join(", ")
98 return yield* Effect.die(
99 `HttpApiGroup "${group.identifier}" not found (key: "${group.key}"). Did you forget to provide HttpApiBuilder.group(api, "${group.identifier}", ...)? Available groups: ${available}`
100 )
101 }
102 routes.push(...groupRoutes)
103 }
104 yield* (router.addAll(routes) as Effect.Effect<void>)
105 if (options?.openapiPath) {
106 const makeResponse = Function.memoize((api: HttpApi.HttpApi<Id, Groups>): HttpServerResponse =>
107 Response.jsonUnsafe(OpenApi.fromApi(api))
108 )
109 yield* router.add(
110 "GET",
111 options.openapiPath,
112 Effect.sync(() => makeResponse(api))
113 )
114 }
115 }))
116
117/**
118 * Create a `Layer` that implements all endpoints in an `HttpApi` group.
119 *
120 * **Details**
121 *
122 * The `build` function receives an unimplemented `Handlers` instance that can
123 * be used to add handlers to the group. Implement endpoints with
124 * `handlers.handle`.
125 *
126 * @stability unstable
127 * @category handlers
128 * @since 4.0.0
129 */
130export const group = <
131 ApiId extends string,
132 Groups extends HttpApiGroup.Constraint,
133 const Identifier extends HttpApiGroup.Identifier<Groups>,
134 Return
135>(
136 api: HttpApi.HttpApi<ApiId, Groups>,
137 groupIdentifier: Identifier,
138 build: (
139 handlers: Handlers.FromGroup<HttpApiGroup.WithIdentifier<Groups, Identifier>>
140 ) => Handlers.ValidateReturn<Return>
141): Layer.Layer<
142 HttpApiGroup.Service<ApiId, Identifier>,
143 Handlers.Error<Return>,
144 Exclude<Handlers.Context<Return>, Scope.Scope>
145> =>
146 Layer.effectContext(Effect.gen(function*() {
147 const services = (yield* Effect.context<any>()).pipe(
148 Context.omit(Scope.Scope)
149 )
150 const group = api.groups[groupIdentifier] as HttpApiGroup.Constraint
151 const result = build(makeHandlers(group))
152 const handlers: Handlers<any, any, any> = Effect.isEffect(result)
153 ? (yield* result as Effect.Effect<any, any, any>)
154 : result
155 const routes: Array<HttpRouter.Route<any, any>> = []
156 for (const item of handlers.handlers.values()) {
157 routes.push(handlerToRoute(api, group as any, item, services))
158 }
159 return Context.makeUnsafe(
160 new Map([[group.key, {
161 routes,
162 handlers: handlers.handlers
163 }]])
164 )
165 })) as any
166
167/**
168 * Creates a reusable handler for a single endpoint in an API group.
169 *
170 * **Details**
171 *
172 * Returns the supplied callback unchanged, with its request, success, and error
173 * types inferred from the endpoint, preserving the callback's service
174 * requirements. Pass the result to `handlers.handle` when implementing the group.
175 *
176 * @stability unstable
177 * @category handlers
178 * @since 4.0.0
179 */
180export const handler = <
181 ApiId extends string,
182 Groups extends HttpApiGroup.Constraint,
183 const GroupIdentifier extends HttpApiGroup.Identifier<Groups>,
184 const EndpointIdentifier extends HttpApiGroup.EndpointsWithIdentifier<Groups, GroupIdentifier>["identifier"],
185 R
186>(
187 _api: HttpApi.HttpApi<ApiId, Groups>,
188 _groupIdentifier: GroupIdentifier,
189 _endpointIdentifier: EndpointIdentifier,
190 f: HttpApiEndpoint.HandlerWithIdentifier<
191 HttpApiGroup.EndpointsWithIdentifier<Groups, GroupIdentifier>,
192 EndpointIdentifier,
193 HttpApiEndpoint.MiddlewareError<
194 HttpApiEndpoint.WithIdentifier<
195 HttpApiGroup.EndpointsWithIdentifier<Groups, GroupIdentifier>,
196 EndpointIdentifier
197 >
198 >,
199 R
200 >
201): HttpApiEndpoint.HandlerWithIdentifier<
202 HttpApiGroup.EndpointsWithIdentifier<Groups, GroupIdentifier>,
203 EndpointIdentifier,
204 HttpApiEndpoint.MiddlewareError<
205 HttpApiEndpoint.WithIdentifier<
206 HttpApiGroup.EndpointsWithIdentifier<Groups, GroupIdentifier>,
207 EndpointIdentifier
208 >
209 >,
210 R
211> => f
212
213const HandlersTypeId = "~effect/http-api/HttpApiBuilder/Handlers"
214
215type EndpointMap<Endpoints extends HttpApiEndpoint.Constraint> = {
216 readonly [Endpoint in Endpoints as HttpApiEndpoint.Identifier<Endpoint>]: Endpoint
217}
218
219type HandlerRequirements<
220 Endpoint extends HttpApiEndpoint.Constraint,
221 R1,
222 R = HttpApiEndpoint.ExcludeProvided<
223 Endpoint,
224 R1 | HttpApiEndpoint.ServerServices<Endpoint>
225 >
226> =
227 | HttpApiEndpoint.Middleware<Endpoint>
228 | HttpApiEndpoint.MiddlewareServices<Endpoint>
229 | ([R] extends [never] ? never : HttpRouter.Request.From<"Requires", R>)
230
231interface HandlerOptions {
232 readonly uninterruptible?: boolean | undefined
233}
234
235/**
236 * @internal
237 */
238export interface HandlerRuntime {
239 readonly endpoint: HttpApiEndpoint.Top
240 readonly handler: HttpApiEndpoint.Handler<HttpApiEndpoint.Constraint, unknown, unknown>
241 readonly isRaw: boolean
242 readonly uninterruptible: boolean
243}
244
245type HandleAllEntry<Endpoint extends HttpApiEndpoint.Constraint> =
246 | HttpApiEndpoint.Handler<
247 Endpoint,
248 HttpApiEndpoint.MiddlewareError<Endpoint>,
249 unknown
250 >
251 | {
252 readonly handler: HttpApiEndpoint.Handler<
253 Endpoint,
254 HttpApiEndpoint.MiddlewareError<Endpoint>,
255 unknown
256 >
257 readonly options?: HandlerOptions | undefined
258 }
259
260type HandleAllHandlers<EndpointsByIdentifier extends Record<string, HttpApiEndpoint.Constraint>> = {
261 readonly [Identifier in keyof EndpointsByIdentifier]?: HandleAllEntry<EndpointsByIdentifier[Identifier]>
262}
263
264type HandleAllExtraKeys<
265 EndpointsByIdentifier extends Record<string, HttpApiEndpoint.Constraint>,
266 HandlersByIdentifier
267> = {
268 readonly [Identifier in Exclude<keyof HandlersByIdentifier, keyof EndpointsByIdentifier>]: never
269}
270
271type NotHandledIdentifier<Identifier extends PropertyKey, HandledIdentifiers extends PropertyKey> = Identifier extends
272 HandledIdentifiers ? never :
273 unknown
274
275type HandleAllEntryHandler<Entry> = Entry extends { readonly handler: infer Handler } ? Handler : Entry
276
277type HandleAllRequirements<
278 EndpointsByIdentifier extends Record<string, HttpApiEndpoint.Constraint>,
279 HandlersByIdentifier extends HandleAllHandlers<EndpointsByIdentifier>
280> = {
281 readonly [Identifier in keyof HandlersByIdentifier & keyof EndpointsByIdentifier]: HandleAllEntryHandler<
282 HandlersByIdentifier[Identifier]
283 > extends HttpApiEndpoint.Handler<
284 EndpointsByIdentifier[Identifier],
285 HttpApiEndpoint.MiddlewareError<EndpointsByIdentifier[Identifier]>,
286 infer R1
287 > ? HandlerRequirements<EndpointsByIdentifier[Identifier], R1> :
288 never
289}[keyof HandlersByIdentifier & keyof EndpointsByIdentifier]
290
291type HandlersResult<A> = A extends Effect.Effect<infer H, any, any> ? H : A
292
293type MissingHandlerNames<H extends Handlers<any, any, any>> = Exclude<
294 keyof H["~EndpointsByIdentifier"],
295 H["~HandledIdentifiers"]
296>
297
298type ValidateHandlersReturn<
299 A,
300 H = HandlersResult<A>,
301 Missing = H extends Handlers<any, any, any> ? MissingHandlerNames<H> : never
302> = H extends Handlers<any, any, any> ? [Missing] extends [never] ? A
303 : `Endpoint not handled: ${Missing & string}`
304 : `Must return the implemented handlers`
305
306/**
307 * Mutable handler collection for one `HttpApi` group.
308 *
309 * **Details**
310 *
311 * Each call to `handle` or `handleRaw` registers an endpoint implementation and
312 * adds that endpoint identifier to the type-level set of implemented endpoints.
313 * Endpoint identifiers that were already handled are rejected at the type level.
314 *
315 * @stability unstable
316 * @category handlers
317 * @since 4.0.0
318 */
319export interface Handlers<
320 R,
321 EndpointsByIdentifier extends Record<string, HttpApiEndpoint.Constraint> = {},
322 HandledIdentifiers extends keyof EndpointsByIdentifier = never
323> extends Pipeable {
324 readonly [HandlersTypeId]: typeof HandlersTypeId
325 readonly "~EndpointsByIdentifier": EndpointsByIdentifier
326 readonly "~HandledIdentifiers": HandledIdentifiers
327 /**
328 * @internal
329 */
330 readonly group: HttpApiGroup.Top
331 /**
332 * @internal
333 */
334 readonly handlers: Map<string, HandlerRuntime>
335
336 /**
337 * Add the implementation for an unhandled `HttpApiEndpoint` to a `Handlers` group.
338 */
339 handle<
340 Identifier extends keyof EndpointsByIdentifier,
341 R1
342 >(
343 identifier: Identifier & NotHandledIdentifier<Identifier, HandledIdentifiers>,
344 handler: HttpApiEndpoint.Handler<
345 EndpointsByIdentifier[Identifier],
346 HttpApiEndpoint.MiddlewareError<EndpointsByIdentifier[Identifier]>,
347 R1
348 >,
349 options?: { readonly uninterruptible?: boolean | undefined } | undefined
350 ): Handlers<
351 R | HandlerRequirements<EndpointsByIdentifier[Identifier], R1>,
352 EndpointsByIdentifier,
353 HandledIdentifiers | Identifier
354 >
355
356 /**
357 * Add implementations for unhandled `HttpApiEndpoint`s in a `Handlers` group.
358 */
359 handleAll<const HandlersByIdentifier extends HandleAllHandlers<Omit<EndpointsByIdentifier, HandledIdentifiers>>>(
360 handlers:
361 & HandlersByIdentifier
362 & HandleAllExtraKeys<Omit<EndpointsByIdentifier, HandledIdentifiers>, HandlersByIdentifier>
363 ): Handlers<
364 R | HandleAllRequirements<EndpointsByIdentifier, HandlersByIdentifier>,
365 EndpointsByIdentifier,
366 HandledIdentifiers | keyof HandlersByIdentifier & keyof EndpointsByIdentifier
367 >
368
369 /**
370 * Add the implementation for an unhandled `HttpApiEndpoint` to a `Handlers` group.
371 * This version opts out of automatic payload decoding and provides the raw request.
372 */
373 handleRaw<
374 Identifier extends keyof EndpointsByIdentifier,
375 R1
376 >(
377 identifier: Identifier & NotHandledIdentifier<Identifier, HandledIdentifiers>,
378 handler: HttpApiEndpoint.HandlerRaw<
379 EndpointsByIdentifier[Identifier],
380 HttpApiEndpoint.MiddlewareError<EndpointsByIdentifier[Identifier]>,
381 R1
382 >,
383 options?: { readonly uninterruptible?: boolean | undefined } | undefined
384 ): Handlers<
385 R | HandlerRequirements<EndpointsByIdentifier[Identifier], R1>,
386 EndpointsByIdentifier,
387 HandledIdentifiers | Identifier
388 >
389}
390
391/**
392 * Namespace containing helper types for `HttpApiBuilder` handler collections.
393 *
394 * @stability unstable
395 * @since 4.0.0
396 */
397export declare namespace Handlers {
398 /**
399 * Creates a handler collection for a group where every endpoint in the group is
400 * still awaiting an implementation.
401 *
402 * @stability unstable
403 * @category handlers
404 * @since 4.0.0
405 */
406 export type FromGroup<Group extends HttpApiGroup.Constraint> = Handlers<
407 never,
408 Group["endpoints"]
409 >
410
411 /**
412 * Validates the return value of a group handler builder, preserving successful
413 * handler collections and producing a descriptive type error when endpoints remain
414 * unhandled.
415 *
416 * @stability unstable
417 * @category handlers
418 * @since 4.0.0
419 */
420 export type ValidateReturn<A> = ValidateHandlersReturn<A>
421
422 /**
423 * Extracts the error channel from an effect that produces a `Handlers`
424 * collection, returning `never` for non-effectful handler collections.
425 *
426 * @stability unstable
427 * @category handlers
428 * @since 4.0.0
429 */
430 export type Error<A> = A extends Effect.Effect<
431 Handlers<
432 infer _R,
433 infer _EndpointsByIdentifier,
434 infer _HandledIdentifiers
435 >,
436 infer _EX,
437 infer _RX
438 > ? _EX :
439 never
440
441 /**
442 * Extracts the services required by a handler collection, including both handler
443 * requirements and the environment required to construct the handlers.
444 *
445 * @stability unstable
446 * @category handlers
447 * @since 4.0.0
448 */
449 export type Context<A> = A extends Handlers<
450 infer _R,
451 infer _EndpointsByIdentifier,
452 infer _HandledIdentifiers
453 > ? _R :
454 A extends Effect.Effect<
455 Handlers<
456 infer _R,
457 infer _EndpointsByIdentifier,
458 infer _HandledIdentifiers
459 >,
460 infer _EX,
461 infer _RX
462 > ? _R | _RX :
463 never
464}
465
466type EndpointReturn<
467 Groups extends HttpApiGroup.Constraint,
468 GroupIdentifier extends HttpApiGroup.Identifier<Groups>,
469 EndpointIdentifier extends HttpApiGroup.Endpoints<
470 HttpApiGroup.WithIdentifier<Groups, GroupIdentifier>
471 >["identifier"],
472 R,
473 Endpoint extends HttpApiEndpoint.Constraint = HttpApiEndpoint.WithIdentifier<
474 HttpApiGroup.Endpoints<HttpApiGroup.WithIdentifier<Groups, GroupIdentifier>>,
475 EndpointIdentifier
476 >
477> = Effect.Effect<
478 Effect.Effect<
479 HttpServerResponse,
480 never,
481 | HttpServerRequest
482 | HttpRouter.RouteContext
483 | Request.ParsedSearchParams
484 | Exclude<R, HttpApiEndpoint.MiddlewareProvides<Endpoint>>
485 >,
486 never,
487 | HttpApiEndpoint.ServerServices<Endpoint>
488 | HttpApiEndpoint.Middleware<Endpoint>
489 | HttpApiEndpoint.MiddlewareServices<Endpoint>
490 | Etag.Generator
491 | FileSystem
492 | HttpPlatform
493 | Path
494>
495
496/**
497 * Builds the server-side HTTP effect for a single endpoint in an API group using
498 * the endpoint metadata, middleware, codecs, and supplied handler.
499 *
500 * @stability unstable
501 * @category handlers
502 * @since 4.0.0
503 */
504export const endpoint = <
505 ApiId extends string,
506 Groups extends HttpApiGroup.Constraint,
507 const GroupIdentifier extends HttpApiGroup.Identifier<Groups>,
508 const EndpointIdentifier extends HttpApiGroup.Endpoints<
509 HttpApiGroup.WithIdentifier<Groups, GroupIdentifier>
510 >["identifier"],
511 R
512>(
513 api: HttpApi.HttpApi<ApiId, Groups>,
514 groupIdentifier: GroupIdentifier,
515 endpointIdentifier: EndpointIdentifier,
516 handler: NoInfer<
517 HttpApiEndpoint.HandlerWithIdentifier<
518 HttpApiGroup.Endpoints<HttpApiGroup.WithIdentifier<Groups, GroupIdentifier>>,
519 EndpointIdentifier,
520 never,
521 R
522 >
523 >
524): EndpointReturn<Groups, GroupIdentifier, EndpointIdentifier, R> =>
525 Effect.contextWith((context: Context.Context<any>) => {
526 const group = api.groups[groupIdentifier] as unknown as HttpApiGroup.Top
527 const endpoint = group.endpoints[endpointIdentifier]
528 return Effect.succeed(handlerToHttpEffect(
529 api,
530 group,
531 endpoint,
532 Context.omit(Scope.Scope)(context),
533 handler as any,
534 false
535 ))
536 })
537
538/**
539 * Decodes credentials for an HTTP API security scheme from the current request,
540 * supporting bearer, API key, and basic authentication inputs.
541 *
542 * @stability unstable
543 * @category security
544 * @since 4.0.0
545 */
546export const securityDecode = <Security extends HttpApiSecurity.HttpApiSecurity>(
547 self: Security
548): Effect.Effect<
549 HttpApiSecurity.HttpApiSecurity.Type<Security>,
550 never,
551 HttpServerRequest | Request.ParsedSearchParams
552> => {
553 switch (self._tag) {
554 case "Http": {
555 return Effect.map(
556 HttpServerRequest,
557 (request) =>
558 Redacted.make(
559 getAuthorizationCredential(request.headers.authorization, self.scheme) ?? ""
560 ) as any
561 )
562 }
563 case "ApiKey": {
564 const key = self.in === "header" ? self.key.toLowerCase() : self.key
565 const schema = Schema.Struct({
566 [key]: Schema.String
567 })
568 const decode: Effect.Effect<
569 { readonly [x: string]: string; readonly [x: number]: string },
570 Schema.SchemaError,
571 Request.ParsedSearchParams | HttpServerRequest
572 > = self.in === "query"
573 ? Request.schemaSearchParams(schema)
574 : self.in === "cookie"
575 ? Request.schemaCookies(schema)
576 : Request.schemaHeaders(schema)
577 return Effect.match(decode, {
578 onFailure: () => Redacted.make("") as any,
579 onSuccess: (match) => Redacted.make(match[key])
580 })
581 }
582 case "Basic": {
583 const empty: HttpApiSecurity.HttpApiSecurity.Type<Security> = {
584 username: "",
585 password: Redacted.make("")
586 } as any
587 return Effect.map(HttpServerRequest, (request) => {
588 const encoded = getAuthorizationCredential(request.headers.authorization, basicScheme)
589 if (encoded === undefined) return empty
590 const decoded = Result.getOrUndefined(Base64.decodeString(encoded))
591 if (decoded === undefined) return empty
592 // RFC 7617, Section 2: only the first colon separates the user-id from the password.
593 // https://www.rfc-editor.org/rfc/rfc7617.html#section-2
594 const separator = decoded.indexOf(":")
595 if (separator === -1) return empty
596 return {
597 username: decoded.slice(0, separator),
598 password: Redacted.make(decoded.slice(separator + 1))
599 } as any
600 })
601 }
602 }
603}
604
605/**
606 * Registers a pre-response handler that sets an API-key cookie on the outgoing
607 * response, defaulting the cookie to `secure` and `httpOnly` unless overridden.
608 *
609 * @stability unstable
610 * @category security
611 * @since 4.0.0
612 */
613export const securitySetCookie = (
614 self: HttpApiSecurity.ApiKey,
615 value: string | Redacted.Redacted,
616 options?: Cookie["options"]
617): Effect.Effect<void, never, HttpServerRequest> =>
618 HttpEffect.appendPreResponseHandler((_req, response) =>
619 Effect.orDie(
620 Response.setCookie(response, self.key, stringOrRedacted(value), {
621 secure: true,
622 httpOnly: true,
623 ...options
624 })
625 )
626 )
627
628// -----------------------------------------------------------------------------
629// Internal
630// -----------------------------------------------------------------------------
631
632const basicScheme = "Basic"
633
634function getAuthorizationCredential(
635 authorization: string | undefined,
636 scheme: string
637): string | undefined {
638 const schemeLength = scheme.length
639 // RFC 9110, Section 11.1: schemes are case-insensitive and credentials follow 1*SP.
640 // https://www.rfc-editor.org/rfc/rfc9110.html#section-11.1
641 if (
642 authorization === undefined ||
643 authorization.length <= schemeLength ||
644 authorization.charCodeAt(schemeLength) !== 32 ||
645 authorization.slice(0, schemeLength).toLowerCase() !== scheme.toLowerCase()
646 ) {
647 return undefined
648 }
649 let credentialStart = schemeLength + 1
650 while (authorization.charCodeAt(credentialStart) === 32) {
651 credentialStart++
652 }
653 return credentialStart === authorization.length ? undefined : authorization.slice(credentialStart)
654}
655
656const registerHandler = (
657 self: Handlers<any, any, any>,
658 identifier: string,
659 handler: HttpApiEndpoint.Handler<HttpApiEndpoint.Constraint, any, any>,
660 isRaw: boolean,
661 options?: HandlerOptions | undefined
662) => {
663 if (!Object.hasOwn(self.group.endpoints, identifier)) {
664 throw new Error(`HttpApiEndpoint "${identifier}" not found in HttpApiGroup "${self.group.identifier}"`)
665 }
666 if (self.handlers.has(identifier)) {
667 throw new Error(
668 `Handler for HttpApiEndpoint "${identifier}" is already registered in HttpApiGroup "${self.group.identifier}"`
669 )
670 }
671 const endpoint = self.group.endpoints[identifier]
672 self.handlers.set(identifier, {
673 endpoint,
674 handler,
675 isRaw,
676 uninterruptible: options?.uninterruptible ?? false
677 })
678 return self
679}
680
681const HandlersProto = {
682 [HandlersTypeId]: HandlersTypeId,
683 pipe() {
684 return pipeArguments(this, arguments)
685 },
686 handle(
687 this: Handlers<any, any, any>,
688 identifier: string,
689 handler: HttpApiEndpoint.Handler<HttpApiEndpoint.Constraint, any, any>,
690 options?: { readonly uninterruptible?: boolean | undefined } | undefined
691 ) {
692 return registerHandler(this, identifier, handler, false, options)
693 },
694 handleAll(
695 this: Handlers<any, any, any>,
696 handlers: Record<
697 string,
698 HttpApiEndpoint.Handler<HttpApiEndpoint.Constraint, any, any> | {
699 readonly handler: HttpApiEndpoint.Handler<HttpApiEndpoint.Constraint, any, any>
700 readonly options?: HandlerOptions | undefined
701 }
702 >
703 ) {
704 for (const [identifier, entry] of Object.entries(handlers)) {
705 const handler = typeof entry === "function" ? entry : entry.handler
706 const options = typeof entry === "function" ? undefined : entry.options
707 registerHandler(this, identifier, handler, false, options)
708 }
709 return this
710 },
711 handleRaw(
712 this: Handlers<any, any, any>,
713 identifier: string,
714 handler: HttpApiEndpoint.Handler<HttpApiEndpoint.Constraint, any, any>,
715 options?: { readonly uninterruptible?: boolean | undefined } | undefined
716 ) {
717 return registerHandler(this, identifier, handler, true, options)
718 }
719}
720
721const makeHandlers = <R, Group extends HttpApiGroup.Constraint>(
722 group: Group
723): Handlers<R, EndpointMap<HttpApiGroup.Endpoints<Group>>> => {
724 const self = Object.create(HandlersProto)
725 self.group = group
726 self.handlers = new Map<string, HandlerRuntime>()
727 return self
728}
729
730type PayloadDecoder =
731 | {
732 readonly _tag: "Multipart"
733 readonly mode: "buffered" | "stream"
734 readonly limits: Multipart.withLimits.Options | undefined
735 readonly decode: (input: unknown) => Effect.Effect<unknown, Schema.SchemaError, unknown>
736 }
737 | {
738 readonly _tag: "Json" | "FormUrlEncoded" | "Uint8Array" | "Text"
739 readonly nullOnEmpty: boolean
740 readonly decode: (input: unknown) => Effect.Effect<unknown, Schema.SchemaError, unknown>
741 }
742
743function buildPayloadDecoders(
744 payloadMap: HttpApiEndpoint.PayloadMap,
745 options: SchemaAST.ParseOptions | undefined
746): Map<string, PayloadDecoder> {
747 const result = new Map<string, PayloadDecoder>()
748 payloadMap.forEach(({ encoding, schemas }, contentType) => {
749 const decode = Schema.decodeUnknownEffect(Schema.Union(schemas), options)
750 if (encoding._tag === "Multipart") {
751 result.set(contentType, { _tag: "Multipart", mode: encoding.mode, limits: encoding.limits, decode })
752 } else {
753 result.set(contentType, {
754 _tag: encoding._tag,
755 decode,
756 nullOnEmpty: schemas.some((s) => SchemaAST.isNull(SchemaAST.toEncoded(s.ast)))
757 })
758 }
759 })
760 return result
761}
762
763function decodePayload(
764 payloadBy: Map<string, PayloadDecoder>,
765 httpRequest: HttpServerRequest,
766 query: Record<string, string | Array<string>>
767): Effect.Effect<unknown, Schema.SchemaError, unknown> | HttpServerResponse | undefined {
768 const hasBody = HttpMethod.hasBody(httpRequest.method)
769 const contentType = hasBody
770 ? MediaType.normalize(httpRequest.headers["content-type"] ?? "application/json")
771 : "application/x-www-form-urlencoded"
772 const existing = payloadBy.get(contentType)
773 if (!existing) {
774 return Response.text(`Unsupported content-type: ${contentType}`, { status: 415 })
775 }
776 const { _tag, decode } = existing
777 switch (_tag) {
778 case "Multipart": {
779 if (existing.mode === "buffered") {
780 let eff = Effect.orDie(httpRequest.multipart)
781 if (existing.limits) {
782 eff = Effect.provideContext(eff, Multipart.limitsServices(existing.limits))
783 }
784 return Effect.flatMap(eff, decode)
785 }
786 return Effect.succeed(
787 existing.limits
788 ? Stream.provideContext(httpRequest.multipartStream, Multipart.limitsServices(existing.limits))
789 : httpRequest.multipartStream
790 )
791 }
792 case "Json":
793 return Effect.flatMap(Effect.orDie(httpRequest.text), (text) => {
794 if (text === "") {
795 return decode(existing.nullOnEmpty ? null : undefined)
796 }
797 try {
798 return decode(JSON.parse(text))
799 } catch {
800 return Effect.fail(
801 new Schema.SchemaError(
802 new SchemaIssue.InvalidValue({ message: "Expected a valid JSON body" })
803 )
804 )
805 }
806 })
807 case "Text":
808 return Effect.flatMap(Effect.orDie(httpRequest.text), decode)
809 case "FormUrlEncoded": {
810 const source = hasBody
811 ? Effect.map(Effect.orDie(httpRequest.urlParamsBody), UrlParams.toRecord)
812 : Effect.succeed(query)
813 return Effect.flatMap(source, decode)
814 }
815 case "Uint8Array":
816 return Effect.flatMap(
817 Effect.map(Effect.orDie(httpRequest.arrayBuffer), (buffer) => new Uint8Array(buffer)),
818 decode
819 )
820 }
821}
822
823function handlerToHttpEffect(
824 api: Pick<HttpApi.Top, "annotations">,
825 group: HttpApiGroup.Top,
826 endpoint: HttpApiEndpoint.Top,
827 context: Context.Context<any>,
828 handler: HttpApiEndpoint.Handler<HttpApiEndpoint.Constraint, any, any>,
829 isRaw: boolean
830) {
831 const annotations = Context.merge(Context.merge(api.annotations, group.annotations), endpoint.annotations)
832 const options = HttpApi.getSlotParseOptions(annotations)
833 const encodeSuccess = Schema.encodeUnknownEffect(makeSuccessSchema(endpoint, options.headers), options.success)
834 const encodeError = Schema.encodeUnknownEffect(makeErrorSchema(endpoint, options.headers), options.error)
835 const decodeParams = UndefinedOr.map(endpoint.params, (schema) => Schema.decodeUnknownEffect(schema, options.params))
836 const decodeHeaders = UndefinedOr.map(
837 endpoint.headers,
838 (schema) => Schema.decodeUnknownEffect(schema, options.headers)
839 )
840 const decodeQuery = UndefinedOr.map(
841 endpoint.query,
842 (schema) => Schema.decodeUnknownEffect(Schema.toCodecArrayFromSingle(schema), options.query)
843 )
844 const encodeStream = makeStreamEncoder(endpoint, options.success)
845 const encodeWithHeaders = makeWithHeadersEncoder(endpoint, options.success, options.headers)
846
847 const shouldParsePayload = endpoint.payload.size > 0 && !isRaw
848 const payloadBy = shouldParsePayload ? buildPayloadDecoders(endpoint.payload, options.payload) : undefined
849
850 return applyMiddleware(
851 group,
852 endpoint,
853 context,
854 Effect.gen(function*() {
855 const fiber = Fiber.getCurrent()!
856 const context = fiber.context
857 const httpRequest = Context.getUnsafe(context, HttpServerRequest)
858 const routeContext = Context.getUnsafe(context, HttpRouter.RouteContext)
859 const query = Context.getUnsafe(context, Request.ParsedSearchParams)
860 const request: any = {
861 request: httpRequest,
862 endpoint,
863 group
864 }
865 if (decodeParams) {
866 request.params = yield* HttpApiSchemaError.wrap("Params", decodeParams(routeContext.params))
867 }
868 if (decodeHeaders) {
869 request.headers = yield* HttpApiSchemaError.wrap("Headers", decodeHeaders(httpRequest.headers))
870 }
871 if (decodeQuery) {
872 request.query = yield* HttpApiSchemaError.wrap("Query", decodeQuery(query))
873 }
874 if (payloadBy) {
875 const result = decodePayload(payloadBy, httpRequest, query)
876 if (Response.isHttpServerResponse(result)) {
877 return result
878 }
879 if (result !== undefined) {
880 request.payload = yield* HttpApiSchemaError.wrap("Payload", result)
881 }
882 }
883 let response = yield* handler(request)
884 if (Response.isHttpServerResponse(response)) {
885 return response
886 }
887 let responseHeaders: unknown | undefined
888 if (encodeWithHeaders !== undefined && HttpApiSchema.isWithHeadersValue(response)) {
889 responseHeaders = response.headers
890 response = response.body
891 }
892 const encoded = yield* HttpApiSchemaError.wrap(
893 "Body",
894 encodeStream?.(response, context) ??
895 (responseHeaders !== undefined ? encodeWithHeaders!.encodeBody(response) : encodeSuccess(response))
896 )
897 if (encodeWithHeaders === undefined || responseHeaders === undefined) return encoded
898 const encodedHeaders = yield* HttpApiSchemaError.wrap(
899 "ResponseHeaders",
900 encodeWithHeaders.encodeHeaders.get(encoded.status)!(responseHeaders)
901 )
902 return Response.setHeaders(encoded, encodedHeaders as any)
903 })
904 ).pipe(
905 Effect.withErrorReporting,
906 Effect.catch((error) => {
907 if (HttpApiSchemaError.is(error)) return Effect.die(error)
908 return Effect.orDie(encodeError(error))
909 }),
910 Effect.provideContext(context)
911 )
912}
913
914/**
915 * @internal
916 */
917export function handlerToRoute(
918 api: Pick<HttpApi.Top, "annotations">,
919 group: HttpApiGroup.Top,
920 handler: HandlerRuntime,
921 context: Context.Context<any>
922): HttpRouter.Route<any, any> {
923 const endpoint = handler.endpoint
924 return HttpRouter.route(
925 endpoint.method,
926 HttpApiPath.toRouterPath(endpoint.path, endpoint.params) as HttpRouter.PathInput,
927 handlerToHttpEffect(api, group, endpoint, context, handler.handler, handler.isRaw),
928 { uninterruptible: handler.uninterruptible }
929 )
930}
931
932const applyMiddleware = <Group extends HttpApiGroup.Constraint, A extends Effect.Effect<any, any, any>>(
933 group: Group,
934 endpoint: HttpApiEndpoint.Top,
935 context: Context.Context<any>,
936 handler: A
937) => {
938 const options = { group, endpoint }
939 for (const key_ of endpoint.middlewares) {
940 const key = key_ as HttpApiMiddleware.AnyService
941 const service = Context.getUnsafe(context, key)
942 const apply = HttpApiMiddleware.isSecurity(key)
943 ? makeSecurityMiddleware(key, service)
944 : service
945 handler = apply(handler, options)
946 }
947 return handler
948}
949
950const securityMiddlewareCache = new WeakMap<
951 object,
952 (effect: Effect.Effect<any, any, any>, options: any) => Effect.Effect<any, any, any>
953>()
954
955const makeSecurityMiddleware = (
956 key: HttpApiMiddleware.AnyServiceSecurity,
957 service: HttpApiMiddleware.HttpApiMiddlewareSecurity<any, any, any, any>
958): (effect: Effect.Effect<any, any, any>, options: any) => Effect.Effect<any, any, any> => {
959 const cached = securityMiddlewareCache.get(service)
960 if (cached !== undefined) {
961 return cached
962 }
963
964 const entries = Object.entries(key.security).map(([securityKey, security]) => ({
965 decode: securityDecode(security),
966 middleware: service[securityKey]
967 }))
968 if (entries.length === 0) {
969 return Function.identity
970 }
971
972 const middleware = Effect.fnUntraced(function*(handler: Effect.Effect<any, any, any>, options: {
973 readonly group: HttpApiGroup.Top
974 readonly endpoint: HttpApiEndpoint.Top
975 }) {
976 handler = Effect.mapError(handler, (error) => new HandlerError(error))
977 let lastResult: Result.Result<any, any> | undefined
978 for (let i = 0; i < entries.length; i++) {
979 const { decode, middleware } = entries[i]
980 const result = yield* Effect.result(Effect.flatMap(decode, (credential) =>
981 middleware(handler, {
982 credential,
983 endpoint: options.endpoint,
984 group: options.group
985 })))
986 if (Result.isFailure(result)) {
987 if (isHandlerError(result.failure)) {
988 return yield* Effect.fail(result.failure.error)
989 }
990 lastResult = result
991 continue
992 }
993 return result.success
994 }
995 return yield* Effect.fromResult(lastResult!)
996 })
997
998 securityMiddlewareCache.set(service, middleware)
999 return middleware
1000}
1001
1002const HandlerErrorTypeId = "~effect/http-api/HttpApiBuilder/HandlerError" as const
1003class HandlerError {
1004 readonly [HandlerErrorTypeId] = HandlerErrorTypeId
1005 readonly error: unknown
1006 constructor(error: unknown) {
1007 this.error = error
1008 }
1009}
1010const isHandlerError = (value: unknown): value is HandlerError => hasProperty(value, HandlerErrorTypeId)
1011
1012const $HttpServerResponse = Schema.declare(Response.isHttpServerResponse)
1013
1014type StreamEncoder = (response: unknown, context: Context.Context<never>) =>
1015 | Effect.Effect<HttpServerResponse, Schema.SchemaError, unknown>
1016 | undefined
1017
1018type WithHeadersEncoder = (headers: unknown) => Effect.Effect<unknown, Schema.SchemaError, unknown>
1019
1020interface WithHeadersEncoders {
1021 readonly encodeBody: (body: unknown) => Effect.Effect<HttpServerResponse, Schema.SchemaError, unknown>
1022 readonly encodeHeaders: ReadonlyMap<number, WithHeadersEncoder>
1023}
1024
1025function makeWithHeadersEncoder(
1026 endpoint: HttpApiEndpoint.Top,
1027 options: SchemaAST.ParseOptions | undefined,
1028 headersOptions: SchemaAST.ParseOptions | undefined
1029): WithHeadersEncoders | undefined {
1030 const encodeHeaders = new Map<number, WithHeadersEncoder>()
1031 const bodySchemas: Array<Schema.ConstraintEncoder<HttpServerResponse, unknown>> = []
1032 for (const schema of endpoint.success) {
1033 if (!HttpApiSchema.isWithHeaders(schema)) continue
1034 encodeHeaders.set(
1035 HttpApiSchema.getStatusSuccessSchema(schema),
1036 Schema.encodeUnknownEffect(schema.headers, headersOptions)
1037 )
1038 if (!HttpApiSchema.isStreamSchema(schema.schema)) {
1039 bodySchemas.push(toResponseSuccessSchema(schema, headersOptions))
1040 }
1041 }
1042 if (encodeHeaders.size === 0) return undefined
1043 // Branded response bodies encode against the header-carrying members only, so
1044 // the encoded status always has a matching header schema.
1045 const bodySchema: Schema.ConstraintEncoder<HttpServerResponse, unknown> = bodySchemas.length === 0
1046 ? Schema.Never
1047 : bodySchemas.length === 1
1048 ? bodySchemas[0]
1049 : Schema.Union(bodySchemas)
1050 return {
1051 encodeBody: Schema.encodeUnknownEffect(bodySchema, options),
1052 encodeHeaders
1053 }
1054}
1055
1056function makeStreamEncoder(
1057 endpoint: HttpApiEndpoint.Top,
1058 options: SchemaAST.ParseOptions | undefined
1059): StreamEncoder | undefined {
1060 const successSchema = getStreamSuccessSchema(endpoint)
1061 if (successSchema === undefined) {
1062 return undefined
1063 }
1064
1065 const streamSchema = successSchema.body
1066 const hasBuffered = hasBufferedSuccess(endpoint)
1067 const status = HttpApiSchema.getStatusSuccessSchema(successSchema.schema)
1068 const contentType = streamSchema.contentType
1069
1070 if (HttpApiSchema.isStreamUint8Array(streamSchema)) {
1071 return (response, context) => {
1072 if (!Stream.isStream(response)) {
1073 return hasBuffered ? undefined : new Schema.SchemaError(
1074 new SchemaIssue.InvalidValue({ message: "Expected a streaming response" })
1075 )
1076 }
1077
1078 return Effect.succeed(Response.stream(
1079 Stream.provideContext(
1080 response as Stream.Stream<Uint8Array, unknown, unknown>,
1081 context as Context.Context<unknown>
1082 ),
1083 { status, contentType }
1084 ))
1085 }
1086 }
1087
1088 const sseEncoder = makeSseEncoder(streamSchema, options)
1089
1090 return (response, context) => {
1091 if (!Stream.isStream(response)) {
1092 return hasBuffered ? undefined : new Schema.SchemaError(
1093 new SchemaIssue.InvalidValue({ message: "Expected a streaming response" })
1094 )
1095 }
1096
1097 return Effect.succeed(Response.stream(
1098 Stream.provideContext(
1099 encodeSseStream(response, sseEncoder),
1100 context as Context.Context<unknown>
1101 ),
1102 { status, contentType }
1103 ))
1104 }
1105}
1106
1107function getStreamSuccessSchema(endpoint: HttpApiEndpoint.Top) {
1108 for (const schema of endpoint.success) {
1109 const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema
1110 if (HttpApiSchema.isStreamSchema(body)) {
1111 return { schema, body }
1112 }
1113 }
1114}
1115
1116function hasBufferedSuccess(endpoint: HttpApiEndpoint.Top): boolean {
1117 for (const schema of endpoint.success) {
1118 const body = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema
1119 if (Schema.isSchema(body) && !HttpApiSchema.isStreamSchema(body)) return true
1120 }
1121 return endpoint.success.size === 0
1122}
1123
1124interface SseStreamEncoder {
1125 readonly sseMode: HttpApiSchema.StreamSseMode
1126 readonly encodeEvents: (
1127 input: NonEmptyReadonlyArray<unknown>
1128 ) => Effect.Effect<NonEmptyReadonlyArray<Sse.EventEncoded>, Schema.SchemaError, unknown>
1129 readonly encodeCause: (input: unknown) => Effect.Effect<string, Schema.SchemaError, unknown>
1130}
1131
1132function makeSseEncoder<Events extends Sse.EventCodec, Error extends Schema.Constraint>(
1133 streamSchema: HttpApiSchema.StreamSse<Events, Error, unknown>,
1134 options: SchemaAST.ParseOptions | undefined
1135): SseStreamEncoder {
1136 const CauseSchema = Schema.toCodecJson(Schema.Cause(streamSchema.error, Schema.Defect()))
1137 return {
1138 sseMode: streamSchema.sseMode,
1139 encodeEvents: Schema.encodeUnknownEffect(Schema.Array(streamSchema.events), options) as any,
1140 encodeCause: Schema.encodeUnknownEffect(Schema.fromJsonString(CauseSchema), options)
1141 }
1142}
1143
1144function encodeSseStream(
1145 stream: Stream.Stream<unknown, unknown, unknown>,
1146 encoder: SseStreamEncoder
1147): Stream.Stream<Uint8Array, unknown, unknown> {
1148 return stream.pipe(
1149 encoder.sseMode === "data" ?
1150 Stream.map((value) => ({
1151 id: undefined,
1152 event: "message",
1153 data: value
1154 })) :
1155 Function.identity,
1156 Stream.mapArrayEffect((chunk) => Effect.orDie(encoder.encodeEvents(chunk))),
1157 Stream.catchCause((cause) => Stream.fromEffect(encodeFailureEvent(cause, encoder))),
1158 Stream.map(renderSseEvent),
1159 Stream.encodeText
1160 )
1161}
1162
1163function encodeFailureEvent(cause: Cause.Cause<unknown>, encoder: SseStreamEncoder) {
1164 return encoder.encodeCause(cause).pipe(
1165 Effect.orDie,
1166 Effect.map((encodedCause) => ({
1167 id: undefined,
1168 event: reservedStreamFailureEvent,
1169 data: encodedCause
1170 }))
1171 )
1172}
1173
1174const reservedStreamFailureEvent = "effect/http-api/stream/failure"
1175
1176function renderSseEvent(event: Sse.EventEncoded) {
1177 return Sse.encoder.write({
1178 _tag: "Event",
1179 event: event.event,
1180 id: event.id,
1181 data: event.data
1182 })
1183}
1184
1185const toResponseSuccessSchema = toResponseSchema(HttpApiSchema.getStatusSuccessSchema)
1186const toResponseErrorSchemaPlain = toResponseSchema(HttpApiSchema.getStatusErrorSchema)
1187
1188function toResponseErrorSchema(
1189 schema: Schema.Constraint,
1190 headersOptions: SchemaAST.ParseOptions | undefined
1191): Schema.ConstraintEncoder<HttpServerResponse, unknown> {
1192 if (!HttpApiSchema.isWithHeaders(schema)) return toResponseErrorSchemaPlain(schema, headersOptions)
1193 return toWithHeadersResponseSchema({
1194 schema,
1195 status: HttpApiSchema.getStatusErrorSchema(schema),
1196 encoding: HttpApiSchema.getResponseEncodingSchema(schema),
1197 body: schema.schema,
1198 headers: schema.headers,
1199 headersOptions,
1200 toPair: (value) => value as HttpApiSchema.withHeaders<unknown, unknown>
1201 })
1202}
1203
1204function makeSuccessSchema(
1205 endpoint: HttpApiEndpoint.Top,
1206 headersOptions: SchemaAST.ParseOptions | undefined
1207): Schema.ConstraintEncoder<HttpServerResponse, unknown> {
1208 const schemas = HttpApiEndpoint.getSuccessSchemas(endpoint).map((schema) =>
1209 toResponseSuccessSchema(schema, headersOptions)
1210 )
1211 return schemas.length === 1 ? schemas[0] : Schema.Union(schemas)
1212}
1213
1214function makeErrorSchema(
1215 endpoint: HttpApiEndpoint.Top,
1216 headersOptions: SchemaAST.ParseOptions | undefined
1217): Schema.ConstraintEncoder<HttpServerResponse, unknown> {
1218 const schemas = HttpApiEndpoint.getErrorSchemas(endpoint).map((schema) =>
1219 toResponseErrorSchema(schema, headersOptions)
1220 )
1221 if (schemas.length === 0) return Schema.Never
1222 return schemas.length === 1 ? schemas[0] : Schema.Union(schemas)
1223}
1224
1225function toResponseSchema(getStatus: (schema: Schema.Constraint) => number) {
1226 // WithHeaders wrappers share a single declaration AST, so they are cached by instance
1227 const cache = new WeakMap<object, Schema.Top>()
1228
1229 return (
1230 schema: Schema.Constraint,
1231 headersOptions: SchemaAST.ParseOptions | undefined
1232 ): Schema.ConstraintEncoder<HttpServerResponse, unknown> => {
1233 const withHeaders = HttpApiSchema.getWithHeadersAnnotation(schema.ast)
1234 if (withHeaders !== undefined) {
1235 // Depends on the endpoint's header options, so it is not cached
1236 return toWithHeadersResponseSchema({
1237 schema: Schema.toType(schema),
1238 status: getStatus(withHeaders.body),
1239 encoding: HttpApiSchema.getResponseEncoding(withHeaders.body.ast),
1240 body: withHeaders.bodyCodec,
1241 headers: withHeaders.headersCodec,
1242 headersOptions,
1243 toPair: withHeaders.transformation.encode
1244 })
1245 }
1246 const key = HttpApiSchema.isWithHeaders(schema) ? schema : schema.ast
1247 const cached = cache.get(key)
1248 if (cached !== undefined) {
1249 return cached as any
1250 }
1251 const bodySchema = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema
1252 const responseSchema = $HttpServerResponse.pipe(
1253 Schema.decodeTo(bodySchema, getResponseTransformation(getStatus, schema))
1254 )
1255 cache.set(key, responseSchema)
1256 return responseSchema
1257 }
1258}
1259
1260function getResponseTransformation(
1261 getStatus: (schema: Schema.Constraint) => number,
1262 schema: Schema.Constraint
1263): SchemaTransformation.Transformation<unknown, Response.HttpServerResponse, never, unknown> {
1264 const bodySchema = HttpApiSchema.isWithHeaders(schema) ? schema.schema : schema
1265 const encode = getResponseEncode(
1266 getStatus(schema),
1267 HttpApiSchema.getResponseEncodingSchema(schema),
1268 HttpApiSchema.isNoContent(bodySchema.ast)
1269 )
1270
1271 return SchemaTransformation.transformEffect({
1272 decode: (input, options) =>
1273 Effect.fail(
1274 new SchemaIssue.Forbidden({ message: "Encode only schema" }, input, options)
1275 ),
1276 encode
1277 })
1278}
1279
1280// Encodes the body with the options passed to the schema and the headers with
1281// the headers slot options.
1282function toWithHeadersResponseSchema(options: {
1283 readonly schema: Schema.Top
1284 readonly status: number
1285 readonly encoding: HttpApiSchema.ResponseEncoding
1286 readonly body: Schema.Top
1287 readonly headers: Schema.Top
1288 readonly headersOptions: SchemaAST.ParseOptions | undefined
1289 readonly toPair: (value: unknown) => { readonly body: unknown; readonly headers: unknown }
1290}): Schema.ConstraintEncoder<HttpServerResponse, unknown> {
1291 const encodeBody = Schema.encodeUnknownEffect(options.body)
1292 const encodeHeaders = Schema.encodeUnknownEffect(options.headers, options.headersOptions)
1293 const encodeResponse = getResponseEncode(
1294 options.status,
1295 options.encoding,
1296 HttpApiSchema.isNoContent(options.body.ast)
1297 )
1298 const toIssue = (error: Schema.SchemaError) => error.issue
1299 const transformation = SchemaTransformation.transformEffect<unknown, Response.HttpServerResponse, never, unknown>({
1300 decode: (input, options) =>
1301 Effect.fail(
1302 new SchemaIssue.Forbidden({ message: "Encode only schema" }, input, options)
1303 ),
1304 encode: (value, parseOptions) => {
1305 const pair = options.toPair(value)
1306 return encodeBody(pair.body, parseOptions).pipe(
1307 Effect.mapError(toIssue),
1308 Effect.flatMap((body) => encodeResponse(body, parseOptions)),
1309 Effect.flatMap((response) =>
1310 Effect.map(
1311 Effect.mapError(encodeHeaders(pair.headers), toIssue),
1312 (headers) => Response.setHeaders(response, headers as any)
1313 )
1314 )
1315 )
1316 }
1317 })
1318 return $HttpServerResponse.pipe(Schema.decodeTo(options.schema, transformation))
1319}
1320
1321function getResponseEncode<E>(
1322 status: number,
1323 encoding: HttpApiSchema.ResponseEncoding,
1324 isNoContent: boolean
1325): (
1326 e: E,
1327 options?: SchemaAST.ParseOptions
1328) => Effect.Effect<Response.HttpServerResponse, SchemaIssue.InvalidValue, never> {
1329 switch (encoding._tag) {
1330 case "Json": {
1331 return ((e, options) => {
1332 if (e === undefined || isNoContent) {
1333 return Effect.succeed(Response.empty({ status }))
1334 }
1335 try {
1336 const s = JSON.stringify(e)
1337 return Effect.succeed(Response.text(s, { status, contentType: encoding.contentType }))
1338 } catch {
1339 return Effect.fail(
1340 new SchemaIssue.InvalidValue(
1341 { expected: "a JSON-serializable response body" },
1342 e,
1343 options
1344 )
1345 )
1346 }
1347 })
1348 }
1349 case "Text":
1350 return (e) =>
1351 Effect.succeed(Response.text(e as string, {
1352 status,
1353 contentType: encoding.contentType
1354 }))
1355 case "Uint8Array":
1356 return (e) =>
1357 Effect.succeed(Response.uint8Array(e as Uint8Array, {
1358 status,
1359 contentType: encoding.contentType
1360 }))
1361 case "FormUrlEncoded":
1362 return (e) =>
1363 Effect.succeed(
1364 Response.urlParams(e as URLSearchParams, { status }).pipe(
1365 Response.setHeader("content-type", encoding.contentType)
1366 )
1367 )
1368 }
1369}