tursodatabase/turso · error · anyhow::Error

Empty request

Error message

Empty request

What it means

After the \r\n\r\n terminator is found, parse_http_request takes the first line of the header block as the request line. This error fires when the header block is empty - i.e. the request bytes begin immediately with \r\n\r\n, so headers.lines().next() yields None. It is distinct from 778: framing was found, but there is no request line in front of it.

Solutions

  1. Start the request bytes directly with the request line: 'POST /v2/pipeline HTTP/1.1\r\n...'
  2. Strip stray leading CRLFs client-side before writing to the socket
  3. When embedding, map this to a 400 Bad Request response

Example fix

// before: stray leading blank lines
stream.write_all(b"\r\n\r\nPOST /v2/pipeline HTTP/1.1\r\n\r\n")?;

// after: request line first, single terminator
stream.write_all(b"POST /v2/pipeline HTTP/1.1\r\nContent-Length: 0\r\n\r\n")?;
Defensive patterns

Strategy: validation

Validate before calling

// Client-side: request must start with the request line, not a blank line.
fn request_starts_with_request_line(raw: &[u8]) -> bool {
    !raw.starts_with(b"\r\n") && !raw.starts_with(b"\n")
}

Try / catch

let (method, path, body) = match parse_http_request(&request_data) {
    Ok(parsed) => parsed,
    Err(err) if err.to_string() == "Empty request" => {
        respond_400(&mut stream)?; // leading blank lines: malformed request
        return Ok(());
    }
    Err(err) => return Err(err),
};

Prevention

When it happens

Trigger: A client sends leading blank lines before the request (bytes start with \r\n\r\n followed by the real request), or sends only a terminator with nothing before it; find_header_end returns 0 and the header slice is empty.

Common situations: Telnet sessions pressing Enter before typing the request; sloppy pipelining that leaves trailing blank lines from the previous response cycle; scripts concatenating request fragments incorrectly.

Related errors


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

Appendix: source

Thrown at cli/sync_server.rs:1290

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 text_response(status: u16, body: &str) -> HttpResponse {
    HttpResponse {
        status,
        content_type: "text/plain".to_string(),
        body: body.as_bytes().to_vec(),

View on GitHub (pinned to 492c4a71cd)