BloopAI/vibe-kanban · warning

invalid close payload

Error message

invalid close payload

What it means

When converting a relay WebSocket frame into an axum Close message, an empty payload means 'close without code', but any non-empty payload must carry at least a 2-byte big-endian status code (RFC 6455). Payloads of 1 byte (or a payload that later fails UTF-8 decoding) are rejected with this error.

Source

Thrown at crates/relay-protocol/src/lib.rs:85

        };
        RelayWsFrame { msg_type, payload }
    }

    fn try_from_frame(frame: RelayWsFrame) -> anyhow::Result<Self> {
        match frame.msg_type {
            RelayWsMessageType::Text => {
                let text = String::from_utf8(frame.payload).context("invalid UTF-8 text frame")?;
                Ok(Self::Text(text.into()))
            }
            RelayWsMessageType::Binary => Ok(Self::Binary(frame.payload.into())),
            RelayWsMessageType::Ping => Ok(Self::Ping(frame.payload.into())),
            RelayWsMessageType::Pong => Ok(Self::Pong(frame.payload.into())),
            RelayWsMessageType::Close => {
                if frame.payload.is_empty() {
                    return Ok(Self::Close(None));
                }
                if frame.payload.len() < 2 {
                    anyhow::bail!("invalid close payload");
                }
                let code = u16::from_be_bytes([frame.payload[0], frame.payload[1]]);
                let reason = String::from_utf8(frame.payload[2..].to_vec())
                    .context("invalid UTF-8 close frame reason")?;
                Ok(Self::Close(Some(AxumCloseFrame {
                    code,
                    reason: reason.into(),
                })))
            }
        }
    }
}

impl RelayTransportMessage for tungstenite::Message {
    fn into_frame(self) -> RelayWsFrame {
        let (msg_type, payload) = match self {
            Self::Text(text) => (RelayWsMessageType::Text, text.to_string().into_bytes()),
            Self::Binary(data) => (RelayWsMessageType::Binary, data.to_vec()),

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Fix the peer to send either an empty close payload or >=2 bytes (code + optional reason)
  2. Log and inspect raw payload length of offending frames
  3. Use Close(None) (empty payload) when no status code is intended

Example fix

// before
frame.payload = vec![0x0a]; // 1 byte: invalid
// after
frame.payload = vec![0x0a, 0x00]; // 2-byte big-endian close code
Defensive patterns

Strategy: type-guard

Validate before calling

if !frame.payload.is_empty() && frame.payload.len() < 2 { log_warn("malformed close payload"); frame.payload.clear(); }

Type guard

fn is_valid_close_payload(p: &[u8]) -> bool { p.is_empty() || p.len() >= 2 }

Try / catch

match RelayCloseMessage::try_from_frame(&frame) { Err(e) if e.to_string() == "invalid close payload" => send_close(1002), ok => ok? }

Prevention

When it happens

Trigger: Receiving a Close frame whose payload is exactly 1 byte; try_from_frame called on a malformed relay Close frame.

Common situations: Peer implementation violating RFC 6455 close-frame encoding; a proxy mangling close frames; truncated frames from buggy serialization.

Related errors


AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29). Data as JSON: /api/errors/f8919480b564db25. Report an issue: GitHub.