elysia 2.0.0-beta.0 → 2.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/adapter/utils.d.ts +1 -1
  2. package/dist/adapter/web-standard/handler.js +11 -7
  3. package/dist/adapter/web-standard/handler.mjs +12 -8
  4. package/dist/base.d.ts +18 -3
  5. package/dist/base.js +31 -15
  6. package/dist/base.mjs +31 -15
  7. package/dist/compile/aot-capture.js +1 -1
  8. package/dist/compile/aot-capture.mjs +2 -2
  9. package/dist/compile/handler/descriptor.d.ts +1 -1
  10. package/dist/compile/handler/frozen-validator.d.ts +2 -1
  11. package/dist/compile/handler/frozen-validator.js +33 -3
  12. package/dist/compile/handler/frozen-validator.mjs +33 -4
  13. package/dist/compile/handler/index.d.ts +1 -1
  14. package/dist/compile/handler/jit.js +5 -8
  15. package/dist/compile/handler/jit.mjs +6 -9
  16. package/dist/compile/handler/params.js +3 -3
  17. package/dist/compile/handler/params.mjs +4 -4
  18. package/dist/compile/handler/utils.d.ts +1 -1
  19. package/dist/compile/handler/utils.js +5 -2
  20. package/dist/compile/handler/utils.mjs +5 -2
  21. package/dist/context.d.ts +2 -2
  22. package/dist/cookie/cookie.js +1 -0
  23. package/dist/cookie/cookie.mjs +1 -0
  24. package/dist/error.d.ts +138 -5
  25. package/dist/error.js +73 -0
  26. package/dist/error.mjs +73 -1
  27. package/dist/handler/error.d.ts +20 -1
  28. package/dist/handler/error.js +120 -8
  29. package/dist/handler/error.mjs +119 -9
  30. package/dist/handler/fetch.js +9 -5
  31. package/dist/handler/fetch.mjs +9 -5
  32. package/dist/index.d.ts +4 -4
  33. package/dist/index.js +1 -0
  34. package/dist/index.mjs +2 -2
  35. package/dist/package.js +1 -1
  36. package/dist/package.mjs +1 -1
  37. package/dist/plugin/aot/core.d.ts +4 -3
  38. package/dist/plugin/aot/core.js +1 -1
  39. package/dist/plugin/aot/core.mjs +1 -1
  40. package/dist/route-table.d.ts +1 -1
  41. package/dist/sucrose.js +118 -16
  42. package/dist/sucrose.mjs +118 -16
  43. package/dist/type/coerce.js +9 -4
  44. package/dist/type/coerce.mjs +9 -4
  45. package/dist/type/elysia/array-string.js +29 -2
  46. package/dist/type/elysia/array-string.mjs +30 -3
  47. package/dist/type/elysia/file-type.js +11 -1
  48. package/dist/type/elysia/file-type.mjs +11 -1
  49. package/dist/type/elysia/form.js +2 -1
  50. package/dist/type/elysia/form.mjs +2 -1
  51. package/dist/type/elysia/object-string.js +26 -2
  52. package/dist/type/elysia/object-string.mjs +27 -3
  53. package/dist/type/validator/index.js +11 -13
  54. package/dist/type/validator/index.mjs +12 -14
  55. package/dist/types.d.ts +104 -18
  56. package/dist/universal/file.js +1 -0
  57. package/dist/universal/file.mjs +1 -0
  58. package/dist/universal/index.d.ts +1 -1
  59. package/dist/utils.d.ts +3 -5
  60. package/dist/utils.js +7 -9
  61. package/dist/utils.mjs +6 -10
  62. package/dist/ws/route.js +1 -1
  63. package/dist/ws/route.mjs +1 -1
  64. package/package.json +2 -8
@@ -1,11 +1,11 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
  const require_utils = require('../../utils.js');
3
- const require_universal_is_production = require('../../universal/is-production.js');
4
3
  const require_error = require('../../error.js');
5
4
  const require_adapter_utils = require('../../adapter/utils.js');
6
5
  const require_parse_query = require('../../parse-query.js');
7
6
  const require_cookie_utils = require('../../cookie/utils.js');
8
7
  const require_handler_utils = require('../../handler/utils.js');
8
+ const require_handler_error = require('../../handler/error.js');
9
9
  const require_compile_handler_utils = require('./utils.js');
10
10
 
11
11
  //#region src/compile/handler/params.ts
