tursodatabase/turso · warning · anyhow::Error

Empty request

Error message

Empty request

What it means

The header block was found (data contains \r\n\r\n) but its first line is missing — in practice the data begins immediately with the terminator, i.e. leading blank lines before the request line, making lines().next() return None. RFC 7230 says servers SHOULD ignore at least one empty line before the request-line; this minimal parser does not, so it rejects. Keep-alive or telnet-style newline pings produce exactly this shape.

Source

Thrown at cli/sync_server.rs:1175

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

fn format_http_response(resp: &HttpResponse) -> Vec<u8> {
    let status_text = match resp.status {
        200 => "OK",
        204 => "No Content",
        404 => "Not Found",

View on GitHub (pinned to bad083fafb)

Solutions

  1. Client-side: send the request line first, with no leading blank lines.
  2. Server-side: skip leading empty lines before parsing (RFC 7230 recommends tolerating at least one).
  3. Check for keep-alive connection reuse bugs that leak a stray CRLF between requests.

Example fix

// before (server)
let first_line = headers.lines().next().ok_or_else(|| anyhow!("Empty request"))?;

// after (server): tolerate leading empty lines per RFC 7230
let first_line = headers
    .lines()
    .find(|line| !line.trim().is_empty())
    .ok_or_else(|| anyhow!("Empty request"))?;
Defensive patterns

Strategy: validation

Validate before calling

fn has_request_line(data: &[u8]) -> bool {
    let Some(header_end) = (0..data.len().saturating_sub(3))
        .find(|&i| &data[i..i + 4] == b"\r\n\r\n")
    else { return false; };
    let headers = String::from_utf8_lossy(&data[..header_end]);
    headers.lines().any(|line| !line.trim().is_empty())
}
// before parsing:
anyhow::ensure!(has_request_line(&request_data), "no request line before terminator");

Type guard

fn has_request_line(data: &[u8]) -> bool {
    let Some(header_end) = (0..data.len().saturating_sub(3))
        .find(|&i| &data[i..i + 4] == b"\r\n\r\n")
    else { return false; };
    String::from_utf8_lossy(&data[..header_end])
        .lines()
        .any(|line| !line.trim().is_empty())
}

Try / catch

match parse_http_request(&request_data) {
    Ok((method, path, body)) => { /* dispatch */ }
    Err(err) if err.to_string() == "Empty request" => {
        // only blank lines before the terminator: reply 400 or tolerate per RFC 7230
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: parse_http_request receives data starting with \r\n\r\n or whose header section before the terminator is only empty lines — a client that flushes a stray CRLF before the request line, or manual telnet testing that presses Enter first.

Common situations: Manual telnet/netcat testing with a leading blank line; HTTP clients that emit a leftover CRLF from a previous keep-alive exchange; load balancers or wrappers injecting empty lines; scripts writing '\r\nGET ...'.

Related errors


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