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 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, {
@@ -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,EAOL,QAAQ,EACR,KAAK,QAAQ,EACd,MAAM,aAAa,CAAA;AAGpB;;GAEG;AACH,wBAAsB,eAAe,CAAC,CAAC,SAAS,GAAG,EACjD,KAAK,EAAE,QAAQ,EACf,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GAChB,OAAO,CAAC,QAAQ,CAAC,CAyDnB;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;AAsCD;;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"}
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"}
@@ -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, ForbiddenError, HttpError, RouteConfigurationError, Redirect, } from './errors.js';
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
- if (error instanceof ForbiddenError) {
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
- if (error instanceof RouteConfigurationError) {
46
- console.error(`[honertia] Route configuration error: ${error.message}`);
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 failure
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
- console.error('Effect defect:', defect.value);
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
- return c.json({ message: 'Internal server error' }, 500);
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "honertia",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "Inertia.js-style server-driven SPA adapter for Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",