honertia 0.1.16 → 0.1.17
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/README.md +199 -2
- package/dist/effect/handler.d.ts +5 -0
- package/dist/effect/handler.d.ts.map +1 -1
- package/dist/effect/handler.js +49 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1243,7 +1243,204 @@ return yield* forbidden('You cannot edit this project')
|
|
|
1243
1243
|
|
|
1244
1244
|
## Error Handling
|
|
1245
1245
|
|
|
1246
|
-
Honertia provides typed errors
|
|
1246
|
+
Honertia provides typed errors that integrate with Effect's error channel. Each error type has specific handling behavior designed for Inertia-style applications.
|
|
1247
|
+
|
|
1248
|
+
### Built-in Error Types
|
|
1249
|
+
|
|
1250
|
+
| Error Type | HTTP Status | Handling Behavior |
|
|
1251
|
+
|------------|-------------|-------------------|
|
|
1252
|
+
| `ValidationError` | 422 / redirect | Re-renders form with field errors, or redirects back |
|
|
1253
|
+
| `UnauthorizedError` | 302/303 | Redirects to login page |
|
|
1254
|
+
| `NotFoundError` | 404 | Uses Hono's `notFound()` handler → renders via Honertia |
|
|
1255
|
+
| `ForbiddenError` | 403 | Returns JSON response (for API compatibility) |
|
|
1256
|
+
| `HttpError` | Custom | Returns JSON with custom status (developer-controlled) |
|
|
1257
|
+
| `RouteConfigurationError` | 500 | Throws to Hono's `onError` → renders error page |
|
|
1258
|
+
| Unexpected errors | 500 | Throws to Hono's `onError` → renders error page |
|
|
1259
|
+
|
|
1260
|
+
### Error Type Details
|
|
1261
|
+
|
|
1262
|
+
#### `ValidationError`
|
|
1263
|
+
|
|
1264
|
+
Thrown when request validation fails. Automatically re-renders the form with field-level errors.
|
|
1265
|
+
|
|
1266
|
+
```typescript
|
|
1267
|
+
import { ValidationError, validateRequest } from 'honertia/effect'
|
|
1268
|
+
|
|
1269
|
+
// Automatic: validateRequest throws ValidationError on failure
|
|
1270
|
+
const input = yield* validateRequest(schema, {
|
|
1271
|
+
errorComponent: 'Projects/Create', // Re-renders this component with errors
|
|
1272
|
+
})
|
|
1273
|
+
|
|
1274
|
+
// Manual: throw ValidationError directly
|
|
1275
|
+
yield* Effect.fail(new ValidationError({
|
|
1276
|
+
errors: { email: 'Invalid email format' },
|
|
1277
|
+
component: 'Auth/Register', // Optional: component to re-render
|
|
1278
|
+
}))
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
**Behavior:**
|
|
1282
|
+
- If request prefers JSON (API calls): returns `{ errors: {...} }` with 422 status
|
|
1283
|
+
- If `component` is set: re-renders that component with errors in props
|
|
1284
|
+
- Otherwise: redirects back to referer with errors in session
|
|
1285
|
+
|
|
1286
|
+
#### `UnauthorizedError`
|
|
1287
|
+
|
|
1288
|
+
Thrown when authentication is required but the user is not logged in.
|
|
1289
|
+
|
|
1290
|
+
```typescript
|
|
1291
|
+
import { UnauthorizedError, authorize } from 'honertia/effect'
|
|
1292
|
+
|
|
1293
|
+
// Automatic: authorize() throws UnauthorizedError if no user
|
|
1294
|
+
const auth = yield* authorize()
|
|
1295
|
+
|
|
1296
|
+
// Manual: throw with custom redirect
|
|
1297
|
+
yield* Effect.fail(new UnauthorizedError({
|
|
1298
|
+
message: 'Please log in to continue',
|
|
1299
|
+
redirectTo: '/login', // Defaults to '/login'
|
|
1300
|
+
}))
|
|
1301
|
+
```
|
|
1302
|
+
|
|
1303
|
+
**Behavior:** Redirects to the specified URL (302 for regular requests, 303 for Inertia requests).
|
|
1304
|
+
|
|
1305
|
+
#### `NotFoundError`
|
|
1306
|
+
|
|
1307
|
+
Thrown when a requested resource doesn't exist.
|
|
1308
|
+
|
|
1309
|
+
```typescript
|
|
1310
|
+
import { NotFoundError, notFound } from 'honertia/effect'
|
|
1311
|
+
|
|
1312
|
+
// Helper function
|
|
1313
|
+
return yield* notFound('Project', projectId)
|
|
1314
|
+
|
|
1315
|
+
// Manual
|
|
1316
|
+
yield* Effect.fail(new NotFoundError({
|
|
1317
|
+
resource: 'Project',
|
|
1318
|
+
id: projectId,
|
|
1319
|
+
}))
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
**Behavior:** Triggers Hono's `notFound()` handler. If you've set up `registerErrorHandlers()`, this renders your error component with status 404.
|
|
1323
|
+
|
|
1324
|
+
#### `ForbiddenError`
|
|
1325
|
+
|
|
1326
|
+
Thrown when the user is authenticated but not authorized to perform an action.
|
|
1327
|
+
|
|
1328
|
+
```typescript
|
|
1329
|
+
import { ForbiddenError, forbidden, authorize } from 'honertia/effect'
|
|
1330
|
+
|
|
1331
|
+
// Automatic: authorize() throws ForbiddenError if check fails
|
|
1332
|
+
const auth = yield* authorize((a) => a.user.role === 'admin')
|
|
1333
|
+
|
|
1334
|
+
// Helper function
|
|
1335
|
+
return yield* forbidden('You cannot edit this project')
|
|
1336
|
+
|
|
1337
|
+
// Manual
|
|
1338
|
+
yield* Effect.fail(new ForbiddenError({
|
|
1339
|
+
message: 'Admin access required',
|
|
1340
|
+
}))
|
|
1341
|
+
```
|
|
1342
|
+
|
|
1343
|
+
**Behavior:** Returns JSON `{ message: "..." }` with 403 status. This is intentionally JSON for API compatibility.
|
|
1344
|
+
|
|
1345
|
+
#### `HttpError`
|
|
1346
|
+
|
|
1347
|
+
A generic error for custom HTTP responses. Use when you need precise control over the response.
|
|
1348
|
+
|
|
1349
|
+
```typescript
|
|
1350
|
+
import { HttpError, httpError } from 'honertia/effect'
|
|
1351
|
+
|
|
1352
|
+
// Helper function
|
|
1353
|
+
return yield* httpError(429, 'Rate limited', { retryAfter: 60 })
|
|
1354
|
+
|
|
1355
|
+
// Manual
|
|
1356
|
+
yield* Effect.fail(new HttpError({
|
|
1357
|
+
status: 429,
|
|
1358
|
+
message: 'Too many requests',
|
|
1359
|
+
body: { retryAfter: 60 }, // Optional additional data
|
|
1360
|
+
}))
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
**Behavior:** Returns JSON `{ message: "...", ...body }` with the specified status code.
|
|
1364
|
+
|
|
1365
|
+
#### `RouteConfigurationError`
|
|
1366
|
+
|
|
1367
|
+
Thrown when there's a developer configuration error, such as using route model binding without providing a schema.
|
|
1368
|
+
|
|
1369
|
+
```typescript
|
|
1370
|
+
import { RouteConfigurationError } from 'honertia/effect'
|
|
1371
|
+
|
|
1372
|
+
// This error is thrown automatically when:
|
|
1373
|
+
// - You use bound('project') but didn't pass schema to effectRoutes()
|
|
1374
|
+
// - Other route configuration mistakes
|
|
1375
|
+
|
|
1376
|
+
// You typically don't throw this manually
|
|
1377
|
+
```
|
|
1378
|
+
|
|
1379
|
+
**Behavior:** Re-throws to Hono's `onError` handler, which renders your error component. The error message and hint are logged to the console for debugging.
|
|
1380
|
+
|
|
1381
|
+
### Setting Up Error Pages
|
|
1382
|
+
|
|
1383
|
+
To render errors via Honertia instead of returning plain text/JSON, use `registerErrorHandlers`:
|
|
1384
|
+
|
|
1385
|
+
```typescript
|
|
1386
|
+
import { registerErrorHandlers } from 'honertia'
|
|
1387
|
+
|
|
1388
|
+
// In your app setup
|
|
1389
|
+
registerErrorHandlers(app, {
|
|
1390
|
+
component: 'Error', // Your error page component
|
|
1391
|
+
showDevErrors: true, // Show detailed errors in development
|
|
1392
|
+
envKey: 'ENVIRONMENT', // Env var to check
|
|
1393
|
+
devValue: 'development', // Value that enables dev errors
|
|
1394
|
+
})
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
Your `Error` component receives:
|
|
1398
|
+
|
|
1399
|
+
```tsx
|
|
1400
|
+
// src/pages/Error.tsx
|
|
1401
|
+
interface ErrorProps {
|
|
1402
|
+
status: number // 404, 500, etc.
|
|
1403
|
+
message: string // Error message (detailed in dev, generic in prod)
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
export default function Error({ status, message }: ErrorProps) {
|
|
1407
|
+
return (
|
|
1408
|
+
<div className="error-page">
|
|
1409
|
+
<h1>{status}</h1>
|
|
1410
|
+
<p>{message}</p>
|
|
1411
|
+
</div>
|
|
1412
|
+
)
|
|
1413
|
+
}
|
|
1414
|
+
```
|
|
1415
|
+
|
|
1416
|
+
### Error Handling Flow
|
|
1417
|
+
|
|
1418
|
+
```
|
|
1419
|
+
Effect Handler
|
|
1420
|
+
│
|
|
1421
|
+
▼
|
|
1422
|
+
┌─────────────────────────────────────────────────────────┐
|
|
1423
|
+
│ errorToResponse() │
|
|
1424
|
+
├─────────────────────────────────────────────────────────┤
|
|
1425
|
+
│ ValidationError → Re-render form / redirect back │
|
|
1426
|
+
│ UnauthorizedError → Redirect to login │
|
|
1427
|
+
│ NotFoundError → c.notFound() → Hono notFound handler │
|
|
1428
|
+
│ ForbiddenError → JSON 403 │
|
|
1429
|
+
│ HttpError → JSON with custom status │
|
|
1430
|
+
│ Other errors → throw → Hono onError handler │
|
|
1431
|
+
└─────────────────────────────────────────────────────────┘
|
|
1432
|
+
│
|
|
1433
|
+
▼ (for thrown errors)
|
|
1434
|
+
┌─────────────────────────────────────────────────────────┐
|
|
1435
|
+
│ Hono onError handler │
|
|
1436
|
+
│ (from registerErrorHandlers) │
|
|
1437
|
+
├─────────────────────────────────────────────────────────┤
|
|
1438
|
+
│ Renders error component via Honertia │
|
|
1439
|
+
│ Shows detailed message in dev, generic in prod │
|
|
1440
|
+
└─────────────────────────────────────────────────────────┘
|
|
1441
|
+
```
|
|
1442
|
+
|
|
1443
|
+
### Usage Examples
|
|
1247
1444
|
|
|
1248
1445
|
```typescript
|
|
1249
1446
|
import {
|
|
@@ -1252,7 +1449,7 @@ import {
|
|
|
1252
1449
|
NotFoundError,
|
|
1253
1450
|
ForbiddenError,
|
|
1254
1451
|
HttpError,
|
|
1255
|
-
} from 'honertia'
|
|
1452
|
+
} from 'honertia/effect'
|
|
1256
1453
|
|
|
1257
1454
|
// Validation errors automatically re-render with field errors
|
|
1258
1455
|
const input = yield* validateRequest(schema, {
|
package/dist/effect/handler.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ import type { Context as HonoContext, MiddlewareHandler, Env } from 'hono';
|
|
|
8
8
|
import { Redirect, type AppError } from './errors.js';
|
|
9
9
|
/**
|
|
10
10
|
* Convert an Effect error to an HTTP response.
|
|
11
|
+
*
|
|
12
|
+
* Most errors are re-thrown so Hono's onError handler can render them
|
|
13
|
+
* via Honertia's error component. Only errors that need special handling
|
|
14
|
+
* (ValidationError for form re-rendering, UnauthorizedError for redirects)
|
|
15
|
+
* return responses directly.
|
|
11
16
|
*/
|
|
12
17
|
export declare function errorToResponse<E extends Env>(error: AppError, c: HonoContext<E>): Promise<Response>;
|
|
13
18
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/effect/handler.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAA+B,MAAM,QAAQ,CAAA;AAC5D,OAAO,KAAK,EAAE,OAAO,IAAI,WAAW,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;AAE1E,OAAO,
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/effect/handler.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,MAAM,EAA+B,MAAM,QAAQ,CAAA;AAC5D,OAAO,KAAK,EAAE,OAAO,IAAI,WAAW,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,MAAM,CAAA;AAE1E,OAAO,EAML,QAAQ,EACR,KAAK,QAAQ,EACd,MAAM,aAAa,CAAA;AAuBpB;;;;;;;GAOG;AACH,wBAAsB,eAAe,CAAC,CAAC,SAAS,GAAG,EACjD,KAAK,EAAE,QAAQ,EACf,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GAChB,OAAO,CAAC,QAAQ,CAAC,CAiDnB;AAgBD;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,GAAG,EAAE,CAAC,EAAE,GAAG,SAAS,QAAQ,EAClE,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,GAAG,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,GACjD,iBAAiB,CAAC,CAAC,CAAC,CAoBtB;AAgDD;;GAEG;AACH,wBAAgB,MAAM,CAAC,CAAC,SAAS,GAAG,EAAE,CAAC,EAAE,GAAG,SAAS,QAAQ,EAC3D,EAAE,EAAE,MAAM,MAAM,CAAC,MAAM,CAAC,QAAQ,GAAG,QAAQ,EAAE,GAAG,EAAE,CAAC,CAAC,GACnD,iBAAiB,CAAC,CAAC,CAAC,CAEtB;AAED;;GAEG;AACH,eAAO,MAAM,MAAM,sBAAgB,CAAA"}
|
package/dist/effect/handler.js
CHANGED
|
@@ -5,11 +5,36 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Effect, Exit, Cause, ManagedRuntime } from 'effect';
|
|
7
7
|
import { getEffectRuntime, buildContextLayer } from './bridge.js';
|
|
8
|
-
import { ValidationError, UnauthorizedError, NotFoundError,
|
|
8
|
+
import { ValidationError, UnauthorizedError, NotFoundError, HttpError, RouteConfigurationError, Redirect, } from './errors.js';
|
|
9
|
+
/**
|
|
10
|
+
* Convert an AppError to a throwable Error for Hono's onError handler.
|
|
11
|
+
* Preserves error metadata like status codes and hints.
|
|
12
|
+
*/
|
|
13
|
+
function toThrowableError(error) {
|
|
14
|
+
const err = new Error(error.message);
|
|
15
|
+
err.name = error._tag;
|
|
16
|
+
// Preserve status for HttpError
|
|
17
|
+
if (error instanceof HttpError) {
|
|
18
|
+
;
|
|
19
|
+
err.status = error.status;
|
|
20
|
+
}
|
|
21
|
+
// Preserve hint for RouteConfigurationError
|
|
22
|
+
if (error instanceof RouteConfigurationError && error.hint) {
|
|
23
|
+
;
|
|
24
|
+
err.hint = error.hint;
|
|
25
|
+
}
|
|
26
|
+
return err;
|
|
27
|
+
}
|
|
9
28
|
/**
|
|
10
29
|
* Convert an Effect error to an HTTP response.
|
|
30
|
+
*
|
|
31
|
+
* Most errors are re-thrown so Hono's onError handler can render them
|
|
32
|
+
* via Honertia's error component. Only errors that need special handling
|
|
33
|
+
* (ValidationError for form re-rendering, UnauthorizedError for redirects)
|
|
34
|
+
* return responses directly.
|
|
11
35
|
*/
|
|
12
36
|
export async function errorToResponse(error, c) {
|
|
37
|
+
// ValidationError: re-render form with errors or redirect back
|
|
13
38
|
if (error instanceof ValidationError) {
|
|
14
39
|
const isInertia = c.req.header('X-Inertia') === 'true';
|
|
15
40
|
const prefersJson = c.req.header('Accept')?.includes('application/json') ||
|
|
@@ -28,34 +53,26 @@ export async function errorToResponse(error, c) {
|
|
|
28
53
|
c.var?.honertia?.setErrors(error.errors);
|
|
29
54
|
return c.redirect(referer, 303);
|
|
30
55
|
}
|
|
56
|
+
// UnauthorizedError: redirect to login
|
|
31
57
|
if (error instanceof UnauthorizedError) {
|
|
32
58
|
const isInertia = c.req.header('X-Inertia') === 'true';
|
|
33
59
|
const redirectTo = error.redirectTo ?? '/login';
|
|
34
60
|
return c.redirect(redirectTo, isInertia ? 303 : 302);
|
|
35
61
|
}
|
|
36
|
-
|
|
37
|
-
return c.json({ message: error.message }, 403);
|
|
38
|
-
}
|
|
62
|
+
// NotFoundError: use Hono's notFound handler (renders via Honertia if configured)
|
|
39
63
|
if (error instanceof NotFoundError) {
|
|
40
64
|
return c.notFound();
|
|
41
65
|
}
|
|
66
|
+
// ForbiddenError: return 403 JSON (useful for API routes)
|
|
67
|
+
if ('_tag' in error && error._tag === 'ForbiddenError') {
|
|
68
|
+
return c.json({ message: error.message }, 403);
|
|
69
|
+
}
|
|
70
|
+
// HttpError: return custom status JSON (gives developers control over HTTP responses)
|
|
42
71
|
if (error instanceof HttpError) {
|
|
43
72
|
return c.json({ message: error.message, ...error.body }, error.status);
|
|
44
73
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (error.hint) {
|
|
48
|
-
console.error(`[honertia] Hint: ${error.hint}`);
|
|
49
|
-
}
|
|
50
|
-
return c.json({
|
|
51
|
-
message: error.message,
|
|
52
|
-
hint: error.hint,
|
|
53
|
-
type: 'RouteConfigurationError'
|
|
54
|
-
}, 500);
|
|
55
|
-
}
|
|
56
|
-
// Unknown error
|
|
57
|
-
console.error('Unhandled error:', error);
|
|
58
|
-
return c.json({ message: 'Internal server error' }, 500);
|
|
74
|
+
// All other errors (RouteConfigurationError, etc.): throw to Hono's onError handler
|
|
75
|
+
throw toThrowableError(error);
|
|
59
76
|
}
|
|
60
77
|
/**
|
|
61
78
|
* Handle a Redirect value (which is not an error).
|
|
@@ -93,6 +110,9 @@ export function effectHandler(effect) {
|
|
|
93
110
|
}
|
|
94
111
|
/**
|
|
95
112
|
* Handle an Effect exit value.
|
|
113
|
+
*
|
|
114
|
+
* Failures are converted to responses via errorToResponse.
|
|
115
|
+
* Defects (unexpected errors) are re-thrown for Hono's onError handler.
|
|
96
116
|
*/
|
|
97
117
|
async function handleExit(exit, c) {
|
|
98
118
|
if (Exit.isSuccess(exit)) {
|
|
@@ -102,7 +122,7 @@ async function handleExit(exit, c) {
|
|
|
102
122
|
}
|
|
103
123
|
return value;
|
|
104
124
|
}
|
|
105
|
-
// Handle
|
|
125
|
+
// Handle typed failures
|
|
106
126
|
const cause = exit.cause;
|
|
107
127
|
if (Cause.isFailure(cause)) {
|
|
108
128
|
const error = Cause.failureOption(cause);
|
|
@@ -110,14 +130,21 @@ async function handleExit(exit, c) {
|
|
|
110
130
|
return await errorToResponse(error.value, c);
|
|
111
131
|
}
|
|
112
132
|
}
|
|
113
|
-
// Handle defects (unexpected errors)
|
|
133
|
+
// Handle defects (unexpected errors) - throw to Hono's onError handler
|
|
114
134
|
if (Cause.isDie(cause)) {
|
|
115
135
|
const defect = Cause.dieOption(cause);
|
|
116
136
|
if (defect._tag === 'Some') {
|
|
117
|
-
|
|
137
|
+
const err = defect.value;
|
|
138
|
+
// If it's already an Error, throw it directly
|
|
139
|
+
if (err instanceof Error) {
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
// Otherwise wrap it
|
|
143
|
+
throw new Error(String(err));
|
|
118
144
|
}
|
|
119
145
|
}
|
|
120
|
-
|
|
146
|
+
// Fallback: throw generic error for Hono's onError handler
|
|
147
|
+
throw new Error('Unknown effect failure');
|
|
121
148
|
}
|
|
122
149
|
/**
|
|
123
150
|
* Create a handler from a function that returns an Effect.
|