neondatabase/neon · error · QueryError

unexpected message type: {msg:?}

Error message

unexpected message type: {msg:?}

What it means

process_message in the Established state accepts Query/Parse/Describe/Bind/Execute/Sync/Flush/Terminate; the message kinds CopyData, CopyDone, CopyFail, and PasswordMessage are explicitly matched to this error. The exhaustive match is deliberate so that new FeMessage variants force a review of every handler -- a COPY-phase message or a password message outside its phase is a protocol-state violation.

Source

Thrown at libs/postgres_backend/src/lib.rs:842

                // ReadyForQuery message and backend just switches back to
                // processing mode after sending CopyDone or ErrorResponse.
            }

            FeMessage::Sync => {
                self.write_message_noflush(&BeMessage::ReadyForQuery)?;
            }

            FeMessage::Terminate => {
                return Ok(ProcessMsgResult::Break);
            }

            // We prefer explicit pattern matching to wildcards, because
            // this helps us spot the places where new variants are missing
            FeMessage::CopyData(_)
            | FeMessage::CopyDone
            | FeMessage::CopyFail
            | FeMessage::PasswordMessage(_) => {
                return Err(QueryError::Other(anyhow::anyhow!(
                    "unexpected message type: {msg:?}",
                )));
            }
        }

        Ok(ProcessMsgResult::Continue)
    }

    /// - Log as info/error result of handling COPY stream and send back
    ///   ErrorResponse if that makes sense.
    /// - Shutdown the stream if we got Terminate.
    /// - Then close the connection because we don't handle exiting from COPY
    ///   stream normally.
    pub async fn handle_copy_stream_end(&mut self, end: CopyStreamHandlerEnd) {
        use CopyStreamHandlerEnd::*;

        let expected_end = match &end {
            ServerInitiated(_) | CopyDone | CopyFail | Terminate | EOF | Cancelled => true,

View on GitHub (pinned to 8f60b04da4)

Solutions

  1. On the client, abort the COPY stream as soon as an ErrorResponse arrives: send CopyFail/Terminate, do not continue sending CopyData
  2. Do not send PasswordMessage after auth; open a new connection instead of re-authenticating in place
  3. If proxying, keep COPY frames strictly within the COPY both-mode window reported by the server
  4. Log msg {:?} to identify which frame was mis-sequenced and by which code path
Defensive patterns

Strategy: try-catch

Try / catch

match backend.run(&mut handler).await {
    Err(QueryError::Other(e))
        if e.to_string().contains("unexpected message type") =>
    {
        // Client violated protocol phase (COPY frame or password outside its window).
        // Send ErrorResponse (already done by the backend), log which FeMessage it was,
        // and close -- the stream state cannot be trusted afterwards.
        tracing::warn!(peer = ?peer, error = %e, "protocol phase violation; closing");
    }
    other => other?,
}

Prevention

When it happens

Trigger: A client sends CopyData/CopyDone/CopyFail outside an active COPY stream (e.g. after an error aborted COPY but the client kept streaming), or sends a PasswordMessage after authentication already completed (re-auth mid-session is not supported).

Common situations: Clients that ignore a mid-COPY ErrorResponse and keep piping COPY frames; drivers attempting SASL/password re-authentication on the same session after a timeout; protocol fuzzers; state machine bugs in custom proxies that reorder frames.

Related errors


AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16). Data as JSON: /api/errors/4940e436d3ead920. Report an issue: GitHub.