fix(gdscript): detect broken TCP connection and fail fast on send error (#1493)

fix(gdscript): detect broken TCP connection and fail fast on reconnect

When the Godot editor restarts or the LSP connection drops, the
TCPLanguageServer read loop exits but previously left self._sock set,
causing is_running() to return True. Subsequent LSP requests would
then send to a dead socket, log a broken-pipe error silently, and
block for the full tool timeout (235 s by default) before raising.

Two fixes in TCPLanguageServer:
- _send_payload: on OSError, clear self._sock/self._file and
  immediately cancel all pending requests so callers fail fast
  instead of waiting out the timeout.
- _read_loop: after unexpected exit, clear self._sock/self._file so
  is_running() returns False and _ensure_functional_ls can trigger
  an automatic reconnect on the next tool call.

Also fixes GodotLanguageServer so that ls_specific_settings.request_timeout
is respected as a cap on the per-request timeout. Previously the value
was read in _create_language_server_interface but then overridden by
the tool-timeout-derived value passed to set_request_timeout(). Now
set_request_timeout() applies min(incoming, configured) so users can
set a low request_timeout in their project.yml to fail fast without
lowering the global tool_timeout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Erik JacobsandClaude Sonnet 4.6 authored and GitHub committed 2026-05-18 13:08:27 +02:00
1 parent 7c7d5eef56
commit 87eaa40553
2 files changed
+16

No files matched your search

@@ -46,9 +46,17 @@ class GodotLanguageServer(SolidLanguageServer):
else:
log.warning("Could not detect Godot version for project at %s", repository_root_path)
self._configured_request_timeout: float | None = None
# Dummy ProcessLaunchInfo — _create_language_server_interface() ignores it
super().__init__(config, repository_root_path, ProcessLaunchInfo(cmd=""), "gdscript", solidlsp_settings)
def set_request_timeout(self, timeout: float | None) -> None:
"""Cap the timeout at the value configured in ls_specific_settings, if set."""
if timeout is not None and self._configured_request_timeout is not None:
timeout = min(timeout, self._configured_request_timeout)
super().set_request_timeout(timeout)
@staticmethod
def _detect_godot_version(repo_path: str) -> int | None:
"""Read project.godot to determine the major Godot version.
@@ -75,6 +83,7 @@ class GodotLanguageServer(SolidLanguageServer):
settings: dict = self._custom_settings or {}
port = settings.get("port", DEFAULT_GODOT_LS_PORT)
request_timeout = settings.get("request_timeout", DEFAULT_GODOT_REQUEST_TIMEOUT)
self._configured_request_timeout = settings.get("request_timeout")
self._conn_info = TCPConnectionInfo(host="127.0.0.1", port=port)
return TCPLanguageServer(
connection_info=self._conn_info,
+7
View File
@@ -727,6 +727,9 @@ class TCPLanguageServer(LanguageServerInterface):
sock.sendall(data)
except OSError as e:
log.error("Failed to write to TCP language server: %s", e)
self._sock = None
self._file = None
self._cancel_pending_requests(LanguageServerTerminatedException("TCP send error", self.language, cause=e))
def _read_loop(self) -> None:
"""Read Content-Length-framed LSP messages from the TCP socket and dispatch them."""
@@ -775,3 +778,7 @@ class TCPLanguageServer(LanguageServerInterface):
exception = LanguageServerTerminatedException("TCP language server read loop terminated unexpectedly", self.language)
log.error(str(exception))
self._cancel_pending_requests(exception)
# Clear the socket so is_running() returns False, allowing _ensure_functional_ls
# to detect the broken connection and restart the language server.
self._sock = None
self._file = None