From bafb5a2b305bc9a4e8942bf51b599679bbe1aa5d Mon Sep 17 00:00:00 2001 From: mq1 Date: Wed, 4 Nov 2026 16:04:09 +0900 Subject: [PATCH 1/2] fix(nuxt): Keep Nitro 2 WebSocket handlers resolvable with the managed tunnel --- .../runtime/plugins/tunnel-legacy.server.ts | 3 +- .../plugins/tunnel-legacy.server.test.ts | 69 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 packages/nuxt/test/runtime/plugins/tunnel-legacy.server.test.ts diff --git a/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts b/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts index 89b1a04e8958..14329835c956 100644 --- a/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts +++ b/packages/nuxt/src/runtime/plugins/tunnel-legacy.server.ts @@ -18,8 +18,9 @@ export default (nitroApp => { // Not the `request` hook: h3 still runs the first layer after a hook answered, and with a `headers` route rule // that layer throws for writing headers on the sent response. + // `/` (not `''`) so h3's WebSocket resolver skips this layer and finds the app's WebSocket handlers. nitroApp.h3App.stack.unshift({ - route: '', + route: '/', handler: eventHandler(event => { // Checked on the method and path first, so no other request's body gets wrapped in a web `Request`. if (!isServedTunnelRequest({ method: event.method, url: event.path })) { diff --git a/packages/nuxt/test/runtime/plugins/tunnel-legacy.server.test.ts b/packages/nuxt/test/runtime/plugins/tunnel-legacy.server.test.ts new file mode 100644 index 000000000000..21a328815764 --- /dev/null +++ b/packages/nuxt/test/runtime/plugins/tunnel-legacy.server.test.ts @@ -0,0 +1,69 @@ +import { createApp, createRouter, defineWebSocketHandler, eventHandler, toWebHandler } from 'h3'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import tunnelPlugin from '../../../src/runtime/plugins/tunnel-legacy.server'; + +const { handleServedTunnelRequest, isServedTunnelRequest } = vi.hoisted(() => ({ + handleServedTunnelRequest: vi.fn(), + isServedTunnelRequest: vi.fn(), +})); + +vi.mock('@sentry/core/server', () => ({ + _INTERNAL_handleServedTunnelRequest: handleServedTunnelRequest, + _INTERNAL_isServedTunnelRequest: isServedTunnelRequest, + _INTERNAL_setManagedTunnelDefaults: vi.fn(), +})); + +vi.mock('nitropack/runtime', () => ({ + useRuntimeConfig: () => ({ app: { baseURL: '/' } }), +})); + +function setupApp() { + const app = createApp(); + const router = createRouter(); + const upgrade = vi.fn(); + router.use('/ws/:code', defineWebSocketHandler({ upgrade })); + router.get( + '/hello', + eventHandler(() => 'hello'), + ); + // Nitro mounts its router on the app base URL + app.use('/', router.handler); + + tunnelPlugin({ h3App: app } as unknown as Parameters[0]); + + return { app, upgrade }; +} + +describe('tunnel-legacy.server', () => { + beforeEach(() => { + vi.resetAllMocks(); + isServedTunnelRequest.mockImplementation(({ url }: { url: string }) => url === '/tunnel'); + handleServedTunnelRequest.mockResolvedValue(new Response('tunneled')); + }); + + it("keeps the app's WebSocket handlers resolvable", async () => { + const { app, upgrade } = setupApp(); + + const hooks = await app.websocket.resolve!(new Request('http://localhost/ws/abc') as never); + + expect(hooks.upgrade).toBe(upgrade); + }); + + it('answers tunnel requests', async () => { + const { app } = setupApp(); + + const response = await toWebHandler(app)(new Request('http://localhost/tunnel', { method: 'POST', body: '{}' })); + + expect(await response.text()).toBe('tunneled'); + expect(handleServedTunnelRequest).toHaveBeenCalledTimes(1); + }); + + it('hands other requests to the next layer', async () => { + const { app } = setupApp(); + + const response = await toWebHandler(app)(new Request('http://localhost/hello')); + + expect(await response.text()).toBe('hello'); + expect(handleServedTunnelRequest).not.toHaveBeenCalled(); + }); +}); From 19229337e4bc2b77f1b1825546c9137efc7c35d3 Mon Sep 17 00:00:00 2001 From: mq1 Date: Tue, 10 Nov 2026 03:07:55 +0900 Subject: [PATCH 2/2] test(nuxt): Fix formatting and types in Nitro 2 tunnel plugin test --- .../plugins/tunnel-legacy.server.test.ts | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/packages/nuxt/test/runtime/plugins/tunnel-legacy.server.test.ts b/packages/nuxt/test/runtime/plugins/tunnel-legacy.server.test.ts index b82d66531f73..e5d2a42c7319 100644 --- a/packages/nuxt/test/runtime/plugins/tunnel-legacy.server.test.ts +++ b/packages/nuxt/test/runtime/plugins/tunnel-legacy.server.test.ts @@ -1,45 +1,44 @@ -import { createApp, createRouter, defineWebSocketHandler } from "h3"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import tunnelPlugin from "../../../src/runtime/plugins/tunnel-legacy.server"; +import { createApp, createRouter, defineWebSocketHandler } from 'h3'; +import type { NitroApp, NitroAppPlugin } from 'nitropack'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import legacyTunnelPlugin from '../../../src/runtime/plugins/tunnel-legacy.server'; const { setTunnelDefaults, useRuntimeConfig } = vi.hoisted(() => ({ setTunnelDefaults: vi.fn(), useRuntimeConfig: vi.fn(), })); -vi.mock("@sentry/core/server", () => ({ +vi.mock('@sentry/core/server', () => ({ _INTERNAL_setTunnelDefaults: setTunnelDefaults, })); -vi.mock("nitropack/runtime", () => ({ useRuntimeConfig })); +vi.mock('nitropack/runtime', () => ({ useRuntimeConfig })); -type NitroApp = Parameters[0]; +const tunnelPlugin: NitroAppPlugin = legacyTunnelPlugin; -describe("tunnel-legacy.server", () => { +describe('tunnel-legacy.server', () => { beforeEach(() => { vi.resetAllMocks(); - useRuntimeConfig.mockReturnValue({ app: { baseURL: "/base/" } }); + useRuntimeConfig.mockReturnValue({ app: { baseURL: '/base/' } }); }); it("serves the tunnel under the app's base URL", () => { tunnelPlugin({ h3App: createApp() } as unknown as NitroApp); - expect(setTunnelDefaults).toHaveBeenCalledWith({ basePath: "/base/" }); + expect(setTunnelDefaults).toHaveBeenCalledWith({ basePath: '/base/' }); }); it("keeps the app's WebSocket handlers resolvable", async () => { const app = createApp(); const router = createRouter(); const upgrade = vi.fn(); - router.use("/ws/:code", defineWebSocketHandler({ upgrade })); + router.use('/ws/:code', defineWebSocketHandler({ upgrade })); // Nitro mounts its router on the app base URL - app.use("/", router.handler); + app.use('/', router.handler); tunnelPlugin({ h3App: app } as unknown as NitroApp); - const hooks = await app.websocket.resolve!( - new Request("http://localhost/ws/abc") as never, - ); + const hooks = await app.websocket.resolve!(new Request('http://localhost/ws/abc') as never); expect(hooks.upgrade).toBe(upgrade); });