n0-computer/iroh · error · SendError
EmptyPacket
EmptyPacket
Error message
Attempted to send empty packet
What it means
The relay server attempted to send a RelayToClientMsg::Datagrams packet whose datagram list is empty. Empty packets are meaningless on the relay stream, so start_send rejects them with SendError::EmptyPacket before serialization.
Solutions
- Check datagrams.contents.is_empty() before constructing/enqueueing the Datagrams message and skip sending if empty.
- Only build Datagrams packets after confirming at least one datagram is pending.
- If an empty message represents a keep-alive, use the relay's dedicated ping/pong message type instead.
Example fix
// before
let msg = RelayToClientMsg::Datagrams { datagrams };
sink.start_send(msg)?;
// after
if !datagrams.contents.is_empty() {
sink.start_send(RelayToClientMsg::Datagrams { datagrams })?;
} Defensive patterns
Strategy: validation
Validate before calling
fn is_sendable(msg: &RelayToClientMsg) -> bool {
match msg { RelayToClientMsg::Datagrams { datagrams, .. } => !datagrams.contents.is_empty(), _ => true }
} Type guard
fn non_empty_datagrams(d: Datagrams) -> Option<Datagrams> { (!d.contents.is_empty()).then_some(d) } Try / catch
match sink.start_send(msg) {
Err(SendError::EmptyPacket) => { /* skip: nothing to send */ }
other => other?,
} Prevention
- Skip building Datagrams packets when the pending queue is empty.
- Assert queue non-emptiness at packet-construction sites.
- Use the relay ping message for keep-alives, not empty datagram packets.
- Review refactors that removed emptiness checks.
When it happens
Trigger: Calling start_send with a RelayToClientMsg::Datagrams variant where datagrams.contents is empty — typically produced by code that drains a queue and builds a datagram packet without checking it collected anything.
Common situations: Flush loops that build a Datagrams message each tick regardless of pending items, race between queue check and packet construction, refactored code paths that removed the emptiness check upstream.
Understand the failure class
Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.
Related errors
- EmptyPacket
- The relay is rate-limiting this endpoint; outbound relay…
- Invalid transport configuration
- UnexpectedUpgradeStatus
- ExceedsMaxPacketSize
AI-assisted analysis of n0-computer/iroh@2b4de030ce (2026-09-08).
Data as JSON: /api/errors/43850a91539e5953.
Report an issue: GitHub.
Appendix: source
Thrown at iroh-relay/src/server/streams.rs:137
impl<S> Sink<RelayToClientMsg> for RelayedStream<S>
where
S: Sink<bytes::Bytes, Error = StreamError> + Unpin,
{
type Error = SendError;
fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Pin::new(&mut self.inner).poll_ready(cx).map_err(Into::into)
}
fn start_send(mut self: Pin<&mut Self>, item: RelayToClientMsg) -> Result<(), Self::Error> {
let size = item.encoded_len();
ensure!(
size <= MAX_PACKET_SIZE,
SendError::ExceedsMaxPacketSize { size }
);
if let RelayToClientMsg::Datagrams { datagrams, .. } = &item {
ensure!(!datagrams.contents.is_empty(), SendError::EmptyPacket);
}
Pin::new(&mut self.inner)
.start_send(item.to_bytes().freeze())
.map_err(Into::into)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Pin::new(&mut self.inner).poll_flush(cx).map_err(Into::into)
}
fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Pin::new(&mut self.inner).poll_close(cx).map_err(Into::into)
}
}
/// Relay receive errors
#[stack_error(derive, add_meta, from_sources)]View on GitHub (pinned to 2b4de030ce)