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
- On the client, abort the COPY stream as soon as an ErrorResponse arrives: send CopyFail/Terminate, do not continue sending CopyData
- Do not send PasswordMessage after auth; open a new connection instead of re-authenticating in place
- If proxying, keep COPY frames strictly within the COPY both-mode window reported by the server
- 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
- Clients: upon any ErrorResponse during COPY, stop sending CopyData and finish with CopyFail or Terminate
- Never attempt re-authentication on an established session; reconnect instead
- In proxies, track COPY mode per connection and drop COPY frames routed to non-COPY backends
- Fuzz your client's error paths: error-during-COPY is the classic trigger for this violation
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
- Cannot refresh compute configuration in state {:?}
- Error parsing {remote_endpoint}, expected host:port, got {er
- Invalid address format {remote_endpoint}, expected host:port
- Invalid host
- Invalid port in {remote_endpoint}
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/4940e436d3ead920.
Report an issue: GitHub.