From 2addbd5f5012405b71f17409725fa7d937f3dd23 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:15:16 -0700 Subject: [PATCH] fix: show a descriptive error when the Rojo server closes while the plugin is connected Fixes #1240 --- plugin/src/ApiContext.lua | 43 +++++++++++++++++++++++++++++++++- plugin/src/ApiContext.spec.lua | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 plugin/src/ApiContext.spec.lua diff --git a/plugin/src/ApiContext.lua b/plugin/src/ApiContext.lua index 2ce6ea14..0d247c16 100644 --- a/plugin/src/ApiContext.lua +++ b/plugin/src/ApiContext.lua @@ -90,8 +90,49 @@ local function rejectWrongPlaceId(infoResponseBody) return Promise.resolve(infoResponseBody) end +local LOST_CONNECTION_MESSAGE = "Lost connection to the Rojo server. Make sure `rojo serve` is still running." + +local LOST_CONNECTION_ERRORS = { + { + code = 400, + messagePatterns = { + "failed ws recv", + "failure when receiving data from the peer", + "connection reset", + }, + }, +} + +local function matchesWebSocketError(errorInfo, code, message) + if errorInfo.code ~= nil and tostring(errorInfo.code) ~= tostring(code) then + return false + end + + for _, pattern in ipairs(errorInfo.messagePatterns) do + if string.find(message, pattern, 1, true) ~= nil then + return true + end + end + + return false +end + +local function formatWebSocketError(code, msg) + local message = if msg == nil then "" else tostring(msg) + local normalizedMessage = string.lower(message) + + for _, errorInfo in ipairs(LOST_CONNECTION_ERRORS) do + if matchesWebSocketError(errorInfo, code, normalizedMessage) then + return LOST_CONNECTION_MESSAGE + end + end + + return ("WebSocket error: %s - %s"):format(tostring(code), message) +end + local ApiContext = {} ApiContext.__index = ApiContext +ApiContext.__formatWebSocketError = formatWebSocketError function ApiContext.new(baseUrl) assert(type(baseUrl) == "string", "baseUrl must be a string") @@ -275,7 +316,7 @@ function ApiContext:connectWebSocket(packetHandlers) errored:Disconnect() received:Disconnect() - reject("WebSocket error: " .. code .. " - " .. msg) + reject(formatWebSocketError(code, msg)) end) end) end diff --git a/plugin/src/ApiContext.spec.lua b/plugin/src/ApiContext.spec.lua new file mode 100644 index 00000000..336a93ea --- /dev/null +++ b/plugin/src/ApiContext.spec.lua @@ -0,0 +1,42 @@ +return function() + local ApiContext = require(script.Parent.ApiContext) + + local formatWebSocketError = ApiContext.__formatWebSocketError + local lostConnectionMessage = "Lost connection to the Rojo server. Make sure `rojo serve` is still running." + + describe("__formatWebSocketError", function() + it("should return a friendly message for failed WebSocket receives", function() + local message = 'Failed ws recv - err: 0 "No error", curlErrBuf: ""' + + expect(formatWebSocketError(400, message)).to.equal(lostConnectionMessage) + end) + + it("should return a friendly message for peer receive failures", function() + local message = 'err: 56 "Failure when receiving data from the peer"' + + expect(formatWebSocketError(400, message)).to.equal(lostConnectionMessage) + end) + + it("should return a friendly message for connection resets", function() + local message = 'Failed ws recv - err: 54 "Connection reset by peer", curlErrBuf: ""' + + expect(formatWebSocketError(400, message)).to.equal(lostConnectionMessage) + end) + + it("should preserve unexpected WebSocket errors", function() + expect(formatWebSocketError(500, "Something else failed")).to.equal( + "WebSocket error: 500 - Something else failed" + ) + end) + + it("should handle empty or missing messages", function() + expect(formatWebSocketError(500, "")).to.equal("WebSocket error: 500 - ") + expect(formatWebSocketError(500, nil)).to.equal("WebSocket error: 500 - ") + end) + + it("should always return a string", function() + expect(typeof(formatWebSocketError(400, "connection reset by peer"))).to.equal("string") + expect(typeof(formatWebSocketError(500, nil))).to.equal("string") + end) + end) +end