Proxy Websocket Messages in Nuxt 3/4

Hey there, im have implemented a websocket server in my Fastify Backend running separatly from nuxt in a diffrent docker container on same host. I already got endpoint /api/[...].ts up running which proxies everything to backend:
import { joinURL } from 'ufo'

export default defineEventHandler(async (event) => {
const apiUrl = useRuntimeConfig().apiUrl as string

const target = joinURL(apiUrl, event.path)

return proxyRequest(event, target)
})
import { joinURL } from 'ufo'

export default defineEventHandler(async (event) => {
const apiUrl = useRuntimeConfig().apiUrl as string

const target = joinURL(apiUrl, event.path)

return proxyRequest(event, target)
})
Now i noticed that this doesnt work with websockets and even if i enable experimental websocket support in nitro. If i enable the experimental websocket support nitro will boot up and websocket but not proxy it to my backend. I know that this is an old topic which came up multiple times in the past, however i tried to find a solution to this topic using ChatGPT and Github Copilot which suggested that i build a websocket server using defineWebSocketHandler and put that into <PROJECT_ROOT>/server/api/ws.ts (Route is currently /api/ws on both frontend and backend). However using the following code peice, i get only errors, because the websocket just reconnects without sending any messages. I got the frontend logic in browser working which sends messages to the nitro server, but the code doesnt send it to the backend. IMPORTANT: Im in the dev envoirment currently in Jetbrains Webstorm
In Console i get WebSocket Connectet several thousand times over a span of 5 minutes.
import WebSocket from 'ws';

export default defineWebSocketHandler({
open(peer) {
const backend = new WebSocket(peer.request.url);
const queue:string[] = [];

backend.on("open", () => {
console.log("WebSocket opened");
for(const msg of queue) {
backend.send(msg);
}
})

// forward backend messages → client
backend.on('message', (data) => peer.send(data));
peer.websocket.onmessage = (msg) => {
if(backend.readyState !== WebSocket.OPEN) {
queue.push(msg.data as string);
return;
}
console.log("<" + msg.data);
backend.send(msg.data);
}

backend.on('close', () => peer.close());
peer.websocket.onclose = () => backend.close()
},
});
import WebSocket from 'ws';

export default defineWebSocketHandler({
open(peer) {
const backend = new WebSocket(peer.request.url);
const queue:string[] = [];

backend.on("open", () => {
console.log("WebSocket opened");
for(const msg of queue) {
backend.send(msg);
}
})

// forward backend messages → client
backend.on('message', (data) => peer.send(data));
peer.websocket.onmessage = (msg) => {
if(backend.readyState !== WebSocket.OPEN) {
queue.push(msg.data as string);
return;
}
console.log("<" + msg.data);
backend.send(msg.data);
}

backend.on('close', () => peer.close());
peer.websocket.onclose = () => backend.close()
},
});
i added the queue because when i send on the frontend, i dont know if the connection to backend is established. Websocket service connection code:
websocket.requestUpdate(["teams", "status"]);
websocket.requestTeamsList();

websocket.on("*", (message: WebsocketMessage) => {
if(message.type === 'error' && message.data){
error.value = message.data.message;
}
else {
useWebsocketRequestHandler(message);
}
})

onMounted(async () => {
// some other logic
websocket.connect(useAuth().findAccessToken()?.token ?? '');
})
websocket.requestUpdate(["teams", "status"]);
websocket.requestTeamsList();

websocket.on("*", (message: WebsocketMessage) => {
if(message.type === 'error' && message.data){
error.value = message.data.message;
}
else {
useWebsocketRequestHandler(message);
}
})

onMounted(async () => {
// some other logic
websocket.connect(useAuth().findAccessToken()?.token ?? '');
})
(websocket service in message below) Nuxt info: - Operating System: Windows_NT - Node Version: v22.19.0 - Nuxt Version: 3.17.5 - CLI Version: 3.25.1 - Nitro Version: 2.11.12 - Package Manager: npm@10.9.3 - Builder: - - User Config: modules, ssr, devtools, site, spaLoadingTemplate, runtimeConfig, build, routeRules, devServer, future, compatibilityDate, vite, typescript, eslint, i18n, image, pwa, security, umami - Runtime Modules: nuxt-security@2.2.0, @nuxtjs/i18n@9.5.5, @pinia/nuxt@0.11.1, nuxt-umami@3.2.0, @galaxybotweb/components@1.0.133, @nuxt/eslint@1.4.1, @nuxt/image@1.10.0, nuxt-tiptap-editor@2.2.1, @vite-pwa/nuxt@1.0.3, @nuxtjs/robots@5.2.10 - Build Modules: -
3 Replies
kapa.ai
kapa.ai2mo ago
Hi @TheBlueMeisterGamer. I'm kapa.ai, your friendly support bot. I will try to answer your question by searching through the knowledge base. :wave: ---------------------- I'm sorry, but the knowledge sources provided do not contain explicit information or working examples for proxying WebSocket connections from a Nuxt 3/4 (Nitro) server to a separate backend WebSocket server. While there is a YouTube video that discusses integrating WebSockets in Nuxt and Nitro, it focuses on implementing WebSocket handlers directly in Nitro, not on proxying to an external backend WebSocket server. The official documentation and examples also do not cover WebSocket proxying scenarios—only standard HTTP proxying with proxyRequest is documented, which does not support WebSockets as you have already discovered Docs: Directory-structure > server. Your approach using defineWebSocketHandler is correct for handling WebSockets natively in Nitro, but there is no documented or officially supported way to forward or proxy WebSocket traffic from Nitro to another backend WebSocket server. The Nitro WebSocket support is intended for handling connections within Nitro itself, not for acting as a transparent proxy to another WebSocket service YouTube: Integrating WebSockets in Nuxt and Nitro. If you need to proxy WebSocket connections, you may need to use an external reverse proxy (like Nginx or Traefik) configured to forward WebSocket traffic directly to your backend WebSocket server, bypassing Nitro/Nuxt for those routes. This is a common pattern for production deployments but is not covered in the Nuxt documentation. In summary:
- Nitro's proxyRequest does not support WebSockets. - defineWebSocketHandler is for handling WebSockets in Nitro, not for proxying. - There is no official or documented way in Nuxt/Nitro to proxy WebSocket connections to another backend server. If you need further guidance, you may want to consult the Nitro or Nuxt GitHub discussions or consider using an external reverse proxy for WebSocket traffic. ---------------------- Note: you can ask me a follow up question by @ mentioning me again :speech_balloon: ----------------------
kapa.ai
kapa.ai2mo ago
<@570917654076981248> Kapa.ai is still learning and improving, please let me know how I did by reacting below.
TheBlueMeisterGamer
Here is the websocket service in frontend. some parts i had to remove because of secrets that should be revealed, but they dont matter either, because they are not required for the problem (i didnt remove any methods, only method bodies)
import type {
MessageCallable,
WebsocketMessage,
WebsocketRequestMessage,
WebsocketUpdateKey
} from "~/type/WebsocketView";
import {useServerStore} from "~/store/ServerStore";
import {SmallGuild} from "~/src/domain/Team";
import z from "zod";

