diff --git a/src/Transport/JsonRpc/LspFraming.php b/src/Transport/JsonRpc/LspFraming.php index 814342a..ab91446 100644 --- a/src/Transport/JsonRpc/LspFraming.php +++ b/src/Transport/JsonRpc/LspFraming.php @@ -62,7 +62,13 @@ private static function extractOneMessage(string $buffer): ?array $contentLength = self::parseContentLength($headers); if (null === $contentLength) { - throw new \InvalidArgumentException('Missing or invalid Content-Length header'); + // resync: skip stray non-LSP output (e.g. browser crash spew) up to the next frame + $resyncPos = strpos($buffer, 'Content-Length:', 1); + if (false === $resyncPos) { + return null; + } + + return self::extractOneMessage(substr($buffer, $resyncPos)); } if (strlen($buffer) < $contentStart + $contentLength) { diff --git a/tests/Unit/Transport/JsonRpc/LspFramingTest.php b/tests/Unit/Transport/JsonRpc/LspFramingTest.php new file mode 100644 index 0000000..4eb8575 --- /dev/null +++ b/tests/Unit/Transport/JsonRpc/LspFramingTest.php @@ -0,0 +1,78 @@ +assertSame(['{"ok":1}'], $decoded['messages']); + $this->assertSame('', $decoded['remainingBuffer']); + } + + public function testDecodesMultipleConsecutiveMessages(): void + { + $buffer = LspFraming::encode('{"ok":1}').LspFraming::encode('{"ok":2}'); + + $decoded = LspFraming::decode($buffer); + + $this->assertSame(['{"ok":1}', '{"ok":2}'], $decoded['messages']); + } + + public function testBuffersAnIncompleteMessageInsteadOfFailing(): void + { + $partial = substr(LspFraming::encode('{"ok":1}'), 0, -3); + + $decoded = LspFraming::decode($partial); + + $this->assertSame([], $decoded['messages']); + $this->assertSame($partial, $decoded['remainingBuffer']); + } + + public function testResyncsPastStrayNonLspOutputInsteadOfThrowing(): void + { + $frame1 = LspFraming::encode('{"ok":1}'); + $frame2 = LspFraming::encode('{"ok":2}'); + $polluted = "crash spew\nstack line\n".$frame1.'garbage'.$frame2; + + $decoded = LspFraming::decode($polluted); + + $this->assertSame(['{"ok":1}', '{"ok":2}'], $decoded['messages']); + } + + public function testBuffersPureGarbageWithNoContentLengthMarkerAtAll(): void + { + $decoded = LspFraming::decode("this is not a frame at all\r\n\r\n"); + + $this->assertSame([], $decoded['messages']); + } + + public function testMalformedContentLengthValueIsSkippedRatherThanThrown(): void + { + $frame = LspFraming::encode('{"ok":1}'); + $polluted = "Content-Length: not-a-number\r\n\r\n".$frame; + + $decoded = LspFraming::decode($polluted); + + $this->assertSame(['{"ok":1}'], $decoded['messages']); + } +}