From b05f8ea5d0b0356bab40e25a702ed6646b8783df Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Wed, 6 May 2026 19:18:32 +0200 Subject: [PATCH 1/7] Explain ways to fix turnstile token invalid/rejected, instead of only displaying raw error to user --- src/server/Worker.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/server/Worker.ts b/src/server/Worker.ts index 13cf1f32c2..b3a46d80a5 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -330,7 +330,10 @@ export async function startWorker() { log.warn(`Invalid token: ${result.message}`, { gameID: clientMsg.gameID, }); - ws.close(1002, `Unauthorized: invalid token`); + ws.close( + 1002, + "Unauthorized: invalid token \n\n Try this to fix: \n 1. Do a hard refresh (Mac: Cmd+Shift+R, Windows: Ctrl+F5) \n 2. Or resync your device clock \n 3. Or close all browser windows and restart your browser \n 4. Or try another browser, preferably without extensions \n 5. Or clear site data for this site", + ); return; } const { persistentId, claims } = result; @@ -443,7 +446,10 @@ export async function startWorker() { gameID: clientMsg.gameID, reason: turnstileResult.reason, }); - ws.close(1002, "Unauthorized: Turnstile token rejected"); + ws.close( + 1002, + "Unauthorized: Turnstile token rejected \n\n Try this to fix: \n 1. Do a hard refresh (Mac: Cmd+Shift+R, Windows: Ctrl+F5) \n 2. Or resync your device clock \n 3. Or close all browser windows and restart your browser \n 4. Or try another browser, preferably without extensions \n 5. Or clear site data for this site", + ); return; case "error": // Fail open, allow the client to join. From 9dadb1ebd36e4c575cfb1f724d6f9f7613f1c6e6 Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Wed, 6 May 2026 22:25:59 +0200 Subject: [PATCH 2/7] Translate 1002 errors. Always starts with translation for "connection refused: " followed by translated error. Cosmetic.reason isn't translated, doesn't seem like going another level deeper really helps. Add translated tips if turnstile is invalid. Append English error for screenshots so devs understand what error user gets. --- resources/lang/en.json | 12 ++++++++++++ src/client/Transport.ts | 25 ++++++++++++++++++++++++- src/client/Utils.ts | 11 ++++++++++- src/server/Worker.ts | 26 ++++++++++---------------- 4 files changed, 56 insertions(+), 18 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index a2c7324eb8..940ad09cf4 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -1219,5 +1219,17 @@ "fullscreen": { "enter": "Enter fullscreen", "exit": "Exit fullscreen" + }, + "worker_error": { + "account_banned": "Account Banned", + "cannot_join_game": "Cannot join game", + "connection_refused": "Connection refused", + "flare_forbidden": "Forbidden", + "game_not_found": "Game not found", + "lobby_full": "Lobby full", + "turnstile_invalid": "Unauthorized: invalid token", + "turnstile_fix_tips": "Try this to fix: \n 1. Do a hard refresh (Mac: Cmd+Shift+R, Windows: Ctrl+F5) \n 2. Or resync your device clock \n 3. Or close all browser windows and restart your browser \n 4. Or try another browser, preferably without extensions \n 5. Or clear site data for this site", + "unauthorized": "Unauthorized", + "user_me_fetch_failed": "Unauthorized: user fetch failed" } } diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 1e4526a8c2..2eb9851e8f 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -29,6 +29,7 @@ import { replacer } from "../core/Util"; import { getPlayToken } from "./Auth"; import { LobbyConfig } from "./ClientGameRunner"; import { LocalServer } from "./LocalServer"; +import { getEnglishText, translateText } from "./Utils"; export class PauseGameIntentEvent implements GameEvent { constructor(public readonly paused: boolean) {} @@ -378,8 +379,30 @@ export class Transport { `WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`, ); if (event.code === 1002) { + const connRefusedKey = `worker_error.connection_refused`; + let alertMsg = translateText(connRefusedKey); + const errorKey = `worker_error.${event.reason}`; + alertMsg += `: ${translateText(errorKey)}`; + + // Add tips if turnstile token invalid + if (event.reason === "turnstile_invalid") { + alertMsg += `\n\n${translateText("worker_error.turnstile_fix_tips")}`; + } + + // Append English error if it differs, for screenshots posted by users + const englishMsg = getEnglishText(errorKey); + if (!alertMsg.includes(englishMsg)) { + const englishConnRefusedMsg = getEnglishText(connRefusedKey); + alertMsg += `\n\n--- English ---\n${englishConnRefusedMsg}: `; + if (englishMsg !== errorKey) { + alertMsg += `${englishMsg}`; + } else if (englishMsg === errorKey) { + alertMsg += `${event.reason}`; + } + } + // TODO: make this a modal - alert(`connection refused: ${event.reason}`); + alert(alertMsg); } else if (event.code !== 1000) { console.log(`received error code ${event.code}, reconnecting`); this.reconnect(); diff --git a/src/client/Utils.ts b/src/client/Utils.ts index 943e764a4a..18130f75a9 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -12,6 +12,7 @@ import { import { GameConfig } from "../core/Schemas"; import type { LangSelector } from "./LangSelector"; import { Platform } from "./Platform"; +import { get } from "http"; export const TUTORIAL_VIDEO_URL = "https://www.youtube.com/embed/EN2oOog3pSs"; @@ -407,9 +408,17 @@ function getCachedLangSelector(): LangSelector | null { return found; } +export function getEnglishText( + key: string, + params?: Record, +): string { + return translateText(key, params, true); +} + export const translateText = ( key: string, params?: Record, + getOnlyEnglish: boolean = false, ): string => { const self = translateText as any; self.formatterCache ??= new Map(); @@ -436,7 +445,7 @@ export const translateText = ( self.lastLang = langSelector.currentLang; } - let message = translations?.[key]; + let message = getOnlyEnglish ? undefined : translations?.[key]; const hasPrimaryTranslation = message !== undefined; message ??= defaultTranslations?.[key]; diff --git a/src/server/Worker.ts b/src/server/Worker.ts index b3a46d80a5..b6855c3947 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -330,16 +330,13 @@ export async function startWorker() { log.warn(`Invalid token: ${result.message}`, { gameID: clientMsg.gameID, }); - ws.close( - 1002, - "Unauthorized: invalid token \n\n Try this to fix: \n 1. Do a hard refresh (Mac: Cmd+Shift+R, Windows: Ctrl+F5) \n 2. Or resync your device clock \n 3. Or close all browser windows and restart your browser \n 4. Or try another browser, preferably without extensions \n 5. Or clear site data for this site", - ); + ws.close(1002, "turnstile_invalid"); return; } const { persistentId, claims } = result; if (claims?.role === "banned") { - ws.close(1002, "Account Banned"); + ws.close(1002, "account_banned"); return; } @@ -358,7 +355,7 @@ export async function startWorker() { log.warn( `game ${clientMsg.gameID} not found on worker ${workerId}`, ); - ws.close(1002, "Game not found"); + ws.close(1002, "game_not_found"); } return; } @@ -388,7 +385,7 @@ export async function startWorker() { if (claims === null) { if (allowedFlares !== undefined) { log.warn("Unauthorized: Anonymous user attempted to join game"); - ws.close(1002, "Unauthorized"); + ws.close(1002, "unauthorized"); return; } } else { @@ -399,7 +396,7 @@ export async function startWorker() { persistentID: persistentId, gameID: clientMsg.gameID, }); - ws.close(1002, "Unauthorized: user me fetch failed"); + ws.close(1002, "user_me_fetch_failed"); return; } flares = result.response.player.flares; @@ -412,7 +409,7 @@ export async function startWorker() { log.warn( "Forbidden: player without an allowed flare attempted to join game", ); - ws.close(1002, "Forbidden"); + ws.close(1002, "forbidden"); return; } } @@ -446,10 +443,7 @@ export async function startWorker() { gameID: clientMsg.gameID, reason: turnstileResult.reason, }); - ws.close( - 1002, - "Unauthorized: Turnstile token rejected \n\n Try this to fix: \n 1. Do a hard refresh (Mac: Cmd+Shift+R, Windows: Ctrl+F5) \n 2. Or resync your device clock \n 3. Or close all browser windows and restart your browser \n 4. Or try another browser, preferably without extensions \n 5. Or clear site data for this site", - ); + ws.close(1002, "turnstile_invalid"); return; case "error": // Fail open, allow the client to join. @@ -479,19 +473,19 @@ export async function startWorker() { if (joinResult === "not_found") { log.info(`game ${clientMsg.gameID} not found on worker ${workerId}`); - ws.close(1002, "Game not found"); + ws.close(1002, "game_not_found"); } else if (joinResult === "kicked") { log.warn(`kicked client tried to join game ${clientMsg.gameID}`, { gameID: clientMsg.gameID, workerId, }); - ws.close(1002, "Cannot join game"); + ws.close(1002, "cannot_join_game"); } else if (joinResult === "rejected") { log.info(`client rejected from game ${clientMsg.gameID}`, { gameID: clientMsg.gameID, workerId, }); - ws.close(1002, "Lobby full"); + ws.close(1002, "lobby_full"); } // Handle other message types From 38c345cfd69a0d20b305d2f6c8e8bf319ea06536 Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Wed, 6 May 2026 22:30:21 +0200 Subject: [PATCH 3/7] Fix --- src/client/Utils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client/Utils.ts b/src/client/Utils.ts index 18130f75a9..e3c4682c63 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -12,7 +12,6 @@ import { import { GameConfig } from "../core/Schemas"; import type { LangSelector } from "./LangSelector"; import { Platform } from "./Platform"; -import { get } from "http"; export const TUTORIAL_VIDEO_URL = "https://www.youtube.com/embed/EN2oOog3pSs"; From 06b35c5ead9d4bee8744640bbe809f42ec7f70c2 Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Wed, 6 May 2026 22:33:05 +0200 Subject: [PATCH 4/7] Prettier --- resources/lang/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 940ad09cf4..c220444ce9 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -1221,7 +1221,7 @@ "exit": "Exit fullscreen" }, "worker_error": { - "account_banned": "Account Banned", + "account_banned": "Account Banned", "cannot_join_game": "Cannot join game", "connection_refused": "Connection refused", "flare_forbidden": "Forbidden", From 035257fd9917c65816b707f2b42cc527012822a5 Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Wed, 6 May 2026 22:52:34 +0200 Subject: [PATCH 5/7] Display raw error.reason if we (on purpose or from oversight) have no translation for it, like "ClientJoinMessageSchema", "WS_ERR_UNEXPECTED_RSV_1" or cosmetics.reason. Fix translation key name for "forbidden" in en.json --- resources/lang/en.json | 2 +- src/client/Transport.ts | 32 +++++++++++++++++--------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index c220444ce9..95d9cec357 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -1224,7 +1224,7 @@ "account_banned": "Account Banned", "cannot_join_game": "Cannot join game", "connection_refused": "Connection refused", - "flare_forbidden": "Forbidden", + "forbidden": "Forbidden", "game_not_found": "Game not found", "lobby_full": "Lobby full", "turnstile_invalid": "Unauthorized: invalid token", diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 2eb9851e8f..8d73be15c1 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -380,24 +380,26 @@ export class Transport { ); if (event.code === 1002) { const connRefusedKey = `worker_error.connection_refused`; - let alertMsg = translateText(connRefusedKey); const errorKey = `worker_error.${event.reason}`; - alertMsg += `: ${translateText(errorKey)}`; - // Add tips if turnstile token invalid - if (event.reason === "turnstile_invalid") { - alertMsg += `\n\n${translateText("worker_error.turnstile_fix_tips")}`; - } + let alertMsg = `${translateText(connRefusedKey)}: `; + let translatedError = translateText(errorKey); + + if (translatedError === errorKey) { + // No translation key in error.reason or no translation or default English found + alertMsg += `${event.reason}`; + } else { + alertMsg += translatedError; + + // Add tips if token invalid + if (event.reason === "turnstile_invalid") { + alertMsg += `\n${translateText("worker_error.turnstile_fix_tips")}`; + } - // Append English error if it differs, for screenshots posted by users - const englishMsg = getEnglishText(errorKey); - if (!alertMsg.includes(englishMsg)) { - const englishConnRefusedMsg = getEnglishText(connRefusedKey); - alertMsg += `\n\n--- English ---\n${englishConnRefusedMsg}: `; - if (englishMsg !== errorKey) { - alertMsg += `${englishMsg}`; - } else if (englishMsg === errorKey) { - alertMsg += `${event.reason}`; + // Append English translation if it differs + const englishMsg = getEnglishText(errorKey); + if (englishMsg !== errorKey && !alertMsg.includes(englishMsg)) { + alertMsg += `\n\n--- English ---\n${getEnglishText(connRefusedKey)}: ${englishMsg}`; } } From 460c456bbd601c22b152369153370d6743fa700b Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Wed, 6 May 2026 22:55:01 +0200 Subject: [PATCH 6/7] ESlint --- src/client/Transport.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/Transport.ts b/src/client/Transport.ts index 8d73be15c1..c510aa0cb0 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -383,7 +383,7 @@ export class Transport { const errorKey = `worker_error.${event.reason}`; let alertMsg = `${translateText(connRefusedKey)}: `; - let translatedError = translateText(errorKey); + const translatedError = translateText(errorKey); if (translatedError === errorKey) { // No translation key in error.reason or no translation or default English found From eb34fe8d880b38300767f4d9a2e318b34764ddd4 Mon Sep 17 00:00:00 2001 From: VariableVince <24507472+VariableVince@users.noreply.github.com> Date: Fri, 8 May 2026 22:12:23 +0200 Subject: [PATCH 7/7] WSErrorSchema --- src/client/Transport.ts | 40 ++++++++++++++++++++++++++++++---------- src/core/Schemas.ts | 6 ++++++ src/server/GameServer.ts | 5 ++++- src/server/Worker.ts | 34 +++++++++++++++++++++------------- 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/src/client/Transport.ts b/src/client/Transport.ts index c510aa0cb0..fcc596a440 100644 --- a/src/client/Transport.ts +++ b/src/client/Transport.ts @@ -24,6 +24,8 @@ import { ServerMessage, ServerMessageSchema, Winner, + WSError, + WSErrorSchema, } from "../core/Schemas"; import { replacer } from "../core/Util"; import { getPlayToken } from "./Auth"; @@ -178,6 +180,7 @@ export class SendStartGameEvent implements GameEvent {} export class Transport { private socket: WebSocket | null = null; + private disconnectWSError: WSError | null = null; private localServer: LocalServer; @@ -334,6 +337,7 @@ export class Transport { const workerPath = this.lobbyConfig.serverConfig.workerPath( this.lobbyConfig.gameID, ); + this.disconnectWSError = null; this.socket = new WebSocket(`${wsProtocol}//${wsHost}/${workerPath}`); this.onconnect = onconnect; this.onmessage = onmessage; @@ -359,6 +363,12 @@ export class Transport { const parsed = JSON.parse(event.data); const result = ServerMessageSchema.safeParse(parsed); if (!result.success) { + const wsErrorResult = WSErrorSchema.safeParse(parsed); + if (wsErrorResult.success) { + this.disconnectWSError = wsErrorResult.data; + return; + } + const error = z.prettifyError(result.error); console.error("Error parsing server message", error); return; @@ -380,26 +390,36 @@ export class Transport { ); if (event.code === 1002) { const connRefusedKey = `worker_error.connection_refused`; - const errorKey = `worker_error.${event.reason}`; + const translationKey = this.disconnectWSError + ? this.disconnectWSError.translationKey + : event.reason; + const args = this.disconnectWSError + ? this.disconnectWSError.args + : undefined; + + const errorKey = `worker_error.${translationKey}`; let alertMsg = `${translateText(connRefusedKey)}: `; - const translatedError = translateText(errorKey); + const translatedError = translateText(errorKey, args); if (translatedError === errorKey) { - // No translation key in error.reason or no translation or default English found - alertMsg += `${event.reason}`; + // Raw string instead of translation key in disconnectWSError/error.reason, + // or no user lang nor default English translation found with the key + // Show the raw string or key as fallback. Eg. "WS_ERR_UNEXPECTED_RSV_1" or "ClientJoinMessageSchema" + alertMsg += `${translationKey}`; } else { alertMsg += translatedError; - // Add tips if token invalid - if (event.reason === "turnstile_invalid") { + // Add tips if turnstile token invalid + if (translationKey === "turnstile_invalid") { alertMsg += `\n${translateText("worker_error.turnstile_fix_tips")}`; } - // Append English translation if it differs - const englishMsg = getEnglishText(errorKey); - if (englishMsg !== errorKey && !alertMsg.includes(englishMsg)) { - alertMsg += `\n\n--- English ---\n${getEnglishText(connRefusedKey)}: ${englishMsg}`; + // Append English translation/key if it's not already there + // Helps debugging if user shares screenshot + const englishError = getEnglishText(errorKey, args); + if (englishError !== errorKey && !alertMsg.includes(englishError)) { + alertMsg += `\n\n--- English ---\n${getEnglishText(connRefusedKey)}: ${englishError}`; } } diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 4a1636e195..2c41cdc774 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -613,6 +613,12 @@ export const ServerErrorSchema = z.object({ message: z.string().optional(), }); +export const WSErrorSchema = z.object({ + translationKey: z.string(), + args: z.record(z.string(), z.string()).optional(), +}); +export type WSError = z.infer; + export const ServerLobbyInfoMessageSchema = z.object({ type: z.literal("lobby_info"), lobby: GameInfoSchema, diff --git a/src/server/GameServer.ts b/src/server/GameServer.ts index 169c159722..769917e785 100644 --- a/src/server/GameServer.ts +++ b/src/server/GameServer.ts @@ -28,6 +28,7 @@ import { createPartialGameRecord } from "../core/Util"; import { archive, finalizeGameRecord } from "./Archive"; import { Client } from "./Client"; import { ClientMsgRateLimiter } from "./ClientMsgRateLimiter"; +import { sendErrorAndClose } from "./Worker"; export enum GamePhase { Lobby = "LOBBY", Active = "ACTIVE", @@ -613,7 +614,9 @@ export class GameServer { }); client.ws.on("error", (error: Error) => { if ((error as any).code === "WS_ERR_UNEXPECTED_RSV_1") { - client.ws.close(1002, "WS_ERR_UNEXPECTED_RSV_1"); + sendErrorAndClose(client.ws, { + translationKey: "WS_ERR_UNEXPECTED_RSV_1", + }); } }); diff --git a/src/server/Worker.ts b/src/server/Worker.ts index b6855c3947..3026381788 100644 --- a/src/server/Worker.ts +++ b/src/server/Worker.ts @@ -14,6 +14,7 @@ import { GameID, PartialGameRecordSchema, ServerErrorMessage, + WSError, } from "../core/Schemas"; import { generateID, replacer } from "../core/Util"; import { CreateGameInputSchema } from "../core/WorkerSchemas"; @@ -34,6 +35,13 @@ import { verifyTurnstileToken } from "./Turnstile"; import { WorkerLobbyService } from "./WorkerLobbyService"; import { initWorkerMetrics } from "./WorkerMetrics"; +export function sendErrorAndClose(ws: WebSocket, error: WSError, code = 1002) { + if (ws.readyState === ws.OPEN) { + ws.send(JSON.stringify(error)); + } + ws.close(code); +} + const config = getServerConfigFromServer(); const workerId = parseInt(process.env.WORKER_ID ?? "0"); @@ -300,7 +308,7 @@ export async function startWorker() { error: error.toString(), } satisfies ServerErrorMessage), ); - ws.close(1002, "ClientJoinMessageSchema"); + sendErrorAndClose(ws, { translationKey: "ClientJoinMessageSchema" }); return; } const clientMsg = parsed.data; @@ -330,13 +338,13 @@ export async function startWorker() { log.warn(`Invalid token: ${result.message}`, { gameID: clientMsg.gameID, }); - ws.close(1002, "turnstile_invalid"); + sendErrorAndClose(ws, { translationKey: "turnstile_invalid" }); return; } const { persistentId, claims } = result; if (claims?.role === "banned") { - ws.close(1002, "account_banned"); + sendErrorAndClose(ws, { translationKey: "account_banned" }); return; } @@ -355,7 +363,7 @@ export async function startWorker() { log.warn( `game ${clientMsg.gameID} not found on worker ${workerId}`, ); - ws.close(1002, "game_not_found"); + sendErrorAndClose(ws, { translationKey: "game_not_found" }); } return; } @@ -385,7 +393,7 @@ export async function startWorker() { if (claims === null) { if (allowedFlares !== undefined) { log.warn("Unauthorized: Anonymous user attempted to join game"); - ws.close(1002, "unauthorized"); + sendErrorAndClose(ws, { translationKey: "unauthorized" }); return; } } else { @@ -396,7 +404,7 @@ export async function startWorker() { persistentID: persistentId, gameID: clientMsg.gameID, }); - ws.close(1002, "user_me_fetch_failed"); + sendErrorAndClose(ws, { translationKey: "user_me_fetch_failed" }); return; } flares = result.response.player.flares; @@ -409,7 +417,7 @@ export async function startWorker() { log.warn( "Forbidden: player without an allowed flare attempted to join game", ); - ws.close(1002, "forbidden"); + sendErrorAndClose(ws, { translationKey: "forbidden" }); return; } } @@ -424,7 +432,7 @@ export async function startWorker() { persistentID: persistentId, gameID: clientMsg.gameID, }); - ws.close(1002, cosmeticResult.reason); + sendErrorAndClose(ws, { translationKey: cosmeticResult.reason }); return; } @@ -443,7 +451,7 @@ export async function startWorker() { gameID: clientMsg.gameID, reason: turnstileResult.reason, }); - ws.close(1002, "turnstile_invalid"); + sendErrorAndClose(ws, { translationKey: "turnstile_invalid" }); return; case "error": // Fail open, allow the client to join. @@ -473,19 +481,19 @@ export async function startWorker() { if (joinResult === "not_found") { log.info(`game ${clientMsg.gameID} not found on worker ${workerId}`); - ws.close(1002, "game_not_found"); + sendErrorAndClose(ws, { translationKey: "game_not_found" }); } else if (joinResult === "kicked") { log.warn(`kicked client tried to join game ${clientMsg.gameID}`, { gameID: clientMsg.gameID, workerId, }); - ws.close(1002, "cannot_join_game"); + sendErrorAndClose(ws, { translationKey: "cannot_join_game" }); } else if (joinResult === "rejected") { log.info(`client rejected from game ${clientMsg.gameID}`, { gameID: clientMsg.gameID, workerId, }); - ws.close(1002, "lobby_full"); + sendErrorAndClose(ws, { translationKey: "lobby_full" }); } // Handle other message types @@ -502,7 +510,7 @@ export async function startWorker() { ws.on("error", (error: Error) => { if ((error as any).code === "WS_ERR_UNEXPECTED_RSV_1") { - ws.close(1002, "WS_ERR_UNEXPECTED_RSV_1"); + sendErrorAndClose(ws, { translationKey: "WS_ERR_UNEXPECTED_RSV_1" }); } }); ws.on("close", () => {