1/**
2 * Built-in error schemas for common HTTP API failure responses.
3 *
4 * This module provides reusable `Schema.Error` values for common HTTP
5 * status codes, plus `HttpApiSchemaError` for request decoding failures raised
6 * by the HTTP API runtime. The status errors can be used in endpoint or
7 * middleware error declarations and are understood by builders, generated
8 * clients, reflection, and OpenAPI generation.
9 *
10 * @stability unstable
11 * @since 4.0.0
12 */
13import * as Data from "../Data.ts"
14import * as Effect from "../Effect.ts"
15import * as ErrorReporter from "../ErrorReporter.ts"
16import * as HttpServerRespondable from "../http/HttpServerRespondable.ts"
17import * as HttpServerResponse from "../http/HttpServerResponse.ts"
18import { hasProperty } from "../Predicate.ts"
19import * as Schema from "../Schema.ts"
20import * as HttpApiSchema from "./HttpApiSchema.ts"
21
22const badRequestResponse = HttpServerResponse.empty({ status: 400 })
23const unauthorizedResponse = HttpServerResponse.empty({ status: 401 })
24const forbiddenResponse = HttpServerResponse.empty({ status: 403 })
25const notFoundResponse = HttpServerResponse.empty({ status: 404 })
26const methodNotAllowedResponse = HttpServerResponse.empty({ status: 405 })
27const notAcceptableResponse = HttpServerResponse.empty({ status: 406 })
28const requestTimeoutResponse = HttpServerResponse.empty({ status: 408 })
29const conflictResponse = HttpServerResponse.empty({ status: 409 })
30const goneResponse = HttpServerResponse.empty({ status: 410 })
31const unprocessableEntityResponse = HttpServerResponse.empty({ status: 422 })
32const internalServerErrorResponse = HttpServerResponse.empty({ status: 500 })
33const notImplementedResponse = HttpServerResponse.empty({ status: 501 })
34const serviceUnavailableResponse = HttpServerResponse.empty({ status: 503 })
35
36/**
37 * Built-in HTTP API error for a `400 Bad Request` response. When used directly as
38 * a server response, it renders as an empty response with status 400.
39 *
40 * @stability unstable
41 * @category errors
42 * @since 4.0.0
43 */
44export class BadRequest extends Schema.Error<BadRequest>("effect/HttpApiError/BadRequest")({
45 _tag: Schema.tag("BadRequest")
46}, {
47 description: "BadRequest",
48 httpApiStatus: 400
49}) {
50 override readonly [ErrorReporter.ignore] = true;
51 [HttpServerRespondable.symbol]() {
52 return Effect.succeed(badRequestResponse)
53 }
54 static readonly singleton = new BadRequest()
55}
56
57/**
58 * No-content schema variant for `BadRequest`, decoding an empty 400 response into
59 * a `BadRequest` error value.
60 *
61 * @stability unstable
62 * @category schemas
63 * @since 4.0.0
64 */
65export const BadRequestNoContent = BadRequest.pipe(HttpApiSchema.asNoContent({
66 decode: () => new BadRequest({})
67}))
68
69/**
70 * Built-in HTTP API error for a `401 Unauthorized` response. When used directly as
71 * a server response, it renders as an empty response with status 401.
72 *
73 * @stability unstable
74 * @category errors
75 * @since 4.0.0
76 */
77export class Unauthorized extends Schema.Error<Unauthorized>("effect/HttpApiError/Unauthorized")({
78 _tag: Schema.tag("Unauthorized")
79}, {
80 description: "Unauthorized",
81 httpApiStatus: 401
82}) {
83 override readonly [ErrorReporter.ignore] = true;
84 [HttpServerRespondable.symbol]() {
85 return Effect.succeed(unauthorizedResponse)
86 }
87}
88
89/**
90 * No-content schema variant for `Unauthorized`, decoding an empty 401 response
91 * into an `Unauthorized` error value.
92 *
93 * @stability unstable
94 * @category schemas
95 * @since 4.0.0
96 */
97export const UnauthorizedNoContent = Unauthorized.pipe(HttpApiSchema.asNoContent({
98 decode: () => new Unauthorized({})
99}))
100
101/**
102 * Built-in HTTP API error for a `403 Forbidden` response. When used directly as a
103 * server response, it renders as an empty response with status 403.
104 *
105 * @stability unstable
106 * @category errors
107 * @since 4.0.0
108 */
109export class Forbidden extends Schema.Error<Forbidden>("effect/HttpApiError/Forbidden")({
110 _tag: Schema.tag("Forbidden")
111}, {
112 description: "Forbidden",
113 httpApiStatus: 403
114}) {
115 override readonly [ErrorReporter.ignore] = true;
116 [HttpServerRespondable.symbol]() {
117 return Effect.succeed(forbiddenResponse)
118 }
119}
120
121/**
122 * No-content schema variant for `Forbidden`, decoding an empty 403 response into a
123 * `Forbidden` error value.
124 *
125 * @stability unstable
126 * @category schemas
127 * @since 4.0.0
128 */
129export const ForbiddenNoContent = Forbidden.pipe(HttpApiSchema.asNoContent({
130 decode: () => new Forbidden({})
131}))
132
133/**
134 * Built-in HTTP API error for a `404 Not Found` response. When used directly as a
135 * server response, it renders as an empty response with status 404.
136 *
137 * @stability unstable
138 * @category errors
139 * @since 4.0.0
140 */
141export class NotFound extends Schema.Error<NotFound>("effect/HttpApiError/NotFound")({
142 _tag: Schema.tag("NotFound")
143}, {
144 description: "NotFound",
145 httpApiStatus: 404
146}) {
147 override readonly [ErrorReporter.ignore] = true;
148 [HttpServerRespondable.symbol]() {
149 return Effect.succeed(notFoundResponse)
150 }
151}
152
153/**
154 * No-content schema variant for `NotFound`, decoding an empty 404 response into a
155 * `NotFound` error value.
156 *
157 * @stability unstable
158 * @category schemas
159 * @since 4.0.0
160 */
161export const NotFoundNoContent = NotFound.pipe(HttpApiSchema.asNoContent({
162 decode: () => new NotFound({})
163}))
164
165/**
166 * Built-in HTTP API error for a `405 Method Not Allowed` response. When used
167 * directly as a server response, it renders as an empty response with status 405.
168 *
169 * @stability unstable
170 * @category errors
171 * @since 4.0.0
172 */
173export class MethodNotAllowed extends Schema.Error<MethodNotAllowed>("effect/HttpApiError/MethodNotAllowed")({
174 _tag: Schema.tag("MethodNotAllowed")
175}, {
176 description: "MethodNotAllowed",
177 httpApiStatus: 405
178}) {
179 override readonly [ErrorReporter.ignore] = true;
180 [HttpServerRespondable.symbol]() {
181 return Effect.succeed(methodNotAllowedResponse)
182 }
183}
184
185/**
186 * No-content schema variant for `MethodNotAllowed`, decoding an empty 405 response
187 * into a `MethodNotAllowed` error value.
188 *
189 * @stability unstable
190 * @category schemas
191 * @since 4.0.0
192 */
193export const MethodNotAllowedNoContent = MethodNotAllowed.pipe(HttpApiSchema.asNoContent({
194 decode: () => new MethodNotAllowed({})
195}))
196
197/**
198 * Built-in HTTP API error for a `406 Not Acceptable` response. When used directly
199 * as a server response, it renders as an empty response with status 406.
200 *
201 * @stability unstable
202 * @category errors
203 * @since 4.0.0
204 */
205export class NotAcceptable extends Schema.Error<NotAcceptable>("effect/HttpApiError/NotAcceptable")({
206 _tag: Schema.tag("NotAcceptable")
207}, {
208 description: "NotAcceptable",
209 httpApiStatus: 406
210}) {
211 override readonly [ErrorReporter.ignore] = true;
212 [HttpServerRespondable.symbol]() {
213 return Effect.succeed(notAcceptableResponse)
214 }
215}
216
217/**
218 * No-content schema variant for `NotAcceptable`, decoding an empty 406 response
219 * into a `NotAcceptable` error value.
220 *
221 * @stability unstable
222 * @category schemas
223 * @since 4.0.0
224 */
225export const NotAcceptableNoContent = NotAcceptable.pipe(HttpApiSchema.asNoContent({
226 decode: () => new NotAcceptable({})
227}))
228
229/**
230 * Built-in HTTP API error for a `408 Request Timeout` response. When used directly
231 * as a server response, it renders as an empty response with status 408.
232 *
233 * @stability unstable
234 * @category errors
235 * @since 4.0.0
236 */
237export class RequestTimeout extends Schema.Error<RequestTimeout>("effect/HttpApiError/RequestTimeout")({
238 _tag: Schema.tag("RequestTimeout")
239}, {
240 description: "RequestTimeout",
241 httpApiStatus: 408
242}) {
243 override readonly [ErrorReporter.ignore] = true;
244 [HttpServerRespondable.symbol]() {
245 return Effect.succeed(requestTimeoutResponse)
246 }
247}
248
249/**
250 * No-content schema variant for `RequestTimeout`, decoding an empty 408 response
251 * into a `RequestTimeout` error value.
252 *
253 * @stability unstable
254 * @category schemas
255 * @since 4.0.0
256 */
257export const RequestTimeoutNoContent = RequestTimeout.pipe(HttpApiSchema.asNoContent({
258 decode: () => new RequestTimeout({})
259}))
260
261/**
262 * Built-in HTTP API error for a `409 Conflict` response. When used directly as a
263 * server response, it renders as an empty response with status 409.
264 *
265 * @stability unstable
266 * @category errors
267 * @since 4.0.0
268 */
269export class Conflict extends Schema.Error<Conflict>("effect/HttpApiError/Conflict")({
270 _tag: Schema.tag("Conflict")
271}, {
272 description: "Conflict",
273 httpApiStatus: 409
274}) {
275 override readonly [ErrorReporter.ignore] = true;
276 [HttpServerRespondable.symbol]() {
277 return Effect.succeed(conflictResponse)
278 }
279}
280
281/**
282 * No-content schema variant for `Conflict`, decoding an empty 409 response into a
283 * `Conflict` error value.
284 *
285 * @stability unstable
286 * @category schemas
287 * @since 4.0.0
288 */
289export const ConflictNoContent = Conflict.pipe(HttpApiSchema.asNoContent({
290 decode: () => new Conflict({})
291}))
292
293/**
294 * Built-in HTTP API error for a `410 Gone` response. When used directly as a
295 * server response, it renders as an empty response with status 410.
296 *
297 * @stability unstable
298 * @category errors
299 * @since 4.0.0
300 */
301export class Gone extends Schema.Error<Gone>("effect/HttpApiError/Gone")({
302 _tag: Schema.tag("Gone")
303}, {
304 description: "Gone",
305 httpApiStatus: 410
306}) {
307 override readonly [ErrorReporter.ignore] = true;
308 [HttpServerRespondable.symbol]() {
309 return Effect.succeed(goneResponse)
310 }
311}
312
313/**
314 * No-content schema variant for `Gone`, decoding an empty 410 response into a
315 * `Gone` error value.
316 *
317 * @stability unstable
318 * @category schemas
319 * @since 4.0.0
320 */
321export const GoneNoContent = Gone.pipe(HttpApiSchema.asNoContent({
322 decode: () => new Gone({})
323}))
324
325/**
326 * Built-in HTTP API error for a `422 Unprocessable Entity` response. When used
327 * directly as a server response, it renders as an empty response with status 422.
328 *
329 * @stability unstable
330 * @category errors
331 * @since 4.0.0
332 */
333export class UnprocessableEntity extends Schema.Error<UnprocessableEntity>("effect/HttpApiError/UnprocessableEntity")({
334 _tag: Schema.tag("UnprocessableEntity")
335}, {
336 description: "UnprocessableEntity",
337 httpApiStatus: 422
338}) {
339 override readonly [ErrorReporter.ignore] = true;
340 [HttpServerRespondable.symbol]() {
341 return Effect.succeed(unprocessableEntityResponse)
342 }
343}
344
345/**
346 * No-content schema variant for `UnprocessableEntity`, decoding an empty 422
347 * response into an `UnprocessableEntity` error value.
348 *
349 * @stability unstable
350 * @category schemas
351 * @since 4.0.0
352 */
353export const UnprocessableEntityNoContent = UnprocessableEntity.pipe(HttpApiSchema.asNoContent({
354 decode: () => new UnprocessableEntity({})
355}))
356
357/**
358 * Built-in HTTP API error for a `500 Internal Server Error` response. When used
359 * directly as a server response, it renders as an empty response with status 500.
360 *
361 * @stability unstable
362 * @category errors
363 * @since 4.0.0
364 */
365export class InternalServerError extends Schema.Error<InternalServerError>("effect/HttpApiError/InternalServerError")({
366 _tag: Schema.tag("InternalServerError")
367}, {
368 description: "InternalServerError",
369 httpApiStatus: 500
370}) {
371 [HttpServerRespondable.symbol]() {
372 return Effect.succeed(internalServerErrorResponse)
373 }
374}
375
376/**
377 * No-content schema variant for `InternalServerError`, decoding an empty 500
378 * response into an `InternalServerError` error value.
379 *
380 * @stability unstable
381 * @category schemas
382 * @since 4.0.0
383 */
384export const InternalServerErrorNoContent = InternalServerError.pipe(HttpApiSchema.asNoContent({
385 decode: () => new InternalServerError({})
386}))
387
388/**
389 * Built-in HTTP API error for a `501 Not Implemented` response. When used directly
390 * as a server response, it renders as an empty response with status 501.
391 *
392 * @stability unstable
393 * @category errors
394 * @since 4.0.0
395 */
396export class NotImplemented extends Schema.Error<NotImplemented>("effect/HttpApiError/NotImplemented")({
397 _tag: Schema.tag("NotImplemented")
398}, {
399 description: "NotImplemented",
400 httpApiStatus: 501
401}) {
402 [HttpServerRespondable.symbol]() {
403 return Effect.succeed(notImplementedResponse)
404 }
405}
406
407/**
408 * No-content schema variant for `NotImplemented`, decoding an empty 501 response
409 * into a `NotImplemented` error value.
410 *
411 * @stability unstable
412 * @category schemas
413 * @since 4.0.0
414 */
415export const NotImplementedNoContent = NotImplemented.pipe(HttpApiSchema.asNoContent({
416 decode: () => new NotImplemented({})
417}))
418
419/**
420 * Built-in HTTP API error for a `503 Service Unavailable` response. When used
421 * directly as a server response, it renders as an empty response with status 503.
422 *
423 * @stability unstable
424 * @category errors
425 * @since 4.0.0
426 */
427export class ServiceUnavailable extends Schema.Error<ServiceUnavailable>("effect/HttpApiError/ServiceUnavailable")({
428 _tag: Schema.tag("ServiceUnavailable")
429}, {
430 description: "ServiceUnavailable",
431 httpApiStatus: 503
432}) {
433 [HttpServerRespondable.symbol]() {
434 return Effect.succeed(serviceUnavailableResponse)
435 }
436}
437
438/**
439 * No-content schema variant for `ServiceUnavailable`, decoding an empty 503
440 * response into a `ServiceUnavailable` error value.
441 *
442 * @stability unstable
443 * @category schemas
444 * @since 4.0.0
445 */
446export const ServiceUnavailableNoContent = ServiceUnavailable.pipe(HttpApiSchema.asNoContent({
447 decode: () => new ServiceUnavailable({})
448}))
449
450/**
451 * Type-level identifier used to mark `HttpApiSchemaError` values.
452 *
453 * @stability unstable
454 * @category type IDs
455 * @since 4.0.0
456 */
457export type HttpApiSchemaErrorTypeId = "~effect/http-api/HttpApiError/HttpApiSchemaError"
458
459/**
460 * Runtime identifier used to mark and detect `HttpApiSchemaError` values.
461 *
462 * @stability unstable
463 * @category type IDs
464 * @since 4.0.0
465 */
466export const HttpApiSchemaErrorTypeId: HttpApiSchemaErrorTypeId = "~effect/http-api/HttpApiError/HttpApiSchemaError"
467
468/**
469 * Error raised when an HTTP API request or response component fails schema
470 * decoding or encoding. It records which component failed and responds as an
471 * empty `400 Bad Request` when rendered as a server response.
472 *
473 * @stability unstable
474 * @category errors
475 * @since 4.0.0
476 */
477export class HttpApiSchemaError extends Data.TaggedClass("HttpApiSchemaError")<{
478 readonly kind: "Params" | "Headers" | "Query" | "Body" | "Payload" | "ResponseHeaders"
479 readonly cause: Schema.SchemaError
480}> {
481 readonly [HttpApiSchemaErrorTypeId]: HttpApiSchemaErrorTypeId = HttpApiSchemaErrorTypeId
482 readonly [ErrorReporter.ignore] = true
483
484 static is(u: unknown): u is HttpApiSchemaError {
485 return hasProperty(u, HttpApiSchemaErrorTypeId)
486 }
487
488 static wrap<A, R>(
489 kind: HttpApiSchemaError["kind"],
490 effect: Effect.Effect<A, Schema.SchemaError, R>
491 ): Effect.Effect<A, HttpApiSchemaError, R> {
492 // Decoders return completed Exit values for synchronous results, so map
493 // eagerly and skip the error wrapper when nothing remains to run.
494 return Effect.mapErrorEager(effect, (error) => new HttpApiSchemaError({ kind, cause: error }))
495 }
496
497 readonly name = "HttpApiSchemaError"
498 readonly message = this.kind;
499
500 [HttpServerRespondable.symbol]() {
501 return Effect.succeed(badRequestResponse)
502 }
503}