Skip to content

@sentry/nuxt 11.10.0 tunnel plugin causes Nitro 2 WebSocket handlers to fail to resolve #25991

Open

Description

@chan-mai

Is there an existing issue for this?

How do you use Sentry?

Sentry Saas (sentry.io)

Which SDK are you using?

@sentry/nuxt

SDK Version

11.10.0 (does not occur with 11.9.0)

Framework Version

nuxt 4.6.3, nitropack 2.13.5 (preset: cloudflare-module, experimental.websocket: true), h3 1.15.11, crossws 0.3.5

Link to Sentry event

No response

Reproduction Example/SDK Setup

A WebSocket route is defined using defineWebSocketHandler under Nitro's server/routes directory. The ticket is validated within the upgrade hook, and either a Durable Object response or an HTTP 401 Response is returned.

// server/routes/ws/[code].ts
export default defineWebSocketHandler({
  upgrade(request) {
    const ticket = new URL(request.url).searchParams.get('ticket');
    if (ticket !== 'valid') return new Response('Unauthorized', { status: 401 });
  },
  open(peer) {
    peer.send('welcome');
  },
});

Steps to Reproduce

  1. Update @sentry/nuxt from 11.9.0 to 11.10.0 with the following configuration, build, and deploy to Cloudflare Workers.
  2. Connect to wss://<host>/ws/<code> from a browser.
  3. Connect with an invalid ticket (?ticket=invalid).

Expected Result

  • The upgrade hook is called.
  • For invalid tickets, the connection is rejected with HTTP 401.

Actual Result

  • The upgrade hook is not called.
  • WebSocket connections are established even with invalid tickets (HTTP 101).
  • No messages are sent from the server after connection.

Additional Context

packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts (#25418), which was added in 11.10.0, directly inserts a layer with an empty route ('') at the beginning of the h3 application's layer list.

nitroApp.h3App.stack.unshift({
  route: '',
  handler: eventHandler(event => { ... }),
});

When passing through h3's use, normalizeLayer normalizes the route with withoutTrailingSlash, so layers with a route of '' are registered as '/'. If inserted directly via stack.unshift, this normalization does not take place, and the route remains ''.

Nitro 2's WebSocket handler resolution logic (h3App.websocket.resolve) inspects layers from the beginning and uses handler.__websocket__ of the first matching layer as the hook. In the deployed bundle, the relevant section was the following code.

for (const layer of stack) {
  if (layer.route === "/" && !layer.handler.__resolve__) continue;
  if (!path.startsWith(layer.route)) continue;
  ...
  return { route: layer.route, handler: layer.handler };
}

Layers with a route of '/' are skipped, but layers with a route of '' are not skipped. Since startsWith('') evaluates to true for all paths, the tunnel layer matches all paths. Because this layer does not have a __websocket__, the hooks become {}.

The Cloudflare adapter in crossws 0.3.5 generates its own WebSocketPair and accepts the connection when the result of the upgrade hook is undefined. As a result, the WebSocket handler defined by the app is not called, and the connection appears successful to the client.

If you set the route to '/' when inserting a layer, that layer is skipped during WebSocket handler resolution. For HTTP request processing, layers with a route length of 1 or less match all paths, so we believe there is no impact on tunnel processing (unverified).

Priority

React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it.

Activity

  1. linear-code commented on Nov 2, 2026

    @linear-code
  2. chan-mai commented on Nov 2, 2026

    @chan-mai
    Author

    I would like to create a fix PR for this issue. Please let me know if I can start working on it.

    This changes the route used when inserting the layer for tunnel processing in packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts from '' to '/'.

    nitroApp.h3App.stack.unshift({
      route: '/',
      handler: eventHandler(event => { ... }),
    });

    Nitro 2's WebSocket handler resolution skips layers where the route is '/' and they do not have __resolve__. Therefore, with this change, the tunnel layer will no longer match during WebSocket handler resolution. For HTTP request processing, layers with a route length of 1 or less match all paths, so we believe the tunnel processing will remain unchanged.

    The PR is planned to include the following two items:

    • A test to verify that the upgrade hook for WebSocket routes is called in Nitro 2
    • A test to verify that requests to the tunnel are still processed after the changes

    Please let me know if you plan to fix this with a different approach.

  3. self-assigned this
    on Nov 3, 2026
  4. logaretm commented on Nov 3, 2026

    @logaretm
    Member

    This will be addressed by #26001, but since that's part of a larger stack, feel free to open a PR with the quick fix ('' -> '/') in the meantime.

  5. chan-mai commented on Nov 4, 2026

    @chan-mai
    Author

    Thank you. I have created a PR for the quick fix: #26082

  6. thoda-dev commented on Nov 5, 2026

    @thoda-dev

    Ran into this this morning after updating the module. I ended up with the workaround below, feel free to use it until the fix is released. It doesn't require pinning @sentry/nuxt: a Nitro plugin rewrites the route of Sentry's tunnel layer. Plugins in server/plugins run after module plugins, so the layer is already in the stack.

    // server/plugins/sentry-tunnel-route.ts
    export default defineNitroPlugin((nitroApp) => {
      for (const layer of nitroApp.h3App.stack) {
        if (layer.route === '') layer.route = '/'
      }
    })

    '' and '/' behave the same for HTTP requests, so the tunnel keeps working, and WebSocket handlers resolve again. It can be removed once #26082 / #26001 is released.

  7. andreiborza commented on Nov 6, 2026

    @andreiborza
    Member

    Thank you @chan-mai for the PR and @thoda-dev for the workaround. We'll look at the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions