Skip to content

fix(hono): Name routes without c.req.matchedRoutes - #25454

Merged
s1gr1d merged 19 commits into
developfrom
sig/hono-5--1
Nov 10, 2026
Merged

s1gr1d merged 19 commits into
developfrom
sig/hono-5--1

Conversation

@s1gr1d

@s1gr1d s1gr1d commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

On Hono 5, every request got the span name GET and an empty http.route. With this PR, names look the same as on Hono 4: GET /users/:id, with http.route set to /users/:id.

Why they broke:

  • Hono 5 removes c.req.matchedRoutes.
  • compose resets c.req.routeIndex after next() (honojs/hono#4424), so the Sentry middleware cannot see which handler responded.

How the name is built now:

  1. The Sentry middleware reads the match result through the getter that hono/route uses (matchResult.ts).
  2. Before the handlers run, it names the span after the first matched route handler, which is the one Hono runs first.
  3. If more than one route handler matched, the SDK wraps them, so the handler that starts last renames the span. If only one matched, the name from step 2 is already correct. The SDK does not use arity (the number of parameters) to decide, because it cannot tell if a handler calls next(). Rest parameters and a default next hide it.

Small changes on Hono 4 too: events captured in a handler get that handler's route (not a later catch-all), and a 404 keeps the runtime name GET instead of GET /*.

Refs #25223

Hono 5 removes c.req.matchedRoutes and c.req.routePath. The vendored HonoRequest type
required both, so app.use(honoMiddleware(app)) failed with TS2769 on Hono 5. Make
matchedRoutes optional and drop routePath, which the SDK does not read.

Add Vitest type tests against Hono 4 and a pinned Hono 5 release candidate, a type
check of the SDK calls in the Hono 4 E2E apps, and a lint rule that keeps hono
imports out of @sentry/server-utils. Pin the hono devDependency of @sentry/hono to
4.13.9, so the package no longer builds against the hoisted version by chance.

Refs #25223
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@s1gr1d
s1gr1d added this pull request to stack #25455 October 16, 2026 12:14
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Compared: 5b3325a (PR test merge using develop at 4316c26)

Against: 4316c26 (develop) · base run #65429

✅ Baseline matches: this diff contains only this PR's changes.

Path Size % Change Change
@sentry/browser 29.92 kB - -
@sentry/browser - with treeshaking flags 28.08 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 27.97 kB - -
@sentry/browser (incl. Tracing) 52.2 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.22 kB - -
@sentry/browser (incl. Tracing, Profiling) 55.18 kB - -
@sentry/browser (incl. Tracing, Replay) 92.52 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 81.2 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 97.25 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 110.25 kB - -
@sentry/browser (incl. Feedback) 47.52 kB - -
@sentry/browser (incl. sendFeedback) 35.02 kB - -
@sentry/browser (incl. FeedbackAsync) 40.13 kB - -
@sentry/browser (incl. Metrics) 31.48 kB - -
@sentry/browser (incl. Logs) 31.77 kB - -
@sentry/browser (incl. Metrics & Logs) 32.45 kB - -
@sentry/react 31.78 kB - -
@sentry/react (incl. Tracing) 54.62 kB - -
@sentry/vue 38.24 kB - -
@sentry/vue (incl. Tracing) 55.24 kB - -
@sentry/svelte 29.95 kB - -
@sentry/remix (Remix 3 client bundle) 57.83 kB - -
CDN Bundle 31.72 kB - -
CDN Bundle (incl. Tracing) 52.81 kB - -
CDN Bundle (incl. Logs, Metrics) 34.52 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 55.32 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 75.9 kB - -
CDN Bundle (incl. Tracing, Replay) 90.98 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 93.54 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 97.11 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 99.71 kB - -
CDN Bundle - uncompressed 92.76 kB - -
CDN Bundle (incl. Tracing) - uncompressed 155.77 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 101.29 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 163.55 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 232.85 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 277.46 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 285.23 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 291.17 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 298.92 kB - -
@sentry/nextjs (client) 57.64 kB - -
@sentry/sveltekit (client) 52.73 kB - -
@sentry/core/server 44.19 kB - -
@sentry/core/browser 13.93 kB - -
@sentry/node 157.34 kB +0.19% +290 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 82.28 kB - -
@sentry/node - without tracing 97.2 kB +0.31% +296 B 🔺
@sentry/node - without channel injection 137.64 kB +0.22% +301 B 🔺
@sentry/aws-serverless 105.78 kB +0.28% +295 B 🔺
@sentry/cloudflare (withSentry) - minified 202.97 kB - -
@sentry/cloudflare (withSentry) 504.05 kB - -
@sentry/nextjs/cloudflare (withSentry) - minified 224.82 kB - -

View base workflow run

The hono devDependency of @sentry/hono (4.13.9) and the integration tests (^4.13.7)
resolved to two Hono copies. Their HonoRequest types differ by a unique symbol, so
sentry(app) did not type-check in the bun and cloudflare integration tests, and
yarn dedupe-deps:check failed. Resolve both to 4.13.9.

Refs #25223
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
s1gr1d and others added 5 commits October 16, 2026 14:41
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Context hook and wrapSubAppMiddleware each had their own copy of the rule that tells
middleware from route handlers. Move the rule to getMiddlewareFlags() and check the shape
of the match result entries before the hook reads them. No behavior change.

Refs #25223
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hono 5 removes c.req.matchedRoutes, so every request was named "GET ". It also restores
c.req.routeIndex after next(), so the Sentry middleware can no longer see which handler
responded. Read the match result through the getter that hono/route uses, and let each
route handler record its routeIndex when it starts. A request with no matched route keeps
the runtime's span name.

On Hono 4 the names stay the same, except where the old rule picked a later catch-all, for
example for a route handler declared with an unused next parameter.

Refs #25223
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Name the span when a route handler starts, so events captured in the handler get its route.
- Fall back to the first matched route handler, then to a middleware path. A request without a route keeps the runtime name.
- Wrap route handlers only when one of them declares `next`.
# Conflicts:
#	packages/hono/package.json
#	yarn.lock
# Conflicts:
#	packages/server-utils/src/integrations/hono/honoIntegration.ts
Base automatically changed from sig/hono-5--1-classifier to develop October 23, 2026 15:06
s1gr1d added a commit that referenced this pull request Oct 23, 2026
Preparation for #25454. No behavior change.

- The Context hook and `wrapSubAppMiddleware` had their own copies of
the rule that tells middleware from route handlers. Both now call
`getMiddlewareFlags()`
- The hook checks the shape of the match result entries once
(`isMatchedHandlerList()`), instead of optional chaining at each step

Ref #25223

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	packages/hono/test/shared/middlewareHandlers.test.ts
#	packages/server-utils/src/integrations/hono/createHonoMiddleware.ts
#	packages/server-utils/src/integrations/hono/middlewareHandlers.ts
# Conflicts:
#	packages/hono/package.json
#	packages/server-utils/src/integrations/hono/honoIntegration.ts
#	packages/server-utils/src/integrations/hono/matchResult.ts
#	pnpm-lock.yaml
@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Optional E2E tests

Some optional checks were not successful. They do not block the merge. 8 not successful, 49 successful. See the run.

Check Status
❌ CI: Optional E2E Tests / E2E cloudflare-pi-durable (latest) Test (optional) Failing after 39s Details
❌ CI: Optional E2E Tests / E2E solidstart-2 Test (optional) Failing after 45s Details
❌ CI: Optional E2E Tests / E2E cloudflare-flue-send-to-sentry Test (optional) Failing after 1m Details
❌ CI: Optional E2E Tests / E2E node-eve Test (optional) Failing after 2m Details
❌ CI: Optional E2E Tests / E2E node-eve (orchestrion) Test (optional) Failing after 2m Details
❌ CI: Optional E2E Tests / E2E node-flue (latest) Test (optional) Failing after 25s Details
❌ CI: Optional E2E Tests / E2E node-eve (latest) Test (optional) Failing after 57s Details
❌ CI: Optional E2E Tests / E2E nextjs-16-cacheComponents (cloudflare) Test (optional) Failing after 10m Details
Successful checks
Check Status
✅ CI: Optional E2E Tests / E2E astro-7-static Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E vue-3 (no Options API) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-agent Test (optional) Successful in 56s Details
✅ CI: Optional E2E Tests / E2E cloudflare-mastra Test (optional) Successful in 54s Details
✅ CI: Optional E2E Tests / E2E react-send-to-sentry Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps (static / nuxt generate) Test (optional) Successful in 38s Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (turbo) Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E nuxt-5 Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nuxt-5 (tunnel-dev) Test (optional) Successful in 59s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext, bun) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-14 (latest) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E deno (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries (cloudflare) Test (optional) Successful in 43s Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps (client source maps kept) Test (optional) Successful in 37s Details
✅ CI: Optional E2E Tests / E2E react-router-7-framework-instrumentation Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E cloudflare-workers-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react-cloudflare Test (optional) Successful in 41s Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries Test (optional) Successful in 47s Details
✅ CI: Optional E2E Tests / E2E debug-id-sourcemaps Test (optional) Successful in 39s Details
✅ CI: Optional E2E Tests / E2E node-pi-durable (latest) Test (optional) Successful in 37s Details
✅ CI: Optional E2E Tests / E2E astro-7 Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-think (ai v6) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-think Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries (latest) Test (optional) Successful in 55s Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (latest) Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E cloudflare-pi-durable Test (optional) Successful in 48s Details
✅ CI: Optional E2E Tests / E2E nextjs-orpc Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E supabase-nextjs Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-16-tunnel (cloudflare) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (deno) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-flue Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react-sourcemaps Test (optional) Successful in 34s Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (cloudflare, latest) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nestjs-12 (latest) Test (optional) Successful in 51s Details
✅ CI: Optional E2E Tests / E2E node-express-mcp-v1 Test (optional) Successful in 51s Details
✅ CI: Optional E2E Tests / E2E node-pi-durable Test (optional) Successful in 37s Details
✅ CI: Optional E2E Tests / E2E node-openai-decisions Test (optional) Successful in 28s Details
✅ CI: Optional E2E Tests / E2E node-mastra (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-openai-decisions (latest) Test (optional) Successful in 27s Details
✅ CI: Optional E2E Tests / E2E node-fastify-6 (tracer provider) Test (optional) Successful in 36s Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps Test (optional) Successful in 43s Details
✅ CI: Optional E2E Tests / E2E node-express-send-to-sentry-static Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-express-send-to-sentry Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-fastify-6 Test (optional) Successful in 42s Details
✅ CI: Optional E2E Tests / E2E node-express-mcp-v2 Test (optional) Successful in 36s Details
✅ CI: Optional E2E Tests / E2E node-mastra Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-cloudflare Test (optional) Successful in 33s Details

@s1gr1d
s1gr1d marked this pull request as ready for review October 27, 2026 13:57
@s1gr1d
s1gr1d requested review from a team as code owners October 27, 2026 13:57
@s1gr1d
s1gr1d removed the request for review from a team October 27, 2026 13:57
@s1gr1d
s1gr1d requested review from chargome, isaacs, mydea and nicohrubec and removed request for a team October 27, 2026 13:57

@isaacs isaacs left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a regression on Hono 4, but otherwise the changes here are all improvements. Fix that and this is good to land, imo.

return current.path;
// Hono runs the first matched route handler first. If a middleware returned early, it is the one that
// would have run.
const firstRouteHandler = routes.find((_route, i) => !middlewareFlags[i]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subtle regression here, this will make a global middleware with arity < 2 name every route GET /*

firstRouteHandler trusts middlewareFlags, which comes from arity for ALL routes. A .use() middleware whose declared arity is below 2 is classified as a route handler. Examples: (c, next = noop) => ... (default params do not count toward length), (c, ...rest) => ..., or a third-party wrapper that forwards ...args. Because app.use(mw) registers path /* and comes first in the match list, it becomes the "first route handler" for every request.

I confirmed this on Hono 4.13.11:

app.use(createHonoRequestMiddleware({ runtime: 'node' }));
app.use((c, ...rest: any[]) => rest[0]());
app.get('/users/:id', c => c.text('u'));
// GET /users/1 -> setTransactionName('GET /*'), expected 'GET /users/:id'

On develop, the old code read routes[routeIndex] at response time. On Hono 4 that index lands on the /users/:id handler, so it named the request correctly. This PR removes the response-time rename, and wrapRouteHandlers does not wrap here (no route handler "declares next" by arity, because the misclassified middleware has arity 1), so nothing corrects the provisional name. The misclassification itself existed before (it affects middleware span wrapping too), but its effect on the transaction name is new, and it is silent: every transaction in the app collapses to one name.

Suggestion: Always wrap the route handlers when the match result is available (drop the canPassOn shortcut in wrapRouteHandlers). The misclassified middleware then records its index first, the real handler records its index later, and the final name is correct. The cost is one Proxy call per route handler call.

Or, another possibility if it's easier, in resolveRouteName, we could skip a /* route as the provisional pick when a more specific route handler follows it in the list.

Either way, might be good to add a unit case for an arity-1 global middleware.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks! I fixed that.

I went with your first idea, but slightly different: we now wrap when two or more route handlers matched (when just one matches, it's the response handler), so arity is not the deciding factor.

Added unit cases (rest params, default next, path middleware) and an integration case. They all failed before the fix.

Known gap: Hono < 4.8 with the manual setup has no match result, so it would still show GET /*. Almost nobody uses that version, so I left it. Download numbers: https://npm-burst.com/package/hono/

@s1gr1d
s1gr1d requested a review from a team as a code owner November 9, 2026 16:01
@s1gr1d
s1gr1d requested review from msonnb and removed request for a team November 9, 2026 16:01

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the most important file

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also contains important changes.

* so this looks for a symbol getter that returns a match result.
* https://github.com/honojs/hono/blob/v5.0.0-rc.1/src/helper/route/index.ts#L32-L34
*/
export function getMatchedHandlers(context: Context): MatchedHandler[] | undefined {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We find the match result through an unexported Symbol getter on the request prototype. This is just like hono does it.

{ method: 'get', path: '/users/me', name: 'GET /users/me', route: '/users/me' },
{ method: 'get', path: '/users/42', name: 'GET /users/:id', route: '/users/:id' },
{ method: 'get', path: '/users/42/posts', name: 'GET /users/*', route: '/users/*' },
{ method: 'get', path: '/early/1', name: 'GET /early/:id', route: '/early/:id' },

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in case the middleware returns early, we still get a parametrized route.

@s1gr1d
s1gr1d merged commit e76488b into develop Nov 10, 2026
793 of 804 checks passed
@s1gr1d
s1gr1d deleted the sig/hono-5--1 branch November 10, 2026 13:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants