Skip to content

fix(capsule/net): TcpStream::{read_bytes,peek} return empty Vec on cancellation, indistinguishable from clean EOF #759

Description

@joshuajbouw

Bug

TcpStream::read_bytes and TcpStream::peek collapse cancellation into an empty Vec<u8>:

// crates/astrid-capsule/src/engine/wasm/host/net/tcp_stream.rs

fn read_bytes(...) -> Result<Vec<u8>, ErrorCode> {
    let result = util::bounded_block_on_cancellable(&rt, &sem, &tok, async {...});
    match result {
        ...
        None => Ok(Vec::new()),       // ← cancellation → empty vec
    }
}

fn peek(...) -> Result<Vec<u8>, ErrorCode> {
    let result = util::bounded_block_on_cancellable(...);
    result.unwrap_or(Ok(Vec::new()))  // ← cancellation → empty vec
}

An empty Vec<u8> is the conventional "clean EOF" signal in byte-stream reads (matches std::io::Read::read / tokio::io::AsyncReadExt::read). The current behaviour collapses two distinct events into the same signal:

Event Should mean Current return
Peer closed cleanly, no more bytes EOF Ok(Vec::new())
Capsule being torn down (cancel) "this read didn't complete" Ok(Vec::new()) ✗ (looks like EOF)

The framed-read path (read_frame) and the write path (write_bytes, shutdown) already return Err(ErrorCode::Closed) on cancellation. read_bytes / peek are the outliers.

Impact

Capsules with EOF-triggered finalizers — write trailers, send last-message IPC, flush log, transition to "stream complete" state — will execute those finalizers under a forced unload when the cancel fires mid-read. The bug surface is narrow (the capsule has milliseconds left to live) but it's a real correctness hole.

Reported by Gemini on PR #758 (CHANGELOG.md:48 thread).

Fix

Map NoneErr(ErrorCode::Closed) in both read_bytes and peek. Empty Vec<u8> keeps its "clean EOF" meaning (peer closed gracefully, no bytes remaining); cancellation now surfaces as a distinct error.

Out of scope

Metadata

Metadata

Assignees

Labels

area/capsuleastrid-capsule: WASM sandboxing and executionbugSomething isn't working

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions