@rdlabo/workers-hono-kit 0.5.0 → 0.6.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/README.md +63 -25
- package/dist/container/isolate-memo.d.ts +35 -0
- package/dist/container/isolate-memo.js +45 -0
- package/dist/container/middleware.d.ts +45 -0
- package/dist/container/middleware.js +26 -0
- package/dist/http/app-error-handler.d.ts +28 -0
- package/dist/http/app-error-handler.js +21 -0
- package/dist/http/defer.d.ts +21 -0
- package/dist/http/defer.js +17 -0
- package/dist/http/nest-error.d.ts +16 -0
- package/dist/http/nest-error.js +8 -0
- package/dist/http/query-failed-error.d.ts +4 -4
- package/dist/http/query-failed-error.js +14 -5
- package/dist/index.d.ts +14 -2
- package/dist/index.js +8 -2
- package/dist/middleware/validation.d.ts +10 -39
- package/dist/middleware/validation.js +15 -19
- package/dist/queue/error-handler.d.ts +24 -0
- package/dist/queue/error-handler.js +21 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,8 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
|
|
|
52
52
|
| --- | --- |
|
|
53
53
|
| `finalizeResponse()` | Middleware that adds an Express-compatible weak `ETag` and JSON `charset=utf-8`. |
|
|
54
54
|
| `validate(target, schema, options?)` | Zod validator → NestJS `ValidationPipe`-shaped `400` (`{ statusCode, message[], error }`). `options.onValidationError(err, c)` to report (e.g. Sentry). |
|
|
55
|
-
| `
|
|
55
|
+
| `createValidate({ sentry? })` | Bound `validate` factory. Pass `sentry` on Sentry apps; omit for console-only (review, cbs-ai). |
|
|
56
|
+
| `createSentryValidate(sentry)` | **Deprecated** — use `createValidate({ sentry })`. |
|
|
56
57
|
| `zNum` / `zNumWithDefault` / `zNumOptional` / `zNumNullable` | Number-coercion zod schemas (mirror class-transformer `@Transform`). |
|
|
57
58
|
| `getAuthenticationSecret<T>(options, secretId)` / `AwsSecretsOptions` | Fetch a secret from AWS Secrets Manager (SigV4 `fetch`, per-isolate cache). |
|
|
58
59
|
| `getCloudFrontSignedUrl(url, privateKeyPem, keyPairId, dateLessThan)` | CloudFront signed URL (canned policy, RSA-SHA1, URL-safe base64) — Web Crypto reimpl of `@aws-sdk/cloudfront-signer`, byte-identical query order. |
|
|
@@ -66,19 +67,24 @@ npm install ai ai-gateway-provider # createAiGatewayProvider
|
|
|
66
67
|
| `resolveAppEnv(env)` / `isProductionEnv(env)` / `AppEnv` | Resolve `'development'` / `'production'` from `env.APP_ENV` (defaults to `'production'` for safety). |
|
|
67
68
|
| `HttpStatus` | HTTP status enum identical to NestJS `@nestjs/common`. |
|
|
68
69
|
| `createNestErrorHandler(options?)` / `NestErrorHandlerOptions` | `app.onError()` handler that maps a thrown `HTTPException` to the NestJS exception-filter body (`{ statusCode, message, error? }`; `401` omits `error`). Configurable field order, reason phrases, error predicate, and unhandled-error report hook. Unhandled errors log via `console.error` (mysql2 errors include `sqlMessage` / `errno` when detectable). |
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
70
|
+
| `createAppErrorHandler(options?)` / `CreateAppErrorHandlerOptions` | Standard `app.onError`: {@link createQueryFailedNestErrorHandler} + default {@link classifyGenericMysqlDriverError} + optional `sentry` (Sentry apps), `getReportError` / `reportError` (tests / container), or neither (no external reporting). |
|
|
71
|
+
| `createQueryFailedNestErrorHandler(options)` / `QueryFailedClassifier` / `ClassifiedDbError` | Lower-level compose when you need full control over `classify` + `onUnhandledError` without defaults. |
|
|
72
|
+
| `classifyGenericMysqlDriverError(err)` | Default classifier for apps without Nest filter parity: 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 parity). |
|
|
71
74
|
| `nestNotFoundHandler(c)` | `app.notFound()` handler with the Express/Nest default `{ message: 'Cannot METHOD path', error, statusCode }` 404 body. |
|
|
72
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. |
|
|
73
76
|
| `NEST_REASON_PHRASES` | `{ 400, 401, 403, 404 }` → NestJS reason phrases. |
|
|
74
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. |
|
|
75
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`. |
|
|
76
79
|
| `ErrorReporter` / `ErrorReportContext` | Types for a `reportError`-style unhandled-error reporter (e.g. wired to Sentry), paired with `createNestErrorHandler`'s `onUnhandledError`. |
|
|
80
|
+
| `createSentryErrorReporter(sentry)` / `SentryExceptionReporterLike` | Build an `ErrorReporter` that forwards to Sentry with an optional `request_id` tag (no hard `@sentry/cloudflare` dependency). |
|
|
81
|
+
| `DeferExecutor` / `defaultDefer` / `createWaitUntilDefer(ctx)` | Fire-and-forget executor for Workers: `defaultDefer` swallows rejections (tests); `createWaitUntilDefer` registers work via `ctx.waitUntil`. |
|
|
77
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`). |
|
|
78
83
|
| `KVCache` / `KVNamespace` / `KVCacheOptions` | Workers-KV cache-aside helper (key `appName+version+table_type_column`, sha256 for string ids, TTL clamped ≥60s). Set `appName` / `version` per application. |
|
|
79
84
|
| `createStripeClient(secret, opts?)` / `verifyStripeWebhook(...)` / `CreateStripeClientOptions` | Workers-native Stripe client (fetch transport) + async webhook verification (SubtleCrypto). `apiVersion` optional (pin to a fixed Stripe API version). |
|
|
80
85
|
| `sendInChunks(queue, messages, options?)` / `QueueLike` / `QueueSendMessage` | Send queue messages in bounded chunks to stay under the Workers subrequest cap per invocation. `options.chunkSize` sets the per-batch size (defaults to and is capped at 100). |
|
|
81
86
|
| `processBatch(batch, handler, options?)` / `MessageBatchLike` / `QueueMessageLike` / `ProcessBatchOptions` / `ProcessBatchResult` | Process a queue batch with bounded concurrency (consumer-side counterpart to `sendInChunks`). |
|
|
87
|
+
| `createQueueErrorHandler(options)` / `CreateQueueErrorHandlerOptions` | Factory for `processBatch`'s `onError`: logs every failure; optional Sentry capture with queue/message context; optional `maxRetries` gate (report only on final attempt). |
|
|
82
88
|
| `ExecutionContextLike` | Minimal `waitUntil`-only Workers execution context shape (for `withMysqlConnections` in worker entry modules without importing `./db`). |
|
|
83
89
|
|
|
84
90
|
### Data layer — `@rdlabo/workers-hono-kit/db`
|
|
@@ -284,40 +290,71 @@ body, and `nestNotFoundHandler` gives the Express/Nest default 404. The defaults
|
|
|
284
290
|
NestJS canonical shape (`{ statusCode, message, error? }`, `401` omits `error`); the options
|
|
285
291
|
let you reproduce any byte-for-byte variation an existing API expects.
|
|
286
292
|
|
|
293
|
+
#### App entry (fleet standard)
|
|
294
|
+
|
|
295
|
+
Use a **singleton** Hono app and inject the request-scoped container in middleware — do **not**
|
|
296
|
+
call `createApp(container).fetch(...)` on every request (rebuilds the route graph each time).
|
|
297
|
+
|
|
287
298
|
```ts
|
|
288
|
-
|
|
299
|
+
// worker.ts — once per isolate
|
|
300
|
+
const app = createApp();
|
|
301
|
+
export default Sentry.withSentry(/* … */, {
|
|
302
|
+
fetch: (req, env, ctx) => app.fetch(req, env, ctx),
|
|
303
|
+
});
|
|
289
304
|
|
|
290
|
-
app.
|
|
291
|
-
|
|
305
|
+
// app.ts — fleet-standard onError (Sentry optional)
|
|
306
|
+
import * as Sentry from '@sentry/cloudflare';
|
|
307
|
+
import { createAppErrorHandler } from '@rdlabo/workers-hono-kit';
|
|
292
308
|
|
|
293
|
-
// Application-specific parity deltas:
|
|
294
309
|
app.onError(
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
isHttpError: (e): e is HttpError => e instanceof HttpError, // a custom error class with a `.body` escape hatch
|
|
310
|
+
createAppErrorHandler({
|
|
311
|
+
sentry: Sentry, // omit on repos without Sentry (airlec, review, cbs-ai)
|
|
312
|
+
getReportError: (c) => c.get('container')?.reportError, // tests + scheduled paths
|
|
299
313
|
}),
|
|
300
314
|
);
|
|
315
|
+
|
|
316
|
+
// odss-mobile: add classify: classifyQueryFailed (repo parity)
|
|
317
|
+
// winecode: sentry + isHttpError / reasonPhrases in errors.ts (no container middleware)
|
|
318
|
+
// foodlabel: sentry + reportError: container.reportError (per-request container closure)
|
|
301
319
|
```
|
|
302
320
|
|
|
303
|
-
|
|
304
|
-
`
|
|
321
|
+
Reference: `winecode/hono` (singleton + container middleware). Legacy repos still using
|
|
322
|
+
per-request `createApp(container)` should migrate to this shape where possible.
|
|
305
323
|
|
|
306
|
-
|
|
307
|
-
`createQueryFailedNestErrorHandler` so classified DB errors still log and report to Sentry:
|
|
324
|
+
**Isolate-scoped memo + container runtime** (shared across the fleet):
|
|
308
325
|
|
|
309
326
|
```ts
|
|
310
|
-
import {
|
|
327
|
+
import { createContainerRuntime, createIsolateMemo } from '@rdlabo/workers-hono-kit';
|
|
311
328
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
})
|
|
318
|
-
);
|
|
329
|
+
// Secrets / env: cache successes per isolate; rejections are NOT cached (retry on next request).
|
|
330
|
+
const resolveSecrets = createIsolateMemo(async (env: Env) => { /* SM or env vars */ });
|
|
331
|
+
|
|
332
|
+
const { middleware: containerMiddleware, withContainer } = createContainerRuntime<Env, Container>({
|
|
333
|
+
hyperdrives: (env) => ({ primary: env.HYPERDRIVE_PRIMARY, replica: env.HYPERDRIVE_REPLICA }),
|
|
334
|
+
createContainer: async ({ env, executionCtx, primary, replica }) => {
|
|
335
|
+
const secret = await resolveSecrets(env);
|
|
336
|
+
return buildContainer({ /* db from primary/replica, secret, … */ });
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
Use `withContainer` from `scheduled` / `queue` handlers; use `containerMiddleware` in `createApp`.
|
|
342
|
+
|
|
343
|
+
```ts
|
|
344
|
+
import { createNestErrorHandler, nestNotFoundHandler } from '@rdlabo/workers-hono-kit';
|
|
345
|
+
|
|
346
|
+
app.notFound(nestNotFoundHandler);
|
|
347
|
+
|
|
348
|
+
// Prefer createAppErrorHandler (see "App entry" above). Lower-level only when needed:
|
|
349
|
+
app.onError(createNestErrorHandler());
|
|
319
350
|
```
|
|
320
351
|
|
|
352
|
+
**Important:** `Sentry.withSentry` does **not** capture errors handled by `app.onError`. Pass `sentry`
|
|
353
|
+
to `createAppErrorHandler` (or wire `getReportError` / `reportError` for tests and scheduled paths).
|
|
354
|
+
|
|
355
|
+
Repos with a Nest `QueryFailedExceptionFilter` parity layer (e.g. odss-mobile) pass `classify` to
|
|
356
|
+
`createAppErrorHandler` — do not call `createQueryFailedNestErrorHandler` directly unless you need full control.
|
|
357
|
+
|
|
321
358
|
### Auth middleware
|
|
322
359
|
|
|
323
360
|
Encodes the shared skeleton (read token header → verify → `getAppInfo` → resolve user id →
|
|
@@ -364,7 +401,8 @@ import { perfLog } from '@rdlabo/workers-hono-kit';
|
|
|
364
401
|
// dataset binding) and `PERF_LOG === '1'` (Workers Logs) off `c.env`.
|
|
365
402
|
app.use('*', perfLog());
|
|
366
403
|
|
|
367
|
-
// B)
|
|
404
|
+
// B) bindings not on Hono env (legacy per-request createApp): pass explicitly — prefer fleet
|
|
405
|
+
// standard singleton app + container middleware so env is always on `c.env`.
|
|
368
406
|
app.use('*', perfLog({ console: env.PERF_LOG === '1', dataset: env.PERF }));
|
|
369
407
|
```
|
|
370
408
|
|
|
@@ -386,7 +424,7 @@ GROUP BY path, colo ORDER BY p90 DESC
|
|
|
386
424
|
```
|
|
387
425
|
|
|
388
426
|
> **Scope of `t_app`**: it covers everything *inside* the app; work done in `fetch` *before* the app
|
|
389
|
-
> (e.g. secrets fetch / DB connect in
|
|
427
|
+
> (e.g. secrets fetch / DB connect in container middleware vs. building the container in `worker.fetch`) is
|
|
390
428
|
> not comparable across differently-wired apps. Instrument the `fetch` seam if you need a secrets/connect
|
|
391
429
|
> cold breakdown. On production Workers `Date.now()` only advances at I/O boundaries, so `t_app` ≈ I/O
|
|
392
430
|
> wait, not CPU time.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isolate-scoped async memoization for container bootstrap (secrets, env resolution, etc.).
|
|
3
|
+
*
|
|
4
|
+
* Successful results are reused across requests in the same isolate. Rejected initializations are
|
|
5
|
+
* **not** cached so a transient failure (e.g. Secrets Manager blip) can be retried on the next call.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The memo is not keyed by argument: the loader runs once per isolate using the arguments from the
|
|
9
|
+
* first successful scheduling attempt (same semantics as hand-rolled `let promise` in Workers apps).
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
/** Callable memo with an explicit reset for tests and isolate teardown. */
|
|
14
|
+
export interface IsolateMemo<T, TArg> {
|
|
15
|
+
(arg: TArg): Promise<T>;
|
|
16
|
+
reset(): void;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create an isolate-scoped memoized async resolver.
|
|
20
|
+
*
|
|
21
|
+
* @param loader - async factory invoked on the first call (and again after a rejection / {@link reset}).
|
|
22
|
+
* @returns a function that returns the cached promise, plus {@link IsolateMemo.reset}.
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const resolveSecrets = createIsolateMemo(async (env: Env) => {
|
|
26
|
+
* const secret = await getAuthenticationSecret(awsOpts(env));
|
|
27
|
+
* return { firebaseSaJson: secret.firebaseProduction };
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* // first request populates the cache; SM failure is not cached:
|
|
31
|
+
* await resolveSecrets(env).catch(() => undefined);
|
|
32
|
+
* await resolveSecrets(env); // retries SM
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function createIsolateMemo<T, TArg>(loader: (arg: TArg) => Promise<T>): IsolateMemo<T, TArg>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Isolate-scoped async memoization for container bootstrap (secrets, env resolution, etc.).
|
|
3
|
+
*
|
|
4
|
+
* Successful results are reused across requests in the same isolate. Rejected initializations are
|
|
5
|
+
* **not** cached so a transient failure (e.g. Secrets Manager blip) can be retried on the next call.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The memo is not keyed by argument: the loader runs once per isolate using the arguments from the
|
|
9
|
+
* first successful scheduling attempt (same semantics as hand-rolled `let promise` in Workers apps).
|
|
10
|
+
*
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Create an isolate-scoped memoized async resolver.
|
|
15
|
+
*
|
|
16
|
+
* @param loader - async factory invoked on the first call (and again after a rejection / {@link reset}).
|
|
17
|
+
* @returns a function that returns the cached promise, plus {@link IsolateMemo.reset}.
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* const resolveSecrets = createIsolateMemo(async (env: Env) => {
|
|
21
|
+
* const secret = await getAuthenticationSecret(awsOpts(env));
|
|
22
|
+
* return { firebaseSaJson: secret.firebaseProduction };
|
|
23
|
+
* });
|
|
24
|
+
*
|
|
25
|
+
* // first request populates the cache; SM failure is not cached:
|
|
26
|
+
* await resolveSecrets(env).catch(() => undefined);
|
|
27
|
+
* await resolveSecrets(env); // retries SM
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function createIsolateMemo(loader) {
|
|
31
|
+
let cached;
|
|
32
|
+
const resolve = (arg) => {
|
|
33
|
+
if (!cached) {
|
|
34
|
+
cached = loader(arg).catch((error) => {
|
|
35
|
+
cached = undefined;
|
|
36
|
+
throw error;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return cached;
|
|
40
|
+
};
|
|
41
|
+
resolve.reset = () => {
|
|
42
|
+
cached = undefined;
|
|
43
|
+
};
|
|
44
|
+
return resolve;
|
|
45
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Env, MiddlewareHandler } from 'hono';
|
|
2
|
+
import type { Connection } from 'mysql2/promise';
|
|
3
|
+
import type { HyperdriveLike } from '../db/connection.js';
|
|
4
|
+
import type { ExecutionContextLike } from '../http/execution-context.js';
|
|
5
|
+
/** Inputs available while building a per-request application container. */
|
|
6
|
+
export interface ContainerBuildContext<TEnv extends Env['Bindings']> {
|
|
7
|
+
env: TEnv;
|
|
8
|
+
executionCtx: ExecutionContextLike;
|
|
9
|
+
primary: Connection;
|
|
10
|
+
replica: Connection;
|
|
11
|
+
}
|
|
12
|
+
/** Options for {@link createContainerRuntime}. */
|
|
13
|
+
export interface ContainerRuntimeOptions<TEnv extends Env['Bindings'], TContainer> {
|
|
14
|
+
/** Resolve Hyperdrive bindings from Worker env. */
|
|
15
|
+
hyperdrives: (env: TEnv) => {
|
|
16
|
+
primary: HyperdriveLike;
|
|
17
|
+
replica: HyperdriveLike;
|
|
18
|
+
};
|
|
19
|
+
/** Forwarded to {@link withMysqlConnections} (e.g. `{ dateStrings: true }`). */
|
|
20
|
+
connectionOptions?: Record<string, unknown>;
|
|
21
|
+
/** Build the request-scoped container after primary/replica connections are open. */
|
|
22
|
+
createContainer: (ctx: ContainerBuildContext<TEnv>) => TContainer | Promise<TContainer>;
|
|
23
|
+
}
|
|
24
|
+
/** Pair returned by {@link createContainerRuntime}. */
|
|
25
|
+
export interface ContainerRuntime<TEnv extends Env['Bindings'], TContainer> {
|
|
26
|
+
/** Hono middleware: `c.set('container', …)` then `next()`. Honors test overrides. */
|
|
27
|
+
middleware: (overrides?: {
|
|
28
|
+
container?: TContainer;
|
|
29
|
+
}) => MiddlewareHandler<{
|
|
30
|
+
Bindings: TEnv;
|
|
31
|
+
Variables: {
|
|
32
|
+
container: TContainer;
|
|
33
|
+
};
|
|
34
|
+
}>;
|
|
35
|
+
/** Shared entry for `scheduled` / `queue` handlers (same connection lifecycle as middleware). */
|
|
36
|
+
withContainer: <T>(env: TEnv, executionCtx: ExecutionContextLike, fn: (container: TContainer) => Promise<T>) => Promise<T>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Standard singleton-app container wiring: per-request Hyperdrive connections + `c.set('container')`.
|
|
40
|
+
*
|
|
41
|
+
* @remarks
|
|
42
|
+
* Isolate-scoped memoization (secrets, env) stays in the app via {@link createIsolateMemo} inside
|
|
43
|
+
* `createContainer` or a helper it calls — this factory only owns the per-request DB lifecycle.
|
|
44
|
+
*/
|
|
45
|
+
export declare function createContainerRuntime<TEnv extends Env['Bindings'], TContainer>(options: ContainerRuntimeOptions<TEnv, TContainer>): ContainerRuntime<TEnv, TContainer>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { withMysqlConnections } from '../db/connection.js';
|
|
2
|
+
/**
|
|
3
|
+
* Standard singleton-app container wiring: per-request Hyperdrive connections + `c.set('container')`.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* Isolate-scoped memoization (secrets, env) stays in the app via {@link createIsolateMemo} inside
|
|
7
|
+
* `createContainer` or a helper it calls — this factory only owns the per-request DB lifecycle.
|
|
8
|
+
*/
|
|
9
|
+
export function createContainerRuntime(options) {
|
|
10
|
+
const withContainer = async (env, executionCtx, fn) => withMysqlConnections(options.hyperdrives(env), executionCtx, async ({ primary, replica }) => {
|
|
11
|
+
const container = await options.createContainer({ env, executionCtx, primary, replica });
|
|
12
|
+
return fn(container);
|
|
13
|
+
}, options.connectionOptions);
|
|
14
|
+
const middleware = (overrides) => (async (c, next) => {
|
|
15
|
+
if (overrides?.container) {
|
|
16
|
+
c.set('container', overrides.container);
|
|
17
|
+
await next();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
await withContainer(c.env, c.executionCtx, async (container) => {
|
|
21
|
+
c.set('container', container);
|
|
22
|
+
await next();
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
return { middleware, withContainer };
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Context, Env } from 'hono';
|
|
2
|
+
import { type QueryFailedClassifier } from './query-failed-error.js';
|
|
3
|
+
import type { ErrorReporter, NestErrorHandlerOptions, SentryExceptionReporterLike } from './nest-error.js';
|
|
4
|
+
/**
|
|
5
|
+
* Options for {@link createAppErrorHandler}.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Wires `createQueryFailedNestErrorHandler` with fleet defaults (`fieldOrder: 'message-first'`,
|
|
9
|
+
* {@link classifyGenericMysqlDriverError}) and optional error reporting.
|
|
10
|
+
* Pass `sentry` for Sentry-backed apps; omit it (or pass `undefined`) when not used.
|
|
11
|
+
* `getReportError` / `reportError` override `sentry` (tests, container injection, scheduled paths).
|
|
12
|
+
*/
|
|
13
|
+
export interface CreateAppErrorHandlerOptions<E extends Env = Env> extends Omit<NestErrorHandlerOptions<E>, 'onUnhandledError'> {
|
|
14
|
+
/** mysql2 driver error classifier. Defaults to {@link classifyGenericMysqlDriverError}. */
|
|
15
|
+
classify?: QueryFailedClassifier;
|
|
16
|
+
/** Optional Sentry client (`@sentry/cloudflare`). Omitted on repos without Sentry. */
|
|
17
|
+
sentry?: SentryExceptionReporterLike;
|
|
18
|
+
/** Static reporter (tests, worker closure). Takes precedence over {@link sentry}. */
|
|
19
|
+
reportError?: ErrorReporter;
|
|
20
|
+
/** Read reporter from Hono context (e.g. `c.get('container')?.reportError`). Takes precedence over {@link sentry}. */
|
|
21
|
+
getReportError?: (c: Context<E>) => ErrorReporter | undefined;
|
|
22
|
+
/** Override auto-wired reporting (rare; prefer sentry / reportError / getReportError). */
|
|
23
|
+
onUnhandledError?: (err: unknown, c: Context<E>) => void;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Standard `app.onError` factory: QueryFailed filter → Nest default filter, with optional error reporting.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createAppErrorHandler<E extends Env = Env>(options?: CreateAppErrorHandlerOptions<E>): (err: Error, c: Context<E, any, {}>) => Response;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { classifyGenericMysqlDriverError, createQueryFailedNestErrorHandler, } from './query-failed-error.js';
|
|
2
|
+
import { createSentryErrorReporter } from './nest-error.js';
|
|
3
|
+
/**
|
|
4
|
+
* Standard `app.onError` factory: QueryFailed filter → Nest default filter, with optional error reporting.
|
|
5
|
+
*/
|
|
6
|
+
export function createAppErrorHandler(options = {}) {
|
|
7
|
+
const { classify = classifyGenericMysqlDriverError, sentry, reportError, getReportError, onUnhandledError, fieldOrder = 'message-first', ...nestOptions } = options;
|
|
8
|
+
const sentryReporter = sentry ? createSentryErrorReporter(sentry) : undefined;
|
|
9
|
+
const resolvedOnUnhandled = onUnhandledError ??
|
|
10
|
+
((err, c) => {
|
|
11
|
+
const reporter = getReportError?.(c) ?? reportError ?? sentryReporter;
|
|
12
|
+
const requestId = c.get('requestId');
|
|
13
|
+
reporter?.(err, { requestId });
|
|
14
|
+
});
|
|
15
|
+
return createQueryFailedNestErrorHandler({
|
|
16
|
+
fieldOrder,
|
|
17
|
+
...nestOptions,
|
|
18
|
+
classify,
|
|
19
|
+
onUnhandledError: resolvedOnUnhandled,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ExecutionContextLike } from './execution-context.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fire-and-forget executor: registers a promise without awaiting it in the request path.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* On Cloudflare Workers, un-awaited work after the response may be killed unless it is registered
|
|
7
|
+
* via `ctx.waitUntil`. Inject a {@link createWaitUntilDefer} instance from the worker entry;
|
|
8
|
+
* use {@link defaultDefer} in tests and other contexts without an execution context.
|
|
9
|
+
*/
|
|
10
|
+
export type DeferExecutor = (promise: Promise<unknown>) => void;
|
|
11
|
+
/**
|
|
12
|
+
* Default defer implementation (NestJS `void promise` equivalent). Swallows rejections.
|
|
13
|
+
* Used when no `ExecutionContext` is available (tests, partial scheduled paths).
|
|
14
|
+
*/
|
|
15
|
+
export declare const defaultDefer: DeferExecutor;
|
|
16
|
+
/**
|
|
17
|
+
* Build a {@link DeferExecutor} that keeps the worker alive until `promise` settles.
|
|
18
|
+
*
|
|
19
|
+
* @param ctx - Workers execution context (`waitUntil`).
|
|
20
|
+
*/
|
|
21
|
+
export declare function createWaitUntilDefer(ctx: ExecutionContextLike): DeferExecutor;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default defer implementation (NestJS `void promise` equivalent). Swallows rejections.
|
|
3
|
+
* Used when no `ExecutionContext` is available (tests, partial scheduled paths).
|
|
4
|
+
*/
|
|
5
|
+
export const defaultDefer = (promise) => {
|
|
6
|
+
void promise.catch(() => undefined);
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Build a {@link DeferExecutor} that keeps the worker alive until `promise` settles.
|
|
10
|
+
*
|
|
11
|
+
* @param ctx - Workers execution context (`waitUntil`).
|
|
12
|
+
*/
|
|
13
|
+
export function createWaitUntilDefer(ctx) {
|
|
14
|
+
return (promise) => {
|
|
15
|
+
ctx.waitUntil(promise.catch(() => undefined));
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -27,6 +27,22 @@ export interface ErrorReportContext {
|
|
|
27
27
|
* @param context - Optional correlation context for the failing request.
|
|
28
28
|
*/
|
|
29
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;
|
|
30
46
|
/**
|
|
31
47
|
* Minimal shape read from a value treated as an HTTP error: its status, message, and optional body.
|
|
32
48
|
*
|
package/dist/http/nest-error.js
CHANGED
|
@@ -12,6 +12,14 @@ export const NEST_REASON_PHRASES = {
|
|
|
12
12
|
403: 'Forbidden',
|
|
13
13
|
404: 'Not Found',
|
|
14
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
|
+
}
|
|
15
23
|
/**
|
|
16
24
|
* Structurally detect Hono's `HTTPException` without relying on `instanceof`.
|
|
17
25
|
*
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Context, Env } from 'hono';
|
|
2
|
-
import type {
|
|
2
|
+
import type { NestErrorHandlerOptions } from './nest-error.js';
|
|
3
3
|
/** Nest QueryFailedExceptionFilter が返す `{ statusCode, message }` 形(error フィールド無し)。 */
|
|
4
4
|
export interface ClassifiedDbError {
|
|
5
5
|
statusCode: 400 | 500;
|
|
@@ -8,10 +8,10 @@ export interface ClassifiedDbError {
|
|
|
8
8
|
/** mysql2 / Drizzle 由来の DB エラーを HTTP 応答用に分類する。非 DB エラーは null。 */
|
|
9
9
|
export type QueryFailedClassifier = (err: unknown) => ClassifiedDbError | null;
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Default classifier for apps without a NestJS `QueryFailedExceptionFilter` parity layer.
|
|
12
|
+
* Maps any mysql2 driver error to generic 500 `{ statusCode, message: 'Internal server error' }`.
|
|
13
13
|
*/
|
|
14
|
-
export declare function
|
|
14
|
+
export declare function classifyGenericMysqlDriverError(err: unknown): ClassifiedDbError | null;
|
|
15
15
|
export interface QueryFailedNestErrorHandlerOptions<E extends Env = Env> extends NestErrorHandlerOptions<E> {
|
|
16
16
|
/** アプリ固有の分類(parity-critical な日本語メッセージ等は consumer 側で定義)。 */
|
|
17
17
|
classify: QueryFailedClassifier;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { logMysqlDriverError } from './mysql-driver-error.js';
|
|
1
|
+
import { findMysqlDriverError, logMysqlDriverError } from './mysql-driver-error.js';
|
|
2
2
|
import { createNestErrorHandler } from './nest-error.js';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Default classifier for apps without a NestJS `QueryFailedExceptionFilter` parity layer.
|
|
5
|
+
* Maps any mysql2 driver error to generic 500 `{ statusCode, message: 'Internal server error' }`.
|
|
6
6
|
*/
|
|
7
|
-
export function
|
|
7
|
+
export function classifyGenericMysqlDriverError(err) {
|
|
8
|
+
if (!findMysqlDriverError(err)) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
return { statusCode: 500, message: 'Internal server error' };
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* @internal Used by {@link createQueryFailedNestErrorHandler} only.
|
|
15
|
+
*/
|
|
16
|
+
function reportClassifiedDbError(err, classified, reportError, requestId) {
|
|
8
17
|
logMysqlDriverError(err, classified.statusCode);
|
|
9
18
|
if (classified.statusCode === 500) {
|
|
10
19
|
reportError?.(err, { requestId });
|
|
@@ -27,7 +36,7 @@ export function createQueryFailedNestErrorHandler(options) {
|
|
|
27
36
|
return (err, c) => {
|
|
28
37
|
const classified = classify(err);
|
|
29
38
|
if (classified) {
|
|
30
|
-
|
|
39
|
+
reportClassifiedDbError(err, classified);
|
|
31
40
|
if (classified.statusCode === 500) {
|
|
32
41
|
try {
|
|
33
42
|
onUnhandledError?.(err, c);
|
package/dist/index.d.ts
CHANGED
|
@@ -10,13 +10,17 @@
|
|
|
10
10
|
* @packageDocumentation
|
|
11
11
|
*/
|
|
12
12
|
export { finalizeResponse } from './middleware/finalize-response.js';
|
|
13
|
-
export { validate, createSentryValidate } from './middleware/validation.js';
|
|
13
|
+
export { validate, createValidate, createSentryValidate } from './middleware/validation.js';
|
|
14
14
|
export type { ValidateOptions, ValidationTarget, ZodErrorLike, SentryLike, SentryScopeLike, } from './middleware/validation.js';
|
|
15
15
|
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
|
|
16
16
|
export { createAuthMiddleware } from './middleware/auth.js';
|
|
17
17
|
export type { AuthMiddlewareOptions } from './middleware/auth.js';
|
|
18
18
|
export { perfLog } from './middleware/perf-log.js';
|
|
19
19
|
export type { PerfLogOptions, AnalyticsEngineDatasetLike } from './middleware/perf-log.js';
|
|
20
|
+
export { createIsolateMemo } from './container/isolate-memo.js';
|
|
21
|
+
export type { IsolateMemo } from './container/isolate-memo.js';
|
|
22
|
+
export { createContainerRuntime } from './container/middleware.js';
|
|
23
|
+
export type { ContainerBuildContext, ContainerRuntime, ContainerRuntimeOptions } from './container/middleware.js';
|
|
20
24
|
export { getUserProtocol } from './http/user-protocol.js';
|
|
21
25
|
export type { IUserProtocol } from './http/user-protocol.js';
|
|
22
26
|
export { getAppInfo } from './http/app-info.js';
|
|
@@ -28,10 +32,16 @@ export { createNestErrorHandler, nestNotFoundHandler, NEST_REASON_PHRASES } from
|
|
|
28
32
|
export type { NestErrorHandlerOptions, ErrorReportContext, ErrorReporter } from './http/nest-error.js';
|
|
29
33
|
export { findMysqlDriverError, logMysqlDriverError } from './http/mysql-driver-error.js';
|
|
30
34
|
export type { MysqlDriverErrorLike } from './http/mysql-driver-error.js';
|
|
31
|
-
export { createQueryFailedNestErrorHandler,
|
|
35
|
+
export { createQueryFailedNestErrorHandler, classifyGenericMysqlDriverError } from './http/query-failed-error.js';
|
|
32
36
|
export type { ClassifiedDbError, QueryFailedClassifier, QueryFailedNestErrorHandlerOptions, } from './http/query-failed-error.js';
|
|
37
|
+
export { createAppErrorHandler } from './http/app-error-handler.js';
|
|
38
|
+
export type { CreateAppErrorHandlerOptions } from './http/app-error-handler.js';
|
|
33
39
|
export { normalizeTrailingSlash } from './http/trailing-slash.js';
|
|
34
40
|
export type { ExecutionContextLike } from './http/execution-context.js';
|
|
41
|
+
export { defaultDefer, createWaitUntilDefer } from './http/defer.js';
|
|
42
|
+
export type { DeferExecutor } from './http/defer.js';
|
|
43
|
+
export { createSentryErrorReporter } from './http/nest-error.js';
|
|
44
|
+
export type { SentryExceptionReporterLike } from './http/nest-error.js';
|
|
35
45
|
export { KVCache } from './cache/kv-cache.js';
|
|
36
46
|
export type { KVNamespace, KVCacheOptions } from './cache/kv-cache.js';
|
|
37
47
|
export { createStripeClient, verifyStripeWebhook } from './stripe/client.js';
|
|
@@ -41,6 +51,8 @@ export { sendInChunks } from './queue/send.js';
|
|
|
41
51
|
export type { QueueLike, QueueSendMessage } from './queue/send.js';
|
|
42
52
|
export { processBatch } from './queue/consumer.js';
|
|
43
53
|
export type { QueueMessageLike, MessageBatchLike, ProcessBatchOptions, ProcessBatchResult } from './queue/consumer.js';
|
|
54
|
+
export { createQueueErrorHandler } from './queue/error-handler.js';
|
|
55
|
+
export type { CreateQueueErrorHandlerOptions } from './queue/error-handler.js';
|
|
44
56
|
export { createAiGatewayProvider } from './ai/gateway.js';
|
|
45
57
|
export type { AiGatewayConfig, AiGatewayProvider, AiGatewayBinding, AiGateway, AiGatewayOptions, } from './ai/gateway.js';
|
|
46
58
|
export { getAuthenticationSecret } from './aws/secrets-manager.js';
|
package/dist/index.js
CHANGED
|
@@ -11,10 +11,12 @@
|
|
|
11
11
|
*/
|
|
12
12
|
// middleware
|
|
13
13
|
export { finalizeResponse } from './middleware/finalize-response.js';
|
|
14
|
-
export { validate, createSentryValidate } from './middleware/validation.js';
|
|
14
|
+
export { validate, createValidate, createSentryValidate } from './middleware/validation.js';
|
|
15
15
|
export { zNum, zNumNullable, zNumOptional, zNumWithDefault } from './middleware/zod-coerce.js';
|
|
16
16
|
export { createAuthMiddleware } from './middleware/auth.js';
|
|
17
17
|
export { perfLog } from './middleware/perf-log.js';
|
|
18
|
+
export { createIsolateMemo } from './container/isolate-memo.js';
|
|
19
|
+
export { createContainerRuntime } from './container/middleware.js';
|
|
18
20
|
// http
|
|
19
21
|
export { getUserProtocol } from './http/user-protocol.js';
|
|
20
22
|
export { getAppInfo } from './http/app-info.js';
|
|
@@ -22,8 +24,11 @@ export { resolveAppEnv, isProductionEnv } from './http/app-env.js';
|
|
|
22
24
|
export { HttpStatus } from './http/http-status.js';
|
|
23
25
|
export { createNestErrorHandler, nestNotFoundHandler, NEST_REASON_PHRASES } from './http/nest-error.js';
|
|
24
26
|
export { findMysqlDriverError, logMysqlDriverError } from './http/mysql-driver-error.js';
|
|
25
|
-
export { createQueryFailedNestErrorHandler,
|
|
27
|
+
export { createQueryFailedNestErrorHandler, classifyGenericMysqlDriverError } from './http/query-failed-error.js';
|
|
28
|
+
export { createAppErrorHandler } from './http/app-error-handler.js';
|
|
26
29
|
export { normalizeTrailingSlash } from './http/trailing-slash.js';
|
|
30
|
+
export { defaultDefer, createWaitUntilDefer } from './http/defer.js';
|
|
31
|
+
export { createSentryErrorReporter } from './http/nest-error.js';
|
|
27
32
|
// cache
|
|
28
33
|
export { KVCache } from './cache/kv-cache.js';
|
|
29
34
|
// stripe
|
|
@@ -33,6 +38,7 @@ export { retryWhenDeadlock } from './db/retry.js';
|
|
|
33
38
|
// queue
|
|
34
39
|
export { sendInChunks } from './queue/send.js';
|
|
35
40
|
export { processBatch } from './queue/consumer.js';
|
|
41
|
+
export { createQueueErrorHandler } from './queue/error-handler.js';
|
|
36
42
|
// ai
|
|
37
43
|
export { createAiGatewayProvider } from './ai/gateway.js';
|
|
38
44
|
// aws
|
|
@@ -108,44 +108,15 @@ export interface SentryLike {
|
|
|
108
108
|
captureException(error: unknown): void;
|
|
109
109
|
}
|
|
110
110
|
/**
|
|
111
|
-
* Create a {@link validate}
|
|
111
|
+
* Create a bound {@link validate} factory with optional Sentry reporting on validation failures.
|
|
112
112
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* `error.type=dto_validation` and carries a `validation` context of `{ errorCount, errors }`.
|
|
116
|
-
*
|
|
117
|
-
* @param sentry - A Sentry-like client used to capture validation failures; see {@link SentryLike}.
|
|
118
|
-
* @returns A function `(target, schema) => MiddlewareHandler` mirroring {@link validate}.
|
|
119
|
-
*
|
|
120
|
-
* @example
|
|
121
|
-
* ```ts
|
|
122
|
-
* import * as Sentry from '@sentry/cloudflare';
|
|
123
|
-
* import { z } from 'zod';
|
|
124
|
-
* import { createSentryValidate } from '@rdlabo/workers-hono-kit';
|
|
125
|
-
*
|
|
126
|
-
* const validate = createSentryValidate(Sentry);
|
|
127
|
-
* app.post('/users', validate('json', z.object({ name: z.string() })), handler);
|
|
128
|
-
* ```
|
|
113
|
+
* @param options.sentry - When set, 400 validation errors are reported (dto_validation tag + context).
|
|
114
|
+
* @returns `(target, schema[, validateOptions])` middleware — same as {@link validate} when sentry is omitted.
|
|
129
115
|
*/
|
|
130
|
-
export declare function
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
form?: {} | undefined;
|
|
138
|
-
};
|
|
139
|
-
out: {
|
|
140
|
-
json: T;
|
|
141
|
-
query: T;
|
|
142
|
-
param: T;
|
|
143
|
-
header: T;
|
|
144
|
-
cookie: T;
|
|
145
|
-
form: T;
|
|
146
|
-
};
|
|
147
|
-
}, Response & import("hono").TypedResponse<{
|
|
148
|
-
statusCode: number;
|
|
149
|
-
message: string[];
|
|
150
|
-
error: string;
|
|
151
|
-
}, 400, "json">>;
|
|
116
|
+
export declare function createValidate(options?: {
|
|
117
|
+
sentry?: SentryLike;
|
|
118
|
+
}): typeof validate;
|
|
119
|
+
/**
|
|
120
|
+
* @deprecated Use {@link createValidate}({ sentry }) instead.
|
|
121
|
+
*/
|
|
122
|
+
export declare function createSentryValidate(sentry: SentryLike): typeof validate;
|
|
@@ -64,26 +64,16 @@ export function validate(target, schema, options) {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
|
-
* Create a {@link validate}
|
|
67
|
+
* Create a bound {@link validate} factory with optional Sentry reporting on validation failures.
|
|
68
68
|
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* `error.type=dto_validation` and carries a `validation` context of `{ errorCount, errors }`.
|
|
72
|
-
*
|
|
73
|
-
* @param sentry - A Sentry-like client used to capture validation failures; see {@link SentryLike}.
|
|
74
|
-
* @returns A function `(target, schema) => MiddlewareHandler` mirroring {@link validate}.
|
|
75
|
-
*
|
|
76
|
-
* @example
|
|
77
|
-
* ```ts
|
|
78
|
-
* import * as Sentry from '@sentry/cloudflare';
|
|
79
|
-
* import { z } from 'zod';
|
|
80
|
-
* import { createSentryValidate } from '@rdlabo/workers-hono-kit';
|
|
81
|
-
*
|
|
82
|
-
* const validate = createSentryValidate(Sentry);
|
|
83
|
-
* app.post('/users', validate('json', z.object({ name: z.string() })), handler);
|
|
84
|
-
* ```
|
|
69
|
+
* @param options.sentry - When set, 400 validation errors are reported (dto_validation tag + context).
|
|
70
|
+
* @returns `(target, schema[, validateOptions])` middleware — same as {@link validate} when sentry is omitted.
|
|
85
71
|
*/
|
|
86
|
-
export function
|
|
72
|
+
export function createValidate(options) {
|
|
73
|
+
if (!options?.sentry) {
|
|
74
|
+
return validate;
|
|
75
|
+
}
|
|
76
|
+
const sentry = options.sentry;
|
|
87
77
|
const onValidationError = (error) => {
|
|
88
78
|
const messages = zodToMessages(error);
|
|
89
79
|
sentry.withScope((scope) => {
|
|
@@ -92,5 +82,11 @@ export function createSentryValidate(sentry) {
|
|
|
92
82
|
sentry.captureException(error);
|
|
93
83
|
});
|
|
94
84
|
};
|
|
95
|
-
return (target, schema) => validate(target, schema, { onValidationError });
|
|
85
|
+
return (target, schema, validateOptions) => validate(target, schema, { ...validateOptions, onValidationError });
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* @deprecated Use {@link createValidate}({ sentry }) instead.
|
|
89
|
+
*/
|
|
90
|
+
export function createSentryValidate(sentry) {
|
|
91
|
+
return createValidate({ sentry });
|
|
96
92
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { QueueMessageLike } from './consumer.js';
|
|
2
|
+
import type { SentryExceptionReporterLike } from '../http/nest-error.js';
|
|
3
|
+
/**
|
|
4
|
+
* Options for {@link createQueueErrorHandler}.
|
|
5
|
+
*/
|
|
6
|
+
export interface CreateQueueErrorHandlerOptions {
|
|
7
|
+
/** Queue name for log prefix and optional capture tags. */
|
|
8
|
+
queue: string;
|
|
9
|
+
/**
|
|
10
|
+
* When set, `captureException` is called only after the final delivery attempt
|
|
11
|
+
* (`message.attempts > maxRetries`). Cloudflare Queues uses 1-based `attempts`; the last delivery
|
|
12
|
+
* before the dead-letter queue has `attempts === maxRetries + 1`.
|
|
13
|
+
*/
|
|
14
|
+
maxRetries?: number;
|
|
15
|
+
/** Optional Sentry client. Omit for console-only reporting (e.g. airlec). */
|
|
16
|
+
sentry?: SentryExceptionReporterLike;
|
|
17
|
+
/** Override {@link sentry}.captureException (custom sink). */
|
|
18
|
+
captureException?: SentryExceptionReporterLike['captureException'];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Factory for {@link processBatch}'s `onError` hook: logs every failure and optionally reports to
|
|
22
|
+
* Sentry (or another sink) with queue / message id / attempts / body context.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createQueueErrorHandler(options: CreateQueueErrorHandlerOptions): (error: unknown, message: QueueMessageLike) => void;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for {@link processBatch}'s `onError` hook: logs every failure and optionally reports to
|
|
3
|
+
* Sentry (or another sink) with queue / message id / attempts / body context.
|
|
4
|
+
*/
|
|
5
|
+
export function createQueueErrorHandler(options) {
|
|
6
|
+
const { queue, maxRetries, sentry, captureException } = options;
|
|
7
|
+
const capture = captureException ?? sentry?.captureException.bind(sentry);
|
|
8
|
+
return (error, message) => {
|
|
9
|
+
console.error(`[Queue:${queue}] message ${message.id} failed (attempt ${message.attempts})`, error);
|
|
10
|
+
if (!capture) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (maxRetries !== undefined && message.attempts <= maxRetries) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
capture(error, {
|
|
17
|
+
tags: { queue, queue_message_id: message.id },
|
|
18
|
+
extra: { attempts: message.attempts, body: message.body },
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
}
|