@setu-ts/exceptions 0.0.0-stage → 0.10.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 (49) hide show
  1. package/AGENT_CARD.md +50 -0
  2. package/LICENSE +21 -0
  3. package/README.md +188 -2
  4. package/dist/errors/exceptions.d.ts +146 -0
  5. package/dist/errors/exceptions.d.ts.map +1 -0
  6. package/dist/errors/exceptions.js +189 -0
  7. package/dist/errors/exceptions.js.map +1 -0
  8. package/dist/errors/http-error.d.ts +111 -0
  9. package/dist/errors/http-error.d.ts.map +1 -0
  10. package/dist/errors/http-error.js +81 -0
  11. package/dist/errors/http-error.js.map +1 -0
  12. package/dist/formatters/error-formatter.d.ts +81 -0
  13. package/dist/formatters/error-formatter.d.ts.map +1 -0
  14. package/dist/formatters/error-formatter.js +58 -0
  15. package/dist/formatters/error-formatter.js.map +1 -0
  16. package/dist/formatters/problem-details.d.ts +118 -0
  17. package/dist/formatters/problem-details.d.ts.map +1 -0
  18. package/dist/formatters/problem-details.js +108 -0
  19. package/dist/formatters/problem-details.js.map +1 -0
  20. package/dist/formatters/rfc7807-formatter.d.ts +20 -0
  21. package/dist/formatters/rfc7807-formatter.d.ts.map +1 -0
  22. package/dist/formatters/rfc7807-formatter.js +27 -0
  23. package/dist/formatters/rfc7807-formatter.js.map +1 -0
  24. package/dist/formatters/rfc9457-formatter.d.ts +29 -0
  25. package/dist/formatters/rfc9457-formatter.d.ts.map +1 -0
  26. package/dist/formatters/rfc9457-formatter.js +43 -0
  27. package/dist/formatters/rfc9457-formatter.js.map +1 -0
  28. package/dist/index.d.ts +33 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +33 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/middleware/error-handler.d.ts +111 -0
  33. package/dist/middleware/error-handler.d.ts.map +1 -0
  34. package/dist/middleware/error-handler.js +221 -0
  35. package/dist/middleware/error-handler.js.map +1 -0
  36. package/dist/middleware/error-responder-impl.d.ts +57 -0
  37. package/dist/middleware/error-responder-impl.d.ts.map +1 -0
  38. package/dist/middleware/error-responder-impl.js +158 -0
  39. package/dist/middleware/error-responder-impl.js.map +1 -0
  40. package/package.json +53 -3
  41. package/src/errors/exceptions.ts +209 -0
  42. package/src/errors/http-error.ts +132 -0
  43. package/src/formatters/error-formatter.ts +127 -0
  44. package/src/formatters/problem-details.ts +177 -0
  45. package/src/formatters/rfc7807-formatter.ts +48 -0
  46. package/src/formatters/rfc9457-formatter.ts +69 -0
  47. package/src/index.ts +57 -0
  48. package/src/middleware/error-handler.ts +357 -0
  49. package/src/middleware/error-responder-impl.ts +192 -0
