@zerotal/arch 1.7.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.
- package/CHANGELOG.md +31 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/api-surface.md +283 -0
- package/docs/about.md +676 -0
- package/docs/admin/actions.md +183 -0
- package/docs/admin/auth.md +89 -0
- package/docs/admin/dashboard.md +155 -0
- package/docs/admin/extending-ui.md +229 -0
- package/docs/admin/extending.md +205 -0
- package/docs/admin/forms.md +155 -0
- package/docs/admin/index.md +144 -0
- package/docs/admin/operations.md +224 -0
- package/docs/admin/references.md +174 -0
- package/docs/admin/resources.md +132 -0
- package/docs/admin/structure.md +158 -0
- package/docs/admin/tables.md +185 -0
- package/docs/admin/testing.md +138 -0
- package/docs/ai.md +373 -0
- package/docs/application.md +435 -0
- package/docs/arch.md +263 -0
- package/docs/assets.md +755 -0
- package/docs/audit.md +367 -0
- package/docs/authentication.md +1000 -0
- package/docs/authorization.md +529 -0
- package/docs/broadcasting/channels.md +170 -0
- package/docs/broadcasting/client.md +108 -0
- package/docs/broadcasting/events.md +190 -0
- package/docs/broadcasting/index.md +163 -0
- package/docs/broadcasting/references.md +84 -0
- package/docs/broadcasting/testing.md +132 -0
- package/docs/cache.md +387 -0
- package/docs/carbon.md +830 -0
- package/docs/changelog.md +465 -0
- package/docs/client/auth.md +113 -0
- package/docs/client/errors.md +139 -0
- package/docs/client/files.md +118 -0
- package/docs/client/index.md +128 -0
- package/docs/client/references.md +58 -0
- package/docs/client/requests.md +131 -0
- package/docs/client/resilience.md +141 -0
- package/docs/client/testing.md +146 -0
- package/docs/commands.md +450 -0
- package/docs/components.md +2427 -0
- package/docs/config-system.md +258 -0
- package/docs/container.md +596 -0
- package/docs/context.md +579 -0
- package/docs/contributing.md +164 -0
- package/docs/controllers.md +354 -0
- package/docs/conventions.md +466 -0
- package/docs/cookies.md +101 -0
- package/docs/csrf.md +206 -0
- package/docs/database.md +476 -0
- package/docs/deployment.md +434 -0
- package/docs/devtools.md +1013 -0
- package/docs/email-verification.md +320 -0
- package/docs/encryption.md +380 -0
- package/docs/errors.md +317 -0
- package/docs/events.md +420 -0
- package/docs/flow/components.md +846 -0
- package/docs/flow/decorators.md +458 -0
- package/docs/flow/events.md +420 -0
- package/docs/flow/forms.md +551 -0
- package/docs/flow/index.md +568 -0
- package/docs/flow/layouts.md +793 -0
- package/docs/flow/lifecycle.md +336 -0
- package/docs/flow/pagination.md +402 -0
- package/docs/flow/performance.md +262 -0
- package/docs/flow/references.md +317 -0
- package/docs/flow/routing.md +550 -0
- package/docs/flow/testing.md +447 -0
- package/docs/getting-started.md +286 -0
- package/docs/health.md +269 -0
- package/docs/helpers.md +423 -0
- package/docs/i18n.md +414 -0
- package/docs/index.md +65 -0
- package/docs/inertia/build.md +95 -0
- package/docs/inertia/devtools.md +197 -0
- package/docs/inertia/index.md +267 -0
- package/docs/inertia/middleware.md +81 -0
- package/docs/inertia/props.md +507 -0
- package/docs/inertia/references.md +144 -0
- package/docs/inertia/rendering.md +189 -0
- package/docs/inertia/ssr.md +102 -0
- package/docs/inspirations.md +257 -0
- package/docs/lifecycle.md +331 -0
- package/docs/lock.md +454 -0
- package/docs/logger.md +517 -0
- package/docs/media.md +582 -0
- package/docs/middleware.md +509 -0
- package/docs/migrations.md +529 -0
- package/docs/monitor.md +377 -0
- package/docs/notifications.md +848 -0
- package/docs/orm/casts.md +365 -0
- package/docs/orm/factories.md +231 -0
- package/docs/orm/index.md +663 -0
- package/docs/orm/lifecycle.md +421 -0
- package/docs/orm/queries.md +692 -0
- package/docs/orm/relationships.md +440 -0
- package/docs/orm/serialization.md +251 -0
- package/docs/package-development.md +536 -0
- package/docs/pagination.md +340 -0
- package/docs/password-reset.md +335 -0
- package/docs/providers.md +447 -0
- package/docs/query-builder.md +465 -0
- package/docs/queue.md +549 -0
- package/docs/rate-limiting.md +253 -0
- package/docs/responses.md +336 -0
- package/docs/roles-and-2fa.md +418 -0
- package/docs/routing.md +1008 -0
- package/docs/scaffolding.md +335 -0
- package/docs/scheduler.md +533 -0
- package/docs/seeding.md +229 -0
- package/docs/session.md +451 -0
- package/docs/social.md +524 -0
- package/docs/storage.md +622 -0
- package/docs/structure.md +230 -0
- package/docs/support-policy.md +114 -0
- package/docs/telemetry.md +505 -0
- package/docs/tenancy.md +608 -0
- package/docs/testing/browser.md +153 -0
- package/docs/testing/console.md +130 -0
- package/docs/testing/database.md +238 -0
- package/docs/testing/flow-browser.md +216 -0
- package/docs/testing/http.md +441 -0
- package/docs/testing/index.md +240 -0
- package/docs/testing/mocking.md +410 -0
- package/docs/upgrade.md +192 -0
- package/docs/validator.md +549 -0
- package/docs/view.md +453 -0
- package/package.json +60 -0
- package/src/bin/mcp.ts +73 -0
- package/src/config.ts +63 -0
- package/src/errors.ts +27 -0
- package/src/index.ts +56 -0
- package/src/install/ArchInstallCommand.ts +189 -0
- package/src/install/detect.ts +98 -0
- package/src/install/guidelines.ts +290 -0
- package/src/install/markers.ts +77 -0
- package/src/install/mcpConfig.ts +81 -0
- package/src/mcp/index.ts +27 -0
- package/src/mcp/jsonrpc.ts +133 -0
- package/src/mcp/server.ts +307 -0
- package/src/mcp/stdio.ts +111 -0
- package/src/mcp/types.ts +146 -0
- package/src/probe/ArchProbeCommand.ts +48 -0
- package/src/probe/sentinel.ts +13 -0
- package/src/probe/topics.ts +291 -0
- package/src/provider/ArchProvider.ts +101 -0
- package/src/tools/_probe.ts +164 -0
- package/src/tools/apiSurface.ts +241 -0
- package/src/tools/appInfo.ts +88 -0
- package/src/tools/baselines.ts +210 -0
- package/src/tools/context.ts +37 -0
- package/src/tools/doctor.ts +93 -0
- package/src/tools/index.ts +50 -0
- package/src/tools/logs.ts +265 -0
- package/src/tools/routes.ts +133 -0
- package/src/tools/schema.ts +119 -0
- package/src/tools/searchDocs.ts +345 -0
package/docs/errors.md
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Error Handling
|
|
3
|
+
description: Turn any unhandled exception into the right HTTP response for the client and environment.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Error Handling
|
|
7
|
+
|
|
8
|
+
Zerotal centralises error handling through an `ExceptionHandler` class: every
|
|
9
|
+
unhandled exception flows through it, and the framework picks the response format
|
|
10
|
+
automatically based on the client type (browser vs. API) and the environment
|
|
11
|
+
(dev vs. production).
|
|
12
|
+
|
|
13
|
+
## Default behaviour
|
|
14
|
+
|
|
15
|
+
In development, an unhandled error shows a styled stack-trace page in the browser
|
|
16
|
+
and a JSON body for API clients. In production, the browser sees a plain
|
|
17
|
+
"Internal Server Error" page and API clients get `{ message: 'Internal Server Error' }`
|
|
18
|
+
— no internals are leaked.
|
|
19
|
+
|
|
20
|
+
HTTP errors (`NotFoundError`, `ForbiddenError`, etc.) always produce a
|
|
21
|
+
status-code-appropriate response regardless of environment.
|
|
22
|
+
|
|
23
|
+
> **Note** — The client type is decided by the `Accept` header. A request whose
|
|
24
|
+
> `Accept` starts with `application/json` gets JSON; anything that includes
|
|
25
|
+
> `text/html` (regular browsers and Inertia XHR) gets the HTML page.
|
|
26
|
+
|
|
27
|
+
## Built-in HTTP errors
|
|
28
|
+
|
|
29
|
+
Throw any of these from a controller, middleware, or model and the framework
|
|
30
|
+
turns it into the correct HTTP response automatically. Each carries a default
|
|
31
|
+
message and a stable `code`:
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
// in a controller
|
|
35
|
+
import { NotFoundError, ForbiddenError, UnauthorizedError } from "zerotal";
|
|
36
|
+
|
|
37
|
+
// 404
|
|
38
|
+
throw new NotFoundError();
|
|
39
|
+
throw new NotFoundError("Article not found");
|
|
40
|
+
|
|
41
|
+
// 403
|
|
42
|
+
throw new ForbiddenError();
|
|
43
|
+
throw new ForbiddenError("You cannot edit this post");
|
|
44
|
+
|
|
45
|
+
// 401
|
|
46
|
+
throw new UnauthorizedError();
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For any other status, throw `HttpError` directly:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// in a controller
|
|
53
|
+
import { HttpError } from "zerotal";
|
|
54
|
+
|
|
55
|
+
throw new HttpError("Payment required", 402, "E_PAYMENT_REQUIRED");
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The constructor is `new HttpError(message, status, code?, headers?)` — when you
|
|
59
|
+
omit `code` it defaults to `E_HTTP_<status>`.
|
|
60
|
+
|
|
61
|
+
The full set exported from `zerotal`:
|
|
62
|
+
|
|
63
|
+
| Class | Status | Default `code` |
|
|
64
|
+
| -------------------------- | ------ | ------------------------ |
|
|
65
|
+
| `BadRequestError` | 400 | `E_BAD_REQUEST` |
|
|
66
|
+
| `UnauthorizedError` | 401 | `E_UNAUTHORIZED` |
|
|
67
|
+
| `ForbiddenError` | 403 | `E_FORBIDDEN` |
|
|
68
|
+
| `NotFoundError` | 404 | `E_NOT_FOUND` |
|
|
69
|
+
| `MethodNotAllowedError` | 405 | `E_METHOD_NOT_ALLOWED` |
|
|
70
|
+
| `ConflictError` | 409 | `E_CONFLICT` |
|
|
71
|
+
| `GoneError` | 410 | `E_GONE` |
|
|
72
|
+
| `UnprocessableEntityError` | 422 | `E_UNPROCESSABLE_ENTITY` |
|
|
73
|
+
| `TooManyRequestsError` | 429 | `E_TOO_MANY_REQUESTS` |
|
|
74
|
+
| `ServiceUnavailableError` | 503 | `E_SERVICE_UNAVAILABLE` |
|
|
75
|
+
|
|
76
|
+
> **Tip** — `MethodNotAllowedError(allowed)`, `TooManyRequestsError(retryAfter)`,
|
|
77
|
+
> and `ServiceUnavailableError(reason, retryAfter)` populate the `Allow` and
|
|
78
|
+
> `Retry-After` response headers for you when you pass those arguments.
|
|
79
|
+
|
|
80
|
+
## Custom exception classes
|
|
81
|
+
|
|
82
|
+
Extend `ZerotalError` for domain errors, or `HttpError` for HTTP errors. The
|
|
83
|
+
`ZerotalError` constructor is `(message, code, status = 500, context?)`:
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
// app/exceptions/PaymentFailedError.ts
|
|
87
|
+
import { ZerotalError, HttpError } from "zerotal";
|
|
88
|
+
|
|
89
|
+
// Domain error — pass status 402 as the third argument
|
|
90
|
+
export class PaymentFailedError extends ZerotalError {
|
|
91
|
+
constructor(public readonly chargeId: string) {
|
|
92
|
+
super(`Payment failed for charge ${chargeId}`, "E_PAYMENT_FAILED", 402);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// HTTP shortcut — HttpError takes (message, status, code?)
|
|
97
|
+
export class QuotaExceededError extends HttpError {
|
|
98
|
+
constructor() {
|
|
99
|
+
super("Quota exceeded", 429, "E_QUOTA_EXCEEDED");
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// in a controller
|
|
106
|
+
throw new PaymentFailedError(charge.id);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Custom exception handler
|
|
110
|
+
|
|
111
|
+
Create a handler in `app/exceptions/Handler.ts` extending `ExceptionHandler`,
|
|
112
|
+
then register it in `bootstrap/app.ts` with `app.withExceptionHandler(Handler)`.
|
|
113
|
+
The framework calls `report()` first, then `render()`, for every unhandled
|
|
114
|
+
exception:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
// app/exceptions/Handler.ts
|
|
118
|
+
import { ExceptionHandler, NotFoundError, ForbiddenError } from "zerotal";
|
|
119
|
+
import type { HttpContext } from "zerotal";
|
|
120
|
+
import { PaymentFailedError } from "./PaymentFailedError.ts";
|
|
121
|
+
|
|
122
|
+
export class Handler extends ExceptionHandler {
|
|
123
|
+
// report() runs first — use it to log to Sentry, Datadog, etc.
|
|
124
|
+
override async report(err: unknown, ctx?: HttpContext): Promise<void> {
|
|
125
|
+
if (err instanceof NotFoundError) return; // skip expected errors
|
|
126
|
+
if (err instanceof ForbiddenError) return;
|
|
127
|
+
|
|
128
|
+
await Sentry.captureException(err, { extra: { path: ctx?.path() } });
|
|
129
|
+
|
|
130
|
+
// Call super to keep the default console logging.
|
|
131
|
+
await super.report(err, ctx);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// render() turns an error into a Response.
|
|
135
|
+
override async render(err: unknown, ctx: HttpContext): Promise<Response> {
|
|
136
|
+
if (err instanceof PaymentFailedError) {
|
|
137
|
+
if (ctx.wantsJson()) {
|
|
138
|
+
return Response.json({ message: err.message, chargeId: err.chargeId }, { status: 402 });
|
|
139
|
+
}
|
|
140
|
+
ctx.flash("error", "Your payment could not be processed.");
|
|
141
|
+
return new Response(null, { status: 303, headers: { Location: "/billing" } });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fall back to the framework default for everything you don't handle.
|
|
145
|
+
return super.render(err, ctx);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
// bootstrap/app.ts
|
|
152
|
+
import { Application } from "zerotal";
|
|
153
|
+
import providers from "./providers.ts";
|
|
154
|
+
import { Handler } from "../app/exceptions/Handler.ts";
|
|
155
|
+
|
|
156
|
+
export default Application.create({ providers }).withExceptionHandler(Handler);
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
> **Warning** — Always call `super.render(err, ctx)` as the final fallback. If you
|
|
160
|
+
> forget it, any error your `render()` doesn't explicitly handle falls through
|
|
161
|
+
> without a response.
|
|
162
|
+
|
|
163
|
+
## Silencing expected errors
|
|
164
|
+
|
|
165
|
+
`report()` is for errors you want tracked externally. Errors that are normal user
|
|
166
|
+
behaviour (404, 403, validation) should not page anyone. The base handler already
|
|
167
|
+
silences a few by name — `ValidationRedirectError`, `ValidationJsonError`,
|
|
168
|
+
`ValidationError`, `PrecognitionResponse`, and `NotFoundError` — so they never
|
|
169
|
+
reach `console.error`.
|
|
170
|
+
|
|
171
|
+
To silence your own classes, list them on the `dontReport` array instead of
|
|
172
|
+
filtering by hand:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// app/exceptions/Handler.ts
|
|
176
|
+
import { ExceptionHandler, ForbiddenError } from "zerotal";
|
|
177
|
+
import { PaymentFailedError } from "./PaymentFailedError.ts";
|
|
178
|
+
|
|
179
|
+
export class Handler extends ExceptionHandler {
|
|
180
|
+
protected override dontReport = [ForbiddenError, PaymentFailedError];
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
> **Tip** — Prefer `dontReport` over a manual `instanceof` chain in `report()`.
|
|
185
|
+
> The base `report()` checks it for you, so any error whose class (or class name)
|
|
186
|
+
> is in the silent list is skipped before logging.
|
|
187
|
+
|
|
188
|
+
## Response format by client type
|
|
189
|
+
|
|
190
|
+
The base handler negotiates the response from the `Accept` header and the
|
|
191
|
+
environment:
|
|
192
|
+
|
|
193
|
+
| Situation | Browser | API client (`Accept: application/json`) |
|
|
194
|
+
| ---------------------- | ----------------------------- | ----------------------------------------------------- |
|
|
195
|
+
| `NotFoundError` | Styled 404 HTML page | `{ message: 'Not Found', code: 'E_NOT_FOUND' }` |
|
|
196
|
+
| `ForbiddenError` | Styled 403 HTML page | `{ message: 'Forbidden', code: 'E_FORBIDDEN' }` |
|
|
197
|
+
| Unhandled error (dev) | Full stack-trace page | `{ message: '...', code: 'E_INTERNAL' }` |
|
|
198
|
+
| Unhandled error (prod) | "Internal Server Error" page | `{ message: 'Internal Server Error' }` |
|
|
199
|
+
| `ValidationError` | 303 redirect + flashed errors | `{ message: 'Validation failed', errors: {...} }` 422 |
|
|
200
|
+
|
|
201
|
+
## Validation errors
|
|
202
|
+
|
|
203
|
+
The validation system throws automatically — you don't catch anything. It throws
|
|
204
|
+
`ValidationRedirectError` (a 303 redirect for browser clients) or
|
|
205
|
+
`ValidationJsonError` (a 422 JSON body for API clients), and both flow straight
|
|
206
|
+
through the handler:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// in a controller
|
|
210
|
+
// Browser → 303 redirect back with flashed errors
|
|
211
|
+
// API → 422 JSON: { message: 'Validation failed', errors: {...} }
|
|
212
|
+
const data = await StorePostRequest.validate();
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
See the [Validator](/docs/validator) guide for details.
|
|
216
|
+
|
|
217
|
+
## Error codes
|
|
218
|
+
|
|
219
|
+
Every `ZerotalError` carries a machine-readable `code` string alongside the HTTP
|
|
220
|
+
`status`. Use it on the client to distinguish errors that share a status:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// the API client receives:
|
|
224
|
+
// { "message": "Payment failed for charge ch_xxx", "code": "E_PAYMENT_FAILED" }
|
|
225
|
+
|
|
226
|
+
if (error.code === "E_PAYMENT_FAILED") {
|
|
227
|
+
showPaymentFailedUI(error.chargeId);
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## afterResponse and errors
|
|
232
|
+
|
|
233
|
+
Errors thrown inside `ctx.afterResponse()` callbacks are logged but never sent to
|
|
234
|
+
the client — the response has already been delivered by the time these fire, so a
|
|
235
|
+
failing callback can't crash the request.
|
|
236
|
+
|
|
237
|
+
## The development error page, and diagnoses
|
|
238
|
+
|
|
239
|
+
Outside production, an unhandled error renders a full-page overlay: the message, the stack split into application and framework frames, source context around the failing line, the request, and a **Copy for AI** button that puts the whole thing on the clipboard as Markdown.
|
|
240
|
+
|
|
241
|
+
Some errors are exact about _what_ happened and useless about _what to do_. `no such table: assets` is the canonical one — the message is precise, and every frame in the stack is inside the SQL driver, because that is where the failure surfaced rather than where it came from. The answer is somewhere else entirely: you have migrations you have not run.
|
|
242
|
+
|
|
243
|
+
So a package that owns an error class can contribute a **diagnosis**, rendered above the stack.
|
|
244
|
+
|
|
245
|
+
### Missing tables and columns
|
|
246
|
+
|
|
247
|
+
`@zerotal/orm` registers one. When a query fails because a table or column does not exist, the overlay checks the migration state and answers one of two ways:
|
|
248
|
+
|
|
249
|
+
- **Migrations are pending** — it names them and offers a **Run migrations** button. Pressing it runs exactly what `bun zt migrate` would, then reloads the page.
|
|
250
|
+
- **Nothing is pending** — it offers **no button**, because running every pending migration when there are none changes nothing and leaves you back where you started. Instead it says whether any migration on disk so much as mentions the missing name: if none does, the migration that would create it was probably never written.
|
|
251
|
+
|
|
252
|
+
It works on SQLite, PostgreSQL and MySQL — matched on the driver's error code where there is one (`42P01`, `42703`, `1146`, `1054`) and on the message otherwise.
|
|
253
|
+
|
|
254
|
+
> **The button exists only in development.** The endpoint behind it refuses unless [`devSurfacesEnabled()`](/docs/deployment) is true, and that gate **fails closed**: an unset `APP_ENV` does not qualify. The route is not even registered otherwise.
|
|
255
|
+
>
|
|
256
|
+
> It also requires a single-use token minted into the page, and passes the same origin check the WebSocket endpoints use. A dev server on `localhost:3000` is reachable by any site you have open in another tab, and "run every pending migration" is not something a random page should be able to trigger.
|
|
257
|
+
|
|
258
|
+
### Writing your own
|
|
259
|
+
|
|
260
|
+
`registerErrorDiagnoser` takes a function that either recognises an error or returns `null`. Register it from a provider's `onRegister()`:
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import { registerErrorDiagnoser } from "zerotal";
|
|
264
|
+
|
|
265
|
+
registerErrorDiagnoser((error) => {
|
|
266
|
+
if (!/ECONNREFUSED .*:6379/.test(error.message)) return null;
|
|
267
|
+
return {
|
|
268
|
+
title: "Redis refused the connection.",
|
|
269
|
+
detail:
|
|
270
|
+
"The cache and queue drivers are both configured to use it. Start it with `docker compose up redis`, or switch `cache.driver` to `memory` while you work.",
|
|
271
|
+
items: ["cache.driver = redis", "queue.driver = redis"],
|
|
272
|
+
};
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The function is an `ErrorDiagnoser`, and what it returns is an `ErrorDiagnosis`.
|
|
277
|
+
|
|
278
|
+
Diagnosers run in registration order and the **first** one to return a result wins, so recognise only errors you genuinely own — a diagnoser that claims an error it cannot explain replaces a real stack trace with a wrong answer. One that throws is skipped rather than taking the error page down with it.
|
|
279
|
+
|
|
280
|
+
| Field | Meaning |
|
|
281
|
+
| -------- | ------------------------------------------------------------------- |
|
|
282
|
+
| `title` | One line: what is actually wrong. |
|
|
283
|
+
| `detail` | A short paragraph: why, and what to do. |
|
|
284
|
+
| `items` | Supporting specifics — file names, config keys. Rendered as a list. |
|
|
285
|
+
| `action` | A button. See below. |
|
|
286
|
+
|
|
287
|
+
An `action` is a `DiagnosisAction`: a `label`, a same-origin `url` to `POST` to, a `token`, and an optional `pendingLabel`.
|
|
288
|
+
|
|
289
|
+
> **An `action` changes server state from a page rendered by a GET.** Whoever registers the endpoint owns its safety, and the type only carries the values to the page. The endpoint must refuse outside development _on its own terms_ rather than trusting that the overlay is dev-only, require the token, and check the origin. If you cannot do all three, ship the diagnosis without a button — a `title` and `detail` that name the fix are most of the value.
|
|
290
|
+
|
|
291
|
+
Offer a button only when you are confident it helps. A button that runs and changes nothing is worse than no button, because it teaches people not to trust the panel.
|
|
292
|
+
|
|
293
|
+
## References
|
|
294
|
+
|
|
295
|
+
`ExceptionHandler` is the class you subclass; the named errors are exported from
|
|
296
|
+
`zerotal`.
|
|
297
|
+
|
|
298
|
+
| Member | Signature | Description |
|
|
299
|
+
| -------------- | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
300
|
+
| `report` | `report(err: unknown, ctx?: HttpContext): Promise<void>` | Called first; log or forward the error. Override to add tracking. |
|
|
301
|
+
| `render` | `render(err: unknown, ctx: HttpContext): Promise<Response>` | Turns an error into a `Response`. Override per error class, call `super`. |
|
|
302
|
+
| `dontReport` | `protected dontReport: Array<new (...args) => unknown>` | Error classes to skip in `report()`. |
|
|
303
|
+
| `ZerotalError` | `new ZerotalError(message, code, status = 500, context?)` | Base framework error with a `code`, `status`, and optional `context`. |
|
|
304
|
+
| `HttpError` | `new HttpError(message, status, code?, headers?)` | Error mapped directly onto an HTTP response; `code` defaults to `E_HTTP_<status>`. |
|
|
305
|
+
|
|
306
|
+
| Method (on `HttpContext`) | Signature | Description |
|
|
307
|
+
| ------------------------- | ------------------------- | --------------------------------------------------------- |
|
|
308
|
+
| `wantsJson` | `wantsJson(): boolean` | `true` when the client expects a JSON response. |
|
|
309
|
+
| `path` | `path(): string` | The request path, handy for error context. |
|
|
310
|
+
| `flash` | `flash(key, value): void` | Stash data for the next request (e.g. before a redirect). |
|
|
311
|
+
|
|
312
|
+
## Next steps
|
|
313
|
+
|
|
314
|
+
- [Validator](/docs/validator) — the validation errors that flow through the handler.
|
|
315
|
+
- [Logger](/docs/logger) — where `report()` writes by default.
|
|
316
|
+
- [HTTP context](/docs/context) — the `ctx` passed to `report()` and `render()`.
|
|
317
|
+
- [Telemetry](/docs/telemetry) — forward reported errors to an external tracker.
|