{"record":{"id":"4d893a8f354c9ba3","repo":"jdx/mise","slug":"piped-stderr","errorCode":null,"errorMessage":"piped stderr","messagePattern":"piped stderr","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/cmd/bounded.rs","lineNumber":28,"sourceCode":"    /// This command owns its child tree even when mise itself is nested.\n    pub(crate) async fn read_isolated(mut self, limit: usize) -> Result<String> {\n        let _read_lock = RAW_LOCK.read().await;\n        let timeout = self.timeout.unwrap_or(Duration::from_secs(5));\n        self.cmd.kill_on_drop(true);\n        self.cmd\n            .stdin(Stdio::null())\n            .stdout(Stdio::piped())\n            .stderr(Stdio::piped());\n        #[cfg(unix)]\n        {\n            self.cmd.env(TASK_PGID_MANAGED_ENV, \"1\");\n            self.cmd.process_group(0);\n        }\n        let mut child = self.spawn_async_with_etxtbsy_retry().await?;\n        let _running = RunningPidGuard::new(child.id());\n        let tree = ChildTree::new(&mut child)?;\n        let stdout = child.stdout.take().expect(\"piped stdout\");\n        let stderr = child.stderr.take().expect(\"piped stderr\");\n        // One budget for both pipes: judging them only once both reach EOF\n        // would let each hold the whole limit first, so a command could\n        // allocate twice what was asked before anyone objected.\n        let budget = AtomicUsize::new(limit);\n        let result = tokio::time::timeout(timeout, async {\n            tokio::try_join!(\n                child.wait(),\n                capture(stdout, &budget, limit),\n                capture(stderr, &budget, limit)\n            )\n        })\n        .await;\n        let (status, stdout, _stderr) = match result {\n            Ok(Ok(output)) => output,\n            Ok(Err(err)) => {\n                end(&mut child, tree).await;\n                return Err(err.into());\n            }","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/cmd/bounded.rs#L10-L46","documentation":"mise panics with 'piped stderr' when a spawned child process has no stderr pipe to take. The `read_isolated` helper requires the command to have been configured with stdout and stderr both piped (via `stdin/stdout/stderr(Stdio::piped())`) before spawning, because it must capture and bound both pipes with a shared budget. If stderr was set to `inherit`, `null`, or another non-piped mode, `child.stderr` is `None` and the `.expect()` panics immediately after spawn.","triggerScenarios":"Calling `Cmd::read_isolated` (or the bounded-run path in src/cmd/bounded.rs) on a `Cmd` whose stderr was overridden to `Stdio::inherit()`, `Stdio::null()`, or a file after the default piped configuration; any code path that constructs the command and then disables stderr piping before `spawn_async_with_etxtbsy_retry`.","commonSituations":"A contributor adds a new caller of the bounded command runner and passes `Stdio::inherit()` for stderr to forward output to the terminal, not realizing `read_isolated` needs to capture stderr; refactoring `cmd!` defaults so one of the pipes is no longer set; platform-specific spawn paths that drop the stderr pipe.","solutions":["Ensure the command has both `stdout(Stdio::piped())` and `stderr(Stdio::piped())` set before calling `read_isolated`","Remove any `stderr(...)` override applied between command construction and the bounded spawn","If output should be discarded, capture it via the pipe and drop it instead of using `Stdio::null()`","If output must be inherited, use a different run path that does not require capturing both pipes"],"exampleFix":"// before\nlet mut cmd = Cmd::new(program);\ncmd.stderr(Stdio::inherit());\nlet out = cmd.read_isolated(limit, timeout).await?;\n// after\nlet mut cmd = Cmd::new(program);\n// keep both pipes so read_isolated can budget them\ncmd.stdout(Stdio::piped()).stderr(Stdio::piped());\nlet out = cmd.read_isolated(limit, timeout).await?;","handlingStrategy":"validation","validationCode":"debug_assert!(cmd.get_stdout().is_piped() && cmd.get_stderr().is_piped(), \"read_isolated requires piped stdout+stderr\");","typeGuard":"fn has_pipes(cmd: &Cmd) -> bool { cmd.piped_stdout() && cmd.piped_stderr() }","tryCatchPattern":null,"preventionTips":["Never override stderr/stdout to inherit or null on commands routed through read_isolated","Centralize bounded command spawning in one constructor that always sets both pipes","Add a debug assertion for piped stdio before spawn"],"tags":["rust","process-spawn","panic","invariant"],"backgroundTag":"internal-invariant-violation","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}