Skip to content

Hono middleware wrapper strips symbol-keyed handler metadata, breaking framework integrations (e.g. hono-openapi) #20952

@ahmadio

Description

@ahmadio

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/node

SDK Version

10.53.1

Framework Version

@sentry/hono 10.53.1

Link to Sentry event

No response

Reproduction Example/SDK Setup

@sentry/hono's wrapMiddlewareWithSpan (called by patchAppUse + wrapSubAppMiddleware) replaces a Hono middleware handler with a sentryTracedMiddleware wrapper but does not copy own-symbol properties from the original handler. Any framework that attaches metadata to a handler via a Symbol key - hono-openapi's describeRoute is the immediate one - loses that metadata at the point Sentry wraps the handler.

The downstream effect for rhinobase/hono-openapi: generateSpecs(app) walks app.routes, finds sentryTracedMiddleware closures with no Symbol(openapi) set, and returns { paths: {} }. Scalar / /openapi.json renders an empty spec.


Reproduction Example/SDK Setup

// index.mjs
import { Hono } from 'hono'
import { sentry } from '@sentry/hono/node'

const META = Symbol('test-meta')

const baseApp = new Hono()
const app = baseApp.use('*', sentry(baseApp))

const handler = async (_c, next) => next()
handler[META] = { example: 'should survive Sentry wrapping' }

app.use('/test', handler)

const testRoute = (app.routes ?? []).find((r) => r.path === '/test')
const symbols = Object.getOwnPropertySymbols(testRoute.handler)
const hasMeta = symbols.some((s) => s.description === 'test-meta')

console.log({
  hasSentryOriginal: '__sentry_original__' in testRoute.handler,
  symbolsOnHandler: symbols.map((s) => s.description),
})
console.log(hasMeta ? '✅ symbol survived' : '❌ symbol stripped — bug reproduced')

SDK / package versions used:

  • @sentry/hono: 10.53.1
  • @sentry/node: 10.53.1
  • hono: 4.12.19 (also reproduces against 4.12.18)
  • Node.js: 24.15.0 (also reproduces against 22.x — not Node-version-dependent)

Steps to Reproduce

mkdir sentry-hono-symbol-repro && cd sentry-hono-symbol-repro
npm init -y
npm pkg set type=module
npm install hono@^4.12 @sentry/hono@^10.53 @sentry/node@^10.53

Creating and adding the index.mjs content mentioned above.

node index.mjs

Expected Result

Symbol(test-meta) is still readable on the registered handler — Sentry's wrapper preserves framework-attached symbol metadata so downstream tools like hono-openapi continue to see it:

{ hasSentryOriginal: true, symbolsOnHandler: [ 'test-meta' ] }
✅ symbol survived

Actual Result

The wrapper carries __sentry_original__ (so the original is recoverable) but does not carry any of the handler's symbol properties, so every downstream consumer that reads Object.getOwnPropertySymbols(handler) sees nothing:

{ hasSentryOriginal: true, symbolsOnHandler: [] }
❌ symbol stripped — bug reproduced

Additional Context

Root cause

packages/hono/src/shared/wrapMiddlewareSpan.ts (10.53.1 build at build/esm/shared/wrapMiddlewareSpan.js):

function wrapMiddlewareWithSpan(handler) {
  if (getOriginalFunction(handler)) return handler
  const wrapped = async function sentryTracedMiddleware(context, next) { /* … */ }
  markFunctionWrapped(wrapped, handler)
  return wrapped
}

The new function has no Symbol(foo) properties inherited from handler. patchAppUse's proxy maps every handler (including the (path, ...handlers) form used in the repro) through wrapMiddlewareWithSpan, so the symbol-stripped wrapper is exactly what ends up on app.routes[i].handler and what downstream consumers walk.

markFunctionWrapped(wrapped, handler) sets __sentry_original__ so the wrapper is unwrappable in principle, but the wrapper itself doesn't carry the framework's metadata, which is what every other consumer reads.

Suggested fix

Two-line patch — copy own-symbol properties to the wrapper after markFunctionWrapped:

   const wrapped = async function sentryTracedMiddleware(context, next) { /* … */ }

   markFunctionWrapped(wrapped, handler)
+  // Preserve framework-attached symbol metadata (e.g., hono-openapi's
+  // Symbol("openapi"), which downstream tooling reads off the handler).
+  for (const sym of Object.getOwnPropertySymbols(handler)) {
+    wrapped[sym] = handler[sym]
+  }
   return wrapped

Same change should be considered for any other wrapper that returns a NEW function while keeping the original referenceable via __sentry_original__.

Workaround in user code

Until this is fixed upstream, projects using hono-openapi + @sentry/hono together can snapshot+swap-and-restore around the spec generation:

let cache = null
async function generateSpecsAroundSentry() {
  if (cache) return cache
  const swapped = []
  for (const route of app.routes) {
    const original = route.handler?.__sentry_original__
    if (original) {
      swapped.push([route, route.handler])
      route.handler = original
    }
  }
  try {
    cache = await generateSpecs(app, { documentation })
    return cache
  } finally {
    for (const [route, wrapped] of swapped) route.handler = wrapped
  }
}

There's a short race window during the swap (a concurrent request hits the unwrapped handler → no Sentry span for that one request) which we mitigate by caching after first call + pre-warming at boot.

Related

Related issue surface but distinct from #20449 (route groups dropping app.use patches). This one is about the patched app.use succeeding but the wrapper losing handler-level symbol metadata.

Priority

No response

Metadata

Metadata

Assignees

No fields configured for issues without a type.

Projects

Status

No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions