| /** | |
| * Describes failures raised while handling HTTP server requests. | |
| * | |
| * `HttpServerError` covers failures that happen while accepting a request, | |
| * matching a route, running a handler, or building and sending a response. | |
| * Request-scoped failures keep the request that caused them, and response | |
| * failures keep the response being produced. This module also includes helpers | |
| * for turning failed causes or exits into HTTP responses and an annotation for | |
| * interrupts caused by client aborts. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Cause from "../../Cause.ts" | |
| import * as Context from "../../Context.ts" | |
| import * as Data from "../../Data.ts" | |
| import * as Effect from "../../Effect.ts" | |
| import * as ErrorReporter from "../../ErrorReporter.ts" | |
| import type * as Exit from "../../Exit.ts" | |
| import { constUndefined } from "../../Function.ts" | |
| import * as Option from "../../Option.ts" | |
| import { hasProperty } from "../../Predicate.ts" | |
| import type * as Request from "./HttpServerRequest.ts" | |
| import * as Respondable from "./HttpServerRespondable.ts" | |
| import * as Response from "./HttpServerResponse.ts" | |
| const TypeId = "~effect/http/HttpServerError" | |
| /** | |
| * Tagged error for failures that occur while handling an HTTP server request. | |
| * | |
| * **Details** | |
| * | |
| * It wraps a `HttpServerErrorReason`, exposes the associated request and optional | |
| * response, and can be converted to an HTTP response through the `Respondable` | |
| * protocol. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class HttpServerError extends Data.TaggedError("HttpServerError")<{ | |
| readonly reason: HttpServerErrorReason | |
| }> implements Respondable.Respondable { | |
| constructor(props: { | |
| readonly reason: HttpServerErrorReason | |
| }) { | |
| if ("cause" in props.reason) { | |
| super({ | |
| ...props, | |
| cause: props.reason.cause | |
| } as any) | |
| } else { | |
| super(props) | |
| } | |
| } | |
| readonly [TypeId] = TypeId | |
| override stack = `${this.name}: ${this.message}` | |
| get request(): Request.HttpServerRequest { | |
| return this.reason.request | |
| } | |
| get response(): Response.HttpServerResponse | undefined { | |
| return "response" in this.reason ? this.reason.response : undefined | |
| } | |
| [Respondable.symbol]() { | |
| return this.reason[Respondable.symbol]() | |
| } | |
| override get [ErrorReporter.ignore](): boolean { | |
| return this.reason[ErrorReporter.ignore] ?? false | |
| } | |
| override get message(): string { | |
| return this.reason.message | |
| } | |
| } | |
| /** | |
| * Error describing a failure to parse or read an incoming request. | |
| * | |
| * **Details** | |
| * | |
| * When converted to a response it produces an empty `400` response. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class RequestParseError extends Data.TaggedError("RequestParseError")<{ | |
| readonly request: Request.HttpServerRequest | |
| readonly description?: string | |
| readonly cause?: unknown | |
| }> implements Respondable.Respondable { | |
| /** | |
| * Converts the request error into a `400 Bad Request` response. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| [Respondable.symbol]() { | |
| return Effect.succeed(Response.empty({ status: 400 })) | |
| } | |
| get methodAndUrl() { | |
| return `${this.request.method} ${this.request.url}` | |
| } | |
| override get message() { | |
| return formatRequestMessage(this._tag, this.description, this.methodAndUrl) | |
| } | |
| } | |
| /** | |
| * Error indicating that no route matched the incoming request. | |
| * | |
| * **Details** | |
| * | |
| * When converted to a response it produces an empty `404` response, and it is | |
| * ignored by the error reporter. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class RouteNotFound extends Data.TaggedError("RouteNotFound")<{ | |
| readonly request: Request.HttpServerRequest | |
| readonly description?: string | |
| readonly cause?: unknown | |
| }> implements Respondable.Respondable { | |
| [Respondable.symbol]() { | |
| return Effect.succeed(Response.empty({ status: 404 })) | |
| } | |
| override readonly [ErrorReporter.ignore] = true | |
| get methodAndUrl() { | |
| return `${this.request.method} ${this.request.url}` | |
| } | |
| override get message() { | |
| return formatRequestMessage(this._tag, this.description, this.methodAndUrl) | |
| } | |
| } | |
| /** | |
| * Error describing an unexpected server-side failure while handling a request. | |
| * | |
| * **Details** | |
| * | |
| * When converted to a response it produces an empty `500` response. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class InternalError extends Data.TaggedError("InternalError")<{ | |
| readonly request: Request.HttpServerRequest | |
| readonly description?: string | |
| readonly cause?: unknown | |
| }> implements Respondable.Respondable { | |
| /** | |
| * Converts the server error into a `500 Internal Server Error` response. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| [Respondable.symbol]() { | |
| return Effect.succeed(Response.empty({ status: 500 })) | |
| } | |
| get methodAndUrl() { | |
| return `${this.request.method} ${this.request.url}` | |
| } | |
| override get message() { | |
| return formatRequestMessage(this._tag, this.description, this.methodAndUrl) | |
| } | |
| } | |
| /** | |
| * Returns `true` when the supplied value is an `HttpServerError`. | |
| * | |
| * @category predicates | |
| * @since 4.0.0 | |
| */ | |
| export const isHttpServerError = (u: unknown): u is HttpServerError => hasProperty(u, TypeId) | |
| /** | |
| * Error describing a failure related to an HTTP response. | |
| * | |
| * **Details** | |
| * | |
| * It carries the request and response involved in the failure. When converted to | |
| * a response it produces an empty `500` response. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class ResponseError extends Data.TaggedError("ResponseError")<{ | |
| readonly request: Request.HttpServerRequest | |
| readonly response: Response.HttpServerResponse | |
| readonly description?: string | |
| readonly cause?: unknown | |
| }> implements Respondable.Respondable { | |
| [Respondable.symbol]() { | |
| return Effect.succeed(Response.empty({ status: 500 })) | |
| } | |
| get methodAndUrl() { | |
| return `${this.request.method} ${this.request.url}` | |
| } | |
| override get message() { | |
| const info = `${this._tag} (${this.response.status} ${this.methodAndUrl})` | |
| return this.description ? `${info}: ${this.description}` : info | |
| } | |
| } | |
| /** | |
| * Union of errors that are tied directly to an incoming server request. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export type RequestError = RequestParseError | RouteNotFound | InternalError | |
| /** | |
| * Reason carried by an `HttpServerError`, either a request-level error or a response-level error. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export type HttpServerErrorReason = RequestError | ResponseError | |
| /** | |
| * Error wrapping a low-level failure from the HTTP server implementation. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class ServeError extends Data.TaggedError("ServeError")<{ | |
| readonly cause: unknown | |
| }> {} | |
| /** | |
| * Context annotation used to mark an interrupt as caused by the client aborting | |
| * the request. | |
| * | |
| * **Details** | |
| * | |
| * `causeResponse` uses this annotation to map a pure client abort to a `499` | |
| * response instead of a server abort response. | |
| * | |
| * @category annotations | |
| * @since 4.0.0 | |
| */ | |
| export class ClientAbort extends Context.Service<ClientAbort, true>()("effect/http/HttpServerError/ClientAbort") { | |
| static annotation = this.context(true).pipe( | |
| Context.add(Cause.StackTrace, { | |
| name: "ClientAbort", | |
| stack: constUndefined, | |
| parent: undefined | |
| }) | |
| ) | |
| } | |
| const formatRequestMessage = (reason: string, description: string | undefined, info: string) => { | |
| const prefix = `${reason} (${info})` | |
| return description ? `${prefix}: ${description}` : prefix | |
| } | |
| /** | |
| * Converts a failed handler cause into the HTTP response that should be sent and | |
| * the cause that should be reported. | |
| * | |
| * **Details** | |
| * | |
| * Respondable failures and defects can choose their own response, defects that | |
| * are already `HttpServerResponse` values are used directly, and pure interrupts | |
| * produce either `499` for client aborts or `503` for server aborts. | |
| * | |
| * @category error handling | |
| * @since 4.0.0 | |
| */ | |
| export const causeResponse = <E>( | |
| cause: Cause.Cause<E> | |
| ): Effect.Effect<readonly [Response.HttpServerResponse, Cause.Cause<E>]> => { | |
| let response: Response.HttpServerResponse | undefined | |
| let effect = succeedInternalServerError | |
| const failures: Array<Cause.Reason<E>> = [] | |
| let interrupts: Array<Cause.Interrupt> = [] | |
| let isClientInterrupt = false | |
| for (let i = 0; i < cause.reasons.length; i++) { | |
| const reason = cause.reasons[i] | |
| switch (reason._tag) { | |
| case "Fail": { | |
| effect = Respondable.toResponseOrElse(reason.error, internalServerError) | |
| failures.push(reason) | |
| break | |
| } | |
| case "Die": { | |
| if (Response.isHttpServerResponse(reason.defect)) { | |
| response = reason.defect | |
| } else { | |
| effect = Respondable.toResponseOrElseDefect(reason.defect, internalServerError) | |
| failures.push(reason) | |
| } | |
| break | |
| } | |
| case "Interrupt": { | |
| isClientInterrupt = reason.annotations.has(ClientAbort.key) | |
| if (failures.length > 0) break | |
| interrupts.push(reason) | |
| break | |
| } | |
| } | |
| } | |
| if (response) { | |
| return Effect.succeed([response, Cause.fromReasons(failures)] as const) | |
| } else if (interrupts.length > 0 && failures.length === 0) { | |
| failures.push(...interrupts) | |
| effect = isClientInterrupt ? clientAbortError : serverAbortError | |
| } | |
| return Effect.mapEager(effect, (response) => { | |
| failures.push(Cause.makeDieReason(response)) | |
| return [response, Cause.fromReasons(failures)] as const | |
| }) | |
| } | |
| /** | |
| * Derives an HTTP response from a failed handler cause synchronously. | |
| * | |
| * **Details** | |
| * | |
| * If the cause contains a defect that is already an `HttpServerResponse`, that | |
| * response is used and removed from the remaining cause. Otherwise the response | |
| * defaults to `500`. | |
| * | |
| * @category error handling | |
| * @since 4.0.0 | |
| */ | |
| export const causeResponseStripped = <E>( | |
| cause: Cause.Cause<E> | |
| ): readonly [response: Response.HttpServerResponse, cause: Option.Option<Cause.Cause<E>>] => { | |
| let response: Response.HttpServerResponse | undefined | |
| const failures = cause.reasons.filter((f) => { | |
| if (f._tag === "Die" && Response.isHttpServerResponse(f.defect)) { | |
| response = f.defect | |
| return false | |
| } | |
| return true | |
| }) | |
| return [ | |
| response ?? internalServerError, | |
| failures.length > 0 ? Option.some(Cause.fromReasons(failures)) : Option.none() | |
| ] | |
| } | |
| const internalServerError = Response.empty({ status: 500 }) | |
| const succeedInternalServerError = Effect.succeed(internalServerError) | |
| const clientAbortError = Effect.succeed(Response.empty({ status: 499 })) | |
| const serverAbortError = Effect.succeed(Response.empty({ status: 503 })) | |
| /** | |
| * Extracts the response from a successful handler exit, or derives a response | |
| * from the failure cause. | |
| * | |
| * @category error handling | |
| * @since 4.0.0 | |
| */ | |
| export const exitResponse = <E>(exit: Exit.Exit<Response.HttpServerResponse, E>): Response.HttpServerResponse => { | |
| if (exit._tag === "Success") { | |
| return exit.value | |
| } | |
| return causeResponseStripped(exit.cause)[0] | |
| } | |
Xet Storage Details
- Size:
- 10.9 kB
- Xet hash:
- fc171d2e1f80499c3b32e4c16e9e6718cd3432c1180a59473ffcb8edcb252b41
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.