neondatabase/neon · warning · QueryError
Unexpected CancelRequest message during handshake
Error message
Unexpected CancelRequest message during handshake
What it means
process_startup_message handles SSLRequest, GssEncRequest, and StartupMessage during the handshake; a FeStartupPacket::CancelRequest arriving there is rejected with QueryError::Other. Cancel requests are expected on their own dedicated connection carrying (pid, secret key) after a normal startup -- receiving one before the handshake completes is a protocol-order violation this backend does not support.
Source
Thrown at libs/postgres_backend/src/lib.rs:723
AuthType::Trust => {
self.write_message_noflush(&BeMessage::AuthenticationOk)?
.write_message_noflush(&BeMessage::CLIENT_ENCODING)?
.write_message_noflush(&BeMessage::INTEGER_DATETIMES)?
// The async python driver requires a valid server_version
.write_message_noflush(&BeMessage::server_version("14.1"))?
.write_message(&BeMessage::ReadyForQuery)
.await?;
self.state = ProtoState::Established;
}
AuthType::NeonJWT => {
self.write_message(&BeMessage::AuthenticationCleartextPassword)
.await?;
self.state = ProtoState::Authentication;
}
}
}
FeStartupPacket::CancelRequest { .. } => {
return Err(QueryError::Other(anyhow::anyhow!(
"Unexpected CancelRequest message during handshake"
)));
}
}
Ok(())
}
// Proto looks like this:
// FeMessage::Query("pagestream_v2{FeMessage::CopyData(PagesetreamFeMessage::GetPage(..))}")
async fn process_message(
&mut self,
handler: &mut impl Handler<IO>,
msg: FeMessage,
unnamed_query_string: &mut Bytes,
) -> Result<ProcessMsgResult, QueryError> {
// Allow only startup and password messages during auth. Otherwise client would be able to bypass auth
// TODO: change that to proper top-level match of protocol state with separate message handling for each stateView on GitHub (pinned to 8f60b04da4)
Solutions
- Send cancel requests only over a fully established connection (open a new one and wait for startup, then CancelRequest)
- Rate-limit or debounce aggressive cancel logic on the client (timers firing before connect completes)
- If you proxy postgres traffic, do not multiplex CancelRequest onto connections still in handshake
- Treat the error as connection-fatal: close and reconnect rather than retrying on the same socket
Defensive patterns
Strategy: try-catch
Try / catch
match backend.handshake(&mut handler).await {
Err(QueryError::Other(e))
if e.to_string().contains("Unexpected CancelRequest") =>
{
// Benign client behavior quirk (cancel raced with startup).
// Close quietly with a debug log; do not alert, do not retry on this connection.
tracing::debug!(peer = ?peer_addr, "cancel request during handshake; dropping connection");
}
other => other?,
} Prevention
- Client-side: only send CancelRequest on a connection that has completed startup (or a fresh dedicated connection)
- Debounce cancel timers so they cannot fire before the connection reaches Established under slow networks
- Proxies: route cancel packets only to already-established backends, never to handshaking ones
- Classify this error as noise in alerting rules to avoid pages for harmless client races
When it happens
Trigger: A client opening a connection and sending CancelRequest as the first (or a pre-handshake) packet -- e.g. a cancel racing with connection setup, a proxy multiplexing cancel frames onto an incomplete connection, or a test harness firing pg_cancel immediately after connect.
Common situations: Statement-timeout cancellation racing connection establishment under load; connection poolers forwarding cancel requests on not-yet-established links; custom drivers that send the cancel packet too eagerly; scanners probing protocol behavior.
Understand the failure class
- SSL/TLS and certificate errors — how TLS handshakes and certificate validation fail.
Related errors
- Unexpected message {:?} while waiting for handshake
- direct SSL negotiation but no TLS support
- client did not connect with TLS
- unexpected message type: {msg:?}
- not implemented
AI-assisted analysis of neondatabase/neon@8f60b04da4 (2026-08-16).
Data as JSON: /api/errors/b530d0227479f85a.
Report an issue: GitHub.