sinelaw/fresh · error

stdin spool: read past the end of the drained region

Error message

stdin spool: read past the end of the drained region

What it means

The stdin spool backs a virtual file (piped stdin) and, once drained, only serves the bytes that were actually read. `read_range` requests `len` bytes at `offset`, and if the spool returns fewer bytes than requested it deliberately fails with `UnexpectedEof` instead of silently truncating, mirroring `File::read_exact` semantics on the local filesystem.

Solutions

  1. Clamp the requested range to the spool's actual current length before calling read_at/read_range
  2. Treat UnexpectedEof as end-of-file: shorten the read or stop reading
  3. Refresh the buffer's known length from the spool after stdin signals EOF rather than caching an older length
  4. If stdin is expected to deliver more data later, wait for the writer to finish instead of reading ahead

Example fix

// before
let data = spool.read_range(path, offset, len)?;
// after
let available = spool.len(path).saturating_sub(offset);
let len = len.min(available);
let data = spool.read_range(path, offset, len)?;
Defensive patterns

Strategy: validation

Validate before calling

let spool_len = spool.len(path)?;
if offset + len > spool_len { len = spool_len - offset; } // clamp before reading

Try / catch

match spool.read_range(path, offset, len) {
    Ok(data) => { /* use data */ }
    Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => { /* treat as EOF */ }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling `read_range` (via the stdin spool's read path) with an offset+len that extends beyond the end of the already-drained stdin data — e.g. reading with stale length metadata, or requesting more bytes than stdin actually provided.

Common situations: Reading a virtual stdin file after new stdin data has fully drained; a buffer/view holding an outdated byte-length and refreshing a region at EOF; tools diffing or tailing a stdin-backed buffer past its end.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of sinelaw/fresh@67894ca546 (2026-09-13). Data as JSON: /api/errors/e51e6b8e0455d048. Report an issue: GitHub.

Appendix: source

Thrown at crates/fresh-editor/src/services/stdin_spool.rs:154

    /// is the part worth seeing in a log line anyway.
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SpoolFileSystem")
            .field("spool", &self.spool.path)
            .finish_non_exhaustive()
    }
}

impl FileSystem for SpoolFileSystem {
    // --- the two the spool actually answers ------------------------------

    fn read_range(&self, path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
        if self.spool.owns(path) {
            let data = self.spool.read_at(offset, len)?;
            if data.len() < len {
                // Matches `File::read_exact`, which is what the local
                // filesystem uses: asking for a range past the end is an
                // error rather than a silent truncation.
                return Err(io::Error::new(
                    io::ErrorKind::UnexpectedEof,
                    "stdin spool: read past the end of the drained region",
                ));
            }
            return Ok(data);
        }
        self.inner.read_range(path, offset, len)
    }

    fn metadata(&self, path: &Path) -> io::Result<FileMetadata> {
        if self.spool.owns(path) {
            return Ok(FileMetadata::new(self.spool.len()?));
        }
        self.inner.metadata(path)
    }

    fn read_file(&self, path: &Path) -> io::Result<Vec<u8>> {
        if self.spool.owns(path) {

View on GitHub (pinned to 67894ca546)