tursodatabase/turso · error · anyhow::Error

Invalid HTTP request

Error message

Invalid HTTP request

What it means

parse_http_request requires the accumulated request bytes to contain a \r\n\r\n header terminator (find_header_end). When none is present the request is unparseable and this error is returned. In the live server the read loop only breaks after a terminator is found, the header cap is hit, or the peer closed the connection - so in practice this means the client closed or sent garbage without ever completing a header block.

Solutions

  1. Send complete requests: full header block terminated by a blank \r\n line, i.e. the literal bytes \r\n\r\n
  2. Use an HTTP client library (reqwest, curl) instead of hand-written sockets so framing is correct
  3. If embedding the server, treat this error as a 400 response rather than a connection-level failure

Example fix

// before
stream.write_all(b"POST /pull-updates HTTP/1.1\nHost: localhost")?; // no blank line, bare \n

// after
stream.write_all(b"POST /pull-updates HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n")?;
Defensive patterns

Strategy: validation

Validate before calling

// Client-side: verify framing before sending.
fn request_is_complete(raw: &[u8]) -> bool {
    raw.windows(4).any(|w| w == b"\r\n\r\n")
}

Type guard

fn looks_like_http_request(data: &[u8]) -> bool {
    data.starts_with(b"GET ")
        || data.starts_with(b"POST ")
        || data.starts_with(b"PUT ")
        || data.starts_with(b"DELETE ")
        || data.starts_with(b"OPTIONS ")
}

Try / catch

// Server-side: map parse failures to 400 instead of dropping the connection.
let (method, path, body) = match parse_http_request(&request_data) {
    Ok(parsed) => parsed,
    Err(_) => {
        respond_400(&mut stream)?;
        return Ok(());
    }
};

Prevention

When it happens

Trigger: A TCP client connects to the sync port, sends bytes without \r\n\r\n (e.g. only 'GET /pull-updates HTTP/1.1' then closes, or uses bare \n line endings), and the connection ends; parse_http_request then finds no terminator.

Common situations: Health checks or port scanners opening raw TCP connections; telnet/manual debugging sessions; clients written with \n line endings; proxies forwarding half a request before timing out.

Related errors


AI-assisted analysis of tursodatabase/turso@492c4a71cd (2026-08-20). Data as JSON: /api/errors/57af175fc74ae5d4. Report an issue: GitHub.

Appendix: source

Thrown at cli/sync_server.rs:1284

}

fn find_header_end(data: &[u8], start: usize) -> Option<usize> {
    (start..data.len().saturating_sub(3)).find(|&i| &data[i..i + 4] == b"\r\n\r\n")
}

fn parse_content_length(headers: &str) -> Option<usize> {
    for line in headers.lines() {
        let lower = line.to_lowercase();
        if lower.starts_with("content-length:") {
            let value = line.split(':').nth(1)?.trim();
            return value.parse().ok();
        }
    }
    None
}

fn parse_http_request(data: &[u8]) -> Result<(String, String, Vec<u8>)> {
    let header_end = find_header_end(data, 0).ok_or_else(|| anyhow!("Invalid HTTP request"))?;
    let headers = String::from_utf8_lossy(&data[..header_end]);

    let first_line = headers
        .lines()
        .next()
        .ok_or_else(|| anyhow!("Empty request"))?;
    let parts: Vec<&str> = first_line.split_whitespace().collect();

    if parts.len() < 2 {
        return Err(anyhow!("Invalid request line"));
    }

    let method = parts[0].to_string();
    let path = parts[1].to_string();
    let body = data[header_end + 4..].to_vec();

    Ok((method, path, body))
}

View on GitHub (pinned to 492c4a71cd)