tursodatabase/turso · warning · anyhow::Error

Invalid HTTP request

Error message

Invalid HTTP request

What it means

parse_http_request first searches the accumulated bytes for the \r\n\r\n header terminator; none found yields this generic rejection. In the connection loop it typically surfaces when the peer closes the connection (read returns 0) before sending a complete header block, because oversized-but-terminator-less headers are caught earlier by the MAX_HEADER_BYTES check. It means: these bytes are not a complete HTTP request.

Source

Thrown at cli/sync_server.rs:1169

}

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 bad083fafb)

Solutions

  1. From the client, send complete HTTP headers terminated by \r\n\r\n before the body.
  2. Use a real HTTP client (curl, reqwest) for health checks instead of raw TCP connects.
  3. If headers legitimately exceed the limit, raise MAX_HEADER_BYTES or split them.
  4. Retry the request in full; the server discards a request whose headers never completed.

Example fix

// before (client)
stream.write_all(body)?; // raw bytes, no header block

// after (client)
stream.write_all(b"POST /sync HTTP/1.1\r\nHost: tursodb\r\nContent-Length: 4\r\n\r\n")?;
stream.write_all(body)?;
Defensive patterns

Strategy: validation

Validate before calling

fn is_complete_http_request(data: &[u8]) -> bool {
    find_header_end(data, 0).is_some()
}
// before parsing:
if !is_complete_http_request(&request_data) {
    // peer closed before finishing headers: drop the connection
    return Ok(());
}
let (method, path, body) = parse_http_request(&request_data)?;

Type guard

fn is_complete_http_request(data: &[u8]) -> bool {
    (0..data.len().saturating_sub(3)).any(|i| &data[i..i + 4] == b"\r\n\r\n")
}

Try / catch

match parse_http_request(&request_data) {
    Ok((method, path, body)) => { /* dispatch */ }
    Err(err) if err.to_string() == "Invalid HTTP request" => {
        // incomplete or non-HTTP bytes: respond 400 and close; do not retry reads
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: A peer sends bytes without \r\n\r\n and closes the connection, so handle_connection exits its read loop and parse_http_request fails; also direct calls to parse_http_request on non-HTTP bytes (raw TCP probes, TLS handshakes sent to the plain port).

Common situations: Health checks or monitoring probing the sync port with raw TCP instead of HTTP; clients timing out and closing mid-header; reverse proxies forwarding partial requests; test scripts sending hand-typed requests without the blank line.

Related errors


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