Skip to content

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

Open
s1gr1d wants to merge 16 commits into
developfrom
sig/hono-5--1
Open

s1gr1d wants to merge 16 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. Only if a route handler declares next (and can pass the request on), the SDK wraps the route handlers, so the handler that starts last renames the span.

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: f586e2d (PR test merge using develop at 869b92c)

Against: 869b92c (develop) · base run #63667

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

Path Size % Change Change
@sentry/browser 29.98 kB - -
@sentry/browser - with treeshaking flags 28.13 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 28.03 kB - -
@sentry/browser (incl. Tracing) 52.17 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 52.18 kB - -
@sentry/browser (incl. Tracing, Profiling) 55.12 kB - -
@sentry/browser (incl. Tracing, Replay) 91.97 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 80.86 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 96.7 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 109.67 kB - -
@sentry/browser (incl. Feedback) 47.54 kB - -
@sentry/browser (incl. sendFeedback) 35.05 kB - -
@sentry/browser (incl. FeedbackAsync) 40.15 kB - -
@sentry/browser (incl. Metrics) 31.01 kB - -
@sentry/browser (incl. Logs) 31.28 kB - -
@sentry/browser (incl. Metrics & Logs) 31.94 kB - -
@sentry/react 31.81 kB - -
@sentry/react (incl. Tracing) 54.5 kB - -
@sentry/vue 38.14 kB - -
@sentry/vue (incl. Tracing) 55.11 kB - -
@sentry/svelte 30.01 kB - -
@sentry/remix (Remix 3 client bundle) 57.19 kB - -
CDN Bundle 31.76 kB - -
CDN Bundle (incl. Tracing) 52.7 kB - -
CDN Bundle (incl. Logs, Metrics) 33.95 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 54.65 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74.93 kB - -
CDN Bundle (incl. Tracing, Replay) 90.4 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 92.33 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 96.55 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 98.49 kB - -
CDN Bundle - uncompressed 93.51 kB - -
CDN Bundle (incl. Tracing) - uncompressed 156.36 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 100.06 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 162.33 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 230.28 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 276.7 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 282.66 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 290.4 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 296.35 kB - -
@sentry/nextjs (client) 57.38 kB - -
@sentry/sveltekit (client) 52.55 kB - -
@sentry/core/server 43.62 kB - -
@sentry/core/browser 13.81 kB - -
@sentry/node 154.26 kB +0.2% +299 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 81.58 kB - -
@sentry/node - without tracing 95.76 kB +0.31% +289 B 🔺
@sentry/node - without channel injection 134.93 kB +0.22% +296 B 🔺
@sentry/aws-serverless 103.94 kB +0.29% +295 B 🔺
@sentry/cloudflare (withSentry) - minified 197.26 kB - -
@sentry/cloudflare (withSentry) 490.05 kB - -
@sentry/nextjs/cloudflare (withSentry) - minified 217.84 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

Copy link
Copy Markdown
Contributor

Optional E2E tests

Some optional checks were not successful. They do not block the merge. 4 not successful, 56 successful. See the run.

Check Status
❌ CI: Optional E2E Tests / E2E node-anthropic-send-to-sentry (latest) Test (optional) Failing after 11s Details
❌ CI: Optional E2E Tests / E2E node-openai-decisions-send-to-sentry (latest) Test (optional) Failing after 4m Details
❌ CI: Optional E2E Tests / E2E node-openai-decisions-send-to-sentry Test (optional) Failing after 4m Details
❌ CI: Optional E2E Tests / E2E node-eve (latest) Test (optional) Failing after 2m Details
Successful checks
Check Status
✅ CI: Optional E2E Tests / E2E cloudflare-flue-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react-cloudflare Test (optional) Successful in 31s Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps (client source maps kept) Test (optional) Successful in 21s Details
✅ CI: Optional E2E Tests / E2E react-router-7-framework-instrumentation Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E cloudflare-pi-durable Test (optional) Successful in 23s Details
✅ CI: Optional E2E Tests / E2E nuxt-4-static (canary) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps Test (optional) Successful in 23s Details
✅ CI: Optional E2E Tests / E2E solidstart-2 Test (optional) Successful in 35s Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries (cloudflare) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-workers-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E astro-7-static Test (optional) Successful in 48s Details
✅ CI: Optional E2E Tests / E2E cloudflare-mastra 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 nextjs-16-tunnel (cloudflare) Test (optional) Successful in 36s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (cloudflare, latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E gen-ai-libraries Test (optional) Successful in 52s Details
✅ CI: Optional E2E Tests / E2E cloudflare-pi-durable (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E cloudflare-think Test (optional) Successful in 39s Details
✅ CI: Optional E2E Tests / E2E node-pi-durable (latest) Test (optional) Successful in 30s Details
✅ CI: Optional E2E Tests / E2E nuxt-3 (canary) Test (optional) Successful in 57s Details
✅ CI: Optional E2E Tests / E2E node-eve Test (optional) Successful in 40s Details
✅ CI: Optional E2E Tests / E2E debug-id-sourcemaps Test (optional) Successful in 19s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (canary, turbopack) Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (turbo) Test (optional) Successful in 3m Details
✅ CI: Optional E2E Tests / E2E astro-7 Test (optional) Successful in 52s Details
✅ CI: Optional E2E Tests / E2E node-eve (orchestrion) Test (optional) Successful in 37s Details
✅ CI: Optional E2E Tests / E2E cloudflare-agent Test (optional) Successful in 32s Details
✅ CI: Optional E2E Tests / E2E deno (latest) Test (optional) Successful in 47s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (deno) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-express-send-to-sentry Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-anthropic-send-to-sentry Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-flue (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nuxt-5-sourcemaps (static / nuxt generate) Test (optional) Successful in 22s Details
✅ CI: Optional E2E Tests / E2E nuxt-5 Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-15 (latest) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-fastify-6 (tracer provider) Test (optional) Successful in 20s Details
✅ CI: Optional E2E Tests / E2E node-openai-send-to-sentry Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-flue Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-express-mcp-v1 Test (optional) Successful in 16s Details
✅ CI: Optional E2E Tests / E2E node-express-send-to-sentry-static Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E node-fastify-6 Test (optional) Successful in 19s Details
✅ CI: Optional E2E Tests / E2E nextjs-14 (latest) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-openai-send-to-sentry (latest) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-orpc Test (optional) Successful in 59s Details
✅ CI: Optional E2E Tests / E2E node-mastra Test (optional) Successful in 45s Details
✅ CI: Optional E2E Tests / E2E node-express-mcp-v2 Test (optional) Successful in 15s Details
✅ CI: Optional E2E Tests / E2E nextjs-14 (canary) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-pi-durable Test (optional) Successful in 20s Details
✅ CI: Optional E2E Tests / E2E nextjs-16-standalone (canary, turbopack) Test (optional) Successful in 20s Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (canary, webpack) Test (optional) Successful in 5m Details
✅ CI: Optional E2E Tests / E2E supabase-nextjs Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E tanstackstart-react-sourcemaps Test (optional) Successful in 19s Details
✅ CI: Optional E2E Tests / E2E nuxt-4 (canary) Test (optional) Successful in 2m Details
✅ CI: Optional E2E Tests / E2E nextjs-16 (vinext, bun) Test (optional) Successful in 1m Details
✅ CI: Optional E2E Tests / E2E node-mastra (latest) Test (optional) Successful in 46s 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.

This branch has not been deployed

No deployments
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.

2 participants