Skip to content

@sentry/hono/bun middleware double-initializes when using Bun preload pattern #21176

@itsjamie

Description

@itsjamie

Environment

  • SDK: @sentry/hono@10.53.1, @sentry/bun@10.53.1
  • Runtime: Bun
  • Framework: Hono

Summary

The Bun Hono middleware does not follow the same initialization pattern as the Node Hono middleware.

For Node, @sentry/hono/node expects Sentry to be initialized separately, usually by loading an instrument.ts file with node --import. The middleware checks whether a client exists and warns if Sentry has not been initialized.

For Bun, @sentry/hono/bun's sentry(app, options) unconditionally calls init(options) internally. That conflicts with the Bun preload/instrumentation pattern, where the SDK is initialized from an instrument.ts file loaded via Bun preload before the application imports.

Documentation context

The preload setup was intentional. The Sentry Bun SDK docs describe creating an instrument.js/instrument.ts file and loading it with Bun preload, for example:

bun --preload ./instrument.ts server.ts

So this is not only an attempt to copy the Node --import pattern onto Bun. It is following the documented Bun SDK setup model where initialization happens in a separate instrumentation file before the application entrypoint is loaded.

The confusing part is that the Hono Bun middleware appears to expect middleware-owned initialization, while the Bun SDK docs point users toward preload-owned initialization.

What happens

When following the preload pattern in a Bun Hono service:

// instrument.ts
import * as Sentry from "@sentry/hono/bun";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
});

and then mounting the Bun Hono middleware:

import * as Sentry from "@sentry/hono/bun";
import { Hono } from "hono";

const app = new Hono();

app.use("*", Sentry.sentry(app, {
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 1.0,
}));

Sentry is initialized twice: once from the Bun preload file and again from the middleware.

In our service this produced duplicate OpenTelemetry registration/debug output and nested/duplicated http.server spans. For health check routes, we also saw catch-all transaction names such as GET /* or GET http://*/, which made it look like route filtering/naming was not working.

Catch-all route naming behavior

There also appears to be a route naming issue in the shared Hono request handler.

@sentry/hono/build/esm/shared/middlewareHandlers.js uses routePath(context, -1) to pick the route name:

const lastMatchedRoute = routePath(context, -1);
activeSpan.updateName(`${context.req.method} ${lastMatchedRoute}`);
updateSpanName(rootSpan, `${context.req.method} ${lastMatchedRoute}`);

In a Hono app with mounted sub-apps and wildcard middleware, routePath(context, -1) can return the last matched wildcard middleware route rather than the route that actually handled the request.

For example, a request to GET /readyz can have a matched route list shaped like:

ALL /*        root middleware, including Sentry
GET /readyz   actual handler
ALL /*        later mounted sub-app middleware

The later mounted ALL /* route may not execute after /readyz handles the request, but it is still present in Hono's matched route list. Because Sentry uses routePath(context, -1), it picks the trailing wildcard and names the transaction GET /*.

After await next(), Hono's current route index points at the handler that produced the response. In our local workaround, using routePath(context) after next() returns the expected route (/readyz), while routePath(context, -1) can still return /*.

So the catch-all transaction names are not only a filtering issue. They can come from using the last matched route instead of the current resolved route after Hono has dispatched the handler.

Why this is surprising

The Node middleware and Bun middleware differ here:

// @sentry/hono/build/esm/node/middleware.js
const sentry = (app) => {
  const sentryClient = getClient();
  if (sentryClient === undefined) {
    debug.warn("Sentry is not initialized...");
  }
  applyPatches(app);
  return async (context, next) => { ... };
};
// @sentry/hono/build/esm/bun/middleware.js
const sentry = (app, options) => {
  const isDebug = options.debug;
  isDebug && debug.log("Initialized Sentry Hono middleware (Bun)");
  init(options);
  applyPatches(app);
  return async (context, next) => { ... };
};

So Node supports the preload/import pattern cleanly, while Bun forces the middleware to own initialization.

Expected behavior

It would be helpful if Bun could support the same shape as Node, for example one of:

  1. @sentry/hono/bun middleware does not call init() when Sentry is already initialized.
  2. A Bun middleware export exists that only installs the Hono request/response handling and app patches, assuming initialization happened in instrument.ts.
  3. The Bun docs explicitly recommend either preload initialization or middleware-owned initialization, but not both, and show the correct Hono + Bun setup.
  4. Hono route naming uses the route that actually handled the request, rather than routePath(context, -1), so mounted wildcard middleware does not collapse transactions into GET /*.

Workaround

We currently have to wrap the Bun middleware behavior locally:

  • initialize Sentry in instrument.ts through Bun preload
  • avoid calling exported Sentry.sentry(app, options) because it calls init() again
  • import/use the shipped Hono requestHandler, responseHandler, and app patching code from the installed package internals
  • guard initialization with if (!Sentry.isInitialized())
  • after await next(), correct the active/root span name using routePath(context) instead of routePath(context, -1)

That works, but it relies on internal package paths because the shared Hono handlers are not exported as public API.

Metadata

Metadata

Assignees

No one assigned
    No fields configured for issues without a type.

    Projects

    Status

    Waiting for: Product Owner

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions