Hmbown/CodeWhale · error

duplicate LSP Content-Length

Error message

duplicate LSP Content-Length

What it means

LSP headers must contain at most one Content-Length field. When parse_header iterates the header lines and finds a second Content-Length, it cannot unambiguously determine the frame size and rejects the message. This protects against ambiguous framing from a non-conformant server.

Solutions

  1. Fix or remove the intermediary that duplicates the header.
  2. Check the server implementation: emit exactly one Content-Length per message.
  3. Capture the raw stream (tee stdout) and confirm the duplicate, then fix the producer.
  4. If writing your own LSP peer, reuse a battle-tested framing library instead of hand-rolling headers.

Example fix

// before (hand-rolled sender)
let header = "Content-Length: 42\r\nContent-Length: 42\r\n\r\n";
// after
let header = format!("Content-Length: {}\r\n\r\n", body.len());
Defensive patterns

Strategy: validation

Validate before calling

// when acting as an LSP peer, assert exactly one Content-Length per frame
let count = header.lines().filter(|l| l.to_ascii_lowercase().starts_with("content-length:")).count();
assert_eq!(count, 1, "duplicate Content-Length in outgoing header");

Try / catch

match parse_result {
    Err(e) if e.to_string().contains("duplicate LSP Content-Length") => {
        report_broken_peer();
        disconnect_and_restart().await
    }
    other => other,
}

Prevention

When it happens

Trigger: Server sends a message whose header block contains two Content-Length lines (e.g. duplicated by a buggy proxy, a hand-rolled client/server implementation, or concatenated malformed frames).

Common situations: Custom LSP middleware that re-writes headers without removing the original, hand-written test doubles emitting both `Content-Length` and `content-length` (eq_ignore_ascii_case makes them collide), or frame concatenation bugs where two headers end up in one block.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/472a9445885a7903. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/lsp/client.rs:602

/// Distinguish incomplete headers from malformed or oversized frames so a
/// broken server cannot cause an indefinitely growing input buffer.
fn parse_header(buf: &[u8]) -> Result<Option<(usize, usize)>> {
    let Some(pos) = buf.windows(4).position(|window| window == b"\r\n\r\n") else {
        if buf.len() > MAX_LSP_HEADER_BYTES {
            return Err(anyhow!("LSP header exceeds size limit"));
        }
        return Ok(None);
    };
    if pos + 4 > MAX_LSP_HEADER_BYTES {
        return Err(anyhow!("LSP header exceeds size limit"));
    }
    let header = std::str::from_utf8(&buf[..pos]).context("invalid LSP header encoding")?;
    let mut content_length = None;
    for line in header.split("\r\n") {
        let (name, value) = line.split_once(':').context("malformed LSP header")?;
        if name.eq_ignore_ascii_case("Content-Length") {
            if content_length.is_some() {
                return Err(anyhow!("duplicate LSP Content-Length"));
            }
            let length = value
                .trim()
                .parse::<usize>()
                .context("invalid LSP Content-Length")?;
            if length == 0 || length > MAX_LSP_FRAME_BYTES {
                return Err(anyhow!("LSP frame exceeds size limit or is empty"));
            }
            content_length = Some(length);
        }
    }
    Ok(Some((
        pos + 4,
        content_length.context("missing LSP Content-Length")?,
    )))
}

/// Background task that consumes inbound JSON values, classifies them as

View on GitHub (pinned to 73e0f67d83)