linera-io/linera-protocol · error · std::io::Error
Server task did not finish successfully
Error message
Server task did not finish successfully
What it means
`ServerHandle::join` awaits the tokio task running the simple-network TCP/UDP server. If awaiting the `TaskHandle` yields a `JoinError` — the task panicked or was cancelled — it is mapped to `std::io::ErrorKind::Interrupted` with this message. The original panic details are discarded, so the root cause must be found in tracing logs; note the trailing `?`: a server task that finishes normally but with `Err` propagates that inner I/O error instead of this message.
Source
Thrown at linera-rpc/src/simple/transport.rs:126
&mut self,
_blob_ids: Vec<BlobId>,
) -> Option<Pin<Box<dyn Stream<Item = RpcMessage> + Send>>> {
None
}
}
/// The result of spawning a server is oneshot channel to track completion, and the set of
/// executing tasks.
pub struct ServerHandle {
/// The handle tracking completion of the server task.
pub handle: TaskHandle<Result<(), std::io::Error>>,
}
impl ServerHandle {
/// Waits for the server task to finish.
pub async fn join(self) -> Result<(), std::io::Error> {
self.handle.await.map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::Interrupted,
"Server task did not finish successfully",
)
})?
}
}
/// A trait alias for a protocol transport.
///
/// A transport is an active connection that can be used to send and receive
/// [`RpcMessage`]s.
pub trait Transport:
Stream<Item = Result<RpcMessage, codec::Error>> + Sink<RpcMessage, Error = codec::Error>
{
}
impl<T> Transport for T where
T: Stream<Item = Result<RpcMessage, codec::Error>> + Sink<RpcMessage, Error = codec::Error>View on GitHub (pinned to 6c226ddcb3)
Solutions
- Inspect tracing output for the panic message that preceded this error — the returned io::Error carries no detail.
- Shut the server down through its `CancellationToken` before dropping the runtime so the task exits cleanly with `Ok(())`.
- For genuine panics, run with `RUST_BACKTRACE=1` or install `std::panic::set_hook` to capture the backtrace.
- Check whether shutdown was in progress when `join` failed, to distinguish cancellation from a crash.
Example fix
// before let handle = TransportProtocol::Tcp.spawn_server(addr, state, token, &mut join_set); runtime.shutdown_background(); // may cancel the server task handle.join().await?; // 'Server task did not finish successfully' // after let handle = TransportProtocol::Tcp.spawn_server(addr, state, token.clone(), &mut join_set); token.cancel(); // graceful shutdown first handle.join().await?;
Defensive patterns
Strategy: try-catch
Try / catch
if let Err(e) = handle.join().await { if e.kind() == std::io::ErrorKind::Interrupted { tracing::error!("server task panicked or was cancelled; inspect logs"); /* decide: restart server or propagate */ } return Err(e); } Prevention
- Always cancel the server's CancellationToken before tearing down the runtime.
- Keep tracing enabled at warn/error so handler and task panics are visible.
- Don't abort the JoinSet that owns the server task while join() is pending.
- Test shutdown ordering in integration tests, not just startup.
When it happens
Trigger: The spawned `UdpServer::run`/`TcpServer::run` loop panicking (e.g. hitting an `unreachable!` arm on a misbehaving stream); the tokio runtime shutting down and cancelling the server task while `join` is still pending; aborting the JoinSet that owns the server task.
Common situations: Dropping or shutting down the runtime before cancelling the server via its `CancellationToken`; a message-handler panic escalating during fuzzing or malformed traffic; embedding linera-rpc in a host that tears down tasks abruptly.
Related errors
- No signature returned
- Invalid options file format: \n {options_string}
- Chain listener exited unexpectedly: {result:?}
- EVM scan loop exited unexpectedly: {result:?}
- Linera scan loop exited unexpectedly: {result:?}
AI-assisted analysis of linera-io/linera-protocol@6c226ddcb3 (2026-08-22).
Data as JSON: /api/errors/426794ce78b31b58.
Report an issue: GitHub.