socket.io icon indicating copy to clipboard operation
socket.io copied to clipboard

Does not work with Nuxt 3 in prod with latest packages

Open beermonsterdota opened this issue 1 year ago • 3 comments

Method from guide worked fine until nuxt/nitro updated crossws version to 0.3.0, where peer.ctx is moved to peer._internal. Simple change from ctx to _internal will work only in dev mode, but _iternal are undefined in production mode.

Possible temp solution Downgrade crossws version from 0.3.x to 0.2.4 - seems like it was last version that works

Not really socket.io bug, but probably you'll want to update guide

beermonsterdota avatar Oct 12 '24 21:10 beermonsterdota

Seems like i've spent too much time trying to figure out reason why it suddenly stopped working.

Solution for 0.3.x crossws versions:

nitroApp.router.use(
    '/socket.io/',
    defineEventHandler({
      handler(event) {
        // @ts-expect-error private method
        engine.handleRequest(event.node.req, event.node.res)
        event._handled = true
      },
      websocket: {
        open(peer) {
          // @ts-expect-error private method
          const internal = peer._internal
          const req = internal.request
          // @ts-expect-error private method
          engine.prepare(req)

          // @ts-expect-error
          const rawSocket = internal.request._req.socket
          const websocket = internal.ws

          // @ts-expect-error private method
          engine.onWebSocket(req, rawSocket, websocket)
        }
      }
    })
  )

beermonsterdota avatar Oct 12 '24 23:10 beermonsterdota

It doesn't work for me with https :

Seems like i've spent too much time trying to figure out reason why it suddenly stopped working.

Solution for 0.3.x crossws versions:

nitroApp.router.use(
    '/socket.io/',
    defineEventHandler({
      handler(event) {
        // @ts-expect-error private method
        engine.handleRequest(event.node.req, event.node.res)
        event._handled = true
      },
      websocket: {
        open(peer) {
          // @ts-expect-error private method
          const internal = peer._internal
          const req = internal.request
          // @ts-expect-error private method
          engine.prepare(req)

          // @ts-expect-error
          const rawSocket = internal.request._req.socket
          const websocket = internal.ws

          // @ts-expect-error private method
          engine.onWebSocket(req, rawSocket, websocket)
        }
      }
    })
  )

It works :

nitroApp.router.use(
    "/socket.io/",
    defineEventHandler({
      handler(event) {
        // @ts-expect-error private method
        engine.handleRequest(event.node.req, event.node.res);
        event._handled = true;
      },
      websocket: {
        open(peer) {
          // @ts-expect-error private method and property
          engine.prepare(peer._internal.nodeReq);
          // @ts-expect-error private method and property
          engine.onWebSocket(peer._internal.nodeReq, peer._internal.nodeReq.socket, peer.websocket);
        }
      }
    }));

mhfeizi avatar Oct 17 '24 20:10 mhfeizi

This sound to me like an issue with either Nitro or unjs/crossws but not Nuxt and not Socket.io ?

MickL avatar Oct 18 '24 13:10 MickL

The guide has been updated to work with crossws@^0.3.0: https://socket.io/how-to/use-with-nuxt#hook-the-socketio-server

The example too: https://github.com/socketio/socket.io/tree/main/examples/nuxt-example

Thanks for the heads-up!

darrachequesne avatar Oct 23 '24 06:10 darrachequesne

This issue seems to not be fixed, and the minimal example does not work because of the issue outlined above. It does work fine in dev but does not for production. I can observe the same behavior in my application after updating. Accessing nodeReq on peer._internal is not possible in production. It throws errors like this and prevents the connection to be established / upgraded properly.

[nitro] [unhandledRejection] TypeError: Cannot read properties of undefined (reading 'nodeReq')

fmgrafikdesign avatar Oct 27 '24 20:10 fmgrafikdesign

The issue was resolved by the latest (3.14) nuxt release, which upgraded nitropack to the 2.10 version which also uses crossws >=0.3 🥳 I speculate that the issue stemmed from socket.io and nitropack (nuxt server layer) using different crossws version, leading to incompatibilities.

To resolve the issue, upgrade to the latest nuxt version (3.14).

If this is for any reason not possible for you, you could try to just try to update the nitropack dependency to >=2.10 (I have not tested this).

fmgrafikdesign avatar Nov 04 '24 20:11 fmgrafikdesign

Socket.IO with Nuxt 3 (Nitro/H3) - Unraveling the "Magic" for WebSocket Integration

