diff options
| author | Adam Malczewski <[email protected]> | 2026-06-25 07:24:47 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-25 07:24:47 +0900 |
| commit | 4bc062c21a830dd58535252fd24ddb392d262c79 (patch) | |
| tree | c8fa16b63660f466fefdf0c957f2cd734cd42519 /packages/lsp/src/rpc.ts | |
| parent | 1b2a13e29e98da04d55c061c2dcadb8c36d783cd (diff) | |
| download | dispatch-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/rpc.ts')
| -rw-r--r-- | packages/lsp/src/rpc.ts | 11 |
1 files changed, 10 insertions, 1 deletions
diff --git a/packages/lsp/src/rpc.ts b/packages/lsp/src/rpc.ts index 45adf42..6b82624 100644 --- a/packages/lsp/src/rpc.ts +++ b/packages/lsp/src/rpc.ts @@ -62,7 +62,16 @@ export class JsonRpcConnection { } async handleMessage(json: string): Promise<void> { - const msg = JSON.parse(json) as JsonRpcMessage; + let msg: JsonRpcMessage; + try { + msg = JSON.parse(json) as JsonRpcMessage; + } catch { + // A malformed LSP message must never crash the server. The most + // common cause is a multi-byte UTF-8 character split across stdout + // chunks (see FrameDecoder). Log and skip — the language server + // will re-send diagnostics on the next file change. + return; + } const { id, method } = msg; if (id !== undefined && method !== undefined) { |
