| 1 | /** |
| 2 | * Builds server-side routers for Effect HTTP applications. |
| 3 | * |
| 4 | * `HttpRouter` collects routes and middleware while an application layer is |
| 5 | * being built. Once the router is complete, it handles each |
| 6 | * `HttpServerRequest` by finding a matching route and producing an |
| 7 | * `HttpServerResponse`. The module also includes helpers for route definitions, |
| 8 | * prefixes, parameters, request decoding, CORS, and running the router. |
| 9 | * |
| 10 | * @stability unstable |
| 11 | * @since 4.0.0 |
| 12 | */ |
| 13 | import * as Arr from "../Array.ts" |
| 14 | import * as Context from "../Context.ts" |
| 15 | import * as Effect from "../Effect.ts" |
| 16 | import { compose, dual, identity } from "../Function.ts" |
| 17 | import { fiberEnterInterruptibleUnsafe } from "../internal/effect.ts" |
| 18 | import * as Layer from "../Layer.ts" |
| 19 | import * as Option from "../Option.ts" |
| 20 | import type { ReadonlyRecord } from "../Record.ts" |
| 21 | import * as Schema from "../Schema.ts" |
| 22 | import type { ParseOptions } from "../SchemaAST.ts" |
| 23 | import * as Scope from "../Scope.ts" |
| 24 | import * as Tracer from "../Tracer.ts" |
| 25 | import type * as Types from "../Types.ts" |
| 26 | import * as FindMyWay from "./FindMyWay.ts" |
| 27 | import * as HttpEffect from "./HttpEffect.ts" |
| 28 | import * as HttpIncomingMessage from "./HttpIncomingMessage.ts" |
| 29 | import type * as HttpMethod from "./HttpMethod.ts" |
| 30 | import * as HttpMiddleware from "./HttpMiddleware.ts" |
| 31 | import * as HttpServer from "./HttpServer.ts" |
| 32 | import * as HttpServerError from "./HttpServerError.ts" |
| 33 | import * as HttpServerRequest from "./HttpServerRequest.ts" |
| 34 | import * as HttpServerResponse from "./HttpServerResponse.ts" |
| 35 | |
| 36 | const TypeId = "~effect/http/HttpRouter" |
| 37 | |
| 38 | /** |
| 39 | * Defines the service interface for registering HTTP routes and middleware. |
| 40 | * |
| 41 | * **Details** |
| 42 | * |
| 43 | * An `HttpRouter` can add routes, apply path prefixes, install global middleware, |
| 44 | * and expose the registered routes as an Effect that handles the current server |
| 45 | * request. |
| 46 | * |
| 47 | * @stability unstable |
| 48 | * @category services |
| 49 | * @since 4.0.0 |
| 50 | */ |
| 51 | export interface HttpRouter { |
| 52 | readonly [TypeId]: typeof TypeId |
| 53 | |
| 54 | readonly prefixed: (prefix: string) => HttpRouter |
| 55 | |
| 56 | readonly add: <E = never, R = never>( |
| 57 | method: "*" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "QUERY", |
| 58 | path: PathInput, |
| 59 | handler: |
| 60 | | HttpServerResponse.HttpServerResponse |
| 61 | | Effect.Effect<HttpServerResponse.HttpServerResponse, E, R> |
| 62 | | ((request: HttpServerRequest.HttpServerRequest) => Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>), |
| 63 | options?: { readonly uninterruptible?: boolean | undefined } | undefined |
| 64 | ) => Effect.Effect< |
| 65 | void, |
| 66 | never, |
| 67 | Request.From<"Requires", Exclude<R, Provided>> | Request.From<"Error", E> |
| 68 | > |
| 69 | |
| 70 | readonly addAll: <const Routes extends ReadonlyArray<Route<any, any>>>( |
| 71 | routes: Routes |
| 72 | ) => Effect.Effect< |
| 73 | void, |
| 74 | never, |
| 75 | | Request.From<"Requires", Exclude<Route.Context<Routes[number]>, Provided>> |
| 76 | | Request.From<"Error", Route.Error<Routes[number]>> |
| 77 | > |
| 78 | |
| 79 | readonly addGlobalMiddleware: <E, R>( |
| 80 | middleware: |
| 81 | & (( |
| 82 | effect: Effect.Effect<HttpServerResponse.HttpServerResponse, Types.unhandled> |
| 83 | ) => Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>) |
| 84 | & (Types.unhandled extends E ? unknown : "You cannot handle any errors") |
| 85 | ) => Effect.Effect< |
| 86 | void, |
| 87 | never, |
| 88 | | Request.From<"GlobalRequires", Exclude<R, GlobalProvided>> |
| 89 | | Request.From<"GlobalError", Exclude<E, Types.unhandled>> |
| 90 | > |
| 91 | |
| 92 | // @effect-diagnostics-next-line lazyEffect:off |
| 93 | readonly asHttpEffect: () => Effect.Effect< |
| 94 | HttpServerResponse.HttpServerResponse, |
| 95 | unknown, |
| 96 | HttpServerRequest.HttpServerRequest | Scope.Scope |
| 97 | > |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Service tag for the HTTP router used while constructing an HTTP application. |
| 102 | * Route and middleware layers require this service to register themselves with |
| 103 | * the router. |
| 104 | * |
| 105 | * @stability unstable |
| 106 | * @category services |
| 107 | * @since 4.0.0 |
| 108 | */ |
| 109 | export const HttpRouter: Context.Service<HttpRouter, HttpRouter> = Context.Service<HttpRouter>( |
| 110 | "effect/http/HttpRouter" |
| 111 | ) |
| 112 | |
| 113 | /** |
| 114 | * Constructs an empty `HttpRouter` service. |
| 115 | * |
| 116 | * **Details** |
| 117 | * |
| 118 | * The returned router accepts route and middleware registrations and later routes |
| 119 | * the current `HttpServerRequest` to the matching `HttpServerResponse`. |
| 120 | * |
| 121 | * @stability unstable |
| 122 | * @category constructors |
| 123 | * @since 4.0.0 |
| 124 | */ |
| 125 | export const make = Effect.gen(function*() { |
| 126 | const router = FindMyWay.make<Route<any, never>>(yield* RouterConfig) |
| 127 | const middleware = new Set<middleware.Fn>() |
| 128 | |
| 129 | const addAll = <const Routes extends ReadonlyArray<Route<any, any>>>( |
| 130 | routes: Routes |
| 131 | ): Effect.Effect< |
| 132 | void, |
| 133 | never, |
| 134 | | Request.From<"Requires", Exclude<Route.Context<Routes[number]>, Provided>> |
| 135 | | Request.From<"Error", Route.Error<Routes[number]>> |
| 136 | > => |
| 137 | Effect.contextWith((context: Context.Context<never>) => { |
| 138 | const middleware = getMiddleware(context) |
| 139 | const applyMiddleware = (effect: Effect.Effect<HttpServerResponse.HttpServerResponse>) => { |
| 140 | for (let i = 0; i < middleware.length; i++) { |
| 141 | effect = middleware[i](effect) |
| 142 | } |
| 143 | return effect |
| 144 | } |
| 145 | for (let i = 0; i < routes.length; i++) { |
| 146 | const route = middleware.length === 0 ? routes[i] : makeRoute({ |
| 147 | ...routes[i], |
| 148 | handler: applyMiddleware(routes[i].handler as Effect.Effect<HttpServerResponse.HttpServerResponse>) |
| 149 | }) |
| 150 | if (route.method === "*") { |
| 151 | if (route.path.endsWith("/*")) { |
| 152 | router.all(route.path, route as any) |
| 153 | router.all(route.path.slice(0, -2) as any, route as any) |
| 154 | } else { |
| 155 | router.all(route.path, route as any) |
| 156 | } |
| 157 | } else { |
| 158 | if (route.path.endsWith("/*")) { |
| 159 | router.on(route.method, route.path, route as any) |
| 160 | router.on(route.method, route.path.slice(0, -2) as any, route as any) |
| 161 | } else { |
| 162 | router.on(route.method, route.path, route as any) |
| 163 | } |
| 164 | } |
| 165 | } |
| 166 | return Effect.void |
| 167 | }) |
| 168 | |
| 169 | return HttpRouter.of({ |
| 170 | [TypeId]: TypeId, |
| 171 | prefixed(this: HttpRouter, prefix: string) { |
| 172 | prefix = removeTrailingSlash(prefix as PathInput) |
| 173 | return HttpRouter.of({ |
| 174 | ...this, |
| 175 | prefixed: (newPrefix: string) => this.prefixed(prefixPath(newPrefix, prefix)), |
| 176 | addAll: (routes) => addAll(routes.map(prefixRoute(prefix))) as any, |
| 177 | add: (method, path, handler, options) => |
| 178 | addAll([ |
| 179 | makeRoute({ |
| 180 | method, |
| 181 | path: prefixPath(path, prefix) as PathInput, |
| 182 | handler: HttpServerResponse.isHttpServerResponse(handler) ? |
| 183 | Effect.succeed(handler) : |
| 184 | Effect.isEffect(handler) |
| 185 | ? handler |
| 186 | : Effect.flatMap(HttpServerRequest.HttpServerRequest, handler), |
| 187 | uninterruptible: options?.uninterruptible ?? false, |
| 188 | prefix |
| 189 | }) |
| 190 | ]) |
| 191 | }) |
| 192 | }, |
| 193 | addAll, |
| 194 | add: (method, path, handler, options) => addAll([route(method, path, handler, options)]), |
| 195 | addGlobalMiddleware: (middleware_) => |
| 196 | Effect.sync(() => { |
| 197 | middleware.add(middleware_ as any) |
| 198 | }), |
| 199 | asHttpEffect() { |
| 200 | let handler = Effect.withFiber<HttpServerResponse.HttpServerResponse, unknown>((fiber) => { |
| 201 | let context = fiber.context |
| 202 | const request = Context.getUnsafe(context, HttpServerRequest.HttpServerRequest) |
| 203 | let result = router.find(request.method, request.url) |
| 204 | if (result === undefined && request.method === "HEAD") { |
| 205 | result = router.find("GET", request.url) |
| 206 | } |
| 207 | if (result === undefined) { |
| 208 | return Effect.fail( |
| 209 | new HttpServerError.HttpServerError({ |
| 210 | reason: new HttpServerError.RouteNotFound({ request }) |
| 211 | }) |
| 212 | ) |
| 213 | } |
| 214 | const route = result.handler |
| 215 | if (Option.isSome(route.prefix)) { |
| 216 | context = Context.add( |
| 217 | context, |
| 218 | HttpServerRequest.HttpServerRequest, |
| 219 | sliceRequestUrl(request, route.prefix.value) |
| 220 | ) |
| 221 | } |
| 222 | context = Context.add(context, HttpServerRequest.ParsedSearchParams, result.searchParams) |
| 223 | context = Context.add(context, RouteContext, { |
| 224 | route, |
| 225 | params: result.params |
| 226 | }) |
| 227 | |
| 228 | if (fiber.getRef(Tracer.Tracer) !== Tracer.nativeTracer) { |
| 229 | const span = Context.getOrUndefined(context, Tracer.ParentSpan) |
| 230 | if (span !== undefined && span._tag === "Span" && span.sampled) { |
| 231 | span.attribute("http.route", route.path) |
| 232 | } |
| 233 | } |
| 234 | // The enclosing request restores the fiber context on exit. |
| 235 | fiber.setContext(context) |
| 236 | if (!route.uninterruptible) { |
| 237 | const interrupted = fiberEnterInterruptibleUnsafe(fiber) |
| 238 | if (interrupted !== undefined) return interrupted |
| 239 | } |
| 240 | return route.handler as Effect.Effect< |
| 241 | HttpServerResponse.HttpServerResponse, |
| 242 | unknown |
| 243 | > |
| 244 | }) |
| 245 | if (middleware.size === 0) return handler |
| 246 | for (const fn of Arr.reverse(middleware)) { |
| 247 | handler = fn(handler as any) |
| 248 | } |
| 249 | return handler |
| 250 | } |
| 251 | }) |
| 252 | }) |
| 253 | |
| 254 | function sliceRequestUrl(request: HttpServerRequest.HttpServerRequest, prefix: string) { |
| 255 | const prefexLen = prefix.length |
| 256 | return request.modify({ url: request.url.length <= prefexLen ? "/" : request.url.slice(prefexLen) }) |
| 257 | } |
| 258 | |
| 259 | /** |
| 260 | * Context reference for low-level router configuration. |
| 261 | * |
| 262 | * **Details** |
| 263 | * |
| 264 | * The value is passed to the route matcher when an `HttpRouter` is created and |
| 265 | * defaults to an empty configuration. |
| 266 | * |
| 267 | * @stability unstable |
| 268 | * @category services |
| 269 | * @since 4.0.0 |
| 270 | */ |
| 271 | export const RouterConfig = Context.Reference<Partial<FindMyWay.RouterConfig>>( |
| 272 | "effect/http/HttpRouter/RouterConfig", |
| 273 | { defaultValue: () => ({}) } |
| 274 | ) |
| 275 | |
| 276 | /** |
| 277 | * Service for the matched HTTP route in the current request. |
| 278 | * |
| 279 | * **When to use** |
| 280 | * |
| 281 | * Use to read captured path parameters and route metadata while handling a |
| 282 | * request matched by the router. |
| 283 | * |
| 284 | * **Details** |
| 285 | * |
| 286 | * It provides the route definition and the path parameters captured by the route |
| 287 | * matcher. |
| 288 | * |
| 289 | * @stability unstable |
| 290 | * @category services |
| 291 | * @since 4.0.0 |
| 292 | */ |
| 293 | export class RouteContext extends Context.Service<RouteContext, { |
| 294 | readonly params: Readonly<Record<string, string | undefined>> |
| 295 | readonly route: Route<unknown, unknown> |
| 296 | }>()("effect/http/HttpRouter/RouteContext") {} |
| 297 | |
| 298 | /** |
| 299 | * Effect that returns the path parameters captured for the current matched route. |
| 300 | * |
| 301 | * @stability unstable |
| 302 | * @category getters |
| 303 | * @since 4.0.0 |
| 304 | */ |
| 305 | export const params: Effect.Effect< |
| 306 | ReadonlyRecord<string, string | undefined>, |
| 307 | never, |
| 308 | RouteContext |
| 309 | > = Effect.withFiberSucceed((fiber) => |
| 310 | Context.getUnsafe(fiber.context as Context.Context<RouteContext>, RouteContext).params |
| 311 | ) |
| 312 | |
| 313 | /** |
| 314 | * Decodes a schema from the current request and its JSON body. |
| 315 | * |
| 316 | * **Details** |
| 317 | * |
| 318 | * The input passed to the schema includes the request method, URL, headers, |
| 319 | * cookies, path parameters, search parameters, and parsed JSON body. The effect |
| 320 | * fails if the body cannot be parsed or the schema decode fails. |
| 321 | * |
| 322 | * @stability unstable |
| 323 | * @category schemas |
| 324 | * @since 4.0.0 |
| 325 | */ |
| 326 | export const schemaJson = < |
| 327 | A, |
| 328 | I extends Partial<{ |
| 329 | readonly method: HttpMethod.HttpMethod |
| 330 | readonly url: string |
| 331 | readonly cookies: Readonly<Record<string, string | undefined>> |
| 332 | readonly headers: Readonly<Record<string, string | undefined>> |
| 333 | readonly pathParams: Readonly<Record<string, string | undefined>> |
| 334 | readonly searchParams: Readonly<Record<string, string | ReadonlyArray<string> | undefined>> |
| 335 | readonly body: any |
| 336 | }>, |
| 337 | RD |
| 338 | >( |
| 339 | schema: Schema.ConstraintCodec<A, I, RD, unknown>, |
| 340 | options?: (ParseOptions & HttpIncomingMessage.JsonOptions) | undefined |
| 341 | ): Effect.Effect< |
| 342 | A, |
| 343 | HttpServerError.HttpServerError | Schema.SchemaError, |
| 344 | HttpServerRequest.HttpServerRequest | HttpServerRequest.ParsedSearchParams | RouteContext | RD |
| 345 | > => { |
| 346 | const parse = Schema.decodeUnknownEffect(schema) |
| 347 | const parseBody = HttpIncomingMessage.schemaBodyJson(Schema.Unknown, options) |
| 348 | return Effect.contextWith( |
| 349 | ( |
| 350 | context: Context.Context< |
| 351 | HttpServerRequest.HttpServerRequest | HttpServerRequest.ParsedSearchParams | RouteContext |
| 352 | > |
| 353 | ) => { |
| 354 | const request = Context.get(context, HttpServerRequest.HttpServerRequest) |
| 355 | const searchParams = Context.get(context, HttpServerRequest.ParsedSearchParams) |
| 356 | const routeContext = Context.get(context, RouteContext) |
| 357 | return Effect.flatMap(parseBody(request), (body) => |
| 358 | parse({ |
| 359 | method: request.method, |
| 360 | url: request.url, |
| 361 | headers: request.headers, |
| 362 | cookies: request.cookies, |
| 363 | pathParams: routeContext.params, |
| 364 | searchParams, |
| 365 | body |
| 366 | }, options)) |
| 367 | } |
| 368 | ) |
| 369 | } |
| 370 | |
| 371 | /** |
| 372 | * Decodes a schema from the current request without reading the request body. |
| 373 | * |
| 374 | * **Details** |
| 375 | * |
| 376 | * The input passed to the schema includes the request method, URL, headers, |
| 377 | * cookies, path parameters, and search parameters. |
| 378 | * |
| 379 | * @stability unstable |
| 380 | * @category schemas |
| 381 | * @since 4.0.0 |
| 382 | */ |
| 383 | export const schemaNoBody = < |
| 384 | A, |
| 385 | I extends Partial<{ |
| 386 | readonly method: HttpMethod.HttpMethod |
| 387 | readonly url: string |
| 388 | readonly cookies: Readonly<Record<string, string | undefined>> |
| 389 | readonly headers: Readonly<Record<string, string | undefined>> |
| 390 | readonly pathParams: Readonly<Record<string, string | undefined>> |
| 391 | readonly searchParams: Readonly<Record<string, string | ReadonlyArray<string> | undefined>> |
| 392 | }>, |
| 393 | RD |
| 394 | >( |
| 395 | schema: Schema.ConstraintCodec<A, I, RD, unknown>, |
| 396 | options?: ParseOptions | undefined |
| 397 | ): Effect.Effect< |
| 398 | A, |
| 399 | Schema.SchemaError, |
| 400 | HttpServerRequest.HttpServerRequest | HttpServerRequest.ParsedSearchParams | RouteContext | RD |
| 401 | > => { |
| 402 | const parse = Schema.decodeUnknownEffect(schema) |
| 403 | return Effect.contextWith( |
| 404 | ( |
| 405 | context: Context.Context< |
| 406 | HttpServerRequest.HttpServerRequest | HttpServerRequest.ParsedSearchParams | RouteContext |
| 407 | > |
| 408 | ) => { |
| 409 | const request = Context.get(context, HttpServerRequest.HttpServerRequest) |
| 410 | const searchParams = Context.get(context, HttpServerRequest.ParsedSearchParams) |
| 411 | const routeContext = Context.get(context, RouteContext) |
| 412 | return parse({ |
| 413 | method: request.method, |
| 414 | url: request.url, |
| 415 | headers: request.headers, |
| 416 | cookies: request.cookies, |
| 417 | pathParams: routeContext.params, |
| 418 | searchParams |
| 419 | }, options) |
| 420 | } |
| 421 | ) |
| 422 | } |
| 423 | |
| 424 | /** |
| 425 | * Decodes a schema from the current route path parameters and search parameters. |
| 426 | * |
| 427 | * **Details** |
| 428 | * |
| 429 | * When the same key appears in both sources, the path parameter value is used. |
| 430 | * |
| 431 | * @stability unstable |
| 432 | * @category schemas |
| 433 | * @since 4.0.0 |
| 434 | */ |
| 435 | export const schemaParams = <A, I extends Readonly<Record<string, string | ReadonlyArray<string> | undefined>>, RD>( |
| 436 | schema: Schema.ConstraintCodec<A, I, RD, unknown>, |
| 437 | options?: ParseOptions | undefined |
| 438 | ): Effect.Effect<A, Schema.SchemaError, HttpServerRequest.ParsedSearchParams | RouteContext | RD> => { |
| 439 | const parse = Schema.decodeUnknownEffect(schema) |
| 440 | return Effect.contextWith((context: Context.Context<HttpServerRequest.ParsedSearchParams | RouteContext>) => { |
| 441 | const searchParams = Context.get(context, HttpServerRequest.ParsedSearchParams) |
| 442 | const routeContext = Context.get(context, RouteContext) |
| 443 | return parse({ ...searchParams, ...routeContext.params }, options) |
| 444 | }) |
| 445 | } |
| 446 | |
| 447 | /** |
| 448 | * Decodes a schema from the path parameters captured for the current matched |
| 449 | * route. |
| 450 | * |
| 451 | * @stability unstable |
| 452 | * @category schemas |
| 453 | * @since 4.0.0 |
| 454 | */ |
| 455 | export const schemaPathParams = <A, I extends Readonly<Record<string, string | undefined>>, RD>( |
| 456 | schema: Schema.ConstraintCodec<A, I, RD, unknown>, |
| 457 | options?: ParseOptions | undefined |
| 458 | ): Effect.Effect<A, Schema.SchemaError, RouteContext | RD> => { |
| 459 | const parse = Schema.decodeUnknownEffect(schema) |
| 460 | return Effect.flatMap(params, (_) => parse(_, options)) |
| 461 | } |
| 462 | |
| 463 | /** |
| 464 | * Creates a layer that accesses the current `HttpRouter` service and runs the |
| 465 | * supplied effect. |
| 466 | * |
| 467 | * **When to use** |
| 468 | * |
| 469 | * Use when you need to register routes or middleware with the router during layer |
| 470 | * construction. |
| 471 | * |
| 472 | * **Example** (Registering routes during layer construction) |
| 473 | * |
| 474 | * ```ts import.meta.vitest |
| 475 | * import { Effect } from "effect" |
| 476 | * import { HttpRouter, HttpServerResponse } from "effect/http" |
| 477 | * |
| 478 | * const Routes = HttpRouter.use((router) => |
| 479 | * router.add("GET", "/health", HttpServerResponse.text("ready")) |
| 480 | * ) |
| 481 | * |
| 482 | * const program = Effect.acquireUseRelease( |
| 483 | * Effect.sync(() => HttpRouter.toWebHandler(Routes, { disableLogger: true })), |
| 484 | * ({ handler }) => |
| 485 | * Effect.gen(function*() { |
| 486 | * const response = yield* Effect.promise(() => handler(new Request("http://localhost/health"))) |
| 487 | * const body = yield* Effect.promise(() => response.text()) |
| 488 | * return body |
| 489 | * }), |
| 490 | * ({ dispose }) => Effect.promise(dispose) |
| 491 | * ) |
| 492 | * |
| 493 | * await Effect.runPromise(program) // => "ready" |
| 494 | * ``` |
| 495 | * |
| 496 | * @stability unstable |
| 497 | * @category layers |
| 498 | * @since 4.0.0 |
| 499 | */ |
| 500 | export const use = <A, E, R>( |
| 501 | f: (router: HttpRouter) => Effect.Effect<A, E, R> |
| 502 | ): Layer.Layer<never, E, HttpRouter | Exclude<R, Scope.Scope>> => Layer.effectDiscard(Effect.flatMap(HttpRouter, f)) |
| 503 | |
| 504 | /** |
| 505 | * Create a layer that adds a single route to the HTTP router. |
| 506 | * |
| 507 | * **Example** (Adding a GET route) |
| 508 | * |
| 509 | * ```ts import.meta.vitest |
| 510 | * import { Layer } from "effect" |
| 511 | * import { HttpRouter, HttpServerResponse } from "effect/http" |
| 512 | * |
| 513 | * const Route = HttpRouter.add( |
| 514 | * "GET", |
| 515 | * "/hello", |
| 516 | * HttpServerResponse.text("Hello, World!") |
| 517 | * ) |
| 518 | * Layer.isLayer(Route) // => true |
| 519 | * ``` |
| 520 | * |
| 521 | * @stability unstable |
| 522 | * @category layers |
| 523 | * @since 4.0.0 |
| 524 | */ |
| 525 | export const add = <E = never, R = never>( |
| 526 | method: "*" | "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "QUERY", |
| 527 | path: PathInput, |
| 528 | handler: |
| 529 | | HttpServerResponse.HttpServerResponse |
| 530 | | Effect.Effect<HttpServerResponse.HttpServerResponse, E, R> |
| 531 | | ((request: HttpServerRequest.HttpServerRequest) => Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>), |
| 532 | options?: { |
| 533 | readonly uninterruptible?: boolean | undefined |
| 534 | } |
| 535 | ): Layer.Layer<never, never, HttpRouter | Request.From<"Requires", Exclude<R, Provided>> | Request.From<"Error", E>> => |
| 536 | use((router) => router.add(method, path, handler, options)) |
| 537 | |
| 538 | /** |
| 539 | * Create a layer that adds multiple routes to the HTTP router. |
| 540 | * |
| 541 | * **Example** (Adding multiple routes) |
| 542 | * |
| 543 | * ```ts import.meta.vitest |
| 544 | * import { Layer } from "effect" |
| 545 | * import { HttpRouter, HttpServerResponse } from "effect/http" |
| 546 | * |
| 547 | * const Routes = HttpRouter.addAll([ |
| 548 | * HttpRouter.route( |
| 549 | * "GET", |
| 550 | * "/hello", |
| 551 | * HttpServerResponse.text("Hello, World!") |
| 552 | * ) |
| 553 | * ]) |
| 554 | * Layer.isLayer(Routes) // => true |
| 555 | * ``` |
| 556 | * |
| 557 | * @stability unstable |
| 558 | * @category layers |
| 559 | * @since 4.0.0 |
| 560 | */ |
| 561 | export const addAll = <Routes extends ReadonlyArray<Route<any, any>>, EX = never, RX = never>( |
| 562 | routes: Routes | Effect.Effect<Routes, EX, RX>, |
| 563 | options?: { |
| 564 | readonly prefix?: string | undefined |
| 565 | } |
| 566 | ): Layer.Layer< |
| 567 | never, |
| 568 | EX, |
| 569 | | HttpRouter |
| 570 | | Exclude<RX, Scope.Scope> |
| 571 | | Request.From<"Requires", Exclude<Route.Context<Routes[number]>, Provided>> |
| 572 | | Request.From<"Error", Route.Error<Routes[number]>> |
| 573 | > => |
| 574 | Layer.effectDiscard(Effect.gen(function*() { |
| 575 | const toAdd = Effect.isEffect(routes) ? yield* routes : routes |
| 576 | let router = yield* HttpRouter |
| 577 | if (options?.prefix) { |
| 578 | router = router.prefixed(options.prefix) |
| 579 | } |
| 580 | yield* router.addAll(toAdd) |
| 581 | })) |
| 582 | |
| 583 | /** |
| 584 | * Layer that provides a newly constructed `HttpRouter`. |
| 585 | * |
| 586 | * @stability unstable |
| 587 | * @category layers |
| 588 | * @since 4.0.0 |
| 589 | */ |
| 590 | export const layer: Layer.Layer<HttpRouter> = Layer.effect(HttpRouter)(make) |
| 591 | |
| 592 | /** |
| 593 | * Builds an application layer with a router and returns the router as an HTTP |
| 594 | * handler effect. |
| 595 | * |
| 596 | * **Details** |
| 597 | * |
| 598 | * The returned effect handles the current `HttpServerRequest` in the current |
| 599 | * `Scope`; route request markers are converted into the ordinary requirements of |
| 600 | * the returned handler. |
| 601 | * |
| 602 | * **Gotchas** |
| 603 | * |
| 604 | * The layer is built with a forked `MemoMap`, so calling `toHttpEffect` twice |
| 605 | * will build the same layer twice. |
| 606 | * |
| 607 | * @stability unstable |
| 608 | * @category converting |
| 609 | * @since 4.0.0 |
| 610 | */ |
| 611 | export const toHttpEffect = <A, E, R>( |
| 612 | appLayer: Layer.Layer<A, E, R> |
| 613 | ): Effect.Effect< |
| 614 | Effect.Effect< |
| 615 | HttpServerResponse.HttpServerResponse, |
| 616 | Request.Only<"Error", R> | Request.Only<"GlobalError", R> | HttpServerError.HttpServerError, |
| 617 | Scope.Scope | HttpServerRequest.HttpServerRequest | Request.Only<"Requires", R> | Request.Only<"GlobalRequires", R> |
| 618 | >, |
| 619 | Request.Without<E>, |
| 620 | Exclude<Request.Without<R>, HttpRouter> | Scope.Scope |
| 621 | > => |
| 622 | Effect.contextWith((context) => { |
| 623 | const scope = Context.get(context, Scope.Scope) |
| 624 | const memoMap = Layer.CurrentMemoMap.forkOrCreate(context) |
| 625 | return Effect.map(Layer.buildWithMemoMap(Layer.provideMerge(appLayer, layer), memoMap, scope), (context) => { |
| 626 | const router = Context.get(context, HttpRouter) |
| 627 | return router.asHttpEffect() |
| 628 | }) |
| 629 | }) as any |
| 630 | |
| 631 | const RouteTypeId = "~effect/http/HttpRouter/Route" |
| 632 | |
| 633 | /** |
| 634 | * Description of a registered HTTP route. |
| 635 | * |
| 636 | * **Details** |
| 637 | * |
| 638 | * A route pairs an HTTP method and path pattern with a response handler, plus |
| 639 | * metadata used for prefix handling and interruptibility. |
| 640 | * |
| 641 | * @stability unstable |
| 642 | * @category routes |
| 643 | * @since 4.0.0 |
| 644 | */ |
| 645 | export interface Route<E = never, R = never> { |
| 646 | readonly [RouteTypeId]: typeof RouteTypeId |
| 647 | readonly method: HttpMethod.HttpMethod | "*" |
| 648 | readonly path: PathInput |
| 649 | readonly handler: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R> |
| 650 | readonly uninterruptible: boolean |
| 651 | readonly prefix: Option.Option<string> |
| 652 | } |
| 653 | |
| 654 | /** |
| 655 | * Helper types for extracting the error and context types carried by `Route` |
| 656 | * values. |
| 657 | * |
| 658 | * @stability unstable |
| 659 | * @since 4.0.0 |
| 660 | */ |
| 661 | export declare namespace Route { |
| 662 | /** |
| 663 | * Extracts the error type produced by a `Route` handler. |
| 664 | * |
| 665 | * @stability unstable |
| 666 | * @category routes |
| 667 | * @since 4.0.0 |
| 668 | */ |
| 669 | export type Error<R extends Route<any, any>> = R extends Route<infer E, infer _R> ? E : never |
| 670 | |
| 671 | /** |
| 672 | * Extracts the context requirements of a `Route` handler. |
| 673 | * |
| 674 | * @stability unstable |
| 675 | * @category routes |
| 676 | * @since 4.0.0 |
| 677 | */ |
| 678 | export type Context<T extends Route<any, any>> = T extends Route<infer _E, infer R> ? R : never |
| 679 | } |
| 680 | |
| 681 | const makeRoute = <E, R>(options: { |
| 682 | readonly method: HttpMethod.HttpMethod | "*" |
| 683 | readonly path: PathInput |
| 684 | readonly handler: Effect.Effect<HttpServerResponse.HttpServerResponse, E, R> |
| 685 | readonly uninterruptible?: boolean | undefined |
| 686 | readonly prefix?: Option.Option<string> | string | undefined |
| 687 | }): Route<E, Exclude<R, Provided>> => (({ |
| 688 | ...options, |
| 689 | uninterruptible: options.uninterruptible ?? false, |
| 690 | prefix: typeof options.prefix === "string" ? Option.some(options.prefix) : options.prefix ?? Option.none(), |
| 691 | [RouteTypeId]: RouteTypeId |
| 692 | }) as Route<E, Exclude<R, Provided>>) |
| 693 | |
| 694 | /** |
| 695 | * Constructs a `Route` from an HTTP method, path, and handler. |
| 696 | * |
| 697 | * **Details** |
| 698 | * |
| 699 | * The handler may be a static response, an effect that produces a response, or a |
| 700 | * function from the current request to a response effect. Set `uninterruptible` to |
| 701 | * prevent the route handler from being made interruptible while it runs. |
| 702 | * |
| 703 | * @stability unstable |
| 704 | * @category routes |
| 705 | * @since 4.0.0 |
| 706 | */ |
| 707 | export const route = <E = never, R = never>( |
| 708 | method: "*" | HttpMethod.HttpMethod, |
| 709 | path: PathInput, |
| 710 | handler: |
| 711 | | HttpServerResponse.HttpServerResponse |
| 712 | | Effect.Effect<HttpServerResponse.HttpServerResponse, E, R> |
| 713 | | ((request: HttpServerRequest.HttpServerRequest) => Effect.Effect<HttpServerResponse.HttpServerResponse, E, R>), |
| 714 | options?: { |
| 715 | readonly uninterruptible?: boolean | undefined |
| 716 | } |
| 717 | ): Route<E, Exclude<R, Provided>> => |
| 718 | makeRoute({ |
| 719 | ...options, |
| 720 | method, |
| 721 | path, |
| 722 | handler: HttpServerResponse.isHttpServerResponse(handler) ? |
| 723 | Effect.succeed(handler) : |
| 724 | Effect.isEffect(handler) |
| 725 | ? handler |
| 726 | : Effect.flatMap(HttpServerRequest.HttpServerRequest, handler), |
| 727 | uninterruptible: options?.uninterruptible ?? false |
| 728 | }) |
| 729 | |
| 730 | /** |
| 731 | * Path pattern accepted by the router. Routes must use an absolute path |
| 732 | * beginning with `/` or the wildcard `*`. |
| 733 | * |
| 734 | * @stability unstable |
| 735 | * @category models |
| 736 | * @since 4.0.0 |
| 737 | */ |
| 738 | export type PathInput = `/${string}` | "*" |
| 739 | |
| 740 | const removeTrailingSlash = ( |
| 741 | path: PathInput |
| 742 | ): PathInput => (path.endsWith("/") ? path.slice(0, -1) : path) as any |
| 743 | |
| 744 | /** |
| 745 | * Adds a path prefix to a route path. |
| 746 | * |
| 747 | * **Details** |
| 748 | * |
| 749 | * Trailing slashes are removed from the prefix; `/` becomes the prefix itself and |
| 750 | * `*` becomes a wildcard route under the prefix. |
| 751 | * |
| 752 | * @stability unstable |
| 753 | * @category transforming |
| 754 | * @since 4.0.0 |
| 755 | */ |
| 756 | export const prefixPath: { |
| 757 | (prefix: string): (self: string) => string |
| 758 | (self: string, prefix: string): string |
| 759 | } = dual(2, (self: string, prefix: string) => { |
| 760 | prefix = removeTrailingSlash(prefix as PathInput) |
| 761 | if (self === "*") return `${prefix}/*` |
| 762 | else if (self === "/") return prefix |
| 763 | return prefix + self |
| 764 | }) |
| 765 | |
| 766 | /** |
| 767 | * Returns a copy of a route with its path prefixed. |
| 768 | * |
| 769 | * **Details** |
| 770 | * |
| 771 | * The prefix is also tracked on the route so that, when the route handles a |
| 772 | * request, the matched prefix can be removed from the request URL seen by the |
| 773 | * handler. |
| 774 | * |
| 775 | * @stability unstable |
| 776 | * @category routes |
| 777 | * @since 4.0.0 |
| 778 | */ |
| 779 | export const prefixRoute: { |
| 780 | (prefix: string): <E, R>(self: Route<E, R>) => Route<E, R> |
| 781 | <E, R>(self: Route<E, R>, prefix: string): Route<E, R> |
| 782 | } = dual(2, <E, R>(self: Route<E, R>, prefix: string): Route<E, R> => |
| 783 | makeRoute({ |
| 784 | ...self, |
| 785 | path: prefixPath(self.path, prefix) as PathInput, |
| 786 | prefix: Option.match(self.prefix, { |
| 787 | onNone: () => removeTrailingSlash(prefix as PathInput), |
| 788 | onSome: (existingPrefix) => prefixPath(existingPrefix, prefix) as string |
| 789 | }) |
| 790 | })) |
| 791 | |
| 792 | /** |
| 793 | * Represents a request-level dependency, that needs to be provided by |
| 794 | * middleware. |
| 795 | * |
| 796 | * @stability unstable |
| 797 | * @category utility types |
| 798 | * @since 4.0.0 |
| 799 | */ |
| 800 | export interface Request<Kind extends string, T> { |
| 801 | readonly _: unique symbol |
| 802 | readonly kind: Kind |
| 803 | readonly type: T |
| 804 | } |
| 805 | |
| 806 | /** |
| 807 | * Helper types for request-level dependency markers used by router layers and |
| 808 | * middleware. |
| 809 | * |
| 810 | * @stability unstable |
| 811 | * @since 4.0.0 |
| 812 | */ |
| 813 | export declare namespace Request { |
| 814 | /** |
| 815 | * Wraps a type in a request-level marker of the supplied kind. |
| 816 | * |
| 817 | * @stability unstable |
| 818 | * @category utility types |
| 819 | * @since 4.0.0 |
| 820 | */ |
| 821 | export type From<Kind extends string, R> = R extends infer T ? Request<Kind, T> : never |
| 822 | |
| 823 | /** |
| 824 | * Extracts the payload types from request-level markers that have the supplied |
| 825 | * kind. |
| 826 | * |
| 827 | * @stability unstable |
| 828 | * @category utility types |
| 829 | * @since 4.0.0 |
| 830 | */ |
| 831 | export type Only<Kind extends string, A> = A extends Request<Kind, infer T> ? T : never |
| 832 | |
| 833 | /** |
| 834 | * Removes request-level markers from a union, leaving only ordinary requirement |
| 835 | * or error types. |
| 836 | * |
| 837 | * @stability unstable |
| 838 | * @category utility types |
| 839 | * @since 4.0.0 |
| 840 | */ |
| 841 | export type Without<A> = A extends Request<infer _Kind, infer _> ? never : A |
| 842 | } |
| 843 | |
| 844 | /** |
| 845 | * Services provided by the HTTP router, which are available in the |
| 846 | * request context. |
| 847 | * |
| 848 | * @stability unstable |
| 849 | * @category utility types |
| 850 | * @since 4.0.0 |
| 851 | */ |
| 852 | export type Provided = |
| 853 | | HttpServerRequest.HttpServerRequest |
| 854 | | Scope.Scope |
| 855 | | HttpServerRequest.ParsedSearchParams |
| 856 | | RouteContext |
| 857 | |
| 858 | /** |
| 859 | * Services provided to global middleware. |
| 860 | * |
| 861 | * @stability unstable |
| 862 | * @category utility types |
| 863 | * @since 4.0.0 |
| 864 | */ |
| 865 | export type GlobalProvided = |
| 866 | | HttpServerRequest.HttpServerRequest |
| 867 | | Scope.Scope |
| 868 | |
| 869 | const MiddlewareTypeId = "~effect/http/HttpRouter/Middleware" |
| 870 | |
| 871 | /** |
| 872 | * Composable descriptor for route-scoped HTTP router middleware. |
| 873 | * |
| 874 | * **Details** |
| 875 | * |
| 876 | * Its `layer` can be provided to route layers, and `combine` composes middleware |
| 877 | * while tracking provided services, handled errors, and remaining requirements at |
| 878 | * the type level. |
| 879 | * |
| 880 | * @stability unstable |
| 881 | * @category middleware |
| 882 | * @since 4.0.0 |
| 883 | */ |
| 884 | export interface Middleware< |
| 885 | Config extends { |
| 886 | provides: any |
| 887 | handles: any |
| 888 | error: any |
| 889 | requires: any |
| 890 | layerError: any |
| 891 | layerRequires: any |
| 892 | } |
| 893 | > { |
| 894 | readonly [MiddlewareTypeId]: Config |
| 895 | |
| 896 | readonly layer: [Config["requires"]] extends [never] ? Layer.Layer< |
| 897 | | Request.From<"Requires", Config["provides"]> |
| 898 | | Request.From<"Error", Config["handles"]>, |
| 899 | Config["layerError"], |
| 900 | | Config["layerRequires"] |
| 901 | | Request.From<"Requires", Config["requires"]> |
| 902 | | Request.From<"Error", Config["error"]> |
| 903 | > |
| 904 | : "Need to .combine(middleware) that satisfy the missing request dependencies" |
| 905 | |
| 906 | readonly combine: < |
| 907 | Config2 extends { |
| 908 | provides: any |
| 909 | handles: any |
| 910 | error: any |
| 911 | requires: any |
| 912 | layerError: any |
| 913 | layerRequires: any |
| 914 | } |
| 915 | >(other: Middleware<Config2>) => Middleware<{ |
| 916 | provides: Config2["provides"] | Config["provides"] |
| 917 | handles: Config2["handles"] | Config["handles"] |
| 918 | error: Config2["error"] | Exclude<Config["error"], Config2["handles"]> |
| 919 | requires: Exclude<Config["requires"], Config2["provides"]> | Config2["requires"] |
| 920 | layerError: Config["layerError"] | Config2["layerError"] |
| 921 | layerRequires: Config["layerRequires"] | Config2["layerRequires"] |
| 922 | }> |
| 923 | } |
| 924 | |
| 925 | /** |
| 926 | * Create a middleware layer that can be used to modify requests and responses. |
| 927 | * |
| 928 | * **Details** |
| 929 | * |
| 930 | * By default, the middleware only affects the routes that it is provided to. |
| 931 | * |
| 932 | * If you want to create a middleware that applies globally to all routes, pass |
| 933 | * the `global` option as `true`. |
| 934 | * |
| 935 | * **Example** (Applying route and global middleware) |
| 936 | * |
| 937 | * ```ts import.meta.vitest |
| 938 | * import { Effect, Layer } from "effect" |
| 939 | * import { HttpRouter, HttpServerResponse } from "effect/http" |
| 940 | * |
| 941 | * const RouteMiddleware = HttpRouter.middleware((httpEffect) => |
| 942 | * Effect.map(httpEffect, HttpServerResponse.setHeader("x-route", "route")) |
| 943 | * ).layer |
| 944 | * |
| 945 | * const GlobalMiddleware = HttpRouter.middleware( |
| 946 | * (httpEffect) => Effect.map(httpEffect, HttpServerResponse.setHeader("x-global", "global")), |
| 947 | * { global: true } |
| 948 | * ) |
| 949 | * |
| 950 | * const Routes = HttpRouter.add("GET", "/hello", HttpServerResponse.text("Hello")).pipe( |
| 951 | * Layer.provide(RouteMiddleware) |
| 952 | * ) |
| 953 | * const App = Layer.mergeAll(Routes, GlobalMiddleware) |
| 954 | * |
| 955 | * const program = Effect.acquireUseRelease( |
| 956 | * Effect.sync(() => HttpRouter.toWebHandler(App, { disableLogger: true })), |
| 957 | * ({ handler }) => |
| 958 | * Effect.gen(function*() { |
| 959 | * const response = yield* Effect.promise(() => handler(new Request("http://localhost/hello"))) |
| 960 | * return [response.headers.get("x-route"), response.headers.get("x-global")] |
| 961 | * }), |
| 962 | * ({ dispose }) => Effect.promise(dispose) |
| 963 | * ) |
| 964 | * |
| 965 | * await Effect.runPromise(program) // => ["route", "global"] |
| 966 | * ``` |
| 967 | * |
| 968 | * @stability unstable |
| 969 | * @category middleware |
| 970 | * @since 4.0.0 |
| 971 | */ |
| 972 | export const middleware: |
| 973 | & middleware.Make<never, never> |
| 974 | & (< |
| 975 | Config extends { |
| 976 | provides?: any |
| 977 | handles?: any |
| 978 | } = {} |
| 979 | >() => middleware.Make< |
| 980 | Config extends { provides: infer R } ? R : never, |
| 981 | Config extends { handles: infer E } ? E : never |
| 982 | >) = function() { |
| 983 | if (arguments.length === 0) { |
| 984 | return makeMiddleware as any |
| 985 | } |
| 986 | return makeMiddleware(arguments[0], arguments[1]) as any |
| 987 | } |
| 988 | |
| 989 | const makeMiddleware = (middleware: any, options?: { |
| 990 | readonly global?: boolean | undefined |
| 991 | }) => |
| 992 | options?.global ? |
| 993 | Layer.effectDiscard(Effect.gen(function*() { |
| 994 | const router = yield* HttpRouter |
| 995 | const fn = Effect.isEffect(middleware) ? yield* middleware : middleware |
| 996 | yield* router.addGlobalMiddleware(fn) |
| 997 | })) |
| 998 | : new MiddlewareImpl( |
| 999 | Effect.isEffect(middleware) ? |
| 1000 | Layer.effectContext(Effect.map(middleware, (fn) => Context.makeUnsafe(new Map([[fnContextKey, fn]])))) : |
| 1001 | Layer.succeedContext(Context.makeUnsafe(new Map([[fnContextKey, middleware]]))) as any |
| 1002 | ) |
| 1003 | |
| 1004 | let middlewareId = 0 |
| 1005 | const fnContextKey = "effect/http/HttpRouter/MiddlewareFn" |
| 1006 | |
| 1007 | class MiddlewareImpl< |
| 1008 | Config extends { |
| 1009 | provides: any |
| 1010 | handles: any |
| 1011 | error: any |
| 1012 | requires: any |
| 1013 | layerError: any |
| 1014 | layerRequires: any |
| 1015 | } |
| 1016 | > implements Middleware<Config> { |
| 1017 | readonly [MiddlewareTypeId]: Config = {} as any |
| 1018 | |
| 1019 | readonly layerFn: Layer.Layer<never> |
| 1020 | readonly dependencies?: Layer.Layer<any, any, any> | undefined |
| 1021 | |
| 1022 | constructor( |
| 1023 | layerFn: Layer.Layer<never>, |
| 1024 | dependencies?: Layer.Layer<any, any, any> |
| 1025 | ) { |
| 1026 | this.layerFn = layerFn |
| 1027 | this.dependencies = dependencies |
| 1028 | const contextKey = `effect/http/HttpRouter/Middleware-${++middlewareId}` as const |
| 1029 | this.layer = Layer.effectContext(Effect.gen({ self: this }, function*() { |
| 1030 | const context = yield* Effect.context<Scope.Scope>() |
| 1031 | const stack = [Context.getOrUndefinedUnsafe(context, fnContextKey)] |
| 1032 | if (this.dependencies) { |
| 1033 | const memoMap = yield* Layer.CurrentMemoMap |
| 1034 | const scope = Context.get(context, Scope.Scope) |
| 1035 | const depsContext = yield* Layer.buildWithMemoMap(this.dependencies, memoMap, scope) |
| 1036 | stack.push(...getMiddleware(depsContext)) |
| 1037 | } |
| 1038 | return Context.makeUnsafe<never>(new Map([[contextKey, stack]])) |
| 1039 | })).pipe(Layer.provide(this.layerFn)) |
| 1040 | } |
| 1041 | |
| 1042 | layer: any |
| 1043 | |
| 1044 | combine< |
| 1045 | Config2 extends { |
| 1046 | provides: any |
| 1047 | handles: any |
| 1048 | error: any |
| 1049 | requires: any |
| 1050 | layerError: any |
| 1051 | layerRequires: any |
| 1052 | } |
| 1053 | >(other: Middleware<Config2>): Middleware<any> { |
| 1054 | return new MiddlewareImpl( |
| 1055 | this.layerFn, |
| 1056 | this.dependencies ? Layer.provideMerge(this.dependencies, other.layer as any) : other.layer as any |
| 1057 | ) as any |
| 1058 | } |
| 1059 | } |
| 1060 | |
| 1061 | const middlewareCache = new WeakMap<Context.Context<never>, any>() |
| 1062 | const getMiddleware = (context: Context.Context<never>): Array<middleware.Fn> => { |
| 1063 | let arr = middlewareCache.get(context) |
| 1064 | if (arr) return arr |
| 1065 | const topLevel = Arr.empty<Array<middleware.Fn>>() |
| 1066 | let maxLength = 0 |
| 1067 | for (const [key, value] of context.mapUnsafe) { |
| 1068 | if (key.startsWith("effect/http/HttpRouter/Middleware-")) { |
| 1069 | topLevel.push(value) |
| 1070 | if (value.length > maxLength) { |
| 1071 | maxLength = value.length |
| 1072 | } |
| 1073 | } |
| 1074 | } |
| 1075 | if (topLevel.length === 0) { |
| 1076 | arr = [] |
| 1077 | } else { |
| 1078 | const middleware = new Set<middleware.Fn>() |
| 1079 | for (let i = maxLength - 1; i >= 0; i--) { |
| 1080 | for (const arr of topLevel) { |
| 1081 | if (i < arr.length) { |
| 1082 | middleware.add(arr[i]) |
| 1083 | } |
| 1084 | } |
| 1085 | } |
| 1086 | arr = Arr.fromIterable(middleware).reverse() |
| 1087 | } |
| 1088 | middlewareCache.set(context, arr) |
| 1089 | return arr |
| 1090 | } |
| 1091 | |
| 1092 | /** |
| 1093 | * Types used by the `middleware` constructor. |
| 1094 | * |
| 1095 | * @stability unstable |
| 1096 | * @since 4.0.0 |
| 1097 | */ |
| 1098 | export declare namespace middleware { |
| 1099 | /** |
| 1100 | * Overloaded constructor type for router middleware. |
| 1101 | * |
| 1102 | * **Details** |
| 1103 | * |
| 1104 | * It builds either a route-scoped `Middleware` or, when `global` is `true`, a |
| 1105 | * layer that installs middleware for all routes. The type tracks provided |
| 1106 | * services, handled errors, middleware failures, and remaining requirements. |
| 1107 | * |
| 1108 | * @stability unstable |
| 1109 | * @category middleware |
| 1110 | * @since 4.0.0 |
| 1111 | */ |
| 1112 | export type Make<Provides = never, Handles = never> = { |
| 1113 | <E, R, EX, RX, const Global extends boolean = false>( |
| 1114 | middleware: Effect.Effect< |
| 1115 | ( |
| 1116 | effect: Effect.Effect< |
| 1117 | HttpServerResponse.HttpServerResponse, |
| 1118 | Types.NoInfer<Handles | Types.unhandled>, |
| 1119 | Types.NoInfer<Provides> |
| 1120 | > |
| 1121 | ) => |
| 1122 | & Effect.Effect< |
| 1123 | HttpServerResponse.HttpServerResponse, |
| 1124 | E, |
| 1125 | R |
| 1126 | > |
| 1127 | & (Types.unhandled extends E ? unknown : "You must only handle the configured errors"), |
| 1128 | EX, |
| 1129 | RX |
| 1130 | >, |
| 1131 | options?: { |
| 1132 | readonly global?: Global | undefined |
| 1133 | } |
| 1134 | ): Global extends true ? Layer.Layer< |
| 1135 | | Request.From<"Requires", Provides> |
| 1136 | | Request.From<"Error", Handles> |
| 1137 | | Request.From<"GlobalRequires", Provides> |
| 1138 | | Request.From<"GlobalError", Handles>, |
| 1139 | EX, |
| 1140 | | HttpRouter |
| 1141 | | Exclude<RX, Scope.Scope> |
| 1142 | | Request.From<"GlobalRequires", Exclude<R, GlobalProvided>> |
| 1143 | | Request.From<"GlobalError", Exclude<E, Types.unhandled>> |
| 1144 | > : |
| 1145 | Middleware<{ |
| 1146 | provides: Provides |
| 1147 | handles: Handles |
| 1148 | error: Exclude<E, Types.unhandled> |
| 1149 | requires: Exclude<R, Provided> |
| 1150 | layerError: EX |
| 1151 | layerRequires: Exclude<RX, Scope.Scope> |
| 1152 | }> |
| 1153 | <E, R, const Global extends boolean = false>( |
| 1154 | middleware: |
| 1155 | & (( |
| 1156 | effect: Effect.Effect< |
| 1157 | HttpServerResponse.HttpServerResponse, |
| 1158 | Types.NoInfer<Handles | Types.unhandled>, |
| 1159 | Types.NoInfer<Provides> |
| 1160 | > |
| 1161 | ) => Effect.Effect< |
| 1162 | HttpServerResponse.HttpServerResponse, |
| 1163 | E, |
| 1164 | R |
| 1165 | >) |
| 1166 | & (Types.unhandled extends E ? unknown : "You must only handle the configured errors"), |
| 1167 | options?: { |
| 1168 | readonly global?: Global | undefined |
| 1169 | } |
| 1170 | ): Global extends true ? Layer.Layer< |
| 1171 | | Request.From<"Requires", Provides> |
| 1172 | | Request.From<"Error", Handles> |
| 1173 | | Request.From<"GlobalRequires", Provides> |
| 1174 | | Request.From<"GlobalError", Handles>, |
| 1175 | never, |
| 1176 | | HttpRouter |
| 1177 | | Request.From<"GlobalRequires", Exclude<R, GlobalProvided>> |
| 1178 | | Request.From<"GlobalError", Exclude<E, Types.unhandled>> |
| 1179 | > : |
| 1180 | Middleware<{ |
| 1181 | provides: Provides |
| 1182 | handles: Handles |
| 1183 | error: Exclude<E, Types.unhandled> |
| 1184 | requires: Exclude<R, Provided> |
| 1185 | layerError: never |
| 1186 | layerRequires: never |
| 1187 | }> |
| 1188 | } |
| 1189 | |
| 1190 | /** |
| 1191 | * Function that transforms an HTTP response effect into another HTTP response |
| 1192 | * effect. |
| 1193 | * |
| 1194 | * @stability unstable |
| 1195 | * @category middleware |
| 1196 | * @since 4.0.0 |
| 1197 | */ |
| 1198 | export type Fn = ( |
| 1199 | effect: Effect.Effect<HttpServerResponse.HttpServerResponse> |
| 1200 | ) => Effect.Effect<HttpServerResponse.HttpServerResponse> |
| 1201 | } |
| 1202 | |
| 1203 | /** |
| 1204 | * Middleware that applies CORS headers to the HTTP response. |
| 1205 | * |
| 1206 | * @stability unstable |
| 1207 | * @category layers |
| 1208 | * @since 4.0.0 |
| 1209 | */ |
| 1210 | export const cors = ( |
| 1211 | options?: { |
| 1212 | readonly allowedOrigins?: ReadonlyArray<string> | undefined |
| 1213 | readonly allowedMethods?: ReadonlyArray<string> | undefined |
| 1214 | readonly allowedHeaders?: ReadonlyArray<string> | undefined |
| 1215 | readonly exposedHeaders?: ReadonlyArray<string> | undefined |
| 1216 | readonly maxAge?: number | undefined |
| 1217 | readonly credentials?: boolean | undefined |
| 1218 | } | undefined |
| 1219 | ): Layer.Layer<never, never, HttpRouter> => middleware(HttpMiddleware.cors(options), { global: true }) |
| 1220 | |
| 1221 | /** |
| 1222 | * Middleware that disables the logger for some routes. |
| 1223 | * |
| 1224 | * **Example** (Disabling route logging) |
| 1225 | * |
| 1226 | * ```ts import.meta.vitest |
| 1227 | * import { Layer } from "effect" |
| 1228 | * import { HttpRouter, HttpServerResponse } from "effect/http" |
| 1229 | * |
| 1230 | * const Route = HttpRouter.add( |
| 1231 | * "GET", |
| 1232 | * "/hello", |
| 1233 | * HttpServerResponse.text("Hello, World!") |
| 1234 | * ).pipe( |
| 1235 | * // disable the logger for this route |
| 1236 | * Layer.provide(HttpRouter.disableLogger) |
| 1237 | * ) |
| 1238 | * Layer.isLayer(Route) // => true |
| 1239 | * ``` |
| 1240 | * |
| 1241 | * @stability unstable |
| 1242 | * @category layers |
| 1243 | * @since 4.0.0 |
| 1244 | */ |
| 1245 | export const disableLogger: Layer.Layer<never> = middleware(HttpMiddleware.withLoggerDisabled).layer |
| 1246 | |
| 1247 | /** |
| 1248 | * Provides request-level dependencies to some routes. |
| 1249 | * |
| 1250 | * @stability unstable |
| 1251 | * @category layers |
| 1252 | * @since 4.0.0 |
| 1253 | */ |
| 1254 | export const provideRequest = |
| 1255 | <A2, E2, R2>(layer: Layer.Layer<A2, E2, R2>) => |
| 1256 | <A, E, R>(self: Layer.Layer<A, E, R>): Layer.Layer< |
| 1257 | A, |
| 1258 | E | E2, |
| 1259 | R2 | Exclude<R, Request.From<"Requires", A2>> |
| 1260 | > => |
| 1261 | Layer.provide( |
| 1262 | self, |
| 1263 | middleware<{ provides: A2 }>()(Effect.gen(function*() { |
| 1264 | const services = yield* Layer.build(layer as Layer.Layer<A2>) |
| 1265 | return (effect) => |
| 1266 | Effect.provideContext(effect, services) as Effect.Effect< |
| 1267 | HttpServerResponse.HttpServerResponse, |
| 1268 | Types.unhandled |
| 1269 | > |
| 1270 | })).layer |
| 1271 | ) |
| 1272 | |
| 1273 | /** |
| 1274 | * Runs the provided application layer as an HTTP server. |
| 1275 | * |
| 1276 | * **Details** |
| 1277 | * |
| 1278 | * Layers first built inside the app are private to this server. Provide |
| 1279 | * services that must be shared with sibling layers outside `serve`. |
| 1280 | * |
| 1281 | * **Gotchas** |
| 1282 | * |
| 1283 | * The layer is built with a forked `MemoMap`, so calling `serve` twice will |
| 1284 | * build the same layer twice. |
| 1285 | * |
| 1286 | * @stability unstable |
| 1287 | * @category layers |
| 1288 | * @since 4.0.0 |
| 1289 | */ |
| 1290 | export const serve = <A, E, R, HE, HR = Request.Only<"Requires", R> | Request.Only<"GlobalRequires", R>>( |
| 1291 | appLayer: Layer.Layer<A, E, R>, |
| 1292 | options?: { |
| 1293 | readonly routerConfig?: Partial<FindMyWay.RouterConfig> | undefined |
| 1294 | readonly disableLogger?: boolean | undefined |
| 1295 | readonly disableListenLog?: boolean |
| 1296 | /** |
| 1297 | * Middleware to apply to the HTTP server. |
| 1298 | * |
| 1299 | * **Gotchas** |
| 1300 | * |
| 1301 | * This middleware is applied to the entire HTTP server chain, including the |
| 1302 | * sending of the response. Changes to the response are not reflected in the |
| 1303 | * final response sent to the client. Use `HttpRouter.middleware` when |
| 1304 | * middleware must modify the response. |
| 1305 | */ |
| 1306 | readonly middleware?: ( |
| 1307 | effect: Effect.Effect< |
| 1308 | HttpServerResponse.HttpServerResponse, |
| 1309 | Request.Only<"Error", R> | Request.Only<"GlobalError", R> | HttpServerError.HttpServerError, |
| 1310 | | Scope.Scope |
| 1311 | | HttpServerRequest.HttpServerRequest |
| 1312 | | Request.Only<"Requires", R> |
| 1313 | | Request.Only<"GlobalRequires", R> |
| 1314 | > |
| 1315 | ) => Effect.Effect<HttpServerResponse.HttpServerResponse, HE, HR> |
| 1316 | } |
| 1317 | ): Layer.Layer< |
| 1318 | A, |
| 1319 | Request.Without<E>, |
| 1320 | HttpServer.HttpServer | Exclude<Request.Without<R> | Exclude<HR, GlobalProvided>, HttpRouter> |
| 1321 | > => { |
| 1322 | let middleware: any = options?.middleware |
| 1323 | if (options?.disableLogger !== true) { |
| 1324 | middleware = middleware ? compose(middleware, HttpMiddleware.logger) : HttpMiddleware.logger |
| 1325 | } |
| 1326 | const layerWithRouter = Layer.provideMerge( |
| 1327 | appLayer, |
| 1328 | options?.routerConfig |
| 1329 | ? Layer.provide(layer, Layer.succeed(RouterConfig)(options.routerConfig)) |
| 1330 | : layer |
| 1331 | ) |
| 1332 | return Effect.contextWith((context) => { |
| 1333 | const memoMap = Layer.CurrentMemoMap.forkOrCreate(context) |
| 1334 | return Layer.buildWithMemoMap(layerWithRouter, memoMap, Context.get(context, Scope.Scope)) |
| 1335 | }).pipe( |
| 1336 | Effect.map((context) => { |
| 1337 | const router = Context.get(context, HttpRouter) |
| 1338 | const handler = router.asHttpEffect() |
| 1339 | return Layer.merge( |
| 1340 | middleware ? HttpServer.serve(handler, middleware) : HttpServer.serve(handler), |
| 1341 | Layer.succeedContext(context) |
| 1342 | ) |
| 1343 | }), |
| 1344 | Layer.unwrap, |
| 1345 | options?.disableListenLog ? identity : HttpServer.withLogAddress |
| 1346 | ) as any |
| 1347 | } |
| 1348 | |
| 1349 | /** |
| 1350 | * Builds a Fetch-compatible request handler from an HTTP router application |
| 1351 | * layer. |
| 1352 | * |
| 1353 | * **Details** |
| 1354 | * |
| 1355 | * The result contains a `handler` function that converts Web `Request` values to |
| 1356 | * Web `Response` values and a `dispose` function for releasing the layer |
| 1357 | * resources. The layer is built immediately, so the cost is paid when the |
| 1358 | * handler is created rather than on the first request. A layer that performs |
| 1359 | * asynchronous work while building may still be in progress when the first |
| 1360 | * request arrives, in which case that request waits for the build to finish. |
| 1361 | * If the build fails, every request rejects with the build error. |
| 1362 | * |
| 1363 | * @stability unstable |
| 1364 | * @category converting |
| 1365 | * @since 4.0.0 |
| 1366 | */ |
| 1367 | export const toWebHandler = < |
| 1368 | A, |
| 1369 | E, |
| 1370 | R extends |
| 1371 | | HttpRouter |
| 1372 | | Request<"Requires", any> |
| 1373 | | Request<"GlobalRequires", any> |
| 1374 | | Request<"Error", any> |
| 1375 | | Request<"GlobalError", any>, |
| 1376 | HE, |
| 1377 | HR = Exclude<Request.Only<"Requires", R> | Request.Only<"GlobalRequires", R>, A>, |
| 1378 | ReqR = Exclude<HR, A | Scope.Scope | HttpServerRequest.HttpServerRequest> |
| 1379 | >( |
| 1380 | appLayer: Layer.Layer<A, E, R>, |
| 1381 | options?: { |
| 1382 | readonly memoMap?: Layer.MemoMap | undefined |
| 1383 | readonly routerConfig?: Partial<FindMyWay.RouterConfig> | undefined |
| 1384 | readonly disableLogger?: boolean | undefined |
| 1385 | /** |
| 1386 | * Middleware to apply to the HTTP server. |
| 1387 | * |
| 1388 | * **Gotchas** |
| 1389 | * |
| 1390 | * This middleware is applied to the entire HTTP server chain, including the |
| 1391 | * sending of the response. Changes to the response are not reflected in the |
| 1392 | * final response sent to the client. Use `HttpRouter.middleware` when |
| 1393 | * middleware must modify the response. |
| 1394 | */ |
| 1395 | readonly middleware?: ( |
| 1396 | effect: Effect.Effect< |
| 1397 | HttpServerResponse.HttpServerResponse, |
| 1398 | Request.Only<"Error", R> | Request.Only<"GlobalError", R> | HttpServerError.HttpServerError, |
| 1399 | | Scope.Scope |
| 1400 | | HttpServerRequest.HttpServerRequest |
| 1401 | | Request.Only<"Requires", R> |
| 1402 | | Request.Only<"GlobalRequires", R> |
| 1403 | > |
| 1404 | ) => Effect.Effect<HttpServerResponse.HttpServerResponse, HE, HR | GlobalProvided> |
| 1405 | } |
| 1406 | ): { |
| 1407 | readonly handler: [ReqR] extends [never] |
| 1408 | ? ((request: globalThis.Request, context?: Context.Context<never> | undefined) => Promise<Response>) |
| 1409 | : (( |
| 1410 | request: globalThis.Request, |
| 1411 | context: Context.Context<ReqR> |
| 1412 | ) => Promise<Response>) |
| 1413 | readonly dispose: () => Promise<void> |
| 1414 | } => { |
| 1415 | let middleware: any = options?.middleware |
| 1416 | if (options?.disableLogger !== true) { |
| 1417 | middleware = middleware ? compose(middleware, HttpMiddleware.logger) : HttpMiddleware.logger |
| 1418 | } |
| 1419 | const RouterLayer = options?.routerConfig |
| 1420 | ? Layer.provide(layer, Layer.succeed(RouterConfig)(options.routerConfig)) |
| 1421 | : layer |
| 1422 | return HttpEffect.toWebHandlerLayerWith( |
| 1423 | Layer.provideMerge(appLayer, RouterLayer) as Layer.Layer<A | HttpRouter, E>, |
| 1424 | { |
| 1425 | toHandler: (s) => Effect.succeed(Context.get(s, HttpRouter).asHttpEffect()), |
| 1426 | middleware, |
| 1427 | memoMap: options?.memoMap |
| 1428 | } |
| 1429 | ) |
| 1430 | } |