@setu-ts/exceptions 0.0.0-bootstrap.0 → 0.9.0

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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +179 -2
  3. package/dist/errors/exceptions.d.ts +146 -0
  4. package/dist/errors/exceptions.d.ts.map +1 -0
  5. package/dist/errors/exceptions.js +189 -0
  6. package/dist/errors/exceptions.js.map +1 -0
  7. package/dist/errors/http-error.d.ts +111 -0
  8. package/dist/errors/http-error.d.ts.map +1 -0
  9. package/dist/errors/http-error.js +81 -0
  10. package/dist/errors/http-error.js.map +1 -0
  11. package/dist/formatters/error-formatter.d.ts +81 -0
  12. package/dist/formatters/error-formatter.d.ts.map +1 -0
  13. package/dist/formatters/error-formatter.js +58 -0
  14. package/dist/formatters/error-formatter.js.map +1 -0
  15. package/dist/formatters/problem-details.d.ts +118 -0
  16. package/dist/formatters/problem-details.d.ts.map +1 -0
  17. package/dist/formatters/problem-details.js +108 -0
  18. package/dist/formatters/problem-details.js.map +1 -0
  19. package/dist/formatters/rfc7807-formatter.d.ts +20 -0
  20. package/dist/formatters/rfc7807-formatter.d.ts.map +1 -0
  21. package/dist/formatters/rfc7807-formatter.js +27 -0
  22. package/dist/formatters/rfc7807-formatter.js.map +1 -0
  23. package/dist/formatters/rfc9457-formatter.d.ts +29 -0
  24. package/dist/formatters/rfc9457-formatter.d.ts.map +1 -0
  25. package/dist/formatters/rfc9457-formatter.js +43 -0
  26. package/dist/formatters/rfc9457-formatter.js.map +1 -0
  27. package/dist/index.d.ts +33 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +33 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/middleware/error-handler.d.ts +112 -0
  32. package/dist/middleware/error-handler.d.ts.map +1 -0
  33. package/dist/middleware/error-handler.js +210 -0
  34. package/dist/middleware/error-handler.js.map +1 -0
  35. package/dist/middleware/error-responder-impl.d.ts +52 -0
  36. package/dist/middleware/error-responder-impl.d.ts.map +1 -0
  37. package/dist/middleware/error-responder-impl.js +148 -0
  38. package/dist/middleware/error-responder-impl.js.map +1 -0
  39. package/package.json +44 -3
  40. package/src/errors/exceptions.ts +212 -0
  41. package/src/errors/http-error.ts +132 -0
  42. package/src/formatters/error-formatter.ts +127 -0
  43. package/src/formatters/problem-details.ts +176 -0
  44. package/src/formatters/rfc7807-formatter.ts +48 -0
  45. package/src/formatters/rfc9457-formatter.ts +69 -0
  46. package/src/index.ts +57 -0
  47. package/src/middleware/error-handler.ts +337 -0
  48. package/src/middleware/error-responder-impl.ts +178 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Setu-TS Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,4 +1,181 @@
1
1
  # @setu-ts/exceptions
2
2
 
