{"record":{"id":"697cb62cd08f06d7","repo":"herdrdev/herdr","slug":"stream-ended-mid-frame","errorCode":null,"errorMessage":"stream ended mid-frame","messagePattern":"stream ended mid-frame","errorType":"error_code","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"src/api/server/pane_graphics_stream.rs","lineNumber":461,"sourceCode":"        let mut chunk = vec![0_u8; STREAM_FRAME_BODY_CHUNK_BYTES.min(len)];\n        let total_deadline = Instant::now() + total_timeout;\n        let mut idle_deadline = Instant::now() + idle_timeout;\n\n        while data.len() < len {\n            if !stream_is_running(running, stream_active) {\n                return Ok(None);\n            }\n            ensure_before_deadlines(\n                Some(idle_deadline),\n                Some(total_deadline),\n                \"timed out reading stream frame body\",\n            )?;\n            let remaining = len - data.len();\n            let read_len = remaining.min(chunk.len());\n            match stream.read(&mut chunk[..read_len]) {\n                Ok(0) if data.is_empty() => return Ok(None),\n                Ok(0) => {\n                    return Err(io::Error::new(\n                        io::ErrorKind::UnexpectedEof,\n                        \"stream ended mid-frame\",\n                    ));\n                }\n                Ok(n) => {\n                    wait.on_progress();\n                    let now = Instant::now();\n                    if now >= total_deadline {\n                        return Err(io::Error::new(\n                            io::ErrorKind::TimedOut,\n                            \"timed out reading stream frame body\",\n                        ));\n                    }\n                    data.extend_from_slice(&chunk[..n]);\n                    idle_deadline = now + idle_timeout;\n                }\n                Err(err) if read_should_retry(&err) => {\n                    wait.after_retry(Some(idle_deadline), Some(total_deadline));","sourceCodeStart":443,"sourceCodeEnd":479,"githubUrl":"https://github.com/herdrdev/herdr/blob/f457cff4f2648eee85d176f8a41861241d4e8428/src/api/server/pane_graphics_stream.rs#L443-L479","documentation":"After a frame header declares a body length, read_exact reads that many bytes. If the stream returns Ok(0) (EOF) after some body bytes were already received, it fails with ErrorKind::UnexpectedEof and 'stream ended mid-frame'. An EOF before any bytes of the frame is a clean end-of-stream (Ok(None)); EOF partway through is a truncated, corrupt frame instead.","triggerScenarios":"The writer closes the pipe/socket after emitting a header (and possibly a partial body) without writing the full declared length — e.g. a crashed child process, a producer that miscomputes the length field, or a test stream that ends mid-body.","commonSituations":"Child process dies while flushing a graphics frame; producer writes header claiming N bytes but only sends fewer; abrupt termination (SIGKILL, panic) mid-write; fixtures in tests that truncate a frame body deliberately.","solutions":["Check the producer process: it likely exited or crashed mid-write; capture its exit status and stderr.","If you control the producer, ensure it writes the full body before closing and computes the length from the exact bytes written.","In the consumer, treat UnexpectedEof mid-frame as a corrupted stream: discard the partial frame and resynchronize or reopen the stream rather than using partial data.","For tests, make fixtures either complete frames or EOF-before-any-body-bytes if a clean end is intended."],"exampleFix":"// producer: before — header claims len, body partially flushed\nwrite_all(format!(\"len={}\\n\", body.len()).as_bytes())?;\nwrite_all(&body[..half])?; // crash here truncates frame\n\n// after — write exactly the declared body, then flush\nwrite_all(format!(\"len={}\\n\", body.len()).as_bytes())?;\nwrite_all(&body)?;\nflush()?;","handlingStrategy":"try-catch","validationCode":"null","typeGuard":null,"tryCatchPattern":"match read_exact(&mut stream, len, idle, total).await {\n    Ok(None) => { /* clean end of stream */ }\n    Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => {\n        // truncated frame: discard partial body, check producer exit status, reopen stream\n    }\n    other => other,\n}","preventionTips":["Producers must write the declared body length exactly and flush before closing.","Check child-process exit status before assuming stream corruption.","Never render a partially received frame; always discard on mid-frame EOF."],"tags":["streaming","graphics","eof","truncated-frame","rust"],"backgroundTag":"unexpected-eof","analyzedSha":"f457cff4f2648eee85d176f8a41861241d4e8428","analyzedAt":"2026-08-28T15:41:09.197Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}