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

  1. Check datagrams.contents.is_empty() before constructing/enqueueing the Datagrams message and skip sending if empty.
  2. Only build Datagrams packets after confirming at least one datagram is pending.
  3. 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

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


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)