3
- This version is a name placeholder. It contains no framework code.
4
- The first real release is 0.9.0, published through GitHub Actions with npm provenance.
3
+ Exception hierarchy, factory functions, error formatters, and the global error-handler middleware.
4
+
5
+ This is a **plain package, not a plugin** — it depends on `@setu-ts/common` only and registers no
6
+ capability. You wire the middleware into the application pipeline yourself.
7
+
8
+ ## Installation
9
+
10
+ ```typescript
11
+ import { errorHandler, notFound } from '@setu-ts/exceptions';
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```typescript
17
+ import { badRequest, errorHandler, HttpError, notFound } from '@setu-ts/exceptions';
18
+
19
+ app.middleware.add(errorHandler({ format: 'rfc9457' }), {
20
+ priority: 0,
21
+ name: 'error-handler',
22
+ });
23
+
24
+ app.router.get('/users/:id', async (ctx) => {
25
+ const user = await users.find(ctx.params.id);
26
+ if (!user) throw notFound('User not found');
27
+ return ctx.response.json(user);
28
+ });
29
+ ```
30
+
31
+ ## Options
32
+
33
+ `errorHandler(options?)` takes:
34
+
35
+ | Option | Type | Default | Description |
36
+ | -------------------- | --------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------ |
37
+ | `format` | `ErrorFormat \| ErrorHandlerFormatter` | `'default'` | Body shape, or a formatter function. |
38
+ | `includeStackTrace` | `boolean` | `false` | Add the stack to the body. Development only. |
39
+ | `maskInternalErrors` | `boolean` | `true` | Replace unhandled non-`HttpError` 5xx diagnostics with their status title before serving the response. |
40
+ | `logErrors` | `boolean` | `true` | Report handled errors to the resolved `ILogger`. |
41
+ | `respond` | `(error: HttpError, ctx: IRequestContext) => HandlerResult \| undefined \| Promise<HandlerResult \| undefined>` | — | Write an application-owned caught-error response; return or resolve `undefined` to use the configured formatter instead. |
42
+
43
+ `format` accepts `'default'`, `'rfc9457'`, the deprecated `'rfc7807'`, or a function. Passing a
44
+ formatter directly is equivalent to naming it — both paths select the same
45
+ `application/problem+json` content type for Problem Details bodies.
46
+
47
+ `respond` is called after error normalization, masking, and status resolution. It can return or
48
+ resolve an HTML (or another application-owned) result through `ctx.response`; the callback owns that
49
+ result's status, headers, and body. The log retains the unmasked diagnostic and records the status
50
+ that result serves. It applies only to errors caught by `errorHandler`, not responder-based
51
+ terminals such as an unmatched-path `404`.
52
+
53
+ The natural spelling of the hook omits the one statement that matters and serves `200` for every
54
+ branded error: `renderView(ctx, Component, props)` takes no status, and nothing else sets it. The
55
+ callback owns the status, so the example carries that line on its own:
56
+
57
+ ```tsx
58
+ import { errorHandler, statusTitle } from '@setu-ts/exceptions';
59
+ import type { HandlerResult, IRequestContext } from '@setu-ts/common';
60
+ import { renderView } from '@setu-ts/view-plugin';
61
+
62
+ interface ErrorPageProps {
63
+ readonly status: number;
64
+ readonly title: string;
65
+ }
66
+
67
+ const ErrorPage = (props: ErrorPageProps) => (
68
+ <main>
69
+ <h1>{props.status}</h1>
70
+ <p>{props.title}</p>
71
+ </main>
72
+ );
73
+
74
+ function wantsHtml(ctx: IRequestContext): boolean {
75
+ // Deliberately simple — the point here is that the callback owns the status,
76
+ // not content negotiation. This does NOT honour `q` values, so a client
77
+ // sending `text/html;q=0` (a refusal) still gets HTML; parse the media
78
+ // ranges if your callers send them.
79
+ return (ctx.request.headers.get('accept') ?? '').includes('text/html');
80
+ }
81
+
82
+ app.middleware.add(
83
+ errorHandler({
84
+ format: 'rfc9457',
85
+ async respond(error, ctx): Promise<HandlerResult | undefined> {
86
+ if (!wantsHtml(ctx)) return undefined; // API clients keep Problem Details
87
+ // The callback owns the result's status, and renderView takes none —
88
+ // without this line every branded error answers 200.
89
+ ctx.response.status(error.statusCode);
90
+ return await renderView(ctx, ErrorPage, {
91
+ status: error.statusCode,
92
+ title: statusTitle(error.statusCode),
93
+ });
94
+ },
95
+ }),
96
+ { priority: 0, name: 'error-handler' },
97
+ );
98
+ ```
99
+
100
+ What the hook does NOT cover is stated by the rule in
101
+ [docs/mvc.md](https://github.com/setu-ts/setu-ts/blob/main/docs/mvc.md#error-pages): every responder
102
+ terminal — an unmatched-path `404`, a guard's `401`, the rate limiter's `429` — is emitted outside
103
+ `errorHandler`, so each keeps its formatter's JSON no matter what this callback renders.
104
+
105
+ ## What it exports
106
+
107
+ - **`HttpError`** — the error type, carrying a status, a title, and optional validation details.
108
+ - **Factories** — `badRequest`, `unauthorized`, `forbidden`, `notFound`, `conflict`,
109
+ `tooManyRequests`, `internalServerError`, `notImplemented`, `serviceUnavailable`,
110
+ `validationError`, plus `statusTitle` / `STATUS_TITLES`.
111
+ - **Formatters** — `defaultFormatter` and `rfc9457Formatter` ([RFC 9457][rfc9457] Problem Details),
112
+ plus a deprecated `rfc7807Formatter`, selected by `selectFormatter`. A Problem Details body
113
+ carries `detail`, never `message`.
114
+ - **Middleware** — `errorHandler(options)`.
115
+
116
+ ## Problem Details and `about:blank`
117
+
118
+ The `'rfc9457'` format emits `type: "about:blank"` for any error whose only semantics are its status
119
+ code — which is every factory in this package except `validationError()`. [RFC 9457][rfc9457] §4.2
120
+ registers `about:blank` for exactly that case, because a URI minted from the status code identifies
121
+ nothing the `status` member does not already carry. Read `status` to distinguish errors.
122
+
123
+ `validationError()` is the exception: it defines an `errors` extension member, so it is a distinct
124
+ problem type identified by `https://setu-ts.dev/errors/validation` — the same URI
125
+ `@setu-ts/validation-plugin` uses.
126
+
127
+ ```json
128
+ {
129
+ "type": "about:blank",
130
+ "title": "Not Found",
131
+ "status": 404,
132
+ "detail": "User 42 does not exist",
133
+ "instance": "/users/42"
134
+ }
135
+ ```
136
+
137
+ > **`'rfc7807'` is deprecated.** RFC 7807 was obsoleted by RFC 9457 in July 2023. The `'rfc7807'`
138
+ > alias and `rfc7807Formatter` still emit the status-derived `type`
139
+ > (`https://setu-ts.dev/errors/404`) they always did — a deprecated symbol must not silently change
140
+ > behavior — so upgrading is a deliberate one-word edit. Removal is scheduled for v1.0.0.
141
+
142
+ ## Exports
143
+
144
+ | Export | Kind |
145
+ | ----------------------- | --------- |
146
+ | `badRequest` | function |
147
+ | `conflict` | function |
148
+ | `defaultFormatter` | function |
149
+ | `errorHandler` | function |
150
+ | `forbidden` | function |
151
+ | `internalServerError` | function |
152
+ | `notFound` | function |
153
+ | `notImplemented` | function |
154
+ | `rfc7807Formatter` | function |
155
+ | `rfc9457Formatter` | function |
156
+ | `selectFormatter` | function |
157
+ | `serviceUnavailable` | function |
158
+ | `statusTitle` | function |
159
+ | `tooManyRequests` | function |
160
+ | `unauthorized` | function |
161
+ | `validationError` | function |
162
+ | `HttpError` | class |
163
+ | `ERROR_TYPE_BASE` | const |
164
+ | `STATUS_TITLES` | const |
165
+ | `DefaultErrorBody` | interface |
166
+ | `ErrorHandlerOptions` | interface |
167
+ | `HttpErrorInit` | interface |
168
+ | `ProblemDetails` | interface |
169
+ | `ValidationError` | interface |
170
+ | `ErrorFormat` | type |
171
+ | `ErrorHandlerFormatter` | type |
172
+
173
+ Generated from the package barrel by `deno task docs:exports`; `deno task check:docs` fails when it
174
+ drifts.
175
+
176
+ ## Full API
177
+
178
+ Every export, its signature, and the exact response shapes are documented in
179
+ [PUBLIC_API.md](https://github.com/setu-ts/setu-ts/blob/main/PUBLIC_API.md#api-reference-setu-tsexceptions).
180
+
181
+ [rfc9457]: https://www.rfc-editor.org/rfc/rfc9457.html
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Exception factory functions.
3
+ *
4
+ * Each factory returns a {@linkcode HttpError} with a fixed `statusCode`,
5
+ * embodying **composition over inheritance** (AI_GUIDELINES §1.4,
6
+ * ARCHITECTURE.md §13). Throw one in a route handler or service and the
7
+ * error-handler middleware will format and send it with the right status.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { notFound, badRequest } from '@setu-ts/exceptions';
12
+ *
13
+ * function getUser(id: string) {
14
+ * if (!id) throw badRequest('id is required');
15
+ * const user = db.find(id);
16
+ * if (!user) throw notFound(`User ${id} not found`);
17
+ * return user;
18
+ * }
19
+ * ```
20
+ *
21
+ * @module
22
+ */
23
+ import { HttpError } from './http-error.ts';
24
+ import type { ValidationError } from './http-error.ts';
25
+ /**
26
+ * A human-readable title for a given HTTP status code. This is the single
27
+ * source of truth used by both the factory functions and the Problem Details
28
+ * formatters so the `title` field never drifts from the produced `statusCode`.
29
+ *
30
+ * Keys are every status the framework itself produces — the ones the factories
31
+ * below return AND the ones first-party code brands onto an error with
32
+ * `withHttpStatusHint` from `@setu-ts/common`, which reaches this table through
33
+ * the responder seam without passing a factory (`413` from the request-size and
34
+ * body-cap refusals, `415` from the form accessor, `501`/`503`/`504` from the
35
+ * database, secrets and resilience refusals). A branded status with no row here
36
+ * is served the generic `'Error'` title, so `test/status-title-coverage.test.ts`
37
+ * enumerates the branded statuses from source and fails when one is missing —
38
+ * `415` reached a client as `"title": "Error"` under `'rfc9457'` while the same
39
+ * error read `"message": "Unsupported Media Type"` under `'default'`, because
40
+ * every test asserted the brand on the error object and none read the served
41
+ * body.
42
+ *
43
+ * @since 0.1.0
44
+ */
45
+ export declare const STATUS_TITLES: Readonly<Record<number, string>>;
46
+ /**
47
+ * Resolves the human-readable title for a status code, falling back to a
48
+ * generic title for codes outside the well-known set.
49
+ *
50
+ * @param statusCode - The HTTP status code
51
+ * @returns A human-readable title
52
+ * @since 0.1.0
53
+ */
54
+ export declare function statusTitle(statusCode: number): string;
55
+ /**
56
+ * Creates a `400 Bad Request` error.
57
+ *
58
+ * @param message - Human-readable error message
59
+ * @param details - Optional structured details appended to the error body
60
+ * @returns A `400` {@linkcode HttpError}
61
+ * @since 0.1.0
62
+ */
63
+ export declare function badRequest(message: string, details?: Readonly<Record<string, unknown>>): HttpError;
64
+ /**
65
+ * Creates a `401 Unauthorized` error.
66
+ *
67
+ * @param message - Human-readable error message
68
+ * @returns A `401` {@linkcode HttpError}
69
+ * @since 0.1.0
70
+ */
71
+ export declare function unauthorized(message: string): HttpError;
72
+ /**
73
+ * Creates a `403 Forbidden` error.
74
+ *
75
+ * @param message - Human-readable error message
76
+ * @returns A `403` {@linkcode HttpError}
77
+ * @since 0.1.0
78
+ */
79
+ export declare function forbidden(message: string): HttpError;
80
+ /**
81
+ * Creates a `404 Not Found` error.
82
+ *
83
+ * @param message - Human-readable error message
84
+ * @returns A `404` {@linkcode HttpError}
85
+ * @since 0.1.0
86
+ */
87
+ export declare function notFound(message: string): HttpError;
88
+ /**
89
+ * Creates a `409 Conflict` error.
90
+ *
91
+ * @param message - Human-readable error message
92
+ * @returns A `409` {@linkcode HttpError}
93
+ * @since 0.1.0
94
+ */
95
+ export declare function conflict(message: string): HttpError;
96
+ /**
97
+ * Creates a `422 Unprocessable Entity` error wrapping a list of validation
98
+ * failures.
99
+ *
100
+ * The `errors` array is stored in the error's `details.errors` so it survives
101
+ * serialization into both the default and RFC 7807 error bodies.
102
+ *
103
+ * @param errors - The validation failures that caused this error
104
+ * @param message - Optional custom message (defaults to a count-based summary)
105
+ * @returns A `422` {@linkcode HttpError}
106
+ * @since 0.1.0
107
+ */
108
+ export declare function validationError(errors: readonly ValidationError[], message?: string): HttpError;
109
+ /**
110
+ * Creates a `429 Too Many Requests` error.
111
+ *
112
+ * @param message - Human-readable error message
113
+ * @param details - Optional structured details (e.g. `retryAfter`)
114
+ * @returns A `429` {@linkcode HttpError}
115
+ * @since 0.1.0
116
+ */
117
+ export declare function tooManyRequests(message: string, details?: Readonly<Record<string, unknown>>): HttpError;
118
+ /**
119
+ * Creates a `500 Internal Server Error` error.
120
+ *
121
+ * Pass the original error as `cause` so the root cause is preserved in the
122
+ * ES2022 error chain for logging and debugging.
123
+ *
124
+ * @param message - Human-readable error message
125
+ * @param cause - Optional underlying error forwarded to the cause chain
126
+ * @returns A `500` {@linkcode HttpError}
127
+ * @since 0.1.0
128
+ */
129
+ export declare function internalServerError(message: string, cause?: Error): HttpError;
130
+ /**
131
+ * Creates a `501 Not Implemented` error.
132
+ *
133
+ * @param message - Human-readable error message
134
+ * @returns A `501` {@linkcode HttpError}
135
+ * @since 0.1.0
136
+ */
137
+ export declare function notImplemented(message: string): HttpError;
138
+ /**
139
+ * Creates a `503 Service Unavailable` error.
140
+ *
141
+ * @param message - Human-readable error message
142
+ * @returns A `503` {@linkcode HttpError}
143
+ * @since 0.1.0
144
+ */
145
+ export declare function serviceUnavailable(message: string): HttpError;
146
+ //# sourceMappingURL=exceptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exceptions.d.ts","sourceRoot":"","sources":["../../src/errors/exceptions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAMvD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAe1D,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CAEtD;AAMD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC1C,SAAS,CAEX;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAEvD;AAED;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAEpD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAEnD;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAEnD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,eAAe,EAAE,EAClC,OAAO,CAAC,EAAE,MAAM,GACf,SAAS,CAGX;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAC1C,SAAS,CAEX;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,SAAS,CAE7E;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAEzD;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAE7D"}
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Exception factory functions.
3
+ *
4
+ * Each factory returns a {@linkcode HttpError} with a fixed `statusCode`,
5
+ * embodying **composition over inheritance** (AI_GUIDELINES §1.4,
6
+ * ARCHITECTURE.md §13). Throw one in a route handler or service and the
7
+ * error-handler middleware will format and send it with the right status.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { notFound, badRequest } from '@setu-ts/exceptions';
12
+ *
13
+ * function getUser(id: string) {
14
+ * if (!id) throw badRequest('id is required');
15
+ * const user = db.find(id);
16
+ * if (!user) throw notFound(`User ${id} not found`);
17
+ * return user;
18
+ * }
19
+ * ```
20
+ *
21
+ * @module
22
+ */
23
+ import { HttpError } from "./http-error.js";
24
+ // ---------------------------------------------------------------------------
25
+ // Status-code → human title (single source of truth, shared with formatters)
26
+ // ---------------------------------------------------------------------------
27
+ /**
28
+ * A human-readable title for a given HTTP status code. This is the single
29
+ * source of truth used by both the factory functions and the Problem Details
30
+ * formatters so the `title` field never drifts from the produced `statusCode`.
31
+ *
32
+ * Keys are every status the framework itself produces — the ones the factories
33
+ * below return AND the ones first-party code brands onto an error with
34
+ * `withHttpStatusHint` from `@setu-ts/common`, which reaches this table through
35
+ * the responder seam without passing a factory (`413` from the request-size and
36
+ * body-cap refusals, `415` from the form accessor, `501`/`503`/`504` from the
37
+ * database, secrets and resilience refusals). A branded status with no row here
38
+ * is served the generic `'Error'` title, so `test/status-title-coverage.test.ts`
39
+ * enumerates the branded statuses from source and fails when one is missing —
40
+ * `415` reached a client as `"title": "Error"` under `'rfc9457'` while the same
41
+ * error read `"message": "Unsupported Media Type"` under `'default'`, because
42
+ * every test asserted the brand on the error object and none read the served
43
+ * body.
44
+ *
45
+ * @since 0.1.0
46
+ */
47
+ export const STATUS_TITLES = {
48
+ 400: 'Bad Request',
49
+ 401: 'Unauthorized',
50
+ 403: 'Forbidden',
51
+ 404: 'Not Found',
52
+ 409: 'Conflict',
53
+ 413: 'Payload Too Large',
54
+ 415: 'Unsupported Media Type',
55
+ 422: 'Unprocessable Entity',
56
+ 429: 'Too Many Requests',
57
+ 500: 'Internal Server Error',
58
+ 501: 'Not Implemented',
59
+ 502: 'Bad Gateway',
60
+ 503: 'Service Unavailable',
61
+ 504: 'Gateway Timeout',
62
+ };
63
+ /**
64
+ * Resolves the human-readable title for a status code, falling back to a
65
+ * generic title for codes outside the well-known set.
66
+ *
67
+ * @param statusCode - The HTTP status code
68
+ * @returns A human-readable title
69
+ * @since 0.1.0
70
+ */
71
+ export function statusTitle(statusCode) {
72
+ return STATUS_TITLES[statusCode] ?? 'Error';
73
+ }
74
+ // ---------------------------------------------------------------------------
75
+ // Factories
76
+ // ---------------------------------------------------------------------------
77
+ /**
78
+ * Creates a `400 Bad Request` error.
79
+ *
80
+ * @param message - Human-readable error message
81
+ * @param details - Optional structured details appended to the error body
82
+ * @returns A `400` {@linkcode HttpError}
83
+ * @since 0.1.0
84
+ */
85
+ export function badRequest(message, details) {
86
+ return new HttpError(400, message, details);
87
+ }
88
+ /**
89
+ * Creates a `401 Unauthorized` error.
90
+ *
91
+ * @param message - Human-readable error message
92
+ * @returns A `401` {@linkcode HttpError}
93
+ * @since 0.1.0
94
+ */
95
+ export function unauthorized(message) {
96
+ return new HttpError(401, message);
97
+ }
98
+ /**
99
+ * Creates a `403 Forbidden` error.
100
+ *
101
+ * @param message - Human-readable error message
102
+ * @returns A `403` {@linkcode HttpError}
103
+ * @since 0.1.0
104
+ */
105
+ export function forbidden(message) {
106
+ return new HttpError(403, message);
107
+ }
108
+ /**
109
+ * Creates a `404 Not Found` error.
110
+ *
111
+ * @param message - Human-readable error message
112
+ * @returns A `404` {@linkcode HttpError}
113
+ * @since 0.1.0
114
+ */
115
+ export function notFound(message) {
116
+ return new HttpError(404, message);
117
+ }
118
+ /**
119
+ * Creates a `409 Conflict` error.
120
+ *
121
+ * @param message - Human-readable error message
122
+ * @returns A `409` {@linkcode HttpError}
123
+ * @since 0.1.0
124
+ */
125
+ export function conflict(message) {
126
+ return new HttpError(409, message);
127
+ }
128
+ /**
129
+ * Creates a `422 Unprocessable Entity` error wrapping a list of validation
130
+ * failures.
131
+ *
132
+ * The `errors` array is stored in the error's `details.errors` so it survives
133
+ * serialization into both the default and RFC 7807 error bodies.
134
+ *
135
+ * @param errors - The validation failures that caused this error
136
+ * @param message - Optional custom message (defaults to a count-based summary)
137
+ * @returns A `422` {@linkcode HttpError}
138
+ * @since 0.1.0
139
+ */
140
+ export function validationError(errors, message) {
141
+ const summary = message ?? `Validation failed with ${errors.length} error(s).`;
142
+ return new HttpError(422, summary, { errors });
143
+ }
144
+ /**
145
+ * Creates a `429 Too Many Requests` error.
146
+ *
147
+ * @param message - Human-readable error message
148
+ * @param details - Optional structured details (e.g. `retryAfter`)
149
+ * @returns A `429` {@linkcode HttpError}
150
+ * @since 0.1.0
151
+ */
152
+ export function tooManyRequests(message, details) {
153
+ return new HttpError(429, message, details);
154
+ }
155
+ /**
156
+ * Creates a `500 Internal Server Error` error.
157
+ *
158
+ * Pass the original error as `cause` so the root cause is preserved in the
159
+ * ES2022 error chain for logging and debugging.
160
+ *
161
+ * @param message - Human-readable error message
162
+ * @param cause - Optional underlying error forwarded to the cause chain
163
+ * @returns A `500` {@linkcode HttpError}
164
+ * @since 0.1.0
165
+ */
166
+ export function internalServerError(message, cause) {
167
+ return new HttpError(500, message, undefined, cause);
168
+ }
169
+ /**
170
+ * Creates a `501 Not Implemented` error.
171
+ *
172
+ * @param message - Human-readable error message
173
+ * @returns A `501` {@linkcode HttpError}
174
+ * @since 0.1.0
175
+ */
176
+ export function notImplemented(message) {
177
+ return new HttpError(501, message);
178
+ }
179
+ /**
180
+ * Creates a `503 Service Unavailable` error.
181
+ *
182
+ * @param message - Human-readable error message
183
+ * @returns A `503` {@linkcode HttpError}
184
+ * @since 0.1.0
185
+ */
186
+ export function serviceUnavailable(message) {
187
+ return new HttpError(503, message);
188
+ }
189
+ //# sourceMappingURL=exceptions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exceptions.js","sourceRoot":"","sources":["../../src/errors/exceptions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,8EAA8E;AAC9E,6EAA6E;AAC7E,8EAA8E;AAE9E;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,CAAC,MAAM,aAAa,GAAqC;IAC7D,GAAG,EAAE,aAAa;IAClB,GAAG,EAAE,cAAc;IACnB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,WAAW;IAChB,GAAG,EAAE,UAAU;IACf,GAAG,EAAE,mBAAmB;IACxB,GAAG,EAAE,wBAAwB;IAC7B,GAAG,EAAE,sBAAsB;IAC3B,GAAG,EAAE,mBAAmB;IACxB,GAAG,EAAE,uBAAuB;IAC5B,GAAG,EAAE,iBAAiB;IACtB,GAAG,EAAE,aAAa;IAClB,GAAG,EAAE,qBAAqB;IAC1B,GAAG,EAAE,iBAAiB;CACvB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,WAAW,CAAC,UAAkB;IAC5C,OAAO,aAAa,CAAC,UAAU,CAAC,IAAI,OAAO,CAAC;AAC9C,CAAC;AAED,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E;;;;;;;GAOG;AACH,MAAM,UAAU,UAAU,CACxB,OAAe,EACf,OAA2C;IAE3C,OAAO,IAAI,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,OAAO,IAAI,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,SAAS,CAAC,OAAe;IACvC,OAAO,IAAI,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,OAAO,IAAI,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,OAAe;IACtC,OAAO,IAAI,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAkC,EAClC,OAAgB;IAEhB,MAAM,OAAO,GAAG,OAAO,IAAI,0BAA0B,MAAM,CAAC,MAAM,YAAY,CAAC;IAC/E,OAAO,IAAI,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AACjD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,OAAe,EACf,OAA2C;IAE3C,OAAO,IAAI,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAC9C,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAe,EAAE,KAAa;IAChE,OAAO,IAAI,SAAS,CAAC,GAAG,EAAE,OAAO,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;AACvD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,OAAO,IAAI,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,OAAO,IAAI,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC"}
@@ -0,0 +1,111 @@
1
+ /**
2
+ * `HttpError` — the framework's single concrete HTTP error type.
3
+ *
4
+ * Rather than a deep class hierarchy (`BadRequestError extends HttpError`,
5
+ * `NotFoundError extends HttpError`, …), the framework uses **composition via
6
+ * factory functions** (AI_GUIDELINES §1.4, ARCHITECTURE.md §13). Every HTTP
7
+ * error is an instance of this one class with a `statusCode` set at
8
+ * construction time by a factory. This keeps the hierarchy flat, makes errors
9
+ * trivially serializable, and allows runtime composition of properties.
10
+ *
11
+ * `cause` is forwarded to the ES2022 `Error` cause chain — pass the original
12
+ * error so callers can inspect the root cause without losing the stack.
13
+ *
14
+ * @module
15
+ */
16
+ /**
17
+ * A single validation failure carried by a `422` error.
18
+ *
19
+ * Mirrors the shape used by `@setu-ts/validation-plugin` so error
20
+ * payloads are consistent across the validation and exceptions packages.
21
+ *
22
+ * @since 0.1.0
23
+ */
24
+ export interface ValidationError {
25
+ /** Dot-path of the offending field (e.g. `"address.zip"`). */
26
+ readonly field: string;
27
+ /** Human-readable description of the failure. */
28
+ readonly message: string;
29
+ /** Optional machine-readable failure code. */
30
+ readonly code?: string;
31
+ }
32
+ /**
33
+ * Options accepted by the {@linkcode HttpError} constructor.
34
+ *
35
+ * Extracted as a type so factory functions can accept a consistent object.
36
+ *
37
+ * @since 0.1.0
38
+ */
39
+ export interface HttpErrorInit {
40
+ /** HTTP status code (e.g. `404`). */
41
+ readonly statusCode: number;
42
+ /** Human-readable error message. */
43
+ readonly message: string;
44
+ /** Optional structured details attached to the error body. */
45
+ readonly details?: Readonly<Record<string, unknown>>;
46
+ /** Optional underlying cause (forwarded to the ES2022 `Error` cause chain). */
47
+ readonly cause?: Error;
48
+ }
49
+ /**
50
+ * The framework's HTTP error type.
51
+ *
52
+ * Throw an instance directly, or — preferably — construct one via a factory
53
+ * function from {@linkcode ./exceptions.ts} so the status code is correct by
54
+ * construction:
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * import { notFound } from '@setu-ts/exceptions';
59
+ *
60
+ * throw notFound(`User ${id} not found`);
61
+ * ```
62
+ *
63
+ * The error handler middleware inspects the `statusCode` to set the response
64
+ * status, and serializes `message` and `details` into the error body.
65
+ *
66
+ * @since 0.1.0
67
+ */
68
+ export declare class HttpError extends Error {
69
+ /** The HTTP status code this error maps to. */
70
+ readonly statusCode: number;
71
+ /**
72
+ * Structured details appended to the error body. Omitted entirely when not
73
+ * supplied (never `undefined`) so serialization stays clean.
74
+ *
75
+ * `declare` suppresses the ES2022 class-field initialization that would
76
+ * otherwise set this to `undefined` on every instance — it only exists on
77
+ * the object when actually assigned by the constructor.
78
+ */
79
+ readonly details?: Readonly<Record<string, unknown>>;
80
+ /**
81
+ * Creates a new `HttpError`.
82
+ *
83
+ * Prefer the factory functions in `exceptions.ts` over calling this
84
+ * constructor directly — they guarantee a correct status code.
85
+ *
86
+ * @param statusCode - HTTP status code (e.g. `404`)
87
+ * @param message - Human-readable error message
88
+ * @param details - Optional structured details; omitted when absent
89
+ * @param cause - Optional underlying error; forwarded to the ES2022 cause chain
90
+ *
91
+ * @example
92
+ * ```typescript
93
+ * const err = new HttpError(404, 'User not found');
94
+ * console.log(err.statusCode); // 404
95
+ * ```
96
+ * @since 0.1.0
97
+ */
98
+ constructor(statusCode: number, message: string, details?: Readonly<Record<string, unknown>>, cause?: Error);
99
+ /**
100
+ * Creates an `HttpError` from an {@linkcode HttpErrorInit} object.
101
+ *
102
+ * Convenience overload used by factory functions so they can branch on
103
+ * optional `details`/`cause` once.
104
+ *
105
+ * @param init - The error options
106
+ * @returns A new `HttpError`
107
+ * @since 0.1.0
108
+ */
109
+ static from(init: HttpErrorInit): HttpError;
110
+ }
111
+ //# sourceMappingURL=http-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-error.d.ts","sourceRoot":"","sources":["../../src/errors/http-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,qCAAqC;IACrC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,oCAAoC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8DAA8D;IAC9D,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACrD,+EAA+E;IAC/E,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,SAAU,SAAQ,KAAK;IAClC,+CAA+C;IAC/C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B;;;;;;;OAOG;IACH,SAAiB,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAE7D;;;;;;;;;;;;;;;;;OAiBG;gBAED,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAC3C,KAAK,CAAC,EAAE,KAAK;IAaf;;;;;;;;;OASG;IACH,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS;CAG5C"}