hasura/graphql-engine · error · ParseError

Unable to parse WebSocket message: {0}

Error message

Unable to parse WebSocket message: {0}

What it means

This variant of ParseError wraps a serde_json::Error raised when a received WebSocket text message cannot be deserialized into the graphql-ws ClientMessage type. It means the frame was valid WebSocket but not a valid graphql-ws protocol message (bad JSON or wrong shape). The server cannot interpret the message and reports this error.

Source

Thrown at v3/crates/graphql/graphql-ws/src/websocket/tasks.rs:166

            )
            .await
            .into_inner();
        if break_loop == BreakLoop::Break {
            break;
        }
    }
}

enum ParsedClientMessage {
    Close,
    Protocol(protocol::types::ClientMessage),
}

#[derive(thiserror::Error, Debug)]
enum ParseError {
    #[error("Unable to fetch message from WebSocket: {0}")]
    WebSocket(#[from] axum::Error),
    #[error("Unable to parse WebSocket message: {0}")]
    Json(#[from] serde_json::Error),
}

impl tracing_util::TraceableError for ParseError {
    fn visibility(&self) -> tracing_util::ErrorVisibility {
        tracing_util::ErrorVisibility::User
    }
}

fn parse_incoming_message(
    message: Result<ws::Message, axum::Error>,
) -> Result<ParsedClientMessage, ParseError> {
    let tracer = tracing_util::global_tracer();
    tracer.in_span(
        "parse_incoming_message",
        "Parse WebSocket message frame",
        tracing_util::SpanVisibility::User,
        || {

View on GitHub (pinned to 724551b9ae)

Solutions

  1. Validate outgoing frames against the graphql-ws message spec (type must be one of connection_initsubscribe... with required fields per message)
  2. Use a maintained client library (graphql-ws npm package) matching the server protocol version instead of hand-writing messages
  3. Capture the offending frame and compare its shape with the protocol's ClientMessage definitions

Example fix

// before (client sends invalid frame)
ws.send(JSON.stringify({ id: '1', type: 'start', payload: { query } }));

// after
ws.send(JSON.stringify({ id: '1', type: 'subscribe', payload: { query } }));
Defensive patterns

Strategy: validation

Validate before calling

const VALID_TYPES = new Set(['connection_init','ping','pong','subscribe','complete']);
function isValidClientMessage(msg: unknown): boolean {{
  const m = msg as {{ type?: string }};
  return typeof m?.type === 'string' && VALID_TYPES.has(m.type);
}}
if (!isValidClientMessage(parsed)) ws.send(JSON.stringify({{ type: 'ping' }}));

Type guard

function isClientMessage(v: unknown): v is {{ type: string; [k: string]: unknown }} {{
  return typeof v === 'object' && v !== null && typeof (v as any).type === 'string';
}}

Prevention

When it happens

Trigger: A client sends a JSON message that doesn't match any ClientMessage variant of the graphql-ws protocol — e.g. unknown message 'type', missing required fields, or a protocol-version mismatch producing different message shapes.

Common situations: Hand-rolled WebSocket clients sending arbitrary JSON; clients implementing a different GraphQL subscription protocol (subscriptions-transport-ws) against a graphql-ws server; version skew between client and server protocol implementations.

Understand the failure class

Related errors


AI-assisted analysis of hasura/graphql-engine@724551b9ae (2026-08-28). Data as JSON: /api/errors/a46d09ffdb8c0b02. Report an issue: GitHub.