{"record":{"id":"15eaab16a41e2e8c","repo":"rtk-ai/rtk","slug":"stderr-streaming-thread-panicked","errorCode":null,"errorMessage":"stderr streaming thread panicked","messagePattern":"stderr streaming thread panicked","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/main.rs","lineNumber":2677,"sourceCode":"                    err.flush()?;\n                }\n\n                Ok(captured)\n            });\n\n            let status = child\n                .0\n                .take()\n                .context(\"Child process missing\")?\n                .wait()\n                .context(format!(\"Failed waiting for command: {}\", cmd_name))?;\n\n            let stdout_bytes = stdout_handle\n                .join()\n                .map_err(|_| anyhow::anyhow!(\"stdout streaming thread panicked\"))??;\n            let stderr_bytes = stderr_handle\n                .join()\n                .map_err(|_| anyhow::anyhow!(\"stderr streaming thread panicked\"))??;\n\n            let stdout = String::from_utf8_lossy(&stdout_bytes);\n            let stderr = String::from_utf8_lossy(&stderr_bytes);\n            let full_output = format!(\"{}{}\", stdout, stderr);\n\n            // Track usage (input = output since no filtering)\n            timer.track(\n                &format!(\"{} {}\", cmd_name, cmd_args.join(\" \")),\n                &format!(\"rtk proxy {} {}\", cmd_name, cmd_args.join(\" \")),\n                &full_output,\n                &full_output,\n            );\n\n            core::utils::exit_code_from_status(&status, &cmd_name)\n        }\n\n        Commands::Trust { list, yes } => {\n            hooks::trust::run_trust(list, yes)?;","sourceCodeStart":2659,"sourceCodeEnd":2695,"githubUrl":"https://github.com/rtk-ai/rtk/blob/d977e1c31621fe8704e6500ceeb9c7a0de2b6836/src/main.rs#L2659-L2695","documentation":"This anyhow error is created at src/main.rs:2675-2677 in the `rtk proxy <cmd>` path when stderr_handle.join() returns Err, i.e. the thread spawned at src/main.rs:2643 to pump the child's stderr terminated via panic instead of returning. The closure is written panic-free on purpose: every I/O failure (read from the pipe, write/flush to the parent's stderr, including EPIPE when the consumer closed the terminal/pipe) is returned as io::Error through `?`, which surfaces as a different, context-less io error via the second `?` — not this message. So this message means a genuine panic ran inside the streaming closure (an indexing/unwrap bug introduced by a later edit, a stack overflow on the thread, or an abort-like failure), and the child's captured stderr bytes are lost.","triggerScenarios":"Running `rtk proxy <command>` where the stderr pump thread panics: (1) any code change to the closure at src/main.rs:2643-2663 that adds unwrap()/expect()/slice indexing instead of `?`; (2) thread stack exhaustion while running the loop (pathological recursion added inside the loop); (3) a panic in std::io::stderr().lock() teardown during signal-driven exit (the custom SIGINT/SIGTERM handler at src/main.rs:2572-2581 kills the child and exits without joining). Note EPIPE on stderr writes does NOT trigger this — it yields an io::Result::Err path instead.","commonSituations":"A maintainer extends the streaming loop (e.g., adding TTY detection or line buffering) and introduces an unwrap or a buf[..n] slice with wrong bounds; tests pass because normal runs never hit the edge, then `rtk proxy cargo test` aborts with this message. Also seen when rtk runs under wrappers that close stderr early (piped into head) combined with a panic-prone closure edit — users misread it as a broken pipe, but the pipe case produces a plain io error.","solutions":["Reproduce with RUST_BACKTRACE=1 (e.g. `RUST_BACKTRACE=1 rtk proxy <cmd> 2>/dev/full`) — the panic hook prints the original panic location inside the closure at src/main.rs:2643-2663, which is the real bug site, not line 2677.","Audit the stderr/stdout closures for panic sources: replace any unwrap()/expect()/direct indexing with `?` and checked slicing; the loop already demonstrates the safe pattern (`let take = count.min(CAP - captured.len())`).","Improve the join error to surface the panic payload (downcast to &str/String) so the next occurrence reports the panic message instead of a generic string.","If you are an rtk user (not editing rtk), run the command directly without rtk to unblock, and report the panic backtrace to the rtk repo."],"exampleFix":"// before (src/main.rs:2675)\nlet stderr_bytes = stderr_handle\n    .join()\n    .map_err(|_| anyhow::anyhow!(\"stderr streaming thread panicked\"))??;\n\n// after: surface the panic payload and keep io errors contextual\nlet stderr_bytes = stderr_handle.join().map_err(|p| {\n    let msg = p\n        .downcast_ref::<&str>()\n        .copied()\n        .or_else(|| p.downcast_ref::<String>().map(|s| s.as_str()))\n        .unwrap_or(\"unknown panic\");\n    anyhow::anyhow!(\"stderr streaming thread panicked: {}\", msg)\n})?.context(\"streaming child stderr\")?;","handlingStrategy":"fallback","validationCode":null,"typeGuard":null,"tryCatchPattern":"// If you embed this proxy pattern in your own code, wrap the joins so a\n// streaming-thread panic degrades instead of aborting the caller:\nuse std::panic::{catch_unwind, AssertUnwindSafe};\n\nlet joined = catch_unwind(AssertUnwindSafe(|| stderr_handle.join()));\nlet stderr_bytes: Vec<u8> = match joined {\n    Ok(Ok(Ok(bytes))) => bytes,\n    Ok(Ok(Err(io_err))) => {\n        // real I/O failure (e.g. EPIPE): report context, keep going with empty capture\n        eprintln!(\"warning: stderr stream failed: {io_err}\");\n        Vec::new()\n    }\n    _ => {\n        // thread panicked: fall back to no capture rather than unwinding here\n        eprintln!(\"warning: stderr streaming thread panicked\");\n        Vec::new()\n    }\n};","preventionTips":["Keep the pump closures panic-free by construction: return io::Result and use `?` for every read/write/flush — never unwrap(), expect(), or unchecked slicing inside the thread.","Mirror the existing bounded-capture idiom (`count.min(CAP - captured.len())`) whenever you edit the loop so slice bounds stay provably in range.","If you are an rtk user hitting this, fall back to running the command without rtk (the raw command always works) and report the RUST_BACKTRACE=1 output, since the panic originates in rtk's thread, not your command.","Test proxy streaming against hostile consumers (`rtk proxy <cmd> | head -1`, stderr to /dev/full) so EPIPE paths are exercised by io errors, confirming panics cannot arise from pipes."],"tags":["threads","join","panic","subprocess","proxy","rtk"],"backgroundTag":null,"analyzedSha":"d977e1c31621fe8704e6500ceeb9c7a0de2b6836","analyzedAt":"2026-08-16T05:40:46.291Z","schemaVersion":2},"datasetVersion":"2026-08-16T08:17:34.114Z"}