export class WebsocketService {
private client: WebSocket|null;
static instance:WebsocketService|null = null;
private handler: Map<string, MessageCallable> = new Map();
private readonly queue: WebsocketRequestMessage[] = [];
private connected = false;

public constructor() {
if(WebsocketService.instance) throw new Error("Websocket instance already initialized");
// Use the static connect method to create an instance
WebsocketService.instance = this;
this.client = null;
}

public static get():WebsocketService {
if(!WebsocketService.instance) {
WebsocketService.instance = new WebsocketService();
}
return WebsocketService.instance;
}

public close() {
this.client?.close();
}

public connect(token:string) {
this.client = new WebSocket('/api/ws?token=' + token);

this.client?.addEventListener("error", (error) => {
console.error('WebSocket Error: ', error);
})

this.client?.addEventListener("open", () => {
console.log('WebSocket Client Connected');
this.connected = true;

while(this.queue.length > 0) {
const message = this.queue.shift();
if(message) {
this.sendMessage(message);
}
}
});

this.client.addEventListener("close", () => {
console.log('WebSocket Client Disconnected');
this.connected = false;
setTimeout(() => {
this.connect(token);
}, 5000);
})

this.client.addEventListener("message", (message) => {
const data:WebsocketMessage = JSON.parse(message.data);
if(this.handler.has(data.type)) {
const handlerCallback = this.handler.get(data.type);
if(handlerCallback) {
handlerCallback(data);
}
}
if(this.handler.has("*")) {
const handlerCallback = this.handler.get("*");
if(handlerCallback) {
handlerCallback(data);
}
}
})
}

private sendMessage(message: WebsocketRequestMessage) {
if(!this.connected) {
this.queue.push(message);
}
console.log(`Websocket Message: ${JSON.stringify(message)}`);
this.client?.send(JSON.stringify(message));
}

public requestUpdate(keys: WebsocketUpdateKey[]) {
// ...
}

public requestTeamsList() {
// ...
}

public requestServerList() {
// ...
}

public requestFetchServer(serverID: string) {
// ...
}

public requestFetchTeam(serverID: string) {
// ...
}

public requestSaveSettings(serverID: string, module: string, settings: any) {
// ...
}

public on(type:string, callback: MessageCallable) {
this.handler.set(type, callback);
}

public off(type:string) {
this.handler.delete(type);
}
}

export const useWebsocketRequestHandler = (message:WebsocketMessage) => {
// ...
}
import type {
MessageCallable,
WebsocketMessage,
WebsocketRequestMessage,
WebsocketUpdateKey
} from "~/type/WebsocketView";
import {useServerStore} from "~/store/ServerStore";
import {SmallGuild} from "~/src/domain/Team";
import z from "zod";

export class WebsocketService {
private client: WebSocket|null;
static instance:WebsocketService|null = null;
private handler: Map<string, MessageCallable> = new Map();
private readonly queue: WebsocketRequestMessage[] = [];
private connected = false;

public constructor() {
if(WebsocketService.instance) throw new Error("Websocket instance already initialized");
// Use the static connect method to create an instance
WebsocketService.instance = this;
this.client = null;
}

public static get():WebsocketService {
if(!WebsocketService.instance) {
WebsocketService.instance = new WebsocketService();
}
return WebsocketService.instance;
}

public close() {
this.client?.close();
}

public connect(token:string) {
this.client = new WebSocket('/api/ws?token=' + token);

this.client?.addEventListener("error", (error) => {
console.error('WebSocket Error: ', error);
})

this.client?.addEventListener("open", () => {
console.log('WebSocket Client Connected');
this.connected = true;

while(this.queue.length > 0) {
const message = this.queue.shift();
if(message) {
this.sendMessage(message);
}
}
});

this.client.addEventListener("close", () => {
console.log('WebSocket Client Disconnected');
this.connected = false;
setTimeout(() => {
this.connect(token);
}, 5000);
})

this.client.addEventListener("message", (message) => {
const data:WebsocketMessage = JSON.parse(message.data);
if(this.handler.has(data.type)) {
const handlerCallback = this.handler.get(data.type);
if(handlerCallback) {
handlerCallback(data);
}
}
if(this.handler.has("*")) {
const handlerCallback = this.handler.get("*");
if(handlerCallback) {
handlerCallback(data);
}
}
})
}

private sendMessage(message: WebsocketRequestMessage) {
if(!this.connected) {
this.queue.push(message);
}
console.log(`Websocket Message: ${JSON.stringify(message)}`);
this.client?.send(JSON.stringify(message));
}

public requestUpdate(keys: WebsocketUpdateKey[]) {
// ...
}

public requestTeamsList() {
// ...
}

public requestServerList() {
// ...
}

public requestFetchServer(serverID: string) {
// ...
}

public requestFetchTeam(serverID: string) {
// ...
}

public requestSaveSettings(serverID: string, module: string, settings: any) {
// ...
}

public on(type:string, callback: MessageCallable) {
this.handler.set(type, callback);
}

public off(type:string) {
this.handler.delete(type);
}
}

export const useWebsocketRequestHandler = (message:WebsocketMessage) => {
// ...
}

Did you find this page helpful?