summaryrefslogtreecommitdiffhomepage
path: root/packages/lsp/src/client.ts
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-25 07:24:47 +0900
committerAdam Malczewski <[email protected]>2026-06-25 07:24:47 +0900
commit4bc062c21a830dd58535252fd24ddb392d262c79 (patch)
treec8fa16b63660f466fefdf0c957f2cd734cd42519 /packages/lsp/src/client.ts
parent1b2a13e29e98da04d55c061c2dcadb8c36d783cd (diff)
downloaddispatch-4bc062c21a830dd58535252fd24ddb392d262c79.tar.gz
dispatch-4bc062c21a830dd58535252fd24ddb392d262c79.zip
fix(lsp): prevent server crash from malformed LSP messages
Two bugs caused the dispatch server to crash (15 times since Jun 24) when chat cc6c edited packages/transport-http/src/app.ts — a 40KB file with 23 multi-byte UTF-8 lines. The edit_file diagnostics hook sends the file to tsserver, which sends back a large publishDiagnostics response. When the response was split across stdout chunks at a multi-byte character boundary, the server crashed. Layer 1 — rpc.ts handleMessage: JSON.parse had no try/catch. A corrupted message threw an unhandled SyntaxError → unhandled rejection → process exit. Wrapped in try/catch; malformed messages are now skipped. Also hardened client.ts handleBytes: the async handleMessage Promise was fire-and-forget. Added .catch(() => {}) as defence-in-depth so no rejection from the RPC layer can ever crash the server. Layer 2 — framing.ts FrameDecoder: used a string buffer with new TextDecoder().decode(chunk) (no { stream: true }), corrupting multi-byte characters split across chunks. Worse, Content-Length counts bytes but the buffer was sliced by character count — for multi-byte content byte length ≠ char length, so the decoder extracted the wrong slice as a message. Rewrote to use a Uint8Array byte buffer: header separator search is byte-level, Content-Length comparison is byte-level, and the body is decoded only after all bytes are confirmed present. Tests: 5 new multi-byte framing tests (split at char boundary, byte-vs-char Content-Length, two messages in one chunk, three-way split) + 1 rpc test (malformed JSON does not throw). All 1545 tests pass.
Diffstat (limited to 'packages/lsp/src/client.ts')
-rw-r--r--packages/lsp/src/client.ts6
1 files changed, 5 insertions, 1 deletions
diff --git a/packages/lsp/src/client.ts b/packages/lsp/src/client.ts
index 743fcb4..677a22a 100644
--- a/packages/lsp/src/client.ts
+++ b/packages/lsp/src/client.ts
@@ -175,7 +175,11 @@ export class LanguageServerClient {
private handleBytes(chunk: Uint8Array): void {
const messages = this.decoder.decode(chunk);
for (const msg of messages) {
- this.rpc?.handleMessage(msg);
+ // handleMessage is async — catch rejections so a malformed
+ // message never becomes an unhandled rejection that crashes
+ // the server. (handleMessage also has its own try/catch around
+ // JSON.parse, but this is the defence-in-depth boundary.)
+ void this.rpc?.handleMessage(msg).catch(() => {});
}
}