@@ -29,10 +29,8 @@ const handlerParams = () => _handlerParams ??= {
29
29
  pe: () => require_error.ParseError,
30
30
  es: () => require_error.ElysiaStatus,
31
31
  rdc: () => require_compile_handler_utils.replaceDeriveContext,
32
- ise: () => require_error.internalServerErrorResponse,
33
32
  emp: () => require_handler_utils.emptyResponse,
34
33
  ea: () => require_compile_handler_utils.armEntryAbort,
35
- isprod: () => require_universal_is_production.isProduction,
36
34
  verr: () => require_error.ValidationError,
37
35
  tee: () => require_adapter_utils.tee,
38
36
  msh: () => require_adapter_utils.materializeSetHeaders,
@@ -47,6 +45,8 @@ const handlerParams = () => _handlerParams ??= {
47
45
  va: (c) => c.vali,
48
46
  fe: () => require_handler_utils.forwardError,
49
47
  fre: () => require_handler_utils.finalizeRouteError,
48
+ fbr: () => require_handler_error.fallbackResponse,
49
+ aet: () => require_handler_error.adoptErrorType,
50
50
  rt: (c) => c.root,
51
51
  ho: (c) => c.hook,
52
52
  tf: (c) => c.hook.transform,
@@ -1,10 +1,10 @@
1
1
  import { requestId } from "../../utils.mjs";
2
- import { isProduction } from "../../universal/is-production.mjs";
3
- import { ElysiaStatus, ParseError, ValidationError, internalServerErrorResponse } from "../../error.mjs";
2
+ import { ElysiaStatus, ParseError, ValidationError } from "../../error.mjs";
4
3
  import { materializeSetHeaders, normalizeContentType, tee } from "../../adapter/utils.mjs";
5
4
  import { parseQueryFromURL } from "../../parse-query.mjs";
6
5
  import { buildCookieJar, parseCookieRaw, parseCookieRawDeferred, parseCookieRawLazy, parseCookieRawSigned, parseCookieRawSync, signCookieValues } from "../../cookie/utils.mjs";
7
6
  import { emptyResponse, finalizeRouteError, forwardError } from "../../handler/utils.mjs";
7
+ import { adoptErrorType, fallbackResponse } from "../../handler/error.mjs";
8
8
  import { armEntryAbort, cloneResponse, getQueryParseChannels, hasRequestBody, replaceDeriveContext, runBeforeHandlePrefix } from "./utils.mjs";
9
9
 
10
10
  //#region src/compile/handler/params.ts
@@ -28,10 +28,8 @@ const handlerParams = () => _handlerParams ??= {
28
28
  pe: () => ParseError,
29
29
  es: () => ElysiaStatus,
30
30
  rdc: () => replaceDeriveContext,
31
- ise: () => internalServerErrorResponse,
32
31
  emp: () => emptyResponse,
33
32
  ea: () => armEntryAbort,
34
- isprod: () => isProduction,
35
33
  verr: () => ValidationError,
36
34
  tee: () => tee,
37
35
  msh: () => materializeSetHeaders,
@@ -46,6 +44,8 @@ const handlerParams = () => _handlerParams ??= {
46
44
  va: (c) => c.vali,
47
45
  fe: () => forwardError,
48
46
  fre: () => finalizeRouteError,
47
+ fbr: () => fallbackResponse,
48
+ aet: () => adoptErrorType,
49
49
  rt: (c) => c.root,
50
50
  ho: (c) => c.hook,
51
51
  tf: (c) => c.hook.transform,
@@ -1,5 +1,5 @@
1
- import { AfterResponseHandler, AppHook, ErrorHandler, MaybeArray, TransformHandler } from "../../types.js";
2
1
  import { CompactBeforeHandlePrefix, DeriveEntry } from "../../utils.js";
2
+ import { AfterResponseHandler, AppHook, ErrorHandler, MaybeArray, TransformHandler } from "../../types.js";
3
3
  import { Context } from "../../context.js";
4
4
  import { emptyResponse } from "../../handler/utils.js";
5
5
 
@@ -5,6 +5,7 @@ const require_adapter_skip_clone = require('../../adapter/skip-clone.js');
5
5
  const require_error = require('../../error.js');
6
6
  const require_compile_utils = require('../utils.js');
7
7
  const require_handler_utils = require('../../handler/utils.js');
8
+ const require_handler_error = require('../../handler/error.js');
8
9
  const require_adapter_origin = require('../../adapter/origin.js');
9
10
  const require_compile_lexer = require('../lexer.js');
10
11
 
@@ -402,11 +403,13 @@ const mapAfterHandle = (_hooks, isAsync, report, abortGuard) => mapChainHook(toA
402
403
  const mapMapResponse = (_hooks, isAsync, report, abortGuard) => mapChainHook(toArray(_hooks), "mr", isAsync, report, abortGuard);
403
404
  const mapAfterResponse = /* @__PURE__ */ map((i, fn, [report]) => {
404
405
  const t = trace(report, fn);
405
- return `try{` + t.begin + `${Await(fn)}ar${at(i)}(c)\n` + t.end() + `}catch(_e){` + t.end("_e") + `}\n`;
406
+ const call = require_compile_utils.isAsyncFunction(fn) ? `await ar${at(i)}(c)\n` : `let _ar=ar${at(i)}(c)\nif(_ar instanceof Promise)await _ar\n`;
407
+ return `try{` + t.begin + call + t.end() + `}catch(_e){` + t.end("_e") + `}\n`;
406
408
  });
407
409
  const mapError = /* @__PURE__ */ map((i, fn, [map, link, mapResponse, schedule, sign, isAsync]) => {
408
410
  link(mapResponse, "rm");
409
- return `_r=${Await(fn)}er${at(i)}(c)\n` + awaitGuard(fn, isAsync, "_r") + "if(_r!==undefined){\nif(_r instanceof Response)c.set.status=_r.status\nelse if(c.set.status===undefined||c.set.status===200)c.set.status=500\n" + schedule + sign + `return _em(c,${map}(_r,c.set,c.request,true))\n}\n`;
411
+ link(require_handler_error.adoptErrorType, "aet");
412
+ return `_r=${Await(fn)}er${at(i)}(c)\n` + awaitGuard(fn, isAsync, "_r") + "if(_r!==undefined){\nif(_r instanceof Response)c.set.status=_r.status\nelse if(c.set.status===undefined||c.set.status===200)c.set.status=500\n" + schedule + sign + `return _em(c,${map}(aet(_r,e),c.set,c.request,true))\n}\n`;
410
413
  });
411
414
  function map(map) {
412
415
  return function(event, rest, abortGuard) {
@@ -4,6 +4,7 @@ import { skipClone } from "../../adapter/skip-clone.mjs";
4
4
  import { ElysiaStatus } from "../../error.mjs";
5
5
  import { isAsyncFunction } from "../utils.mjs";
6
6
  import { emptyResponse } from "../../handler/utils.mjs";
7
+ import { adoptErrorType } from "../../handler/error.mjs";
7
8
  import { origin } from "../../adapter/origin.mjs";
8
9
  import { isIdentChar, isSpace, skipString } from "../lexer.mjs";
9
10
 
@@ -401,11 +402,13 @@ const mapAfterHandle = (_hooks, isAsync, report, abortGuard) => mapChainHook(toA
401
402
  const mapMapResponse = (_hooks, isAsync, report, abortGuard) => mapChainHook(toArray(_hooks), "mr", isAsync, report, abortGuard);
402
403
  const mapAfterResponse = /* @__PURE__ */ map((i, fn, [report]) => {
403
404
  const t = trace(report, fn);
404
- return `try{` + t.begin + `${Await(fn)}ar${at(i)}(c)\n` + t.end() + `}catch(_e){` + t.end("_e") + `}\n`;
405
+ const call = isAsyncFunction(fn) ? `await ar${at(i)}(c)\n` : `let _ar=ar${at(i)}(c)\nif(_ar instanceof Promise)await _ar\n`;
406
+ return `try{` + t.begin + call + t.end() + `}catch(_e){` + t.end("_e") + `}\n`;
405
407
  });
406
408
  const mapError = /* @__PURE__ */ map((i, fn, [map, link, mapResponse, schedule, sign, isAsync]) => {
407
409
  link(mapResponse, "rm");
408
- return `_r=${Await(fn)}er${at(i)}(c)\n` + awaitGuard(fn, isAsync, "_r") + "if(_r!==undefined){\nif(_r instanceof Response)c.set.status=_r.status\nelse if(c.set.status===undefined||c.set.status===200)c.set.status=500\n" + schedule + sign + `return _em(c,${map}(_r,c.set,c.request,true))\n}\n`;
410
+ link(adoptErrorType, "aet");
411
+ return `_r=${Await(fn)}er${at(i)}(c)\n` + awaitGuard(fn, isAsync, "_r") + "if(_r!==undefined){\nif(_r instanceof Response)c.set.status=_r.status\nelse if(c.set.status===undefined||c.set.status===200)c.set.status=500\n" + schedule + sign + `return _em(c,${map}(aet(_r,e),c.set,c.request,true))\n}\n`;
409
412
  });
410
413
  function map(map) {
411
414
  return function(event, rest, abortGuard) {
package/dist/context.d.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  import { BaseCookie } from "./cookie/types.js";
2
2
  import { Cookie } from "./cookie/cookie.js";
3
3
  import { StatusMap } from "./constants.js";
4
- import { SelectiveStatus, status } from "./error.js";
5
4
  import { Server } from "./universal/server.js";
6
- import { AfterResponseHandler, DefaultSingleton, HTTPHeaders, InputSchema, MaybePromise, Prettify, ResolvePath, RouteSchema, SingletonBase } from "./types.js";
7
5
  import { redirect } from "./utils.js";
6
+ import { AfterResponseHandler, DefaultSingleton, HTTPHeaders, InputSchema, MaybePromise, Prettify, ResolvePath, RouteSchema, SingletonBase } from "./types.js";
7
+ import { SelectiveStatus, status } from "./error.js";
8
8
  import { AnyElysia } from "./base.js";
9
9
 
10
10
  //#region src/context.d.ts
@@ -90,6 +90,7 @@ var Cookie = class {
90
90
  return typeof this.value === "object" ? JSON.stringify(this.value) : this.value?.toString() ?? "";
91
91
  }
92
92
  };
93
+ Object.defineProperty(Cookie, "name", { value: "Cookie" });
93
94
  for (const key of FORWARDED_KEYS) Object.defineProperty(Cookie.prototype, key, {
94
95
  get() {
95
96
  return this.cookie[key];
@@ -89,6 +89,7 @@ var Cookie = class {
89
89
  return typeof this.value === "object" ? JSON.stringify(this.value) : this.value?.toString() ?? "";
90
90
  }
91
91
  };
92
+ Object.defineProperty(Cookie, "name", { value: "Cookie" });
92
93
  for (const key of FORWARDED_KEYS) Object.defineProperty(Cookie.prototype, key, {
93
94
  get() {
94
95
  return this.cookie[key];
package/dist/error.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { StatusMap, StatusMapBack } from "./constants.js";
2
2
  import { isProduction } from "./universal/is-production.js";
3
+ import { MaybePromise } from "./types.js";
3
4
 
4
5
  //#region src/error.d.ts
5
6
  declare class ElysiaError<Status extends number = number, Response extends string = string> extends Error {
@@ -193,13 +194,14 @@ type NumericStatus<Code extends number | keyof StatusMap> = Code extends keyof S
193
194
  type ProblemStatus<P> = P extends {
194
195
  status: infer S extends number | keyof StatusMap;
195
196
  } ? NumericStatus<S> : 500;
196
- type ProblemResponseBody<Status extends number, P> = {
197
+ type ProblemResponseBody<Status extends number, P> = Omit<{
197
198
  type: string;
198
199
  title: string;
199
- status: Status;
200
200
  detail?: string;
201
201
  instance?: string;
202
- } & Omit<P, keyof Problem>;
202
+ }, keyof P> & Omit<P, 'status'> & {
203
+ status: Status;
204
+ };
203
205
  declare function problemBody(p: Problem): Record<string, unknown> & {
204
206
  status: number;
205
207
  };
@@ -217,7 +219,9 @@ declare function internalServerErrorResponse(error: any): Response;
217
219
  *
218
220
  * @see https://www.rfc-editor.org/info/rfc9457
219
221
  */
220
- declare function problem<const Code extends number | keyof StatusMap, const P extends Record<string, unknown> = {}>(status: Code, detail?: P & Omit<Problem<Code>, 'status'>): ElysiaStatus<NumericStatus<Code>, ProblemResponseBody<NumericStatus<Code>, P>>;
222
+ declare function problem<const Code extends number | keyof StatusMap, const P extends Record<string, unknown> & {
223
+ status?: never;
224
+ } = {}>(status: Code, detail?: P & Omit<Problem<Code>, 'status'>): ElysiaStatus<NumericStatus<Code>, ProblemResponseBody<NumericStatus<Code>, P>>;
221
225
  /**
222
226
  * RFC 9457 Problem Details function
223
227
  *
@@ -231,5 +235,134 @@ declare function problem<const Code extends number | keyof StatusMap, const P ex
231
235
  declare function problem<const P extends Problem>(detail: P): ElysiaStatus<ProblemStatus<P>, ProblemResponseBody<ProblemStatus<P>, P>>;
232
236
  type CheckExcessProps<T, U> = 0 extends 1 & T ? T : U extends U ? Exclude<keyof T, keyof U> extends never ? T : { [K in keyof U]: U[K] } & { [K in Exclude<keyof T, keyof U>]: never } : never;
233
237
  type SelectiveStatus<in out Res> = <const Code extends keyof Res | StatusMapBack[Extract<keyof StatusMapBack, keyof Res>], T extends (Code extends keyof Res ? Res[Code] : Code extends keyof StatusMap ? Res[StatusMap[Code]] : never)>(code: Code, response: CheckExcessProps<T, Code extends keyof Res ? Res[Code] : Code extends keyof StatusMap ? Res[StatusMap[Code]] : never>) => ElysiaStatus<Code, T>;
238
+ /**
239
+ * Class returned by {@link HTTPError.id}
240
+ */
241
+ type TaggedHTTPError<Type extends string, Annotation = {}> = {
242
+ new (message?: string, options?: ErrorOptions): HTTPError<Type> & {
243
+ type: Type;
244
+ } & Annotation;
245
+ prototype: HTTPError<Type>;
246
+ };
247
+ /**
248
+ * Self-describing HTTP error
249
+ *
250
+ * Annotate `status`, `headers` and `value` on the subclass and Elysia maps
251
+ * the instance to a response on its own, no `.error(Class, handler)` needed
252
+ *
253
+ * Everything an owned `HTTPError` serves is an RFC 9457 problem document:
254
+ * `type` is the problem type carried on the wire, an annotated object body
255
+ * merges into the envelope, and any other body (or the error message, when
256
+ * there is none) becomes `detail`
257
+ *
258
+ * - `detail()` - the common case. Whatever it returns becomes the `detail`
259
+ * member of the problem document, verbatim, objects included. The envelope
260
+ * still carries `type`, `status` and an auto-filled `title`
261
+ * - `value()` - the escape hatch. Whatever it returns replaces the *whole*
262
+ * response: no envelope, no `application/problem+json`. The annotated
263
+ * `status` and `headers` still apply, only the content is yours
264
+ *
265
+ * `value` wins over `detail`, `detail` over the error message. A knob
266
+ * returning `undefined` falls through to the next one.
267
+ *
268
+ * @example
269
+ * ```ts
270
+ * class OutOfCredit extends HTTPError<'OUT_OF_CREDIT'> {
271
+ * type = 'OUT_OF_CREDIT' as const
272
+ * override readonly status = 402
273
+ *
274
+ * detail() {
275
+ * return { balance: 0, currency: 'usd' }
276
+ * }
277
+ * }
278
+ * // → { type: 'OUT_OF_CREDIT', title: 'Payment Required', status: 402,
279
+ * // detail: { balance: 0, currency: 'usd' } }
280
+ *
281
+ * class Overdrawn extends HTTPError<'OVERDRAWN'> {
282
+ * type = 'OVERDRAWN' as const
283
+ * override readonly status = 402
284
+ *
285
+ * async detail() {
286
+ * return await describeBalance()
287
+ * }
288
+ * }
289
+ *
290
+ * class Legacy extends HTTPError<'LEGACY'> {
291
+ * type = 'LEGACY' as const
292
+ * override readonly status = 402
293
+ *
294
+ * // full control, served as-is
295
+ * value() {
296
+ * return { code: 'OUT_OF_CREDIT', ok: false }
297
+ * }
298
+ * }
299
+ * ```
300
+ *
301
+ * @see https://www.rfc-editor.org/info/rfc9457
302
+ * @since 2.0.0
303
+ */
304
+ declare abstract class HTTPError<const in out T extends string = string> extends Error {
305
+ /**
306
+ * RFC 9457 problem type served in the `type` member, a URI or a slug.
307
+ * Doubles as the tag that discriminates one error class from another
308
+ */
309
+ abstract type: T;
310
+ /**
311
+ * HTTP status served when the error reaches the error pipeline,
312
+ * as a number or a status name (`'Payment Required'`)
313
+ */
314
+ readonly status?: number | keyof StatusMap;
315
+ /** Headers merged into `set.headers` when the error is served */
316
+ readonly headers?: Record<string, string>;
317
+ /**
318
+ * Return a value to serve as the whole response
319
+ *
320
+ * Supports `status()`/`problem()` for status-based Eden response
321
+ */
322
+ value?(): MaybePromise<unknown>;
323
+ /**
324
+ * Return the `detail` member of the RFC 9457 problem document,
325
+ * similar to `problem()`
326
+ *
327
+ * Use `value()` to override the entire response
328
+ */
329
+ detail?(): MaybePromise<unknown>;
330
+ /**
331
+ * Create a concrete `HTTPError` subclass carrying `type`
332
+ *
333
+ * `type` carries no status annotation, annotate `status` in the subclass
334
+ * body as a number or a status name
335
+ *
336
+ * @example
337
+ * ```ts
338
+ * class OutOfCredit extends HTTPError.id('OUT_OF_CREDIT', 402) {
339
+ * headers = {
340
+ * 'x-powered-by': 'Elysia'
341
+ * }
342
+ * }
343
+ * ```
344
+ */
345
+ static id<const Type extends string>(type: Type): TaggedHTTPError<Type>;
346
+ /**
347
+ * Create a concrete `HTTPError` subclass carrying `type`, annotated with
348
+ * `status` as a number or a status name
349
+ *
350
+ * Annotate what is served with `detail()` (problem `detail` member) or
351
+ * `value()` (whole-response override), either of which may be `async`
352
+ *
353
+ * @example
354
+ * ```ts
355
+ * class OutOfCredit extends HTTPError.id('OUT_OF_CREDIT', 402) {}
356
+ * class Denied extends HTTPError.id('https://example.com/denied', 'Payment Required') {
357
+ * async detail() {
358
+ * return await describeBalance()
359
+ * }
360
+ * }
361
+ * ```
362
+ */
363
+ static id<const Type extends string, const S extends number | keyof StatusMap>(type: Type, status: S): TaggedHTTPError<Type, {
364
+ readonly status: S extends keyof StatusMap ? StatusMap[S] : S;
365
+ }>;
366
+ }
234
367
  //#endregion
235
- export { ElysiaError, ElysiaStatus, InternalServerError, NotFound, PROBLEM_JSON, ParseError, Problem, SelectiveStatus, ValidationError, internalServerErrorBody, internalServerErrorBodyString, internalServerErrorResponse, isProduction, problem, problemBody, problemResponse, status, subValueAt, validationDetail };
368
+ export { ElysiaError, ElysiaStatus, HTTPError, InternalServerError, NotFound, PROBLEM_JSON, ParseError, Problem, ProblemResponseBody, SelectiveStatus, TaggedHTTPError, ValidationError, internalServerErrorBody, internalServerErrorBodyString, internalServerErrorResponse, isProduction, problem, problemBody, problemResponse, status, subValueAt, validationDetail };
package/dist/error.js CHANGED
@@ -437,6 +437,7 @@ var ElysiaStatus = class {
437
437
  return this.code;
438
438
  }
439
439
  };
440
+ Object.defineProperty(ElysiaStatus, "name", { value: "ElysiaStatus" });
440
441
  const status = (code, response) => new ElysiaStatus(code, response);
441
442
  const PROBLEM_JSON = "application/problem+json";
442
443
  function problemBody(p) {
@@ -507,10 +508,82 @@ function problem(statusOrDetail, detail) {
507
508
  };
508
509
  return new ElysiaStatus(body.status ?? 500, problemBody(body), { "content-type": PROBLEM_JSON });
509
510
  }
511
+ /**
512
+ * Self-describing HTTP error
513
+ *
514
+ * Annotate `status`, `headers` and `value` on the subclass and Elysia maps
515
+ * the instance to a response on its own, no `.error(Class, handler)` needed
516
+ *
517
+ * Everything an owned `HTTPError` serves is an RFC 9457 problem document:
518
+ * `type` is the problem type carried on the wire, an annotated object body
519
+ * merges into the envelope, and any other body (or the error message, when
520
+ * there is none) becomes `detail`
521
+ *
522
+ * - `detail()` - the common case. Whatever it returns becomes the `detail`
523
+ * member of the problem document, verbatim, objects included. The envelope
524
+ * still carries `type`, `status` and an auto-filled `title`
525
+ * - `value()` - the escape hatch. Whatever it returns replaces the *whole*
526
+ * response: no envelope, no `application/problem+json`. The annotated
527
+ * `status` and `headers` still apply, only the content is yours
528
+ *
529
+ * `value` wins over `detail`, `detail` over the error message. A knob
530
+ * returning `undefined` falls through to the next one.
531
+ *
532
+ * @example
533
+ * ```ts
534
+ * class OutOfCredit extends HTTPError<'OUT_OF_CREDIT'> {
535
+ * type = 'OUT_OF_CREDIT' as const
536
+ * override readonly status = 402
537
+ *
538
+ * detail() {
539
+ * return { balance: 0, currency: 'usd' }
540
+ * }
541
+ * }
542
+ * // → { type: 'OUT_OF_CREDIT', title: 'Payment Required', status: 402,
543
+ * // detail: { balance: 0, currency: 'usd' } }
544
+ *
545
+ * class Overdrawn extends HTTPError<'OVERDRAWN'> {
546
+ * type = 'OVERDRAWN' as const
547
+ * override readonly status = 402
548
+ *
549
+ * async detail() {
550
+ * return await describeBalance()
551
+ * }
552
+ * }
553
+ *
554
+ * class Legacy extends HTTPError<'LEGACY'> {
555
+ * type = 'LEGACY' as const
556
+ * override readonly status = 402
557
+ *
558
+ * // full control, served as-is
559
+ * value() {
560
+ * return { code: 'OUT_OF_CREDIT', ok: false }
561
+ * }
562
+ * }
563
+ * ```
564
+ *
565
+ * @see https://www.rfc-editor.org/info/rfc9457
566
+ * @since 2.0.0
567
+ */
568
+ var HTTPError = class HTTPError extends Error {
569
+ static id(type, status) {
570
+ const Tagged = class extends HTTPError {};
571
+ Tagged.prototype.type = type;
572
+ Tagged.prototype.name = type;
573
+ Object.defineProperty(Tagged, "name", {
574
+ value: type,
575
+ configurable: true
576
+ });
577
+ const resolved = typeof status === "string" ? require_constants.StatusMap[status] : status;
578
+ if (typeof resolved === "number") Tagged.prototype.status = resolved;
579
+ return Tagged;
580
+ }
581
+ };
510
582
 
511
583
  //#endregion
512
584
  exports.ElysiaError = ElysiaError;
513
585
  exports.ElysiaStatus = ElysiaStatus;
586
+ exports.HTTPError = HTTPError;
514
587
  exports.InternalServerError = InternalServerError;
515
588
  exports.NotFound = NotFound;
516
589
  exports.PROBLEM_JSON = PROBLEM_JSON;
package/dist/error.mjs CHANGED
@@ -436,6 +436,7 @@ var ElysiaStatus = class {
436
436
  return this.code;
437
437
  }
438
438
  };
439
+ Object.defineProperty(ElysiaStatus, "name", { value: "ElysiaStatus" });
439
440
  const status = (code, response) => new ElysiaStatus(code, response);
440
441
  const PROBLEM_JSON = "application/problem+json";
441
442
  function problemBody(p) {
@@ -506,6 +507,77 @@ function problem(statusOrDetail, detail) {
506
507
  };
507
508
  return new ElysiaStatus(body.status ?? 500, problemBody(body), { "content-type": PROBLEM_JSON });
508
509
  }
510
+ /**
511
+ * Self-describing HTTP error
512
+ *
513
+ * Annotate `status`, `headers` and `value` on the subclass and Elysia maps
514
+ * the instance to a response on its own, no `.error(Class, handler)` needed
515
+ *
516
+ * Everything an owned `HTTPError` serves is an RFC 9457 problem document:
517
+ * `type` is the problem type carried on the wire, an annotated object body
518
+ * merges into the envelope, and any other body (or the error message, when
519
+ * there is none) becomes `detail`
520
+ *
521
+ * - `detail()` - the common case. Whatever it returns becomes the `detail`
522
+ * member of the problem document, verbatim, objects included. The envelope
523
+ * still carries `type`, `status` and an auto-filled `title`
524
+ * - `value()` - the escape hatch. Whatever it returns replaces the *whole*
525
+ * response: no envelope, no `application/problem+json`. The annotated
526
+ * `status` and `headers` still apply, only the content is yours
527
+ *
528
+ * `value` wins over `detail`, `detail` over the error message. A knob
529
+ * returning `undefined` falls through to the next one.
530
+ *
531
+ * @example
532
+ * ```ts
533
+ * class OutOfCredit extends HTTPError<'OUT_OF_CREDIT'> {
534
+ * type = 'OUT_OF_CREDIT' as const
535
+ * override readonly status = 402
536
+ *
537
+ * detail() {
538
+ * return { balance: 0, currency: 'usd' }
539
+ * }
540
+ * }
541
+ * // → { type: 'OUT_OF_CREDIT', title: 'Payment Required', status: 402,
542
+ * // detail: { balance: 0, currency: 'usd' } }
543
+ *
544
+ * class Overdrawn extends HTTPError<'OVERDRAWN'> {
545
+ * type = 'OVERDRAWN' as const
546
+ * override readonly status = 402
547
+ *
548
+ * async detail() {
549
+ * return await describeBalance()
550
+ * }
551
+ * }
552
+ *
553
+ * class Legacy extends HTTPError<'LEGACY'> {
554
+ * type = 'LEGACY' as const
555
+ * override readonly status = 402
556
+ *
557
+ * // full control, served as-is
558
+ * value() {
559
+ * return { code: 'OUT_OF_CREDIT', ok: false }
560
+ * }
561
+ * }
562
+ * ```
563
+ *
564
+ * @see https://www.rfc-editor.org/info/rfc9457
565
+ * @since 2.0.0
566
+ */
567
+ var HTTPError = class HTTPError extends Error {
568
+ static id(type, status) {
569
+ const Tagged = class extends HTTPError {};
570
+ Tagged.prototype.type = type;
571
+ Tagged.prototype.name = type;
572
+ Object.defineProperty(Tagged, "name", {
573
+ value: type,
574
+ configurable: true
575
+ });
576
+ const resolved = typeof status === "string" ? StatusMap[status] : status;
577
+ if (typeof resolved === "number") Tagged.prototype.status = resolved;
578
+ return Tagged;
579
+ }
580
+ };
509
581
 
510
582
  //#endregion
511
- export { ElysiaError, ElysiaStatus, InternalServerError, NotFound, PROBLEM_JSON, ParseError, ValidationError, internalServerErrorBody, internalServerErrorBodyString, internalServerErrorResponse, isProduction, problem, problemBody, problemResponse, status, subValueAt, validationDetail };
583
+ export { ElysiaError, ElysiaStatus, HTTPError, InternalServerError, NotFound, PROBLEM_JSON, ParseError, ValidationError, internalServerErrorBody, internalServerErrorBodyString, internalServerErrorResponse, isProduction, problem, problemBody, problemResponse, status, subValueAt, validationDetail };
@@ -2,6 +2,25 @@ import { AppHook } from "../types.js";
2
2
  import { Context } from "../context.js";
3
3
 
4
4
  //#region src/handler/error.d.ts
5
+ /**
6
+ * Adopt the intercepted error's `type` into a problem the hook produced
7
+ * without one.
8
+ *
9
+ * `'about:blank'` is RFC 9457's "unspecified" default, so it's the only value
10
+ * worth replacing — an explicit type the handler set always wins.
11
+ *
12
+ * The result is copied rather than mutated: `.error(Class, value)` wraps a
13
+ * non-function into `() => value`, so the very same `ElysiaStatus` is replayed
14
+ * on every request and an in-place write would leak across requests
15
+ */
16
+ declare function adoptErrorType(result: any, error: any): any;
17
+ /**
18
+ * Response served once every error hook has declined the error.
19
+ *
20
+ * Exported for the JIT: a route with error hooks emits its own catch block,
21
+ * which must land on *this* function rather than re-implementing it
22
+ */
23
+ declare function fallbackResponse(context: Context, error: any, mapResponse: (response: unknown, set: Context['set'], context?: Context) => unknown, defaultError?: Response): unknown;
5
24
  declare function createErrorHandler(onErrors: AppHook['error'] | undefined, mapResponse: (response: unknown, set: Context['set'], ...any: unknown[]) => unknown, defaultError?: Response, allowUnsafe?: boolean): (context: Context, error: Error) => unknown;
6
25
  //#endregion
7
- export { createErrorHandler };
26
+ export { adoptErrorType, createErrorHandler, fallbackResponse };