herdrdev/herdr · error · io::Error

timed out reading api request

Error message

timed out reading api request

What it means

While reading a client's initial request line, the server polls the local stream in small intervals (CONNECTION_POLL_INTERVAL). If the stream stays Pending (no data) until the deadline, the read aborts with ErrorKind::TimedOut and 'timed out reading api request'. This bounds how long a connected-but-silent client can hold the reader. It fires only before any complete line has arrived.

Source

Thrown at src/api/server.rs:543

        match read {
            LocalStreamRead::Closed => break Ok(None),
            LocalStreamRead::Data => {
                bytes.push(byte[0]);
                if byte[0] == b'\n' {
                    break String::from_utf8(bytes)
                        .map(Some)
                        .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err));
                }
                if bytes.len() > max_bytes {
                    break Err(io::Error::new(
                        io::ErrorKind::InvalidData,
                        "api request line is too large",
                    ));
                }
            }
            LocalStreamRead::Pending => {
                if Instant::now() >= deadline {
                    break Err(io::Error::new(
                        io::ErrorKind::TimedOut,
                        "timed out reading api request",
                    ));
                }
                std::thread::sleep(CONNECTION_POLL_INTERVAL);
            }
        }
    };
    set_local_stream_polling(stream, false)?;
    result
}

#[cfg(all(test, windows))]
mod windows_tests {
    use super::*;
    use interprocess::local_socket::traits::Listener as _;
    use std::io::{BufRead, BufReader};
    use std::sync::mpsc::{self, Receiver};

View on GitHub (pinned to f457cff4f2)

Solutions

  1. Make the client send its complete newline-terminated request line immediately after connecting.
  2. If the client legitimately waits (e.g. user input), send the request only after it is ready rather than connecting early and idling.
  3. Increase the timeout passed to read_initial_request_line_with_timeout if slow clients are expected.
  4. Verify the client and server agree on the line framing (trailing newline required).

Example fix

// before
let mut stream = UnixStream::connect(path)?;
thread::sleep(Duration::from_secs(30));
stream.write_all(req.as_bytes())?;

// after
let mut stream = UnixStream::connect(path)?;
stream.write_all(req.as_bytes())?;
stream.write_all(b"\n")?;
Defensive patterns

Strategy: retry

Validate before calling

let mut stream = UnixStream::connect(path)?;
stream.set_write_timeout(Some(Duration::from_secs(5)))?;
// write the full line immediately after connect, before anything else
stream.write_all(request_line_with_newline.as_bytes())?;

Try / catch

match read_initial_request_line_with_timeout(&mut stream, timeout) {
    Err(e) if e.kind() == io::ErrorKind::TimedOut => {
        // client never sent data: reconnect and resend once
    }
    other => other,
}

Prevention

When it happens

Trigger: A client opens the local API socket but sends no bytes (or no newline-terminated line) before the read deadline in read_initial_request_line_with_limits; e.g. connect() without a write, a paused client, or a probe tool that just opens the socket.

Common situations: Health-check scripts that only open a connection; a client blocked on its own I/O before sending; network/VM file-descriptor stalls on the local socket; leftover sockets from a crashed client.

Understand the failure class

Related errors


AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28). Data as JSON: /api/errors/d796f80875d09b4f. Report an issue: GitHub.