BigPizzaV3/CodexPlusPlus · error · anyhow::Error

first message must be registration json

Error message

first message must be registration json

What it means

Mobile relay WebSocket clients must send their registration as the first message, either as a Text frame or a Binary frame containing utf-8 JSON. parse_registration rejects any other frame type (Ping, Pong, Close, etc.) for the first message, since it cannot contain a registration payload.

Solutions

  1. Send the registration JSON as the very first WebSocket message (Text or Binary utf-8)
  2. Disable/inhibit automatic ping frames before registration in the client
  3. On reconnect, always resend the registration message on the new socket

Example fix

// before
ws.ping();
ws.send(JSON.stringify(reg));
// after
ws.send(JSON.stringify({ message_type: "register", room, token, role }));
Defensive patterns

Strategy: validation

Validate before calling

// client: ensure first frame is registration
ws.onopen = () => ws.send(JSON.stringify({ message_type: 'register', room, token, role }));

Try / catch

ws.onerror = (e) => { if (String(e).includes('first message must be registration json')) reconnectAndRegisterFirst(); };

Prevention

When it happens

Trigger: A client connected to the relay's WebSocket endpoint (handle_websocket_connection -> parse_registration, main.rs ~360) whose first frame is neither Message::Text nor Message::Binary — e.g. sending Ping/Pong/Close first, or an empty stream frame.

Common situations: Custom client implementations that ping on connect before registering; proxy or library code injecting keep-alive frames before app-level messages; reconnect logic reusing a socket where registration was skipped.

Related errors


AI-assisted analysis of BigPizzaV3/CodexPlusPlus@b1ed92e5e4 (2026-09-19). Data as JSON: /api/errors/d8542428e1bdc615. Report an issue: GitHub.

Appendix: source

Thrown at apps/codex-plus-mobile-relay/src/main.rs:360

            "Connection: close\r\n\r\n"
        ),
        status,
        content_type,
        body.len()
    );
    stream.write_all(response.as_bytes()).await?;
    stream.write_all(&body).await?;
    stream.shutdown().await?;
    Ok(())
}

fn parse_registration(message: Message) -> anyhow::Result<Registration> {
    let text = match message {
        Message::Text(text) => text.to_string(),
        Message::Binary(bytes) => {
            String::from_utf8(bytes.to_vec()).context("binary registration must be utf-8 json")?
        }
        _ => bail!("first message must be registration json"),
    };
    let registration: RegisterMessage =
        serde_json::from_str(&text).context("registration is not valid json")?;
    if registration.message_type != "register" {
        bail!("registration type must be register");
    }
    if registration.room.trim().is_empty() {
        bail!("room is required");
    }
    if registration.token.trim().is_empty() {
        bail!("token is required");
    }
    let role = Role::from_str(&registration.role).context("role must be host or client")?;
    Ok(Registration {
        role,
        room: registration.room,
        token: registration.token,
    })

View on GitHub (pinned to b1ed92e5e4)