1/**
2 * Declares middleware for schema-driven HTTP APIs.
3 *
4 * HTTP API middleware wraps endpoint execution on the server and, when the API
5 * requires it, can also wrap requests made by generated clients. It is used for
6 * cross-cutting behavior that belongs to the API contract, such as
7 * authentication, authorization, logging, tracing, rate limiting,
8 * request-scoped services, schema-error handling, and client request
9 * decoration. This module defines the middleware service keys and helpers used
10 * by `HttpApi` declarations.
11 *
12 * @stability unstable
13 * @since 4.0.0
14 */
15/** @effect-diagnostics floatingEffect:skip-file */
16/** @effect-diagnostics classSelfMismatch:off */
17import * as Context from "../Context.ts"
18import * as Effect from "../Effect.ts"
19import type * as HttpClientError from "../http/HttpClientError.ts"
20import type * as HttpClientRequest from "../http/HttpClientRequest.ts"
21import type * as HttpClientResponse from "../http/HttpClientResponse.ts"
22import type * as HttpRouter from "../http/HttpRouter.ts"
23import type { HttpServerResponse } from "../http/HttpServerResponse.ts"
24import { getStackTraceLimit, setStackTraceLimit } from "../internal/stackTraceLimit.ts"
25import * as Layer from "../Layer.ts"
26import { hasProperty } from "../Predicate.ts"
27import type * as Schema from "../Schema.ts"
28import { Scope } from "../Scope.ts"
29import type { unhandled } from "../Types.ts"
30import type * as HttpApiEndpoint from "./HttpApiEndpoint.ts"
31import { HttpApiSchemaError } from "./HttpApiError.ts"
32import type * as HttpApiGroup from "./HttpApiGroup.ts"
33import type * as HttpApiSecurity from "./HttpApiSecurity.ts"
34
35const TypeId = "~effect/http-api/HttpApiMiddleware"
36
37const SecurityTypeId = "~effect/http-api/HttpApiMiddleware/Security"
38
39/**
40 * Returns `true` when an HTTP API middleware service is security middleware.
41 *
42 * @stability unstable
43 * @category guards
44 * @since 4.0.0
45 */
46export const isSecurity = (u: AnyService): u is AnyServiceSecurity => hasProperty(u, SecurityTypeId)
47
48type ErrorConstraint = Schema.Top | ReadonlyArray<Schema.Top>
49
50type ErrorSchemaFromConstraint<E> = E extends ReadonlyArray<Schema.Constraint> ? E[number]
51 : E extends Schema.Constraint ? E
52 : never
53
54/**
55 * Server-side middleware function for an HTTP API endpoint.
56 *
57 * **Details**
58 *
59 * It receives the endpoint response effect and endpoint/group metadata, and returns
60 * a new response effect that may require additional services and fail with the
61 * middleware's declared error schema.
62 *
63 * @stability unstable
64 * @category models
65 * @since 4.0.0
66 */
67export type HttpApiMiddleware<Provides, E extends ErrorConstraint, Requires> = (
68 httpEffect: Effect.Effect<HttpServerResponse, unhandled, Provides>,
69 options: {
70 readonly endpoint: HttpApiEndpoint.Top
71 readonly group: HttpApiGroup.Top
72 }
73) => Effect.Effect<HttpServerResponse, unhandled | ErrorSchemaFromConstraint<E>["Type"], Requires | HttpRouter.Provided>
74
75/**
76 * Server-side middleware implementations for one or more security schemes.
77 *
78 * **Details**
79 *
80 * Each property handles the credential decoded for that scheme and wraps the
81 * endpoint response effect with the middleware's declared requirements and errors.
82 *
83 * @stability unstable
84 * @category models
85 * @since 4.0.0
86 */
87export type HttpApiMiddlewareSecurity<
88 Security extends Record<string, HttpApiSecurity.HttpApiSecurity>,
89 Provides,
90 E extends ErrorConstraint,
91 Requires
92> = {
93 readonly [K in keyof Security]: (
94 httpEffect: Effect.Effect<HttpServerResponse, unhandled, Provides>,
95 options: {
96 readonly credential: HttpApiSecurity.HttpApiSecurity.Type<Security[K]>
97 readonly endpoint: HttpApiEndpoint.Top
98 readonly group: HttpApiGroup.Top
99 }
100 ) => Effect.Effect<
101 HttpServerResponse,
102 unhandled | ErrorSchemaFromConstraint<E>["Type"],
103 Requires | HttpRouter.Provided
104 >
105}
106
107/**
108 * Client-side middleware function for generated HTTP API clients.
109 *
110 * **Details**
111 *
112 * It receives endpoint/group metadata, the outgoing request, and a `next` function
113 * for continuing the request pipeline.
114 *
115 * @stability unstable
116 * @category models
117 * @since 4.0.0
118 */
119export interface HttpApiMiddlewareClient<_E, CE, R> {
120 (options: {
121 readonly endpoint: HttpApiEndpoint.Top
122 readonly group: HttpApiGroup.Top
123 readonly request: HttpClientRequest.HttpClientRequest
124 readonly next: (
125 request: HttpClientRequest.HttpClientRequest
126 ) => Effect.Effect<HttpClientResponse.HttpClientResponse, HttpClientError.HttpClientError>
127 }): Effect.Effect<HttpClientResponse.HttpClientResponse, CE | HttpClientError.HttpClientError, R>
128}
129
130/**
131 * Client-side service marker required when a middleware declares `requiredForClient`.
132 *
133 * @stability unstable
134 * @category utility types
135 * @since 4.0.0
136 */
137export interface ForClient<Id> {
138 readonly _: unique symbol
139 readonly id: Id
140}
141
142/**
143 * Base service key shape for HTTP API middleware services, including provided services, declared error schemas, and client requirements.
144 *
145 * @stability unstable
146 * @category services
147 * @since 4.0.0
148 */
149export interface AnyService extends Context.Key<any, any> {
150 readonly [TypeId]: typeof TypeId
151 readonly provides: any
152 readonly error: ReadonlySet<Schema.Top>
153 readonly requiredForClient: boolean
154 readonly "~ClientError": any
155}
156
157/**
158 * Middleware service key shape for security middleware, including the security schemes handled by the service.
159 *
160 * @stability unstable
161 * @category services
162 * @since 4.0.0
163 */
164export interface AnyServiceSecurity extends AnyService {
165 readonly [SecurityTypeId]: typeof SecurityTypeId
166 readonly security: Record<string, HttpApiSecurity.HttpApiSecurity>
167}
168
169/**
170 * Type-level identifier carried by middleware services to track provided services, required services, errors, client errors, and client requirements.
171 *
172 * @stability unstable
173 * @category utility types
174 * @since 4.0.0
175 */
176export interface AnyId {
177 readonly [TypeId]: {
178 readonly provides: any
179 readonly requires: any
180 readonly error: ErrorConstraint
181 readonly clientError: any
182 readonly requiredForClient: boolean
183 }
184}
185
186/**
187 * Extracts the services provided by a middleware identifier.
188 *
189 * @stability unstable
190 * @category utility types
191 * @since 4.0.0
192 */
193export type Provides<A> = A extends { readonly [TypeId]: { readonly provides: infer P } } ? P : never
194
195/**
196 * Extracts the services required to run a middleware implementation.
197 *
198 * @stability unstable
199 * @category utility types
200 * @since 4.0.0
201 */
202export type Requires<A> = A extends { readonly [TypeId]: { readonly requires: infer R } } ? R : never
203
204/**
205 * Applies a middleware's service changes to an existing requirement type by removing services it provides and adding services it requires.
206 *
207 * @stability unstable
208 * @category utility types
209 * @since 4.0.0
210 */
211export type ApplyServices<A extends AnyId, R> = Exclude<R, Provides<A>> | Requires<A>
212
213/**
214 * Extracts the schema or schema union used for errors declared by a middleware identifier.
215 *
216 * @stability unstable
217 * @category utility types
218 * @since 4.0.0
219 */
220export type ErrorSchema<A> = A extends { readonly [TypeId]: { readonly error: infer E } } ? ErrorSchemaFromConstraint<E>
221 : never
222
223/**
224 * Extracts the decoded error type declared by a middleware identifier.
225 *
226 * @stability unstable
227 * @category utility types
228 * @since 4.0.0
229 */
230export type Error<A> = ErrorSchema<A>["Type"]
231
232/**
233 * Extracts the client-side error type for middleware that is required on generated clients.
234 *
235 * @stability unstable
236 * @category utility types
237 * @since 4.0.0
238 */
239export type ClientError<A> = A extends {
240 readonly [TypeId]: {
241 readonly clientError: infer CE
242 readonly requiredForClient: true
243 }
244} ? CE
245 : never
246
247/**
248 * Computes the client-side service marker required for middleware that must also run in generated clients.
249 *
250 * @stability unstable
251 * @category utility types
252 * @since 4.0.0
253 */
254export type MiddlewareClient<A> = A extends {
255 readonly [TypeId]: {
256 readonly requiredForClient: true
257 }
258} ? ForClient<A>
259 : never
260
261/**
262 * Extracts the schema services required to encode errors declared by a middleware identifier.
263 *
264 * @stability unstable
265 * @category utility types
266 * @since 4.0.0
267 */
268export type ErrorServicesEncode<A> = ErrorSchema<A>["EncodingServices"]
269
270/**
271 * Extracts the schema services required to decode errors declared by a middleware identifier.
272 *
273 * @stability unstable
274 * @category utility types
275 * @since 4.0.0
276 */
277export type ErrorServicesDecode<A> = ErrorSchema<A>["DecodingServices"]
278
279/**
280 * Class type produced by `Service` for an HTTP API middleware service.
281 *
282 * **Details**
283 *
284 * It combines a `Context.Service` class with the middleware metadata used by
285 * endpoints, builders, and generated clients.
286 *
287 * @stability unstable
288 * @category services
289 * @since 4.0.0
290 */
291export type ServiceClass<
292 Self,
293 Id extends string,
294 Config extends {
295 requires: any
296 provides: any
297 error: ErrorConstraint
298 clientError: any
299 requiredForClient: boolean
300 security: Record<string, HttpApiSecurity.HttpApiSecurity>
301 },
302 Service =
303 ([Config["security"]] extends [never] ? HttpApiMiddleware<Config["provides"], Config["error"], Config["requires"]>
304 : HttpApiMiddlewareSecurity<Config["security"], Config["provides"], Config["error"], Config["requires"]>)
305> =
306 & Context.Service<Self, Service>
307 & {
308 new(_: never): Context.ServiceClass.Shape<Id, Service> & {
309 readonly [TypeId]: {
310 readonly error: Config["error"]
311 readonly requires: Config["requires"]
312 readonly provides: Config["provides"]
313 readonly clientError: Config["clientError"]
314 readonly requiredForClient: Config["requiredForClient"]
315 }
316 }
317 readonly [TypeId]: typeof TypeId
318 readonly error: ReadonlySet<Schema.Top>
319 readonly requiredForClient: Config["requiredForClient"]
320 readonly "~ClientError": Config["clientError"]
321 }
322 & ([keyof Config["security"]] extends [never] ? {} : {
323 readonly [SecurityTypeId]: typeof SecurityTypeId
324 readonly security: Config["security"]
325 })
326
327/**
328 * Creates a `Context.Service` class for an HTTP API middleware implementation.
329 *
330 * **When to use**
331 *
332 * Use when you need an HTTP API middleware service whose configuration declares
333 * required services, provided services, typed error schemas, security schemes,
334 * client errors, or a matching client middleware requirement.
335 *
336 * @stability unstable
337 * @category constructors
338 * @since 4.0.0
339 */
340export const Service = <
341 Self,
342 Config extends {
343 requires?: any
344 provides?: any
345 clientError?: any
346 } = { requires: never; provides: never; clientError: never }
347>(): <
348 const Id extends string,
349 const Error extends ErrorConstraint = never,
350 const Security extends Record<string, HttpApiSecurity.HttpApiSecurity> = never,
351 RequiredForClient extends boolean = false
352>(
353 id: Id,
354 options?: {
355 readonly error?: Error | undefined
356 readonly security?: Security | undefined
357 readonly requiredForClient?: RequiredForClient | undefined
358 } | undefined
359) => ServiceClass<Self, Id, {
360 requires: "requires" extends keyof Config ? Config["requires"] : never
361 provides: "provides" extends keyof Config ? Config["provides"] : never
362 error: Error
363 clientError: "clientError" extends keyof Config ? Config["clientError"] : never
364 requiredForClient: RequiredForClient
365 security: Security
366}> =>
367(
368 id: string,
369 options?: {
370 readonly security?: Record<string, HttpApiSecurity.HttpApiSecurity> | undefined
371 readonly error?: ErrorConstraint | undefined
372 readonly requiredForClient?: boolean | undefined
373 } | undefined
374) => {
375 const limit = getStackTraceLimit()
376 let creationError: globalThis.Error | undefined
377 if (limit !== 0) {
378 setStackTraceLimit(2)
379 creationError = new globalThis.Error()
380 setStackTraceLimit(limit)
381 }
382
383 class Service extends Context.Service<Self, any>()(id) {}
384 const self = Service as any
385 Object.defineProperty(Service, "stack", {
386 get() {
387 return creationError?.stack
388 }
389 })
390 self[TypeId] = TypeId
391 self.error = getError(options?.error)
392 self.requiredForClient = options?.requiredForClient ?? false
393 if (options?.security !== undefined) {
394 if (Object.keys(options.security).length === 0) {
395 throw new Error("HttpApiMiddleware.Service: security object must not be empty")
396 }
397 self[SecurityTypeId] = SecurityTypeId
398 self.security = options.security
399 }
400 return self
401}
402
403function getError(error: ErrorConstraint | undefined): ReadonlySet<Schema.Top> {
404 if (error === undefined) return new Set()
405 return new Set(Array.isArray(error) ? error : [error])
406}
407
408/**
409 * Creates a middleware layer that transforms `HttpApiSchemaError` failures.
410 *
411 * **Details**
412 *
413 * The middleware catches schema errors produced while running an endpoint and uses
414 * the supplied `transform` function to convert them into the middleware's declared
415 * error schema.
416 *
417 * **Example** (Mapping schema errors to custom errors)
418 *
419 * ```ts import.meta.vitest
420 * import { Effect, Schema, type Types } from "effect"
421 * import { HttpRouter, HttpServerResponse } from "effect/http"
422 * import {
423 * HttpApiEndpoint,
424 * HttpApiError,
425 * HttpApiGroup,
426 * HttpApiMiddleware
427 * } from "effect/http-api"
428 *
429 * class CustomError extends Schema.TaggedError<CustomError>()("CustomError", {}) {}
430 *
431 * class ErrorHandler extends HttpApiMiddleware.Service<ErrorHandler>()("api/ErrorHandler", {
432 * error: CustomError
433 * }) {}
434 *
435 * const messages: Array<string> = []
436 * const ErrorHandlerLayer = HttpApiMiddleware.layerSchemaErrorTransform(
437 * ErrorHandler,
438 * (schemaError) =>
439 * Effect.sync(() => messages.push(`Mapping ${schemaError.kind} schema error`)).pipe(
440 * Effect.andThen(Effect.fail(new CustomError()))
441 * )
442 * )
443 *
444 * const endpoint = HttpApiEndpoint.get("example", "/")
445 * const group = HttpApiGroup.make("examples").add(endpoint)
446 * const middlewareContext = {
447 * endpoint: endpoint as unknown as HttpApiEndpoint.Top,
448 * group: group as unknown as HttpApiGroup.Top
449 * }
450 * const Routes = HttpRouter.add(
451 * "GET",
452 * "/",
453 * Effect.gen(function*() {
454 * const applySchemaErrorTransform = yield* ErrorHandler
455 * const schemaError = yield* HttpApiError.HttpApiSchemaError.wrap(
456 * "Body",
457 * Schema.decodeUnknownEffect(Schema.String)(42)
458 * ).pipe(Effect.flip)
459 * const failingResponse = Effect.fail(schemaError as unknown as Types.unhandled)
460 * const result = yield* applySchemaErrorTransform(failingResponse, middlewareContext).pipe(
461 * Effect.match({
462 * onFailure: (error) => error instanceof CustomError ? error._tag : "UnexpectedError",
463 * onSuccess: () => "Success"
464 * })
465 * )
466 * return HttpServerResponse.text(result)
467 * })
468 * ).pipe(HttpRouter.provideRequest(ErrorHandlerLayer))
469 *
470 * const program = Effect.acquireUseRelease(
471 * Effect.sync(() => HttpRouter.toWebHandler(Routes, { disableLogger: true })),
472 * ({ handler }) =>
473 * Effect.gen(function*() {
474 * const response = yield* Effect.promise(() => handler(new Request("http://localhost/")))
475 * const body = yield* Effect.promise(() => response.text())
476 * return body
477 * }),
478 * ({ dispose }) => Effect.promise(dispose)
479 * )
480 *
481 * const body = await Effect.runPromise(program)
482 * const result = [messages, body] // => [["Mapping Body schema error"], "CustomError"]
483 * ```
484 *
485 * @stability unstable
486 * @category layers
487 * @since 4.0.0
488 */
489export const layerSchemaErrorTransform = <Id, E extends ErrorConstraint, Requires>(
490 service: Context.Service<Id, HttpApiMiddleware<never, E, Requires>>,
491 transform: (
492 error: HttpApiSchemaError,
493 context: {
494 readonly endpoint: HttpApiEndpoint.Top
495 readonly group: HttpApiGroup.Top
496 }
497 ) => Effect.Effect<
498 HttpServerResponse,
499 ErrorSchemaFromConstraint<E>["Type"] | HttpApiSchemaError,
500 Requires | HttpRouter.Provided
501 >
502): Layer.Layer<Id> =>
503 Layer.succeed(
504 service,
505 (httpEffect, options) =>
506 Effect.catch(
507 httpEffect,
508 (e): Effect.Effect<
509 HttpServerResponse,
510 unhandled | HttpApiSchemaError | ErrorSchemaFromConstraint<E>["Type"],
511 Requires | HttpRouter.Provided
512 > => HttpApiSchemaError.is(e) ? transform(e, options) : Effect.fail(e)
513 )
514 )
515
516/**
517 * Provides a client-side middleware implementation for a middleware that is required by generated clients.
518 *
519 * **Details**
520 *
521 * The layer captures the surrounding services and makes the middleware available
522 * through the `ForClient` service marker used by HTTP API clients.
523 *
524 * @stability unstable
525 * @category layers
526 * @since 4.0.0
527 */
528export const layerClient = <Id extends AnyId, S, R, EX = never, RX = never>(
529 tag: Context.Key<Id, S>,
530 service:
531 | HttpApiMiddlewareClient<Error<Id>, Id[typeof TypeId]["clientError"], R>
532 | Effect.Effect<
533 HttpApiMiddlewareClient<Error<Id>, Id[typeof TypeId]["clientError"], R>,
534 EX,
535 RX
536 >
537): Layer.Layer<ForClient<Id>, EX, R | Exclude<RX, Scope>> =>
538 Layer.effectContext(Effect.gen(function*() {
539 const services = (yield* Effect.context<R | Scope>()).pipe(
540 Context.omit(Scope)
541 ) as Context.Context<R>
542 const middleware = Effect.isEffect(service) ? yield* service : service
543 return Context.makeUnsafe(
544 new Map([[
545 `${tag.key}/Client`,
546 (options: any) =>
547 Effect.updateContext(
548 middleware(options),
549 (requestContext) => Context.merge(services, requestContext)
550 )
551 ]])
552 )
553 }))