Skip to content

Commit 556bcb3

Browse files
authored
feat(hono): Add shouldHandleError as middleware option (#21205)
Adds the error filtering logic to the Hono `sentry()` middleware. It already exists in the to-be deprecated `honoIntegration`: #17743 `isExpectedError` was renamed to `defaultShouldHandleError` so the name is matching with the user-provided parameter. I also removed the special catching **within** the middleware, as it's just "duplicated code". The error bubbles through the Hono context anyway - it's a bit simpler now. Closes #21204
1 parent 7a67ea4 commit 556bcb3

17 files changed

Lines changed: 434 additions & 128 deletions

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@
1414
[Sentry TanStack Start SDK docs](https://docs.sentry.io/platforms/javascript/guides/tanstackstart-react/). Please reach out on
1515
[GitHub](https://github.com/getsentry/sentry-javascript/issues/new/choose) if you have any feedback or concerns.
1616

17+
- **feat(hono): Add `shouldHandleError` option to `sentry()` middleware**
18+
19+
The `sentry()` middleware now accepts a `shouldHandleError` callback to control which errors are captured and sent to Sentry. By default, 3xx/4xx HTTP errors are ignored and 5xx errors and plain `Error` objects are captured. Return `true` from the callback to capture an error, `false` to suppress it.
20+
21+
```ts
22+
app.use(
23+
sentry(app, {
24+
dsn: '__DSN__',
25+
shouldHandleError(error) {
26+
const status = (error as { status?: number })?.status;
27+
// Capture 401/403 in addition to the default 5xx errors
28+
return status === 401 || status === 403 || typeof status !== 'number' || status >= 500;
29+
},
30+
}),
31+
);
32+
```
33+
1734
- **test(tanstackstart-react): Move initialization to client entry point ([#21161](https://github.com/getsentry/sentry-javascript/pull/21161))**
1835

1936
Change the recommended setup for the SDK to do `Sentry.init()` in the client entry file to capture telemetry that is emitted ahead of page hydration.

dev-packages/e2e-tests/test-applications/hono-4/tests/errors.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ test.describe('middleware errors', () => {
146146

147147
const errorEvent = await errorPromise;
148148
expect(errorEvent.exception?.values?.[0]?.value).toBe('Service Unavailable from middleware');
149-
expect(errorEvent.exception?.values?.[0]?.mechanism?.type).toBe('auto.middleware.hono');
149+
expect(errorEvent.exception?.values?.[0]?.mechanism?.type).toBe('auto.http.hono.context_error');
150150
expect(errorEvent.exception?.values?.[0]?.mechanism?.handled).toBe(false);
151151
expect(errorEvent.transaction).toBe('GET /test-errors/middleware-http-exception');
152152

dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ for (const { name, prefix } of SCENARIOS) {
113113
expect(errorEvent.exception?.values?.[0]?.mechanism).toEqual(
114114
expect.objectContaining({
115115
handled: false,
116-
type: 'auto.middleware.hono',
116+
type: 'auto.http.hono.context_error',
117117
}),
118118
);
119119

packages/hono/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,24 @@ app.use(
188188

189189
serve(app);
190190
```
191+
192+
## Filtering errors
193+
194+
By default, `@sentry/hono` captures 5xx errors and plain `Error` objects, and ignores 3xx/4xx HTTP errors (redirects, not-found, bad request, etc.).
195+
196+
Use `shouldHandleError` to override this on a per-error basis:
197+
198+
```ts
199+
app.use(
200+
sentry(app, {
201+
dsn: '__DSN__',
202+
shouldHandleError(error) {
203+
const status = (error as { status?: number })?.status;
204+
// Capture 401/403 in addition to the default 5xx errors
205+
return status === 401 || status === 403 || typeof status !== 'number' || status >= 500;
206+
},
207+
}),
208+
);
209+
```
210+
211+
Return `true` to capture the error, `false` to suppress it.

packages/hono/src/bun/middleware.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,15 @@ import { init } from './sdk';
33
import type { Env, Hono, MiddlewareHandler } from 'hono';
44
import { requestHandler, responseHandler } from '../shared/middlewareHandlers';
55
import { applyPatches } from '../shared/applyPatches';
6+
import type { SentryHonoMiddlewareOptions } from '../shared/types';
67

7-
export interface HonoBunOptions extends Options<BaseTransportOptions> {}
8+
export interface HonoBunOptions extends Options<BaseTransportOptions>, SentryHonoMiddlewareOptions {}
89

910
/**
1011
* Sentry middleware for Hono running in a Bun runtime environment.
1112
*/
1213
export const sentry = <E extends Env>(app: Hono<E>, options: HonoBunOptions): MiddlewareHandler => {
13-
const isDebug = options.debug;
14-
15-
isDebug && debug.log('Initialized Sentry Hono middleware (Bun)');
14+
options.debug && debug.log('Initialized Sentry Hono middleware (Bun)');
1615

1716
init(options);
1817

@@ -23,6 +22,6 @@ export const sentry = <E extends Env>(app: Hono<E>, options: HonoBunOptions): Mi
2322

2423
await next(); // Handler runs in between Request above ⤴ and Response below ⤵
2524

26-
responseHandler(context);
25+
responseHandler(context, options.shouldHandleError);
2726
};
2827
};

packages/hono/src/cloudflare/middleware.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import type { Env, Hono, MiddlewareHandler } from 'hono';
44
import { buildFilteredIntegrations } from '../shared/buildFilteredIntegrations';
55
import { requestHandler, responseHandler } from '../shared/middlewareHandlers';
66
import { applyPatches } from '../shared/applyPatches';
7+
import type { SentryHonoMiddlewareOptions } from '../shared/types';
78

8-
export interface HonoCloudflareOptions extends Options<BaseTransportOptions> {}
9+
export interface HonoCloudflareOptions extends Options<BaseTransportOptions>, SentryHonoMiddlewareOptions {}
910

1011
/**
1112
* Sentry middleware for Hono on Cloudflare Workers.
@@ -36,10 +37,15 @@ export function sentry<E extends Env>(
3637
applyPatches(app);
3738

3839
return async (context, next) => {
40+
const shouldHandleError =
41+
typeof options === 'function'
42+
? options(context.env as E['Bindings']).shouldHandleError
43+
: options.shouldHandleError;
44+
3945
requestHandler(context);
4046

4147
await next(); // Handler runs in between Request above ⤴ and Response below ⤵
4248

43-
responseHandler(context);
49+
responseHandler(context, shouldHandleError);
4450
};
4551
}

packages/hono/src/node/middleware.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type BaseTransportOptions, debug, type Options, getClient } from '@sent
22
import type { Env, Hono, MiddlewareHandler } from 'hono';
33
import { requestHandler, responseHandler } from '../shared/middlewareHandlers';
44
import { applyPatches } from '../shared/applyPatches';
5+
import type { SentryHonoMiddlewareOptions } from '../shared/types';
56

67
export interface HonoNodeOptions extends Options<BaseTransportOptions> {}
78

@@ -13,7 +14,7 @@ export interface HonoNodeOptions extends Options<BaseTransportOptions> {}
1314
*
1415
* **Note:** You must initialize Sentry separately before using this middleware. Typically, this is done by calling `Sentry.init()` in an `instrument.ts` file and loading it via the Node `--import` flag.
1516
*/
16-
export const sentry = <E extends Env>(app: Hono<E>): MiddlewareHandler => {
17+
export const sentry = <E extends Env>(app: Hono<E>, options?: SentryHonoMiddlewareOptions): MiddlewareHandler => {
1718
const sentryClient = getClient();
1819
if (sentryClient === undefined) {
1920
debug.warn(
@@ -31,6 +32,6 @@ export const sentry = <E extends Env>(app: Hono<E>): MiddlewareHandler => {
3132

3233
await next(); // Handler runs in between Request above ⤴ and Response below ⤵
3334

34-
responseHandler(context);
35+
responseHandler(context, options?.shouldHandleError);
3536
};
3637
};
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Default implementation of the `shouldHandleError` callback.
3+
*
4+
* Returns `true` (capture) for 5xx errors and any error without a `status` property
5+
*
6+
* Returns `false` (skip) for 3xx and 4xx errors (they still generate spans and transactions for tracing)
7+
*
8+
* Checks any error-like value that carries a numeric `status` property. This covers
9+
* Hono's `HTTPException`, third-party middleware errors, and custom error subclasses.
10+
*/
11+
export function defaultShouldHandleError(error: unknown): boolean {
12+
if (typeof error !== 'object' || error === null) {
13+
return true;
14+
}
15+
16+
const status = (error as { status?: unknown }).status;
17+
18+
return !(typeof status === 'number' && status >= 300 && status < 500);
19+
}

packages/hono/src/shared/isExpectedError.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

packages/hono/src/shared/middlewareHandlers.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import {
1212
import type { Context } from 'hono';
1313
import { routePath } from 'hono/route';
1414
import { hasFetchEvent } from '../utils/hono-context';
15-
import { isExpectedError } from './isExpectedError';
15+
import { defaultShouldHandleError } from './defaultShouldHandleError';
16+
import { type SentryHonoMiddlewareOptions } from '../shared/types';
1617

1718
/**
1819
* Request handler for Hono framework
@@ -33,11 +34,16 @@ export function requestHandler(context: Context): void {
3334
/**
3435
* Response handler for Hono framework
3536
*/
36-
export function responseHandler(context: Context): void {
37-
if (context.error && !isExpectedError(context.error)) {
38-
getClient()?.captureException(context.error, {
39-
mechanism: { handled: false, type: 'auto.http.hono.context_error' },
40-
});
37+
export function responseHandler(
38+
context: Context,
39+
shouldHandleError?: SentryHonoMiddlewareOptions['shouldHandleError'],
40+
): void {
41+
if (context.error) {
42+
if ((shouldHandleError ?? defaultShouldHandleError)(context.error)) {
43+
getClient()?.captureException(context.error, {
44+
mechanism: { handled: false, type: 'auto.http.hono.context_error' },
45+
});
46+
}
4147
}
4248
}
4349

0 commit comments

Comments
 (0)