herdrdev/herdr · error · io::Error

app response channel closed

Error message

app response channel closed

What it means

dispatch_to_app waits on the response channel after forwarding a request to the app. If the app side has dropped the response sender, recv returns Disconnected, mapped to ErrorKind::BrokenPipe with 'app response channel closed'. This means the app (or its handler task) exited or was shut down while the request was outstanding — the server outlived its app counterpart for that channel.

Source

Thrown at src/api/server.rs:859

            active.store(false, Ordering::Release);
        }
        return error_response_json(
            request_id,
            "server_unavailable",
            format!("failed to dispatch request: {err}"),
        );
    }

    let response = match timeout {
        Some(timeout) => response_rx.recv_timeout(timeout).map_err(|err| match err {
            std::sync::mpsc::RecvTimeoutError::Timeout => std::io::Error::new(
                std::io::ErrorKind::TimedOut,
                format!(
                    "timed out waiting for app response after {} ms",
                    timeout.as_millis()
                ),
            ),
            std::sync::mpsc::RecvTimeoutError::Disconnected => std::io::Error::new(
                std::io::ErrorKind::BrokenPipe,
                "app response channel closed",
            ),
        }),
        None => response_rx
            .recv()
            .map_err(|err| std::io::Error::new(std::io::ErrorKind::BrokenPipe, err)),
    };

    match response {
        Ok(response) => response,
        Err(err) => {
            if let Some(active) = request_active {
                active.store(false, Ordering::Release);
            }
            error_response_json(
                request_id,
                "server_unavailable",

View on GitHub (pinned to f457cff4f2)

Solutions

  1. Treat it as a shutdown signal: reconnect or re-establish the session instead of retrying the same channel.
  2. Check the app process / server logs for a panic that dropped the channel sender.
  3. Ensure request handlers always send exactly one response before returning, including on error paths.
  4. In tests, join the app thread before issuing further API calls, or expect BrokenPipe during teardown windows.

Example fix

// before
match dispatch_to_app(&tx, req, timeout) {
    Ok(resp) => { /* ... */ }
    Err(e) => panic!("api failed: {e}"),
}

// after
match dispatch_to_app(&tx, req, timeout) {
    Ok(resp) => { /* ... */ }
    Err(e) if e.kind() == std::io::ErrorKind::BrokenPipe => {
        // app shut down mid-request; reconnect or exit gracefully
    }
    Err(e) => return Err(e),
}
Defensive patterns

Strategy: fallback

Validate before calling

null

Try / catch

match dispatch_to_app(&tx, req, timeout) {
    Err(e) if e.kind() == io::ErrorKind::BrokenPipe => {
        // app is gone: mark session dead, reconnect or exit; do not retry the same channel
    }
    other => other,
}

Prevention

When it happens

Trigger: The app closes/shuts down while an API request is in flight; the handler task that owns the response sender panics or returns early; dispatch after the app has begun teardown (e.g. during quit) so every receiver of the response mpsc is gone.

Common situations: Client sends a request at the exact moment the user quits the app; a panic in the app's request handler dropping the channel; tests that shut the app down but keep the API server running; races during restart/update flows.

Related errors


AI-assisted analysis of herdrdev/herdr@f457cff4f2 (2026-08-28). Data as JSON: /api/errors/bc747c3ed7ba3853. Report an issue: GitHub.