valeriansaliou/sonic · warning · std::io::Error

BrokenPipe

Error message

BrokenPipe

What it means

In the generated blocking wrapper quit_blocking_, the oneshot recv_timeout(RECV_TIMEOUT) on the quit response is remapped to ErrorKind::BrokenPipe on timeout. A timeout here means the response never arrived because the channel's worker/dispatcher is gone. It indicates the channel died before the quit acknowledgement could be delivered.

Source

Thrown at client/src/util.rs:138

                pass: impl AsRef<str>,
                multiplexer: &$crate::SonicMultiplexer,
            ) -> std::io::Result<Self> {
                SonicChannel::<self::Mode>::connect::<T>(addr, pass, multiplexer)
                    .map(|inner| Self { inner })
            }

            pub fn server_info(&self) -> &crate::events::ServerInfo {
                &self.inner.server_info
            }

            pub fn channel_info(&self) -> &crate::events::ChannelInfo {
                &self.inner.channel_info
            }

            fn quit_blocking_(&mut self) -> std::io::Result<()> {
                self.quit()?
                    .recv_timeout($crate::RECV_TIMEOUT)
                    .map_err(|error| std::io::Error::new(std::io::ErrorKind::BrokenPipe, error))?
            }
        }

        impl Drop for $low_level_ty {
            #[inline]
            fn drop(&mut self) {
                if !self.inner.is_closed() {
                    $crate::logging::log_trace!(concat!("[Drop] Quitting ", stringify!($low_level_ty)));
                    self.quit_blocking_().unwrap_or_else(|error| crate::logging::log_error!("{error:?}"));
                }
            }
        }

        #[cfg(feature = "sync")]
        type BlockingChannel = $blocking_ty;

        #[doc = concat!("A blocking way to interact with a Sonic Channel in ", stringify!($mode), " mode.")]
        #[doc = concat!("\n\nWhen in an asynchronous context (e.g. using `tokio`), use [`", stringify!($async_ty), "`] instead.")]

View on GitHub (pinned to e6a72da6a5)

Solutions

  1. Treat BrokenPipe on quit as benign — the channel is already down; ignore or log it.
  2. Quit only once and guard against Drop racing manual quit.
  3. Reconnect if you still need a clean session teardown.
  4. Verify the runtime is not being torn down while quit's reply is pending.

Example fix

// before
channel.quit_blocking()?;
// after
if let Err(e) = channel.quit_blocking() {
    if e.kind() != std::io::ErrorKind::BrokenPipe {
        return Err(e);
    } // already closed: fine
}
Defensive patterns

Strategy: try-catch

Type guard

fn is_broken_pipe(e: &std::io::Error) -> bool {
    e.kind() == std::io::ErrorKind::BrokenPipe
}

Try / catch

if let Err(e) = channel.quit_blocking() {
    if e.kind() != std::io::ErrorKind::BrokenPipe {
        return Err(e);
    }
    // channel already dead: treat quit as done
}

Prevention

When it happens

Trigger: Calling the blocking quit variant when the connection task already exited (send succeeded but nobody responds), or the worker was killed between enqueue and reply; also RECV_TIMEOUT expiring due to extreme stalls.

Common situations: Double-quit or quit after a prior connection failure; Drop impls racing with manual quit; shutdown ordering issues in multithreaded apps.

Related errors


AI-assisted analysis of valeriansaliou/sonic@e6a72da6a5 (2026-09-01). Data as JSON: /api/errors/d99abc73e60145ea. Report an issue: GitHub.