Skip to content

Commit 8a9a99a

Browse files
committed
fix(hono): Use arity heuristic to distinguish .use() middleware from .all() handlers
Hono registers both .use() middleware and .all() route handlers with method:'ALL' internally. Previously patchRoute wrapped all method:'ALL' handlers as middleware spans, incorrectly tracing .all() final handlers. Use handler.length >= 2 (accepts context + next) to identify middleware vs handler.length < 2 (accepts only context) for route handlers. Also guard responseHandler against re-capturing errors already captured by wrapMiddlewareWithSpan via isAlreadyCaptured check. Made-with: Cursor
1 parent 2648aca commit 8a9a99a

2 files changed

Lines changed: 46 additions & 11 deletions

File tree

packages/hono/src/shared/middlewareHandlers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getDefaultIsolationScope,
55
getIsolationScope,
66
getRootSpan,
7+
isAlreadyCaptured,
78
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
89
updateSpanName,
910
winterCGRequestToRequestData,
@@ -42,7 +43,7 @@ export function responseHandler(context: Context): void {
4243

4344
getIsolationScope().setTransactionName(`${context.req.method} ${routePath(context)}`);
4445

45-
if (context.error) {
46+
if (context.error && !isAlreadyCaptured(context.error)) {
4647
getClient()?.captureException(context.error, {
4748
mechanism: { handled: false, type: 'auto.http.hono.context_error' },
4849
});

packages/hono/src/shared/patchRoute.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
import { getOriginalFunction, markFunctionWrapped } from '@sentry/core';
22
import type { WrappedFunction } from '@sentry/core';
3-
import type { Env, Hono } from 'hono';
3+
import type { Env, Hono, MiddlewareHandler } from 'hono';
44
import { wrapMiddlewareWithSpan } from './wrapMiddlewareSpan';
55

6+
interface HonoRoute {
7+
method: string;
8+
path: string;
9+
handler: MiddlewareHandler;
10+
}
11+
612
interface HonoBaseProto {
713
// oxlint-disable-next-line typescript/no-explicit-any
814
route: (path: string, app: Hono<any>) => Hono<any>;
@@ -29,19 +35,47 @@ export function patchRoute<E extends Env>(app: Hono<E>): void {
2935
// oxlint-disable-next-line typescript/no-explicit-any
3036
const patchedRoute = function (this: Hono<any>, path: string, subApp: Hono<any>): Hono<any> {
3137
if (subApp && Array.isArray(subApp.routes)) {
32-
for (const route of subApp.routes) {
33-
/* Internally, `app.use()` always registers with `method: 'ALL'` (via the constant `METHOD_NAME_ALL`),
34-
* while `app.get()` / `.post()` / etc. use their respective uppercase method name.
35-
* https://github.com/honojs/hono/blob/18fe604c8cefc2628240651b1af219692e1918c1/src/hono-base.ts#L156-L168
36-
*/
37-
if (route.method === 'ALL' && typeof route.handler === 'function') {
38-
route.handler = wrapMiddlewareWithSpan(route.handler);
39-
}
40-
}
38+
wrapSubAppMiddleware(subApp.routes as HonoRoute[]);
4139
}
4240
return originalRoute.call(this, path, subApp);
4341
};
4442

4543
markFunctionWrapped(patchedRoute as unknown as WrappedFunction, originalRoute as unknown as WrappedFunction);
4644
honoBaseProto.route = patchedRoute;
4745
}
46+
47+
/**
48+
* Wraps middleware handlers in a sub-app's routes array with Sentry spans.
49+
*
50+
* When multiple handlers share the same method+path (e.g. `app.get('/path', mw, handler)`),
51+
* Hono registers each as a separate route entry. We wrap all but the last entry per group
52+
* (those are the middleware), leaving the final handler unwrapped.
53+
*
54+
* For `method: 'ALL'` handlers that are last-for-group (from `.use()` or `.all()`),
55+
* we use an arity (# of params) heuristic: middleware takes `(context, next)` (length >= 2),
56+
* while final handlers take only `(context)` (length < 2). This distinguishes
57+
* `.use()` middleware (should be traced) from `.all()` route handlers (should not).
58+
*
59+
* Hono's .use() and .all() both register as method 'ALL', but .use() middleware
60+
* always accepts (context, next) while .all() handlers typically accept only (context).
61+
* https://github.com/honojs/hono/blob/18fe604c8cefc2628240651b1af219692e1918c1/src/hono-base.ts#L156-L168
62+
*/
63+
function wrapSubAppMiddleware(routes: HonoRoute[]): void {
64+
const lastIndexByKey = new Map<string, number>();
65+
for (const [i, route] of routes.entries()) {
66+
lastIndexByKey.set(`${route.method}\0${route.path}`, i);
67+
}
68+
69+
for (const [i, route] of routes.entries()) {
70+
if (typeof route.handler !== 'function') {
71+
continue;
72+
}
73+
74+
const isLastForGroup = lastIndexByKey.get(`${route.method}\0${route.path}`) === i;
75+
76+
const isMiddleware = !isLastForGroup || (route.method === 'ALL' && route.handler.length >= 2);
77+
if (isMiddleware) {
78+
route.handler = wrapMiddlewareWithSpan(route.handler);
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)