@rdlabo/workers-hono-kit 0.6.0 → 0.6.3
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 +18 -20
- package/dist/http/app-error-handler.d.ts +5 -6
- package/dist/http/app-error-handler.js +6 -7
- package/dist/http/http-error.d.ts +113 -0
- package/dist/http/http-error.js +106 -0
- package/dist/http/query-failed-error.d.ts +9 -10
- package/dist/http/query-failed-error.js +11 -12
- package/dist/index.d.ts +8 -7
- package/dist/index.js +7 -4
- package/dist/queue/error-handler.d.ts +1 -1
- package/package.json +1 -1
- package/dist/http/nest-error.d.ts +0 -149
- package/dist/http/nest-error.js +0 -120
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ It provides the building blocks a NestJS-style API needs but that don't run on `
|
|
|
7
7
|
- **Firebase ID-token verification** on Workers via [`jose`](https://github.com/panva/jose) (RS256 against Google's securetoken JWKS), with optional Identity Toolkit REST for `getUser` / `deleteUser`.
|
|
8
8
|
- **AWS Secrets Manager** via SigV4-signed `fetch` ([`aws4fetch`](https://github.com/mhart/aws4fetch)) — no AWS SDK.
|
|
9
9
|
- **Middleware**: `finalizeResponse` (Express-compatible weak ETag + JSON charset), `validate` (NestJS `ValidationPipe`-shaped 400), and zod number-coercion helpers.
|
|
10
|
-
- **
|
|
10
|
+
- **Standard API errors**: `createHttpErrorHandler` / `notFoundHandler` / `HttpStatus`.
|
|
11
11
|
- **Deadlock retry** (`ER_LOCK_DEADLOCK` exponential backoff) and an optional **MySQL data layer** (`@rdlabo/workers-hono-kit/db`) for Hyperdrive + Drizzle.
|
|
12
12
|
- **AI Gateway**: route `@ai-sdk` models through the Cloudflare AI Gateway.
|
|
13
13
|
- **Stripe** Workers-native client + async webhook verification.
|
|
@@ -66,17 +66,17 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
|
|
|
66
66
|
| `getAppInfo(c)` / `AppInfo` | Read `x-amz-meta-version` / `x-amz-meta-uuid`. |
|
|
67
67
|
| `resolveAppEnv(env)` / `isProductionEnv(env)` / `AppEnv` | Resolve `'development'` / `'production'` from `env.APP_ENV` (defaults to `'production'` for safety). |
|
|
68
68
|
| `HttpStatus` | HTTP status enum identical to NestJS `@nestjs/common`. |
|
|
69
|
-
| `
|
|
70
|
-
| `createAppErrorHandler(options?)` / `CreateAppErrorHandlerOptions` | Standard `app.onError`: {@link
|
|
71
|
-
| `
|
|
72
|
-
| `classifyGenericMysqlDriverError(err)` | Default classifier
|
|
73
|
-
| `findMysqlDriverError(err)` / `logMysqlDriverError(err, statusCode)` | Low-level mysql2 driver-error detection (follows `err.cause`) and structured logging. For custom classifiers (e.g. odss
|
|
74
|
-
| `
|
|
69
|
+
| `createHttpErrorHandler(options?)` / `HttpErrorHandlerOptions` | `app.onError()` handler that maps a thrown `HTTPException` to `{ statusCode, message, error? }` (`401` omits `error`). Optional custom error predicate and unhandled-error report hook. Unhandled errors log via `console.error` (mysql2 errors include `sqlMessage` / `errno` when detectable). |
|
|
70
|
+
| `createAppErrorHandler(options?)` / `CreateAppErrorHandlerOptions` | Standard `app.onError`: {@link createQueryFailedErrorHandler} + default {@link classifyGenericMysqlDriverError} + optional `sentry` (Sentry apps), `getReportError` / `reportError` (tests / container), or neither (no external reporting). |
|
|
71
|
+
| `createQueryFailedErrorHandler(options)` / `QueryFailedClassifier` / `ClassifiedDbError` | Lower-level compose when you need full control over `classify` + `onUnhandledError` without defaults. |
|
|
72
|
+
| `classifyGenericMysqlDriverError(err)` | Default classifier: any mysql2 driver error → `{ statusCode: 500, message: 'Internal server error' }`; non-DB errors → `null`. |
|
|
73
|
+
| `findMysqlDriverError(err)` / `logMysqlDriverError(err, statusCode)` | Low-level mysql2 driver-error detection (follows `err.cause`) and structured logging. For custom classifiers (e.g. odss). |
|
|
74
|
+
| `notFoundHandler(c)` | `app.notFound()` handler with `{ message: 'Cannot METHOD path', error, statusCode }` 404 body. |
|
|
75
75
|
| `normalizeTrailingSlash(request)` | Strip trailing slash(es) from the request URL before routing (Express/Nest parity). Does **not** 301-redirect — preserves POST/PUT/DELETE bodies. |
|
|
76
|
-
| `
|
|
76
|
+
| `HTTP_ERROR_PHRASES` | `{ 400, 401, 403, 404 }` → standard `error` field phrases. |
|
|
77
77
|
| `createAuthMiddleware(options)` / `AuthMiddlewareOptions` | Factory for a Firebase-token auth middleware: reads the token header, verifies, resolves the DB user id, and stashes the result on the context. Omit `resolveUserId` for a token-only (login) guard. |
|
|
78
78
|
| `perfLog(options?)` / `PerfLogOptions` / `AnalyticsEngineDatasetLike` | Middleware that records one per-request latency data point (`t_app`, colo, cold/warm, route, status) and emits it to **Workers Logs** (`console.log`) and/or **Workers Analytics Engine** (`writeDataPoint`). Lets you measure low-traffic Workers without a live `wrangler tail`. |
|
|
79
|
-
| `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `
|
|
79
|
+
| `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createHttpErrorHandler`'s `onUnhandledError`. |
|
|
80
80
|
| `createSentryErrorReporter(sentry)` / `SentryExceptionReporterLike` | Build an `ErrorReporter` that forwards to Sentry with an optional `request_id` tag (no hard `@sentry/cloudflare` dependency). |
|
|
81
81
|
| `DeferExecutor` / `defaultDefer` / `createWaitUntilDefer(ctx)` | Fire-and-forget executor for Workers: `defaultDefer` swallows rejections (tests); `createWaitUntilDefer` registers work via `ctx.waitUntil`. |
|
|
82
82
|
| `createAiGatewayProvider(config)` / `AiGatewayConfig` / `AiGatewayProvider` | Route `@ai-sdk` models through the Cloudflare AI Gateway, via either a Workers `AI` binding or REST credentials (`accountId` / `gateway` / `token`). |
|
|
@@ -283,12 +283,10 @@ const appInfo = getAppInfo(c);
|
|
|
283
283
|
return c.json(body, HttpStatus.CREATED);
|
|
284
284
|
```
|
|
285
285
|
|
|
286
|
-
###
|
|
286
|
+
### HTTP error / 404 handlers
|
|
287
287
|
|
|
288
|
-
`
|
|
289
|
-
|
|
290
|
-
NestJS canonical shape (`{ statusCode, message, error? }`, `401` omits `error`); the options
|
|
291
|
-
let you reproduce any byte-for-byte variation an existing API expects.
|
|
288
|
+
`createHttpErrorHandler()` renders a thrown `HTTPException` as standard API error JSON,
|
|
289
|
+
and `notFoundHandler` gives the default unmatched-route 404 body.
|
|
292
290
|
|
|
293
291
|
#### App entry (fleet standard)
|
|
294
292
|
|
|
@@ -314,7 +312,7 @@ app.onError(
|
|
|
314
312
|
);
|
|
315
313
|
|
|
316
314
|
// odss-mobile: add classify: classifyQueryFailed (repo parity)
|
|
317
|
-
// winecode: sentry + isHttpError
|
|
315
|
+
// winecode: sentry + isHttpError in errors.ts (no container middleware)
|
|
318
316
|
// foodlabel: sentry + reportError: container.reportError (per-request container closure)
|
|
319
317
|
```
|
|
320
318
|
|
|
@@ -341,19 +339,19 @@ const { middleware: containerMiddleware, withContainer } = createContainerRuntim
|
|
|
341
339
|
Use `withContainer` from `scheduled` / `queue` handlers; use `containerMiddleware` in `createApp`.
|
|
342
340
|
|
|
343
341
|
```ts
|
|
344
|
-
import {
|
|
342
|
+
import { createHttpErrorHandler, notFoundHandler } from '@rdlabo/workers-hono-kit';
|
|
345
343
|
|
|
346
|
-
app.notFound(
|
|
344
|
+
app.notFound(notFoundHandler);
|
|
347
345
|
|
|
348
346
|
// Prefer createAppErrorHandler (see "App entry" above). Lower-level only when needed:
|
|
349
|
-
app.onError(
|
|
347
|
+
app.onError(createHttpErrorHandler());
|
|
350
348
|
```
|
|
351
349
|
|
|
352
350
|
**Important:** `Sentry.withSentry` does **not** capture errors handled by `app.onError`. Pass `sentry`
|
|
353
351
|
to `createAppErrorHandler` (or wire `getReportError` / `reportError` for tests and scheduled paths).
|
|
354
352
|
|
|
355
|
-
Repos with a
|
|
356
|
-
`createAppErrorHandler` — do not call `
|
|
353
|
+
Repos with a custom DB error classifier (e.g. odss-mobile) pass `classify` to
|
|
354
|
+
`createAppErrorHandler` — do not call `createQueryFailedErrorHandler` directly unless you need full control.
|
|
357
355
|
|
|
358
356
|
### Auth middleware
|
|
359
357
|
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { Context, Env } from 'hono';
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
2
|
+
import type { ErrorReporter, HttpErrorHandlerOptions, SentryExceptionReporterLike } from './http-error.js';
|
|
3
|
+
import type { QueryFailedClassifier } from './query-failed-error.js';
|
|
4
4
|
/**
|
|
5
5
|
* Options for {@link createAppErrorHandler}.
|
|
6
6
|
*
|
|
7
7
|
* @remarks
|
|
8
|
-
* Wires `
|
|
9
|
-
* {@link classifyGenericMysqlDriverError}) and optional error reporting.
|
|
8
|
+
* Wires `createQueryFailedErrorHandler` with fleet defaults ({@link classifyGenericMysqlDriverError}) and optional error reporting.
|
|
10
9
|
* Pass `sentry` for Sentry-backed apps; omit it (or pass `undefined`) when not used.
|
|
11
10
|
* `getReportError` / `reportError` override `sentry` (tests, container injection, scheduled paths).
|
|
12
11
|
*/
|
|
13
|
-
export interface CreateAppErrorHandlerOptions<E extends Env = Env> extends Omit<
|
|
12
|
+
export interface CreateAppErrorHandlerOptions<E extends Env = Env> extends Omit<HttpErrorHandlerOptions<E>, 'onUnhandledError'> {
|
|
14
13
|
/** mysql2 driver error classifier. Defaults to {@link classifyGenericMysqlDriverError}. */
|
|
15
14
|
classify?: QueryFailedClassifier;
|
|
16
15
|
/** Optional Sentry client (`@sentry/cloudflare`). Omitted on repos without Sentry. */
|
|
@@ -23,6 +22,6 @@ export interface CreateAppErrorHandlerOptions<E extends Env = Env> extends Omit<
|
|
|
23
22
|
onUnhandledError?: (err: unknown, c: Context<E>) => void;
|
|
24
23
|
}
|
|
25
24
|
/**
|
|
26
|
-
* Standard `app.onError` factory: QueryFailed filter →
|
|
25
|
+
* Standard `app.onError` factory: QueryFailed filter → HTTP error handler, with optional error reporting.
|
|
27
26
|
*/
|
|
28
27
|
export declare function createAppErrorHandler<E extends Env = Env>(options?: CreateAppErrorHandlerOptions<E>): (err: Error, c: Context<E, any, {}>) => Response;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createSentryErrorReporter } from './http-error.js';
|
|
2
|
+
import { classifyGenericMysqlDriverError, createQueryFailedErrorHandler } from './query-failed-error.js';
|
|
3
3
|
/**
|
|
4
|
-
* Standard `app.onError` factory: QueryFailed filter →
|
|
4
|
+
* Standard `app.onError` factory: QueryFailed filter → HTTP error handler, with optional error reporting.
|
|
5
5
|
*/
|
|
6
6
|
export function createAppErrorHandler(options = {}) {
|
|
7
|
-
const { classify = classifyGenericMysqlDriverError, sentry, reportError, getReportError, onUnhandledError,
|
|
7
|
+
const { classify = classifyGenericMysqlDriverError, sentry, reportError, getReportError, onUnhandledError, ...httpOptions } = options;
|
|
8
8
|
const sentryReporter = sentry ? createSentryErrorReporter(sentry) : undefined;
|
|
9
9
|
const resolvedOnUnhandled = onUnhandledError ??
|
|
10
10
|
((err, c) => {
|
|
@@ -12,9 +12,8 @@ export function createAppErrorHandler(options = {}) {
|
|
|
12
12
|
const requestId = c.get('requestId');
|
|
13
13
|
reporter?.(err, { requestId });
|
|
14
14
|
});
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
...nestOptions,
|
|
15
|
+
return createQueryFailedErrorHandler({
|
|
16
|
+
...httpOptions,
|
|
18
17
|
classify,
|
|
19
18
|
onUnhandledError: resolvedOnUnhandled,
|
|
20
19
|
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { Context, Env } from 'hono';
|
|
2
|
+
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
3
|
+
/**
|
|
4
|
+
* Standard `error` field phrases for common HTTP status codes.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Used by {@link createHttpErrorHandler} for the `error` field on client-error statuses.
|
|
8
|
+
*/
|
|
9
|
+
export declare const HTTP_ERROR_PHRASES: Record<number, string>;
|
|
10
|
+
/**
|
|
11
|
+
* Contextual metadata passed to an {@link ErrorReporter} when reporting an unexpected error.
|
|
12
|
+
*/
|
|
13
|
+
export interface ErrorReportContext {
|
|
14
|
+
/** Correlation id for the failing request, if one is tracked. */
|
|
15
|
+
requestId?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Signature of a function that reports an unexpected (non-HTTP) error to an external sink such as Sentry.
|
|
19
|
+
*
|
|
20
|
+
* @remarks
|
|
21
|
+
* Wire it into {@link createHttpErrorHandler} via `onUnhandledError`, e.g.
|
|
22
|
+
* `(err, c) => reporter(err, { requestId: c.get('requestId') })`. The reporting client itself is
|
|
23
|
+
* intentionally kept out of this kit; the consumer supplies the implementation.
|
|
24
|
+
*
|
|
25
|
+
* @param error - The thrown value being reported.
|
|
26
|
+
* @param context - Optional correlation context for the failing request.
|
|
27
|
+
*/
|
|
28
|
+
export type ErrorReporter = (error: unknown, context?: ErrorReportContext) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Minimal Sentry-like client for {@link createSentryErrorReporter} and {@link createQueueErrorHandler}.
|
|
31
|
+
*
|
|
32
|
+
* @remarks
|
|
33
|
+
* Declared structurally to avoid a hard dependency on `@sentry/cloudflare`.
|
|
34
|
+
*/
|
|
35
|
+
export interface SentryExceptionReporterLike {
|
|
36
|
+
captureException(exception: unknown, captureContext?: {
|
|
37
|
+
tags?: Record<string, string>;
|
|
38
|
+
extra?: Record<string, unknown>;
|
|
39
|
+
}): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Build an {@link ErrorReporter} that forwards unhandled errors to Sentry with an optional `request_id` tag.
|
|
43
|
+
*/
|
|
44
|
+
export declare function createSentryErrorReporter(sentry: SentryExceptionReporterLike): ErrorReporter;
|
|
45
|
+
/**
|
|
46
|
+
* Minimal shape read from a value treated as an HTTP error: its status, message, and optional body.
|
|
47
|
+
*
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
interface HttpErrorLike {
|
|
51
|
+
/** HTTP status code to respond with. */
|
|
52
|
+
status: ContentfulStatusCode;
|
|
53
|
+
/** Human-readable error message placed in the response body. */
|
|
54
|
+
message: string;
|
|
55
|
+
/**
|
|
56
|
+
* Escape hatch for a fully custom response body. When present, it is rendered verbatim instead of
|
|
57
|
+
* the standard error JSON shape.
|
|
58
|
+
*/
|
|
59
|
+
body?: unknown;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Options controlling how {@link createHttpErrorHandler} shapes error responses.
|
|
63
|
+
*
|
|
64
|
+
* @typeParam E - The Hono environment type, so `onUnhandledError` receives a correctly typed context.
|
|
65
|
+
*/
|
|
66
|
+
export interface HttpErrorHandlerOptions<E extends Env = Env> {
|
|
67
|
+
/**
|
|
68
|
+
* Predicate identifying which thrown values are HTTP errors. Defaults to detecting Hono's `HTTPException`.
|
|
69
|
+
* Override it (e.g. `(e) => e instanceof MyHttpError`) when the app throws a custom HTTP error type.
|
|
70
|
+
*/
|
|
71
|
+
isHttpError?: (err: unknown) => err is HttpErrorLike;
|
|
72
|
+
/**
|
|
73
|
+
* Hook invoked before an unexpected (non-HTTP) error is returned as a 500, typically used to report the
|
|
74
|
+
* error (e.g. to Sentry). Any exception thrown by this hook is swallowed so reporting cannot alter the
|
|
75
|
+
* error response.
|
|
76
|
+
*/
|
|
77
|
+
onUnhandledError?: (err: unknown, c: Context<E>) => void;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create a Hono `onError` handler that maps thrown errors to standard API error JSON.
|
|
81
|
+
*
|
|
82
|
+
* @remarks
|
|
83
|
+
* - HTTP errors (by default `HTTPException`) map to `{ statusCode, message, error? }`; `401` omits `error`.
|
|
84
|
+
* - Errors with a custom `body` are returned verbatim.
|
|
85
|
+
* - Unexpected errors trigger `onUnhandledError`, are logged, and return generic 500.
|
|
86
|
+
*
|
|
87
|
+
* @typeParam E - The Hono environment type propagated to `onUnhandledError`.
|
|
88
|
+
* @param options - Optional custom HTTP error detection and reporting hook.
|
|
89
|
+
* @returns A handler suitable for `app.onError(...)`.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* app.onError(
|
|
94
|
+
* createHttpErrorHandler({
|
|
95
|
+
* onUnhandledError: (err, c) => reportError(err, { requestId: c.get('requestId') }),
|
|
96
|
+
* }),
|
|
97
|
+
* );
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export declare function createHttpErrorHandler<E extends Env = Env>(options?: HttpErrorHandlerOptions<E>): (err: Error, c: Context<E>) => Response;
|
|
101
|
+
/**
|
|
102
|
+
* Hono `notFound` handler for unmatched routes.
|
|
103
|
+
*
|
|
104
|
+
* @param c - The Hono request context for the unmatched route.
|
|
105
|
+
* @returns A 404 JSON response: `{ message: 'Cannot METHOD path', error, statusCode }`.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* app.notFound(notFoundHandler);
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export declare function notFoundHandler(c: Context): Response;
|
|
113
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { findMysqlDriverError, logMysqlDriverError } from './mysql-driver-error.js';
|
|
2
|
+
/** Statuses that return only `{ statusCode, message }` (no `error` field). */
|
|
3
|
+
const BARE_STATUSES = [401];
|
|
4
|
+
const INTERNAL_SERVER_ERROR_BODY = { statusCode: 500, message: 'Internal server error' };
|
|
5
|
+
/**
|
|
6
|
+
* Standard `error` field phrases for common HTTP status codes.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Used by {@link createHttpErrorHandler} for the `error` field on client-error statuses.
|
|
10
|
+
*/
|
|
11
|
+
export const HTTP_ERROR_PHRASES = {
|
|
12
|
+
400: 'Bad Request',
|
|
13
|
+
401: 'Unauthorized',
|
|
14
|
+
403: 'Forbidden',
|
|
15
|
+
404: 'Not Found',
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Build an {@link ErrorReporter} that forwards unhandled errors to Sentry with an optional `request_id` tag.
|
|
19
|
+
*/
|
|
20
|
+
export function createSentryErrorReporter(sentry) {
|
|
21
|
+
return (error, context) => {
|
|
22
|
+
sentry.captureException(error, context?.requestId ? { tags: { request_id: context.requestId } } : undefined);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Structurally detect Hono's `HTTPException` without relying on `instanceof`.
|
|
27
|
+
*
|
|
28
|
+
* @remarks
|
|
29
|
+
* When this kit is symlinked into a consumer, the `hono` instance it resolves can differ from the
|
|
30
|
+
* consumer's `hono`, so an `HTTPException` from one copy fails an `instanceof` check against the other.
|
|
31
|
+
* Detecting the presence of a `getResponse()` method and a numeric `status` is stable across module
|
|
32
|
+
* boundaries and production bundles.
|
|
33
|
+
*
|
|
34
|
+
* @param err - The thrown value to test.
|
|
35
|
+
* @returns `true` when `err` looks like a Hono `HTTPException`.
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
const isHTTPException = (err) => err instanceof Error &&
|
|
40
|
+
typeof err.getResponse === 'function' &&
|
|
41
|
+
typeof err.status === 'number';
|
|
42
|
+
/**
|
|
43
|
+
* Create a Hono `onError` handler that maps thrown errors to standard API error JSON.
|
|
44
|
+
*
|
|
45
|
+
* @remarks
|
|
46
|
+
* - HTTP errors (by default `HTTPException`) map to `{ statusCode, message, error? }`; `401` omits `error`.
|
|
47
|
+
* - Errors with a custom `body` are returned verbatim.
|
|
48
|
+
* - Unexpected errors trigger `onUnhandledError`, are logged, and return generic 500.
|
|
49
|
+
*
|
|
50
|
+
* @typeParam E - The Hono environment type propagated to `onUnhandledError`.
|
|
51
|
+
* @param options - Optional custom HTTP error detection and reporting hook.
|
|
52
|
+
* @returns A handler suitable for `app.onError(...)`.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* app.onError(
|
|
57
|
+
* createHttpErrorHandler({
|
|
58
|
+
* onUnhandledError: (err, c) => reportError(err, { requestId: c.get('requestId') }),
|
|
59
|
+
* }),
|
|
60
|
+
* );
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export function createHttpErrorHandler(options = {}) {
|
|
64
|
+
const { isHttpError = isHTTPException, onUnhandledError } = options;
|
|
65
|
+
return (err, c) => {
|
|
66
|
+
if (isHttpError(err)) {
|
|
67
|
+
if (err.body !== undefined) {
|
|
68
|
+
return c.json(err.body, err.status);
|
|
69
|
+
}
|
|
70
|
+
const reason = BARE_STATUSES.includes(err.status)
|
|
71
|
+
? undefined
|
|
72
|
+
: HTTP_ERROR_PHRASES[err.status];
|
|
73
|
+
if (reason === undefined) {
|
|
74
|
+
return c.json({ statusCode: err.status, message: err.message }, err.status);
|
|
75
|
+
}
|
|
76
|
+
return c.json({ statusCode: err.status, message: err.message, error: reason }, err.status);
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
onUnhandledError?.(err, c);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Reporting must never change the behavior of the error response.
|
|
83
|
+
}
|
|
84
|
+
if (findMysqlDriverError(err)) {
|
|
85
|
+
logMysqlDriverError(err, 500);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
console.error(err);
|
|
89
|
+
}
|
|
90
|
+
return c.json(INTERNAL_SERVER_ERROR_BODY, 500);
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Hono `notFound` handler for unmatched routes.
|
|
95
|
+
*
|
|
96
|
+
* @param c - The Hono request context for the unmatched route.
|
|
97
|
+
* @returns A 404 JSON response: `{ message: 'Cannot METHOD path', error, statusCode }`.
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* ```ts
|
|
101
|
+
* app.notFound(notFoundHandler);
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
export function notFoundHandler(c) {
|
|
105
|
+
return c.json({ message: `Cannot ${c.req.method} ${new URL(c.req.url).pathname}`, error: 'Not Found', statusCode: 404 }, 404);
|
|
106
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Context, Env } from 'hono';
|
|
2
|
-
import type {
|
|
3
|
-
/**
|
|
2
|
+
import type { HttpErrorHandlerOptions } from './http-error.js';
|
|
3
|
+
/** DB エラー分類結果: `{ statusCode, message }` 形(error フィールド無し)。 */
|
|
4
4
|
export interface ClassifiedDbError {
|
|
5
5
|
statusCode: 400 | 500;
|
|
6
6
|
message: string;
|
|
@@ -8,22 +8,21 @@ export interface ClassifiedDbError {
|
|
|
8
8
|
/** mysql2 / Drizzle 由来の DB エラーを HTTP 応答用に分類する。非 DB エラーは null。 */
|
|
9
9
|
export type QueryFailedClassifier = (err: unknown) => ClassifiedDbError | null;
|
|
10
10
|
/**
|
|
11
|
-
* Default classifier
|
|
12
|
-
* Maps any mysql2 driver error to generic 500 `{ statusCode, message: 'Internal server error' }`.
|
|
11
|
+
* Default classifier: any mysql2 driver error → generic 500 `{ statusCode, message: 'Internal server error' }`.
|
|
13
12
|
*/
|
|
14
13
|
export declare function classifyGenericMysqlDriverError(err: unknown): ClassifiedDbError | null;
|
|
15
|
-
export interface
|
|
16
|
-
/**
|
|
14
|
+
export interface QueryFailedErrorHandlerOptions<E extends Env = Env> extends HttpErrorHandlerOptions<E> {
|
|
15
|
+
/** アプリ固有の分類(日本語メッセージ等は consumer 側で定義)。 */
|
|
17
16
|
classify: QueryFailedClassifier;
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
19
|
+
* DB エラー分類 → 標準 HTTP エラーハンドラの合成 onError。
|
|
21
20
|
*
|
|
22
21
|
* @remarks
|
|
23
|
-
* classify が non-null
|
|
24
|
-
* 非 DB エラーは {@link
|
|
22
|
+
* classify が non-null のときは分類結果の body を返しつつログ(+ 500 は onUnhandledError)を残す。
|
|
23
|
+
* 非 DB エラーは {@link createHttpErrorHandler} に委譲する。
|
|
25
24
|
*
|
|
26
25
|
* `Sentry.withSentry` だけでは onError 握りエラーは capture されないため、
|
|
27
26
|
* `onUnhandledError: (err, c) => container.reportError?.(err, { requestId: c.get('requestId') })` を必ず配線する。
|
|
28
27
|
*/
|
|
29
|
-
export declare function
|
|
28
|
+
export declare function createQueryFailedErrorHandler<E extends Env = Env>(options: QueryFailedErrorHandlerOptions<E>): (err: Error, c: Context<E>) => Response;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
+
import { createHttpErrorHandler } from './http-error.js';
|
|
1
2
|
import { findMysqlDriverError, logMysqlDriverError } from './mysql-driver-error.js';
|
|
2
|
-
import { createNestErrorHandler } from './nest-error.js';
|
|
3
3
|
/**
|
|
4
|
-
* Default classifier
|
|
5
|
-
* Maps any mysql2 driver error to generic 500 `{ statusCode, message: 'Internal server error' }`.
|
|
4
|
+
* Default classifier: any mysql2 driver error → generic 500 `{ statusCode, message: 'Internal server error' }`.
|
|
6
5
|
*/
|
|
7
6
|
export function classifyGenericMysqlDriverError(err) {
|
|
8
7
|
if (!findMysqlDriverError(err)) {
|
|
@@ -11,7 +10,7 @@ export function classifyGenericMysqlDriverError(err) {
|
|
|
11
10
|
return { statusCode: 500, message: 'Internal server error' };
|
|
12
11
|
}
|
|
13
12
|
/**
|
|
14
|
-
* @internal Used by {@link
|
|
13
|
+
* @internal Used by {@link createQueryFailedErrorHandler} only.
|
|
15
14
|
*/
|
|
16
15
|
function reportClassifiedDbError(err, classified, reportError, requestId) {
|
|
17
16
|
logMysqlDriverError(err, classified.statusCode);
|
|
@@ -20,19 +19,19 @@ function reportClassifiedDbError(err, classified, reportError, requestId) {
|
|
|
20
19
|
}
|
|
21
20
|
}
|
|
22
21
|
/**
|
|
23
|
-
*
|
|
22
|
+
* DB エラー分類 → 標準 HTTP エラーハンドラの合成 onError。
|
|
24
23
|
*
|
|
25
24
|
* @remarks
|
|
26
|
-
* classify が non-null
|
|
27
|
-
* 非 DB エラーは {@link
|
|
25
|
+
* classify が non-null のときは分類結果の body を返しつつログ(+ 500 は onUnhandledError)を残す。
|
|
26
|
+
* 非 DB エラーは {@link createHttpErrorHandler} に委譲する。
|
|
28
27
|
*
|
|
29
28
|
* `Sentry.withSentry` だけでは onError 握りエラーは capture されないため、
|
|
30
29
|
* `onUnhandledError: (err, c) => container.reportError?.(err, { requestId: c.get('requestId') })` を必ず配線する。
|
|
31
30
|
*/
|
|
32
|
-
export function
|
|
33
|
-
const { classify, ...
|
|
34
|
-
const
|
|
35
|
-
const { onUnhandledError } =
|
|
31
|
+
export function createQueryFailedErrorHandler(options) {
|
|
32
|
+
const { classify, ...httpOptions } = options;
|
|
33
|
+
const httpErrorHandler = createHttpErrorHandler(httpOptions);
|
|
34
|
+
const { onUnhandledError } = httpOptions;
|
|
36
35
|
return (err, c) => {
|
|
37
36
|
const classified = classify(err);
|
|
38
37
|
if (classified) {
|
|
@@ -47,6 +46,6 @@ export function createQueryFailedNestErrorHandler(options) {
|
|
|
47
46
|
}
|
|
48
47
|
return c.json({ statusCode: classified.statusCode, message: classified.message }, classified.statusCode);
|
|
49
48
|
}
|
|
50
|
-
return
|
|
49
|
+
return httpErrorHandler(err, c);
|
|
51
50
|
};
|
|
52
51
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
* @packageDocumentation
|
|
11
11
|
*/
|
|
12
12
|
export { finalizeResponse } from './middleware/finalize-response.js';
|
|
13
|
-
export { validate, createValidate
|
|
13
|
+
export { validate, createValidate } from './middleware/validation.js';
|
|
14
|
+
export { createSentryValidate } from './middleware/validation.js';
|
|
14
15
|
export type { ValidateOptions, ValidationTarget, ZodErrorLike, SentryLike, SentryScopeLike, } from './middleware/validation.js';
|
|
15
16
|
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
|
|
16
17
|
export { createAuthMiddleware } from './middleware/auth.js';
|
|
@@ -28,20 +29,20 @@ export type { AppInfo } from './http/app-info.js';
|
|
|
28
29
|
export { resolveAppEnv, isProductionEnv } from './http/app-env.js';
|
|
29
30
|
export type { AppEnv } from './http/app-env.js';
|
|
30
31
|
export { HttpStatus } from './http/http-status.js';
|
|
31
|
-
export {
|
|
32
|
-
export type {
|
|
32
|
+
export { createHttpErrorHandler, notFoundHandler, HTTP_ERROR_PHRASES } from './http/http-error.js';
|
|
33
|
+
export type { HttpErrorHandlerOptions, ErrorReportContext, ErrorReporter } from './http/http-error.js';
|
|
33
34
|
export { findMysqlDriverError, logMysqlDriverError } from './http/mysql-driver-error.js';
|
|
34
35
|
export type { MysqlDriverErrorLike } from './http/mysql-driver-error.js';
|
|
35
|
-
export {
|
|
36
|
-
export type { ClassifiedDbError, QueryFailedClassifier,
|
|
36
|
+
export { createQueryFailedErrorHandler, classifyGenericMysqlDriverError } from './http/query-failed-error.js';
|
|
37
|
+
export type { ClassifiedDbError, QueryFailedClassifier, QueryFailedErrorHandlerOptions, } from './http/query-failed-error.js';
|
|
37
38
|
export { createAppErrorHandler } from './http/app-error-handler.js';
|
|
38
39
|
export type { CreateAppErrorHandlerOptions } from './http/app-error-handler.js';
|
|
39
40
|
export { normalizeTrailingSlash } from './http/trailing-slash.js';
|
|
40
41
|
export type { ExecutionContextLike } from './http/execution-context.js';
|
|
41
42
|
export { defaultDefer, createWaitUntilDefer } from './http/defer.js';
|
|
42
43
|
export type { DeferExecutor } from './http/defer.js';
|
|
43
|
-
export { createSentryErrorReporter } from './http/
|
|
44
|
-
export type { SentryExceptionReporterLike } from './http/
|
|
44
|
+
export { createSentryErrorReporter } from './http/http-error.js';
|
|
45
|
+
export type { SentryExceptionReporterLike } from './http/http-error.js';
|
|
45
46
|
export { KVCache } from './cache/kv-cache.js';
|
|
46
47
|
export type { KVNamespace, KVCacheOptions } from './cache/kv-cache.js';
|
|
47
48
|
export { createStripeClient, verifyStripeWebhook } from './stripe/client.js';
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
// middleware
|
|
13
13
|
export { finalizeResponse } from './middleware/finalize-response.js';
|
|
14
|
-
export { validate, createValidate
|
|
14
|
+
export { validate, createValidate } from './middleware/validation.js';
|
|
15
|
+
// Backward-compat alias; prefer createValidate({ sentry }).
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated -- intentional public re-export
|
|
17
|
+
export { createSentryValidate } from './middleware/validation.js';
|
|
15
18
|
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
|
|
16
19
|
export { createAuthMiddleware } from './middleware/auth.js';
|
|
17
20
|
export { perfLog } from './middleware/perf-log.js';
|
|
@@ -22,13 +25,13 @@ export { getUserProtocol } from './http/user-protocol.js';
|
|
|
22
25
|
export { getAppInfo } from './http/app-info.js';
|
|
23
26
|
export { resolveAppEnv, isProductionEnv } from './http/app-env.js';
|
|
24
27
|
export { HttpStatus } from './http/http-status.js';
|
|
25
|
-
export {
|
|
28
|
+
export { createHttpErrorHandler, notFoundHandler, HTTP_ERROR_PHRASES } from './http/http-error.js';
|
|
26
29
|
export { findMysqlDriverError, logMysqlDriverError } from './http/mysql-driver-error.js';
|
|
27
|
-
export {
|
|
30
|
+
export { createQueryFailedErrorHandler, classifyGenericMysqlDriverError } from './http/query-failed-error.js';
|
|
28
31
|
export { createAppErrorHandler } from './http/app-error-handler.js';
|
|
29
32
|
export { normalizeTrailingSlash } from './http/trailing-slash.js';
|
|
30
33
|
export { defaultDefer, createWaitUntilDefer } from './http/defer.js';
|
|
31
|
-
export { createSentryErrorReporter } from './http/
|
|
34
|
+
export { createSentryErrorReporter } from './http/http-error.js';
|
|
32
35
|
// cache
|
|
33
36
|
export { KVCache } from './cache/kv-cache.js';
|
|
34
37
|
// stripe
|
package/package.json
CHANGED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import type { Context, Env } from 'hono';
|
|
2
|
-
import type { ContentfulStatusCode } from 'hono/utils/http-status';
|
|
3
|
-
/**
|
|
4
|
-
* Reason phrases attached by the NestJS default exception filter, keyed by HTTP status code.
|
|
5
|
-
*
|
|
6
|
-
* @remarks
|
|
7
|
-
* Mirrors the `error` field values NestJS produces for common client-error statuses, so a Hono app can
|
|
8
|
-
* return byte-identical error bodies. Used as the default `reasonPhrases` map by {@link createNestErrorHandler}.
|
|
9
|
-
*/
|
|
10
|
-
export declare const NEST_REASON_PHRASES: Record<number, string>;
|
|
11
|
-
/**
|
|
12
|
-
* Contextual metadata passed to an {@link ErrorReporter} when reporting an unexpected error.
|
|
13
|
-
*/
|
|
14
|
-
export interface ErrorReportContext {
|
|
15
|
-
/** Correlation id for the failing request, if one is tracked. */
|
|
16
|
-
requestId?: string;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Signature of a function that reports an unexpected (non-HTTP) error to an external sink such as Sentry.
|
|
20
|
-
*
|
|
21
|
-
* @remarks
|
|
22
|
-
* Wire it into {@link createNestErrorHandler} via `onUnhandledError`, e.g.
|
|
23
|
-
* `(err, c) => reporter(err, { requestId: c.get('requestId') })`. The reporting client itself is
|
|
24
|
-
* intentionally kept out of this kit; the consumer supplies the implementation.
|
|
25
|
-
*
|
|
26
|
-
* @param error - The thrown value being reported.
|
|
27
|
-
* @param context - Optional correlation context for the failing request.
|
|
28
|
-
*/
|
|
29
|
-
export type ErrorReporter = (error: unknown, context?: ErrorReportContext) => void;
|
|
30
|
-
/**
|
|
31
|
-
* Minimal Sentry-like client for {@link createSentryErrorReporter} and {@link createQueueErrorHandler}.
|
|
32
|
-
*
|
|
33
|
-
* @remarks
|
|
34
|
-
* Declared structurally to avoid a hard dependency on `@sentry/cloudflare`.
|
|
35
|
-
*/
|
|
36
|
-
export interface SentryExceptionReporterLike {
|
|
37
|
-
captureException(exception: unknown, captureContext?: {
|
|
38
|
-
tags?: Record<string, string>;
|
|
39
|
-
extra?: Record<string, unknown>;
|
|
40
|
-
}): void;
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Build an {@link ErrorReporter} that forwards unhandled errors to Sentry with an optional `request_id` tag.
|
|
44
|
-
*/
|
|
45
|
-
export declare function createSentryErrorReporter(sentry: SentryExceptionReporterLike): ErrorReporter;
|
|
46
|
-
/**
|
|
47
|
-
* Minimal shape read from a value treated as an HTTP error: its status, message, and optional body.
|
|
48
|
-
*
|
|
49
|
-
* @internal
|
|
50
|
-
*/
|
|
51
|
-
interface HttpErrorLike {
|
|
52
|
-
/** HTTP status code to respond with. */
|
|
53
|
-
status: ContentfulStatusCode;
|
|
54
|
-
/** Human-readable error message placed in the response body. */
|
|
55
|
-
message: string;
|
|
56
|
-
/**
|
|
57
|
-
* Escape hatch for a fully custom response body. When present, it is rendered verbatim instead of
|
|
58
|
-
* the NestJS-shaped body.
|
|
59
|
-
*/
|
|
60
|
-
body?: unknown;
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Options controlling how {@link createNestErrorHandler} shapes error responses.
|
|
64
|
-
*
|
|
65
|
-
* @typeParam E - The Hono environment type, so `onUnhandledError` receives a correctly typed context.
|
|
66
|
-
*/
|
|
67
|
-
export interface NestErrorHandlerOptions<E extends Env = Env> {
|
|
68
|
-
/** Status-to-reason-phrase map for the `error` field. Defaults to {@link NEST_REASON_PHRASES}. */
|
|
69
|
-
reasonPhrases?: Record<number, string>;
|
|
70
|
-
/**
|
|
71
|
-
* Statuses that return only `{ statusCode, message }`, omitting the `error` field. Defaults to `[401]`,
|
|
72
|
-
* matching NestJS where a generic `HttpException(msg, 401)` carries no `error`.
|
|
73
|
-
*/
|
|
74
|
-
bareStatuses?: readonly number[];
|
|
75
|
-
/**
|
|
76
|
-
* Field order of the non-bare error body. Defaults to `'statusCode-first'` (the NestJS canonical order).
|
|
77
|
-
* Use `'message-first'` to emit `{ message, error, statusCode }` when byte parity requires it.
|
|
78
|
-
*/
|
|
79
|
-
fieldOrder?: 'statusCode-first' | 'message-first';
|
|
80
|
-
/**
|
|
81
|
-
* Fallback `error` value for statuses that are neither bare nor present in `reasonPhrases`. Defaults to
|
|
82
|
-
* `undefined`, meaning the `error` field is omitted when no reason phrase is known. Set to a string such
|
|
83
|
-
* as `'Error'` to always include an `error` field, faithfully reproducing the NestJS default exception
|
|
84
|
-
* filter behavior where `error` is always present.
|
|
85
|
-
*/
|
|
86
|
-
fallbackReason?: string;
|
|
87
|
-
/**
|
|
88
|
-
* Predicate identifying which thrown values are HTTP errors. Defaults to detecting Hono's `HTTPException`.
|
|
89
|
-
* Override it (e.g. `(e) => e instanceof MyHttpError`) when the app throws a custom HTTP error type.
|
|
90
|
-
*/
|
|
91
|
-
isHttpError?: (err: unknown) => err is HttpErrorLike;
|
|
92
|
-
/**
|
|
93
|
-
* Hook invoked before an unexpected (non-HTTP) error is returned as a 500, typically used to report the
|
|
94
|
-
* error (e.g. to Sentry). Any exception thrown by this hook is swallowed so reporting cannot alter the
|
|
95
|
-
* error response.
|
|
96
|
-
*/
|
|
97
|
-
onUnhandledError?: (err: unknown, c: Context<E>) => void;
|
|
98
|
-
/**
|
|
99
|
-
* Response body for unexpected errors returned as 500. Defaults to
|
|
100
|
-
* `{ statusCode: 500, message: 'Internal server error' }`.
|
|
101
|
-
*/
|
|
102
|
-
internalServerErrorBody?: unknown;
|
|
103
|
-
}
|
|
104
|
-
/**
|
|
105
|
-
* Create a Hono `onError` handler that maps thrown errors to NestJS-shaped error JSON.
|
|
106
|
-
*
|
|
107
|
-
* @remarks
|
|
108
|
-
* Reproduces the NestJS default exception filter so a Hono app returns byte-identical error bodies:
|
|
109
|
-
* - HTTP errors (by default `HTTPException`) are mapped to a NestJS-shaped body; if the error carries a
|
|
110
|
-
* custom `body`, that body is returned verbatim.
|
|
111
|
-
* - Statuses listed in `bareStatuses` (default `[401]`) omit the `error` field.
|
|
112
|
-
* - Any other (unexpected) error triggers `onUnhandledError`, is logged via `console.error`, and returns 500.
|
|
113
|
-
*
|
|
114
|
-
* Per-app differences in body field order, HTTP error type, and reporting hook are absorbed through
|
|
115
|
-
* {@link NestErrorHandlerOptions}, while the branching logic stays shared.
|
|
116
|
-
*
|
|
117
|
-
* @typeParam E - The Hono environment type propagated to `onUnhandledError`.
|
|
118
|
-
* @param options - Overrides for reason phrases, bare statuses, field order, error detection, and reporting.
|
|
119
|
-
* @returns A handler suitable for `app.onError(...)`.
|
|
120
|
-
*
|
|
121
|
-
* @example
|
|
122
|
-
* ```ts
|
|
123
|
-
* app.onError(
|
|
124
|
-
* createNestErrorHandler({
|
|
125
|
-
* fieldOrder: 'message-first',
|
|
126
|
-
* fallbackReason: 'Error',
|
|
127
|
-
* onUnhandledError: (err, c) => reportError(err, { requestId: c.get('requestId') }),
|
|
128
|
-
* }),
|
|
129
|
-
* );
|
|
130
|
-
* ```
|
|
131
|
-
*/
|
|
132
|
-
export declare function createNestErrorHandler<E extends Env = Env>(options?: NestErrorHandlerOptions<E>): (err: Error, c: Context<E>) => Response;
|
|
133
|
-
/**
|
|
134
|
-
* Hono `notFound` handler that returns the canonical Express/NestJS unmatched-route 404 body.
|
|
135
|
-
*
|
|
136
|
-
* @remarks
|
|
137
|
-
* Produces `{ message: "Cannot <METHOD> <path>", error: 'Not Found', statusCode: 404 }`, matching the
|
|
138
|
-
* NestJS default 404 response so unmatched routes stay byte-identical.
|
|
139
|
-
*
|
|
140
|
-
* @param c - The Hono request context for the unmatched route.
|
|
141
|
-
* @returns A 404 JSON response.
|
|
142
|
-
*
|
|
143
|
-
* @example
|
|
144
|
-
* ```ts
|
|
145
|
-
* app.notFound(nestNotFoundHandler);
|
|
146
|
-
* ```
|
|
147
|
-
*/
|
|
148
|
-
export declare function nestNotFoundHandler(c: Context): Response;
|
|
149
|
-
export {};
|
package/dist/http/nest-error.js
DELETED
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { findMysqlDriverError, logMysqlDriverError } from './mysql-driver-error.js';
|
|
2
|
-
/**
|
|
3
|
-
* Reason phrases attached by the NestJS default exception filter, keyed by HTTP status code.
|
|
4
|
-
*
|
|
5
|
-
* @remarks
|
|
6
|
-
* Mirrors the `error` field values NestJS produces for common client-error statuses, so a Hono app can
|
|
7
|
-
* return byte-identical error bodies. Used as the default `reasonPhrases` map by {@link createNestErrorHandler}.
|
|
8
|
-
*/
|
|
9
|
-
export const NEST_REASON_PHRASES = {
|
|
10
|
-
400: 'Bad Request',
|
|
11
|
-
401: 'Unauthorized',
|
|
12
|
-
403: 'Forbidden',
|
|
13
|
-
404: 'Not Found',
|
|
14
|
-
};
|
|
15
|
-
/**
|
|
16
|
-
* Build an {@link ErrorReporter} that forwards unhandled errors to Sentry with an optional `request_id` tag.
|
|
17
|
-
*/
|
|
18
|
-
export function createSentryErrorReporter(sentry) {
|
|
19
|
-
return (error, context) => {
|
|
20
|
-
sentry.captureException(error, context?.requestId ? { tags: { request_id: context.requestId } } : undefined);
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Structurally detect Hono's `HTTPException` without relying on `instanceof`.
|
|
25
|
-
*
|
|
26
|
-
* @remarks
|
|
27
|
-
* When this kit is symlinked into a consumer, the `hono` instance it resolves can differ from the
|
|
28
|
-
* consumer's `hono`, so an `HTTPException` from one copy fails an `instanceof` check against the other.
|
|
29
|
-
* Detecting the presence of a `getResponse()` method and a numeric `status` is stable across module
|
|
30
|
-
* boundaries and production bundles.
|
|
31
|
-
*
|
|
32
|
-
* @param err - The thrown value to test.
|
|
33
|
-
* @returns `true` when `err` looks like a Hono `HTTPException`.
|
|
34
|
-
*
|
|
35
|
-
* @internal
|
|
36
|
-
*/
|
|
37
|
-
const isHTTPException = (err) => err instanceof Error &&
|
|
38
|
-
typeof err.getResponse === 'function' &&
|
|
39
|
-
typeof err.status === 'number';
|
|
40
|
-
/**
|
|
41
|
-
* Create a Hono `onError` handler that maps thrown errors to NestJS-shaped error JSON.
|
|
42
|
-
*
|
|
43
|
-
* @remarks
|
|
44
|
-
* Reproduces the NestJS default exception filter so a Hono app returns byte-identical error bodies:
|
|
45
|
-
* - HTTP errors (by default `HTTPException`) are mapped to a NestJS-shaped body; if the error carries a
|
|
46
|
-
* custom `body`, that body is returned verbatim.
|
|
47
|
-
* - Statuses listed in `bareStatuses` (default `[401]`) omit the `error` field.
|
|
48
|
-
* - Any other (unexpected) error triggers `onUnhandledError`, is logged via `console.error`, and returns 500.
|
|
49
|
-
*
|
|
50
|
-
* Per-app differences in body field order, HTTP error type, and reporting hook are absorbed through
|
|
51
|
-
* {@link NestErrorHandlerOptions}, while the branching logic stays shared.
|
|
52
|
-
*
|
|
53
|
-
* @typeParam E - The Hono environment type propagated to `onUnhandledError`.
|
|
54
|
-
* @param options - Overrides for reason phrases, bare statuses, field order, error detection, and reporting.
|
|
55
|
-
* @returns A handler suitable for `app.onError(...)`.
|
|
56
|
-
*
|
|
57
|
-
* @example
|
|
58
|
-
* ```ts
|
|
59
|
-
* app.onError(
|
|
60
|
-
* createNestErrorHandler({
|
|
61
|
-
* fieldOrder: 'message-first',
|
|
62
|
-
* fallbackReason: 'Error',
|
|
63
|
-
* onUnhandledError: (err, c) => reportError(err, { requestId: c.get('requestId') }),
|
|
64
|
-
* }),
|
|
65
|
-
* );
|
|
66
|
-
* ```
|
|
67
|
-
*/
|
|
68
|
-
export function createNestErrorHandler(options = {}) {
|
|
69
|
-
const { reasonPhrases = NEST_REASON_PHRASES, bareStatuses = [401], fieldOrder = 'statusCode-first', isHttpError = isHTTPException, onUnhandledError, internalServerErrorBody = { statusCode: 500, message: 'Internal server error' }, fallbackReason, } = options;
|
|
70
|
-
return (err, c) => {
|
|
71
|
-
if (isHttpError(err)) {
|
|
72
|
-
// Escape hatch for a custom error body: render it verbatim.
|
|
73
|
-
if (err.body !== undefined) {
|
|
74
|
-
return c.json(err.body, err.status);
|
|
75
|
-
}
|
|
76
|
-
// reasonPhrases[status] is typed as string, but with noUncheckedIndexedAccess disabled it can be
|
|
77
|
-
// undefined at runtime. The fallbackReason fallback for unregistered statuses is intentional.
|
|
78
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
79
|
-
const reason = bareStatuses.includes(err.status) ? undefined : (reasonPhrases[err.status] ?? fallbackReason);
|
|
80
|
-
if (reason === undefined) {
|
|
81
|
-
return c.json({ statusCode: err.status, message: err.message }, err.status);
|
|
82
|
-
}
|
|
83
|
-
const body = fieldOrder === 'message-first'
|
|
84
|
-
? { message: err.message, error: reason, statusCode: err.status }
|
|
85
|
-
: { statusCode: err.status, message: err.message, error: reason };
|
|
86
|
-
return c.json(body, err.status);
|
|
87
|
-
}
|
|
88
|
-
try {
|
|
89
|
-
onUnhandledError?.(err, c);
|
|
90
|
-
}
|
|
91
|
-
catch {
|
|
92
|
-
// Reporting must never change the behavior of the error response.
|
|
93
|
-
}
|
|
94
|
-
if (findMysqlDriverError(err)) {
|
|
95
|
-
logMysqlDriverError(err, 500);
|
|
96
|
-
}
|
|
97
|
-
else {
|
|
98
|
-
console.error(err);
|
|
99
|
-
}
|
|
100
|
-
return c.json(internalServerErrorBody, 500);
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
/**
|
|
104
|
-
* Hono `notFound` handler that returns the canonical Express/NestJS unmatched-route 404 body.
|
|
105
|
-
*
|
|
106
|
-
* @remarks
|
|
107
|
-
* Produces `{ message: "Cannot <METHOD> <path>", error: 'Not Found', statusCode: 404 }`, matching the
|
|
108
|
-
* NestJS default 404 response so unmatched routes stay byte-identical.
|
|
109
|
-
*
|
|
110
|
-
* @param c - The Hono request context for the unmatched route.
|
|
111
|
-
* @returns A 404 JSON response.
|
|
112
|
-
*
|
|
113
|
-
* @example
|
|
114
|
-
* ```ts
|
|
115
|
-
* app.notFound(nestNotFoundHandler);
|
|
116
|
-
* ```
|
|
117
|
-
*/
|
|
118
|
-
export function nestNotFoundHandler(c) {
|
|
119
|
-
return c.json({ message: `Cannot ${c.req.method} ${new URL(c.req.url).pathname}`, error: 'Not Found', statusCode: 404 }, 404);
|
|
120
|
-
}
|