have worker send error back to client (#1178)

## Description:
On error, send the message back to the client before closing the
websocket.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

<DISCORD USERNAME>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
evanpelle
2025-06-17 20:13:37 -07:00
committed by GitHub
parent c052ab04e0
commit 4480871f65
6 changed files with 73 additions and 12 deletions
+1
View File
@@ -453,6 +453,7 @@
},
"error_modal": {
"crashed": "Game crashed!",
"connection_error": "Connection error!",
"paste_discord": "Please paste the following in your bug report in Discord:",
"copy_clipboard": "Copy to clipboard",
"copied": "Copied!",
+29 -3
View File
@@ -99,6 +99,17 @@ export function joinLobby(
terrainLoad,
).then((r) => r.start());
}
if (message.type === "error") {
showErrorModal(
message.error,
"",
lobbyConfig.gameID,
lobbyConfig.clientID,
true,
false,
"error_modal.connection_error",
);
}
};
transport.connect(onconnect, onmessage);
return () => {
@@ -309,7 +320,19 @@ export class ClientGameRunner {
this.lobby.gameStartInfo.gameID,
this.lobby.clientID,
true,
translateText("error_modal.desync_notice"),
false,
"error_modal.desync_notice",
);
}
if (message.type === "error") {
showErrorModal(
message.error,
"",
this.lobby.gameID,
this.lobby.clientID,
true,
false,
"error_modal.connection_error",
);
}
if (message.type === "turn") {
@@ -538,7 +561,8 @@ function showErrorModal(
gameID: GameID,
clientID: ClientID,
closable = false,
heading = translateText("error_modal.crashed"),
showDiscord = true,
heading = "error_modal.crashed",
) {
const errorText = `Error: ${errMsg}\nStack: ${stack}`;
@@ -550,7 +574,9 @@ function showErrorModal(
modal.id = "error-modal";
const content = `${translateText(heading)}\n game id: ${gameID}, client id: ${clientID}\n${translateText("error_modal.paste_discord")}\n${errorText}`;
const discord = showDiscord ? translateText("error_modal.paste_discord") : "";
const content = `${discord}\n${translateText(heading)}\n game id: ${gameID}, client id: ${clientID}\n${errorText}`;
// Create elements
const pre = document.createElement("pre");
+2 -2
View File
@@ -327,8 +327,8 @@ export class Transport {
console.log(
`WebSocket closed. Code: ${event.code}, Reason: ${event.reason}`,
);
if (event.code !== 1000) {
console.log(`reconnecting`);
if (event.code !== 1000 && event.code !== 1002) {
console.log(`recieved error code ${event.code}, reconnecting`);
this.reconnect();
}
};
+10 -2
View File
@@ -80,7 +80,8 @@ export type ServerMessage =
| ServerStartGameMessage
| ServerPingMessage
| ServerDesyncMessage
| ServerPrestartMessage;
| ServerPrestartMessage
| ServerErrorMessage;
export type ServerSyncMessage = z.infer<typeof ServerTurnMessageSchema>;
export type ServerStartGameMessage = z.infer<
@@ -89,6 +90,7 @@ export type ServerStartGameMessage = z.infer<
export type ServerPingMessage = z.infer<typeof ServerPingMessageSchema>;
export type ServerDesyncMessage = z.infer<typeof ServerDesyncSchema>;
export type ServerPrestartMessage = z.infer<typeof ServerPrestartMessageSchema>;
export type ServerErrorMessage = z.infer<typeof ServerErrorSchema>;
export type ClientSendWinnerMessage = z.infer<typeof ClientSendWinnerSchema>;
export type ClientPingMessage = z.infer<typeof ClientPingMessageSchema>;
export type ClientIntentMessage = z.infer<typeof ClientIntentMessageSchema>;
@@ -330,7 +332,7 @@ export const TurnSchema = z.object({
// Server
const ServerBaseMessageSchema = z.object({
type: z.enum(["turn", "ping", "prestart", "start", "desync"]),
type: z.enum(["turn", "ping", "prestart", "start", "desync", "error"]),
});
export const ServerTurnMessageSchema = ServerBaseMessageSchema.extend({
@@ -375,12 +377,18 @@ export const ServerDesyncSchema = ServerBaseMessageSchema.extend({
yourHash: z.number().optional(),
});
export const ServerErrorSchema = ServerBaseMessageSchema.extend({
type: z.literal("error"),
error: z.string(),
});
export const ServerMessageSchema = z.discriminatedUnion("type", [
ServerTurnMessageSchema,
ServerPrestartMessageSchema,
ServerStartGameMessageSchema,
ServerPingMessageSchema,
ServerDesyncSchema,
ServerErrorSchema,
]);
// Client
+24 -5
View File
@@ -13,6 +13,7 @@ import {
Intent,
PlayerRecord,
ServerDesyncSchema,
ServerErrorMessage,
ServerPrestartMessageSchema,
ServerStartGameMessageSchema,
ServerTurnMessageSchema,
@@ -195,7 +196,16 @@ export class GameServer {
this.log.error("Failed to parse client message", error, {
clientID: client.clientID,
});
client.ws.close(1002, "ClientMessageSchema");
client.ws.send(
JSON.stringify({
type: "error",
error: error.toString(),
} satisfies ServerErrorMessage),
);
// Add a small delay before closing the connection to ensure the error message is received
setTimeout(() => {
client.ws.close(1002, "ClientMessageSchema");
}, 100);
return;
}
const clientMsg = parsed.data;
@@ -543,11 +553,20 @@ export class GameServer {
clientID: client.clientID,
persistentID: client.persistentID,
});
client.ws.close(1000, "Kicked from game");
this.activeClients = this.activeClients.filter(
(c) => c.clientID !== clientID,
client.ws.send(
JSON.stringify({
type: "error",
error: "Kicked from game (you may have been playing on another tab)",
} satisfies ServerErrorMessage),
);
this.kickedClients.add(clientID);
// Add a small delay before closing the connection to ensure the error message is received
setTimeout(() => {
client.ws.close(1000, "Kicked from game");
this.activeClients = this.activeClients.filter(
(c) => c.clientID !== clientID,
);
this.kickedClients.add(clientID);
}, 100);
} else {
this.log.warn(`cannot kick client, not found in game`, {
clientID,
+7
View File
@@ -13,6 +13,7 @@ import {
ClientJoinMessageSchema,
GameRecord,
GameRecordSchema,
ServerErrorMessage,
} from "../core/Schemas";
import { CreateGameInputSchema, GameInputSchema } from "../core/WorkerSchemas";
import { archive, readGameRecord } from "./Archive";
@@ -300,6 +301,12 @@ export function startWorker() {
if (!parsed.success) {
const error = z.prettifyError(parsed.error);
log.warn("Error parsing join message client", error);
ws.send(
JSON.stringify({
type: "error",
error: error.toString(),
} satisfies ServerErrorMessage),
);
ws.close(1002, "ClientJoinMessageSchema");
return;
}