Date: May 14, 2025 Environment:

  • Nuxt: 3.17.3
  • Nitro: 2.11.11
  • Socket.IO: v4.x (specifically [email protected] and associated engine.io)
  • Node.js: (Version from user's environment, likely Node 18+ or 20+)

Executive Summary of the "Magic"

After extensive debugging, the "magic" that enables a fully functional Socket.IO server (including WebSocket upgrades) within a Nuxt 3 (Nitro/H3) environment lies in a specific, potentially undocumented, call signature for engine.io's onWebSocket method when interacting with the WebSocket objects provided by Nitro's internal WebSocket handler (crossws).

The core working pattern, derived from the user's original functional code, is:

In server/plugins/socket.io.ts:

  1. Configuration (nuxt.config.ts):

    • nitro: { experimental: { websocket: true } } MUST be true. This enables Nitro's underlying WebSocket handling mechanism (crossws), which provides the peer object with the necessary (albeit internal) properties.
  2. Plugin Structure (server/plugins/socket.io.ts):

    • Explicitly create an EngineIOServer instance with desired options (CORS, transports).
    • Create a SocketIOServer instance.
    • Bind the SocketIOServer to the EngineIOServer (io.bind(engine)).
    • Use defineEventHandler for the /socket.io/ path, with separate handler (for HTTP polling) and websocket: (for WebSocket upgrades) sub-handlers.
  3. The "Magic" Call in websocket: { open(peer) { ... } }:

    // Inside the websocket.open(peer) handler:
    
    // 1. Correctly access the original IncomingMessage:
    // @ts-expect-error: _internal.nodeReq is not an official H3Peer property but works.
    const nodeReq = peer._internal.nodeReq as (IncomingMessage & { socket: any; _upgradeHead?: Buffer });
    
    // 2. Correctly access the underlying net.Socket (TCP/TLS socket):
    const nodeNetSocket = nodeReq.socket; 
    
    // 3. Correctly access the raw WebSocket object from Nitro/crossws:
    // @ts-expect-error: .websocket is not an official H3Peer property but works.
    const rawWsObjectFromPeer = peer.websocket; 
    
    // 4. (Optional but good practice) Prepare engine.io with the request:
    engine.prepare(nodeReq); 
    
    // 5. THE CRITICAL "MAGIC" CALL to engine.onWebSocket:
    // Invoke with 3 main arguments (req, socket, ws), OMITTING the 'head' Buffer argument.
    // This specific signature is what works with the WebSocket object provided by crossws.
    // @ts-expect-error: Intentionally using the 3-argument signature that works.
    engine.onWebSocket(nodeReq, nodeNetSocket, rawWsObjectFromPeer); 
    

Why "Best Practice" Attempts Failed and Contradicted the "Magic"

The prolonged debugging primarily stemmed from my (the AI's) attempts to "improve" or "clean" the user's original working code by applying what seemed like best practices or more type-strict approaches, which inadvertently broke the "magic."

  1. Incorrect engine.onWebSocket Call Signature (The Main Culprit):

    • My Mistake: The engine.io (v6.x.x) type definition for onWebSocket typically shows four arguments: (req: IncomingMessage, socket: Duplex, head: Buffer, ws: unknown): void;. I consistently tried to make the code adhere to this by explicitly providing a head buffer (e.g., Buffer.alloc(0)).
    • The "Magic": The user's original, working code omitted the head argument, effectively calling engine.onWebSocket(nodeReq, nodeNetSocket, rawWsObjectFromPeer).
    • Why it Broke: Forcing the head argument likely pushed engine.io into a different internal code path that was incompatible with the rawWsObjectFromPeer provided by crossws. This incompatibility manifested as the persistent TypeError: websocket.on is not a function error, even though rawWsObjectFromPeer did have an .on method. The 3-argument call signature is likely an undocumented overload or a specific path that correctly handles the crossws WebSocket object.
  2. Incorrectly Identifying/Accessing Core Objects:

    • My Mistake: I initially tried various ways to get the IncomingMessage, net.Socket, and raw WebSocket object from the peer object (e.g., peer.upgradeReq, peer.request, peer._internal.ws, or creating complex Adapters) before fully understanding that the user's original approach (peer._internal.nodeReq and peer.websocket) was already correct for their environment.
    • The "Magic": The user's original code correctly identified peer._internal.nodeReq (for req and net.Socket) and peer.websocket (for the raw WebSocket) as the valid sources.
  3. Over-reliance on Strict Type Definitions vs. Runtime Behavior:

    • My Mistake: I focused heavily on satisfying TypeScript's strict type definitions for engine.onWebSocket (which indicated 4 arguments) rather than trusting the runtime behavior of the user's original 3-argument call that was confirmed to work.
    • The "Magic": JavaScript's duck-typing réalité, where peer.websocket was "compatible enough" for the 3-argument engine.onWebSocket call, was overlooked in favor of strict typing.
  4. Unnecessary Complexity (e.g., Adapters):

    • My Mistake: The attempts to create NitroWsAdapter were a consequence of not identifying the core issue (the call signature of engine.onWebSocket). If the raw WebSocket object (peer.websocket) can be used directly (as in the user's original code), an adapter is redundant and adds complexity.

Key Takeaways for Future Socket.IO + Nuxt/Nitro Integration:

  1. Prioritize Working Code: If a specific, albeit seemingly "internal" or "undocumented," code pattern is confirmed to work in a particular framework version, treat it as the ground truth for that environment.
  2. The websocket.open(peer) Handler is Crucial: When nitro.experimental.websocket: true is set, this handler is the designated point for integrating with Nitro's WebSocket mechanism (crossws).
  3. Identify Key Objects from peer:
    • IncomingMessage (with .socket as net.Socket): Reliably obtain this via peer._internal.nodeReq.
    • Raw WebSocket Object: Reliably obtain this via peer.websocket.
  4. The engine.onWebSocket Call Signature is Environment-Specific:
    • Crucially, for the user's Nuxt 3.17.3/Nitro 2.11.11 setup, the working call is engine.onWebSocket(nodeReq, nodeNetSocket, rawWsObjectFromPeer); (omitting the head Buffer).
    • Be extremely cautious about altering this call signature based solely on generic type definitions if a specific signature is known to work.
  5. Minimalism is Key: Avoid unnecessary adapters or complex logic if a direct approach (as shown by the original working code) is functional. "Cleaning" should not break functionality.
  6. TypeScript Comments for "Magic" Code: Use // @ts-expect-error or as any with clear explanations for lines of code that rely on internal or undocumented behaviors that are known to work but might not align perfectly with strict type definitions. This maintains type safety elsewhere while acknowledging environment-specific necessities.

This debugging journey underscores the challenges of integrating libraries across multiple abstraction layers. The "magic" often lies in understanding the precise interaction points and expected interfaces at the boundaries of these layers, which may not always be perfectly documented or align with generic "best practices."

iPLAYCAFE avatar May 13 '25 17:05 iPLAYCAFE