socket.io
socket.io copied to clipboard
Does not work with Nuxt 3 in prod with latest packages
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
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 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);
}
}
}));
This sound to me like an issue with either Nitro or unjs/crossws but not Nuxt and not Socket.io ?
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!
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')
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).
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 associatedengine.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:
-
Configuration (
nuxt.config.ts):nitro: { experimental: { websocket: true } }MUST betrue. This enables Nitro's underlying WebSocket handling mechanism (crossws), which provides thepeerobject with the necessary (albeit internal) properties.
-
Plugin Structure (
server/plugins/socket.io.ts):- Explicitly create an
EngineIOServerinstance with desired options (CORS, transports). - Create a
SocketIOServerinstance. - Bind the
SocketIOServerto theEngineIOServer(io.bind(engine)). - Use
defineEventHandlerfor the/socket.io/path, with separatehandler(for HTTP polling) andwebsocket:(for WebSocket upgrades) sub-handlers.
- Explicitly create an
-
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."
-
Incorrect
engine.onWebSocketCall Signature (The Main Culprit):- My Mistake: The
engine.io(v6.x.x) type definition foronWebSockettypically 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 aheadbuffer (e.g.,Buffer.alloc(0)). - The "Magic": The user's original, working code omitted the
headargument, effectively callingengine.onWebSocket(nodeReq, nodeNetSocket, rawWsObjectFromPeer). - Why it Broke: Forcing the
headargument likely pushedengine.iointo a different internal code path that was incompatible with therawWsObjectFromPeerprovided bycrossws. This incompatibility manifested as the persistentTypeError: websocket.on is not a functionerror, even thoughrawWsObjectFromPeerdid have an.onmethod. The 3-argument call signature is likely an undocumented overload or a specific path that correctly handles thecrosswsWebSocket object.
- My Mistake: The
-
Incorrectly Identifying/Accessing Core Objects:
- My Mistake: I initially tried various ways to get the
IncomingMessage,net.Socket, and raw WebSocket object from thepeerobject (e.g.,peer.upgradeReq,peer.request,peer._internal.ws, or creating complex Adapters) before fully understanding that the user's original approach (peer._internal.nodeReqandpeer.websocket) was already correct for their environment. - The "Magic": The user's original code correctly identified
peer._internal.nodeReq(forreqandnet.Socket) andpeer.websocket(for the raw WebSocket) as the valid sources.
- My Mistake: I initially tried various ways to get the
-
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.websocketwas "compatible enough" for the 3-argumentengine.onWebSocketcall, was overlooked in favor of strict typing.
- My Mistake: I focused heavily on satisfying TypeScript's strict type definitions for
-
Unnecessary Complexity (e.g., Adapters):
- My Mistake: The attempts to create
NitroWsAdapterwere a consequence of not identifying the core issue (the call signature ofengine.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.
- My Mistake: The attempts to create
Key Takeaways for Future Socket.IO + Nuxt/Nitro Integration:
- 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.
- The
websocket.open(peer)Handler is Crucial: Whennitro.experimental.websocket: trueis set, this handler is the designated point for integrating with Nitro's WebSocket mechanism (crossws). - Identify Key Objects from
peer:IncomingMessage(with.socketasnet.Socket): Reliably obtain this viapeer._internal.nodeReq.- Raw WebSocket Object: Reliably obtain this via
peer.websocket.
- The
engine.onWebSocketCall 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 theheadBuffer). - Be extremely cautious about altering this call signature based solely on generic type definitions if a specific signature is known to work.
- Crucially, for the user's Nuxt 3.17.3/Nitro 2.11.11 setup, the working call is
- 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.
- TypeScript Comments for "Magic" Code: Use
// @ts-expect-errororas anywith 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."