package/AGENT_CARD.md ADDED
@@ -0,0 +1,50 @@
1
+ # @setu-ts/exceptions — HTTP errors and error responses
2
+
3
+ **Add it:** REST scaffolds install this package; there is no `setu add` entry or capability. In
4
+ `setu.config.ts`, register `errorHandler` on the global middleware pipeline.
5
+
6
+ ## Format thrown errors consistently
7
+
8
+ ```ts
9
+ import { errorHandler } from "@setu-ts/exceptions";
10
+
11
+ app.middleware.add(errorHandler({ format: "rfc9457" }), {
12
+ priority: 0,
13
+ name: "error-handler",
14
+ });
15
+ ```
16
+
17
+ ## Refuse a request with an HTTP error
18
+
19
+ ```ts
20
+ import type { IRouterApi } from "@setu-ts/common";
21
+ import { notFound } from "@setu-ts/exceptions";
22
+
23
+ export function registerUserRoutes(router: IRouterApi): void {
24
+ router.get("/users/:id", {
25
+ handler: (ctx) => {
26
+ throw notFound(`User ${ctx.params.id} not found`);
27
+ },
28
+ });
29
+ }
30
+ ```
31
+
32
+ ## Behaviour the types do not show
33
+
34
+ - Priority `0` makes the handler wrap downstream middleware and routes; it catches their throws.
35
+ - `format` defaults to `"default"`; `"rfc9457"` sends `application/problem+json`.
36
+ - `maskInternalErrors` defaults to `true`: unexpected 5xx messages become the status title;
37
+ deliberately thrown `HttpError` messages are preserved.
38
+ - `includeStackTrace` defaults to `false`; masked errors never expose a stack even when enabled.
39
+ - `logErrors` defaults to `true`: warn below 500, error otherwise. Without a logger, only 5xx
40
+ reaches the runtime error sink.
41
+ - `respond` sees the normalized, masked error and resolved status. A returned response owns its
42
+ status, headers and body; `undefined` falls through to the configured formatter.
43
+
44
+ ## Other options
45
+
46
+ - `badRequest`, `unauthorized`, `forbidden`, `conflict`, `tooManyRequests` and `serviceUnavailable`
47
+ create other HTTP refusals.
48
+ - `validationError` stores field failures in `details.errors`; `internalServerError` accepts a
49
+ cause.
50
+ - `format` also accepts a formatter function or the deprecated `"rfc7807"` arm.
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,3 +1,189 @@
1
- # Temporary Holding Version
1
+ # @setu-ts/exceptions
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ Caught errors produce one diagnostic: `warn` for a served status below 500, `error` otherwise. The
34
+ request logger records completion rather than a second failure diagnostic. With no registered
35
+ logger, handled 5xx goes to `IRuntimeServices.reportError`, the runtime's last-resort JSON stderr
36
+ sink (Workers uses its platform log channel); handled 4xx stays silent. `logErrors: false` disables
37
+ both paths. The responder's optional, pure `statusFor(error)` lets request logging predict this
38
+ middleware's status without reporting a clamp twice. A custom `respond` hook's different status is
39
+ not reflected in request completion logs.
40
+
41
+ `errorHandler(options?)` takes:
42
+
43
+ | Option | Type | Default | Description |
44
+ | -------------------- | --------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------ |
45
+ | `format` | `ErrorFormat \| ErrorHandlerFormatter` | `'default'` | Body shape, or a formatter function. |
46
+ | `includeStackTrace` | `boolean` | `false` | Add the stack to the body. Development only. |
47
+ | `maskInternalErrors` | `boolean` | `true` | Replace unhandled non-`HttpError` 5xx diagnostics with their status title before serving the response. |
48
+ | `logErrors` | `boolean` | `true` | Report handled errors to the resolved `ILogger`. |
49
+ | `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. |
50
+
51
+ `format` accepts `'default'`, `'rfc9457'`, the deprecated `'rfc7807'`, or a function. Passing a
52
+ formatter directly is equivalent to naming it — both paths select the same
53
+ `application/problem+json` content type for Problem Details bodies.
54
+
55
+ `respond` is called after error normalization, masking, and status resolution. It can return or
56
+ resolve an HTML (or another application-owned) result through `ctx.response`; the callback owns that
57
+ result's status, headers, and body. The log retains the unmasked diagnostic and records the status
58
+ that result serves. It applies only to errors caught by `errorHandler`, not responder-based
59
+ terminals such as an unmatched-path `404`.
60
+
61
+ The natural spelling of the hook omits the one statement that matters and serves `200` for every
62
+ branded error: `renderView(ctx, Component, props)` takes no status, and nothing else sets it. The
63
+ callback owns the status, so the example carries that line on its own:
64
+
65
+ ```tsx
66
+ import { errorHandler, statusTitle } from '@setu-ts/exceptions';
67
+ import type { HandlerResult, IRequestContext } from '@setu-ts/common';
68
+ import { renderView } from '@setu-ts/view-plugin';
69
+
70
+ interface ErrorPageProps {
71
+ readonly status: number;
72
+ readonly title: string;
73
+ }
74
+
75
+ const ErrorPage = (props: ErrorPageProps) => (
76
+ <main>
77
+ <h1>{props.status}</h1>
78
+ <p>{props.title}</p>
79
+ </main>
80
+ );
81
+
82
+ function wantsHtml(ctx: IRequestContext): boolean {
83
+ // Deliberately simple — the point here is that the callback owns the status,
84
+ // not content negotiation. This does NOT honour `q` values, so a client
85
+ // sending `text/html;q=0` (a refusal) still gets HTML; parse the media
86
+ // ranges if your callers send them.
87
+ return (ctx.request.headers.get('accept') ?? '').includes('text/html');
88
+ }
89
+
90
+ app.middleware.add(
91
+ errorHandler({
92
+ format: 'rfc9457',
93
+ async respond(error, ctx): Promise<HandlerResult | undefined> {
94
+ if (!wantsHtml(ctx)) return undefined; // API clients keep Problem Details
95
+ // The callback owns the result's status, and renderView takes none —
96
+ // without this line every branded error answers 200.
97
+ ctx.response.status(error.statusCode);
98
+ return await renderView(ctx, ErrorPage, {
99
+ status: error.statusCode,
100
+ title: statusTitle(error.statusCode),
101
+ });
102
+ },
103
+ }),
104
+ { priority: 0, name: 'error-handler' },
105
+ );
106
+ ```
107
+
108
+ What the hook does NOT cover is stated by the rule in
109
+ [docs/mvc.md](https://github.com/setu-ts/setu-ts/blob/main/docs/mvc.md#error-pages): every responder
110
+ terminal — an unmatched-path `404`, a guard's `401`, the rate limiter's `429` — is emitted outside
111
+ `errorHandler`, so each keeps its formatter's JSON no matter what this callback renders.
112
+
113
+ ## What it exports
114
+
115
+ - **`HttpError`** — the error type, carrying a status, a title, and optional validation details.
116
+ - **Factories** — `badRequest`, `unauthorized`, `forbidden`, `notFound`, `conflict`,
117
+ `tooManyRequests`, `internalServerError`, `notImplemented`, `serviceUnavailable`,
118
+ `validationError`, plus `statusTitle` / `STATUS_TITLES`.
119
+ - **Formatters** — `defaultFormatter` and `rfc9457Formatter` ([RFC 9457][rfc9457] Problem Details),
120
+ plus a deprecated `rfc7807Formatter`, selected by `selectFormatter`. A Problem Details body
121
+ carries `detail`, never `message`.
122
+ - **Middleware** — `errorHandler(options)`.
123
+
124
+ ## Problem Details and `about:blank`
125
+
126
+ The `'rfc9457'` format emits `type: "about:blank"` for any error whose only semantics are its status
127
+ code — which is every factory in this package except `validationError()`. [RFC 9457][rfc9457] §4.2
128
+ registers `about:blank` for exactly that case, because a URI minted from the status code identifies
129
+ nothing the `status` member does not already carry. Read `status` to distinguish errors.
130
+
131
+ `validationError()` is the exception: it defines an `errors` extension member, so it is a distinct
132
+ problem type identified by `https://setu-ts.dev/errors/validation` — the same URI
133
+ `@setu-ts/validation-plugin` uses.
134
+
135
+ ```json
136
+ {
137
+ "type": "about:blank",
138
+ "title": "Not Found",
139
+ "status": 404,
140
+ "detail": "User 42 does not exist",
141
+ "instance": "/users/42"
142
+ }
143
+ ```
144
+
145
+ > **`'rfc7807'` is deprecated.** RFC 7807 was obsoleted by RFC 9457 in July 2023. The `'rfc7807'`
146
+ > alias and `rfc7807Formatter` still emit the status-derived `type`
147
+ > (`https://setu-ts.dev/errors/404`) they always did — a deprecated symbol must not silently change
148
+ > behavior — so upgrading is a deliberate one-word edit. Removal is scheduled for v1.0.0.
149
+
150
+ ## Exports
151
+
152
+ | Export | Kind |
153
+ | ----------------------- | --------- |
154
+ | `badRequest` | function |
155
+ | `conflict` | function |
156
+ | `defaultFormatter` | function |
157
+ | `errorHandler` | function |
158
+ | `forbidden` | function |
159
+ | `internalServerError` | function |
160
+ | `notFound` | function |
161
+ | `notImplemented` | function |
162
+ | `rfc7807Formatter` | function |
163
+ | `rfc9457Formatter` | function |
164
+ | `selectFormatter` | function |
165
+ | `serviceUnavailable` | function |
166
+ | `statusTitle` | function |
167
+ | `tooManyRequests` | function |
168
+ | `unauthorized` | function |
169
+ | `validationError` | function |
170
+ | `HttpError` | class |
171
+ | `ERROR_TYPE_BASE` | const |
172
+ | `STATUS_TITLES` | const |
173
+ | `DefaultErrorBody` | interface |
174
+ | `ErrorHandlerOptions` | interface |
175
+ | `HttpErrorInit` | interface |
176
+ | `ProblemDetails` | interface |
177
+ | `ValidationError` | interface |
178
+ | `ErrorFormat` | type |
179
+ | `ErrorHandlerFormatter` | type |
180
+
181
+ Generated from the package barrel by `pnpm run docs:exports`; `pnpm run check:docs` fails when it
182
+ drifts.
183
+
184
+ ## Full API
185
+
186
+ Every export, its signature, and the exact response shapes are documented in
187
+ [PUBLIC_API.md](https://github.com/setu-ts/setu-ts/blob/main/PUBLIC_API.md#api-reference-setu-tsexceptions).
188
+
189
+ [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,CAAC,MAAM,EAAE,SAAS,eAAe,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAG/F;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,CAAC,MAAkC,EAAE,OAAgB;IAClF,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"}