hyperium/hyper · warning · hyper::Error

error shutting down connection

Error message

error shutting down connection

What it means

Thrown via Error::new_shutdown() (src/error.rs:478, Kind::Shutdown), HTTP/1 only. It wraps the std::io::Error returned by AsyncWrite::shutdown on the underlying stream — i.e. the graceful connection close sequence itself failed. Produced at proto/h1/conn.rs:851 (debug! "error shutting down IO"). Detect with Error::is_shutdown().

Source

Thrown at src/error.rs:478

        Error::new_user(User::Service).with(cause)
    }

    #[cfg(all(
        any(feature = "client", feature = "server"),
        any(feature = "http1", feature = "http2")
    ))]
    pub(super) fn new_user_body<E: Into<Cause>>(cause: E) -> Error {
        Error::new_user(User::Body).with(cause)
    }

    #[cfg(all(feature = "client", feature = "http2"))]
    pub(super) fn new_user_invalid_connect() -> Error {
        Error::new_user(User::InvalidConnectWithBody)
    }

    #[cfg(all(any(feature = "client", feature = "server"), feature = "http1"))]
    pub(super) fn new_shutdown(cause: std::io::Error) -> Error {
        Error::new(Kind::Shutdown).with(cause)
    }

    #[cfg(feature = "ffi")]
    pub(super) fn new_user_aborted_by_callback() -> Error {
        Error::new_user(User::AbortedByCallback)
    }

    #[cfg(all(feature = "client", any(feature = "http1", feature = "http2")))]
    pub(super) fn new_user_dispatch_gone() -> Error {
        Error::new(Kind::User(User::DispatchGone))
    }

    #[cfg(all(any(feature = "client", feature = "server"), feature = "http2"))]
    pub(super) fn new_h2(cause: ::h2::Error) -> Error {
        if cause.is_io() {
            Error::new_io(cause.into_io().expect("h2::Error::is_io"))
        } else {
            Error::new(Kind::Http2).with(cause)

View on GitHub (pinned to 084473f728)

Solutions

  1. Treat it as best-effort: the message exchange already completed in most cases, so log at debug/trace and continue rather than failing the request.
  2. Detect with Error::is_shutdown() and downgrade severity in logs.
  3. If using a custom IO/TLS layer, ensure its shutdown is robust to an already-closed peer.

Example fix

// before: shutdown error fails the whole connection future
let (_tx, conn) = hyper::client::conn::http1::handshake(stream).await?;
conn.await?;

// after: a shutdown error after the work is done is not fatal
tokio::spawn(async move {
    if let Err(e) = conn.await {
        if !e.is_shutdown() {
            tracing::error!("connection error: {e}");
        } else {
            tracing::debug!("connection shutdown reported error: {e}");
        }
    }
});
Defensive patterns

Strategy: try-catch

Type guard

fn is_shutdown_err(err: &hyper::Error) -> bool {
    err.is_shutdown()
}

Try / catch

tokio::spawn(async move {
    if let Err(e) = conn.await {
        if e.is_shutdown() {
            tracing::debug!("shutdown reported error (usually harmless): {e}");
        } else {
            tracing::error!("conn error: {e}");
        }
    }
});

Prevention

When it happens

Trigger: Conn::poll_shutdown calls io.poll_shutdown (conn.rs:844-855) and the stream returns an io::Error (e.g. the peer already RST the connection, or TLS close_notify failed). Fires during the teardown path, typically after the real work is already done.

Common situations: Peer has already hard-closed the connection by the time hyper sends the shutdown; a TLS layer fails to exchange close_notify; a custom AsyncWrite (e.g. a proxy) errors on shutdown. Usually harmless because the response was already sent.

Related errors


AI-assisted analysis of hyperium/hyper@084473f728 (2026-08-06). Data as JSON: /data/errors/f0bbd56b52cc14b2.json. Report an issue: GitHub.