rwf2/Rocket · warning · io::Error

BrokenPipe

BrokenPipe

Error message

I/O driver terminated

What it means

Internal runtime error from Rocket's cancellable I/O (core/lib/src/listener/cancellable.rs): after the async I/O driver (tokio reactor) has terminated during shutdown, any further poll of connection I/O returns io::ErrorKind::BrokenPipe with message 'I/O driver terminated' (the gone() constructor). It is the definitive 'this runtime is gone' signal emitted once shutdown has progressed past the grace stage and resources are being torn down.

Source

Thrown at core/lib/src/listener/cancellable.rs:50

pub trait CancellableExt: Sized {
    fn cancellable(self, stages: Stages) -> Cancellable<Self> {
        Cancellable {
            io: Some(self),
            state: State::Active,
            stages,
        }
    }
}

impl<T> CancellableExt for T { }

fn time_out() -> io::Error {
    io::Error::new(io::ErrorKind::TimedOut, "shutdown grace period elapsed")
}

fn gone() -> io::Error {
    io::Error::new(io::ErrorKind::BrokenPipe, "I/O driver terminated")
}

impl<I: AsyncCancel> Cancellable<I> {
    pub fn inner(&self) -> Option<&I> {
        self.io.as_ref()
    }
}

pub trait AsyncCancel {
    fn poll_cancel(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>>;
}

impl<T: AsyncWrite> AsyncCancel for T {
    fn poll_cancel(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
        <T as AsyncWrite>::poll_shutdown(self, cx)
    }
}

View on GitHub (pinned to 3a54d079ae)

Solutions

  1. Accept it as part of shutdown: ensure connection-scoped tasks are tied to request/shutdown lifetimes so they stop when shutdown begins
  2. Use graceful shutdown signals (Rocket.toml [shutdown] with adequate grace) so connections close before the driver terminates
  3. In custom runtimes, keep the runtime alive until all connection tasks finish (JoinHandle them before dropping)
  4. Filter this message from production alerting — it is expected at process exit

Example fix

// before: detached task outlives the runtime
tokio::spawn(async move { loop { socket.read(&mut b).await; } });

// after: tie the task to shutdown
let mut shutdown = rocket.shutdown().clone();
tokio::spawn(async move {
    tokio::select! {
        _ = shutdown.notified() => {},
        _ = socket.read(&mut b) => {},
    }
});
Defensive patterns

Strategy: try-catch

Try / catch

// treat BrokenPipe 'I/O driver terminated' as end-of-life, not an error to propagate
match conn_io.poll() {
    Err(e) if e.kind() == io::ErrorKind::BrokenPipe
        && e.to_string().contains("driver terminated") => { info_!("runtime gone, stopping"); return; }
    other => other,
}

Prevention

When it happens

Trigger: Server is shutting down; grace expired; the tokio I/O driver is dropped; code still holding connection futures (e.g. a task blocked on read/write) polls them and receives this error. Also seen when a Rocket server future is dropped while connection tasks are still running.

Common situations: Tasks spawned per-connection that outlive the #[launch] runtime (e.g. manually spawned tasks holding request I/O); dropping the Rocket server inside a custom multi-runtime setup; shutdown-time log noise from in-flight requests.

Related errors


AI-assisted analysis of rwf2/Rocket@3a54d079ae (2026-08-16). Data as JSON: /api/errors/bb4f7b46dbbb31e8. Report an issue: GitHub.