jdx/mise · error
piped stdout
Error message
piped stdout
What it means
`read_isolated` (in src/cmd/bounded.rs) runs a command in its own process group with a bounded output budget, which requires both pipes. This `.expect("piped stdout")` (and its stderr sibling) panics if the child was not spawned with piped stdio — a spawn-configuration invariant for the task-output capture path used by mise tasks.
Source
Thrown at src/cmd/bounded.rs:27
/// Capture a finite response, with one deadline for the process and pipes.
/// This command owns its child tree even when mise itself is nested.
pub(crate) async fn read_isolated(mut self, limit: usize) -> Result<String> {
let _read_lock = RAW_LOCK.read().await;
let timeout = self.timeout.unwrap_or(Duration::from_secs(5));
self.cmd.kill_on_drop(true);
self.cmd
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
#[cfg(unix)]
{
self.cmd.env(TASK_PGID_MANAGED_ENV, "1");
self.cmd.process_group(0);
}
let mut child = self.spawn_async_with_etxtbsy_retry().await?;
let _running = RunningPidGuard::new(child.id());
let tree = ChildTree::new(&mut child)?;
let stdout = child.stdout.take().expect("piped stdout");
let stderr = child.stderr.take().expect("piped stderr");
// One budget for both pipes: judging them only once both reach EOF
// would let each hold the whole limit first, so a command could
// allocate twice what was asked before anyone objected.
let budget = AtomicUsize::new(limit);
let result = tokio::time::timeout(timeout, async {
tokio::try_join!(
child.wait(),
capture(stdout, &budget, limit),
capture(stderr, &budget, limit)
)
})
.await;
let (status, stdout, _stderr) = match result {
Ok(Ok(output)) => output,
Ok(Err(err)) => {
end(&mut child, tree).await;
return Err(err.into());View on GitHub (pinned to afd2eddd3a)
Solutions
- Ensure the command sets `.stdout(Stdio::piped()).stderr(Stdio::piped())` before `spawn_async_with_etxtbsy_retry()`
- Keep interactive (non-captured) task execution on a separate code path from `read_isolated`
- Report with `MISE_DEBUG=1` and the failing task definition if seen on an unmodified build
- Add a debug_assert or early error right after spawn verifying both pipes exist
Example fix
// before let mut cmd = build_task_cmd(task); // stdio unset -> inherits // after let mut cmd = build_task_cmd(task); cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
Defensive patterns
Strategy: validation
Validate before calling
// In task spawn helpers, before read_isolated: assert!(matches!(cmd.get_stdout(), Stdio::piped()) && matches!(cmd.get_stderr(), Stdio::piped()));
Prevention
- Keep interactive task modes on a separate execution path from bounded capture
- Re-verify stdio after any wrapper that mutates process groups or env
- Cover task capture with tests that assert output is fully captured
When it happens
Trigger: A task command spawned for `read_isolated` with inherited or null stdio; a wrapper (e.g. one setting process groups or env) that also overrides stdio; refactors in task spawn configuration that drop the pipes.
Common situations: Changes to `src/cmd/bounded.rs` or task spawn helpers that reconfigure stdio; custom task run modes (e.g. interactive tasks) accidentally routed through the bounded/isolated reader.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- stdout must be piped
- stderr must be piped
- command wait must complete
- GitHub API URL should be valid
- GitHub API URL should support path segments
AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09).
Data as JSON: /api/errors/86c5bd949bb6a216.
Report an issue: GitHub.