NousResearch/hermes-agent · error · LSPProtocolError
malformed LSP header line: {line!r}
Error message
malformed LSP header line: {line!r} What it means
LSPProtocolError raised when a decoded header line has no ':' separator — after stripping CRLF, partition(':') found an empty key. The base protocol requires 'Header-Name: value' lines, so a separator-free line means the server is emitting non-header content in the header region.
Source
Thrown at agent/lsp/protocol.py:104
f"unexpected EOF while reading LSP headers (partial={e.partial!r})"
) from e
# Defensive cap against a server streaming headers without ever
# emitting CRLF-CRLF. Caps total header bytes at 8 KiB — a
# well-behaved server fits in well under 200 bytes.
header_bytes += len(line)
if header_bytes > 8192:
raise LSPProtocolError(
"LSP header block exceeded 8 KiB without terminator"
)
line = line[:-2] # strip CRLF
if not line:
break # blank line ends header block
try:
key, _, value = line.decode("ascii").partition(":")
except UnicodeDecodeError as e:
raise LSPProtocolError(f"non-ASCII LSP header: {line!r}") from e
if not key:
raise LSPProtocolError(f"malformed LSP header line: {line!r}")
headers[key.strip().lower()] = value.strip()
cl = headers.get("content-length")
if cl is None:
raise LSPProtocolError(f"LSP message missing Content-Length: {headers!r}")
try:
n = int(cl)
except ValueError as e:
raise LSPProtocolError(f"non-integer Content-Length: {cl!r}") from e
if n < 0 or n > 64 * 1024 * 1024: # 64 MiB sanity cap
raise LSPProtocolError(f"unreasonable Content-Length: {n}")
try:
body = await reader.readexactly(n)
except asyncio.IncompleteReadError as e:
raise LSPProtocolError(
f"truncated LSP body: expected {n} bytes, got {len(e.partial)}"
) from eView on GitHub (pinned to c896c09c42)
Solutions
- Eliminate all stdout output from the server and its launcher; route logs to stderr or a file.
- Confirm the server emits strict CRLF line endings in the base protocol (try a different/older version if it is a server bug).
- Reproduce the server's raw stdout (e.g. capture with a tee shim) to see exactly which line lacks the colon.
Example fix
# capture the raw wire to diagnose which line is malformed # shim: stdio-tee.sh # #!/bin/sh # "$@" 2>>/tmp/lsp-server.log | tee /tmp/lsp-stdout.raw LSPClient(cmd=["/path/stdio-tee.sh", "pyright-langserver", "--stdio"], ...)
Defensive patterns
Strategy: fallback
Try / catch
try:
msg = await read_message(reader)
except LSPProtocolError as e:
if "malformed LSP header" in str(e):
capture_raw_stream_and_restart() # tee stdout to a file, then fresh session
else:
raise Prevention
- Use a tee shim to capture the raw wire when diagnosing — you need the exact offending line.
- Ensure the launcher/wrapper adds no banners or status lines to stdout.
- Suspect framing desync whenever header-region errors appear; restart rather than resync.
When it happens
Trigger: A blank-but-not-empty line (e.g. only spaces — the parser only breaks on a truly empty line), a stray status line like 'Server ready' printed to stdout, or framing desync landing the parser mid-body.
Common situations: Servers or launch wrappers printing human-readable status/banners to stdout; a server writing LF-only line endings so CRLF stripping leaves residue; desync after a bad Content-Length.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- unexpected EOF while reading LSP headers (partial={e.partial
- LSP header block exceeded 8 KiB without terminator
- non-ASCII LSP header: {line!r}
- LSP message missing Content-Length: {headers!r}
- non-integer Content-Length: {cl!r}
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/e62e302736470ab2.
Report an issue: GitHub.