diff options
| author | akbash <akbash@herdr.dev> | 2026-07-15 23:43:21 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-15 23:43:21 +0300 |
| commit | 600753b47b75898c311ecfb625bd1887031b3475 (patch) | |
| tree | f4e63d16c62d3efdf7d5a554a54398bc4f66f4d4 /src/cli | |
| parent | 7767f3ed6453ae573d7414d31cfe982f99a73782 (diff) | |
fix: guard cli requests against protocol mismatches (#1453)
refs #1435
Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
Co-authored-by: Can Celik <ogulcancelik@gmail.com>
Diffstat (limited to 'src/cli')
| -rw-r--r-- | src/cli/plugin.rs | 3 | ||||
| -rw-r--r-- | src/cli/protocol_guard.rs | 110 | ||||
| -rw-r--r-- | src/cli/server.rs | 4 |
3 files changed, 116 insertions, 1 deletions
diff --git a/src/cli/plugin.rs b/src/cli/plugin.rs index 5cc528f4..68cf0282 100644 --- a/src/cli/plugin.rs +++ b/src/cli/plugin.rs @@ -932,6 +932,9 @@ fn register_installed_plugin( ), ))); } + Err(unlink_err) if super::protocol_mismatch_was_reported(&unlink_err) => { + return Err(InstallFailure::KeepCheckout(unlink_err)); + } Err(unlink_err) => { return Err(InstallFailure::KeepCheckout(std::io::Error::other( format!( diff --git a/src/cli/protocol_guard.rs b/src/cli/protocol_guard.rs new file mode 100644 index 00000000..21302bbf --- /dev/null +++ b/src/cli/protocol_guard.rs @@ -0,0 +1,110 @@ +use std::fmt; + +use crate::api::schema::{ErrorBody, ErrorResponse}; + +#[derive(Debug)] +pub(super) struct ProtocolMismatchReported; + +impl fmt::Display for ProtocolMismatchReported { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("protocol mismatch was already reported") + } +} + +impl std::error::Error for ProtocolMismatchReported {} + +pub(super) fn mismatch_response( + request_id: &str, + server_protocol: u32, + restart_guidance: &str, +) -> Option<ErrorResponse> { + let client_protocol = crate::protocol::PROTOCOL_VERSION; + if client_protocol == server_protocol { + return None; + } + + let message = if client_protocol > server_protocol { + format!( + "client protocol {client_protocol} is newer than server protocol {server_protocol}; restart the Herdr server before using this command. {restart_guidance}" + ) + } else { + format!( + "client protocol {client_protocol} is older than server protocol {server_protocol}; upgrade the Herdr client before using this command" + ) + }; + + Some(ErrorResponse { + id: request_id.to_string(), + error: ErrorBody { + code: "protocol_mismatch".into(), + message, + }, + }) +} + +pub(super) fn reported_error() -> std::io::Error { + std::io::Error::other(ProtocolMismatchReported) +} + +pub(super) fn was_reported(err: &std::io::Error) -> bool { + err.get_ref() + .and_then(|source| source.downcast_ref::<ProtocolMismatchReported>()) + .is_some() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn matching_protocol_has_no_error() { + assert!(mismatch_response("req", crate::protocol::PROTOCOL_VERSION, "restart").is_none()); + } + + #[test] + fn older_server_error_preserves_request_id_and_guidance() { + let response = mismatch_response( + "cli:wait:agent-status", + crate::protocol::PROTOCOL_VERSION - 1, + "Run the session stop command, then restart.", + ) + .unwrap(); + + assert_eq!(response.id, "cli:wait:agent-status"); + assert_eq!(response.error.code, "protocol_mismatch"); + assert!(response.error.message.contains(&format!( + "client protocol {}", + crate::protocol::PROTOCOL_VERSION + ))); + assert!(response.error.message.contains(&format!( + "server protocol {}", + crate::protocol::PROTOCOL_VERSION - 1 + ))); + assert!(response.error.message.contains("restart")); + } + + #[test] + fn newer_server_error_tells_user_to_upgrade_client() { + let response = mismatch_response( + "cli:pane:list", + crate::protocol::PROTOCOL_VERSION + 1, + "unused restart guidance", + ) + .unwrap(); + + assert!(response + .error + .message + .contains("older than server protocol")); + assert!(response.error.message.contains("upgrade the Herdr client")); + assert!(!response.error.message.contains("unused restart guidance")); + } + + #[test] + fn reported_error_is_recognizable_without_string_matching() { + assert!(was_reported(&reported_error())); + assert!(!was_reported(&std::io::Error::other( + "protocol mismatch was already reported" + ))); + } +} diff --git a/src/cli/server.rs b/src/cli/server.rs index 29775999..ff87cab1 100644 --- a/src/cli/server.rs +++ b/src/cli/server.rs @@ -201,7 +201,9 @@ fn server_live_handoff(args: &[String]) -> std::io::Result<i32> { return Ok(2); }; - let response = super::send_request(&Request { + // Live handoff is itself a protocol-mismatch recovery path, so it must + // reach the running server without the normal CLI compatibility guard. + let response = super::send_request_unchecked(&Request { id: "cli:server:live-handoff".into(), method: Method::ServerLiveHandoff(params), })?; |
