n0-computer/iroh · error · SendError

EmptyPacket

EmptyPacket

Error message

Attempted to send empty packet

What it means

SendError::EmptyPacket is raised in start_send when a ClientToRelayMsg::Datagrams frame carries an empty datagram list. Sending an empty packet would waste a round trip and violates the relay protocol, so it is rejected before encoding. Note the size check happens first, so an empty batch must still be non-empty to pass.

Solutions

  1. Skip the send entirely when datagrams.contents.is_empty() before constructing/sending the frame.
  2. Only build a Datagrams batch once at least one datagram has been queued.
  3. Audit flush logic so empty batches are dropped instead of sent.

Example fix

// before
let msg = ClientToRelayMsg::Datagrams { datagrams, .. };
client.send(msg).await?;

// after
if !datagrams.contents.is_empty() {
    client.send(ClientToRelayMsg::Datagrams { datagrams, .. }).await?;
}
Defensive patterns

Strategy: validation

Validate before calling

if datagrams.contents.is_empty() {
    return; // skip send
}

Prevention

When it happens

Trigger: Sending a Datagrams message after its contents were fully drained/flushed, or constructing ClientToRelayMsg::Datagrams with datagrams.contents == vec![] and sending it on the connection.

Common situations: Queue-based senders that pop datagrams into a batch, then flush an empty batch when the queue was momentarily empty; race between batch assembly and connection flush.

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/a8ba57576ac9c7f5. Report an issue: GitHub.

Appendix: source

Thrown at iroh-relay/src/client/conn.rs:154

        }
    }
}

impl Sink<ClientToRelayMsg> for Conn {
    type Error = SendError;

    fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Pin::new(&mut self.conn).poll_ready(cx).map_err(Into::into)
    }

    fn start_send(mut self: Pin<&mut Self>, frame: ClientToRelayMsg) -> Result<(), Self::Error> {
        let size = frame.encoded_len();
        ensure!(
            size <= MAX_PACKET_SIZE,
            SendError::ExceedsMaxPacketSize { size }
        );
        if let ClientToRelayMsg::Datagrams { datagrams, .. } = &frame {
            ensure!(!datagrams.contents.is_empty(), SendError::EmptyPacket);
        }

        Pin::new(&mut self.conn)
            .start_send(frame.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.conn).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.conn).poll_close(cx).map_err(Into::into)
    }
}

View on GitHub (pinned to 2b4de030ce)