aboutsummaryrefslogtreecommitdiff
path: root/src/cli/protocol_guard.rs
diff options
context:
space:
mode:
authorakbash <akbash@herdr.dev>2026-07-15 23:43:21 +0300
committerGitHub <noreply@github.com>2026-07-15 23:43:21 +0300
commit600753b47b75898c311ecfb625bd1887031b3475 (patch)
treef4e63d16c62d3efdf7d5a554a54398bc4f66f4d4 /src/cli/protocol_guard.rs
parent7767f3ed6453ae573d7414d31cfe982f99a73782 (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/protocol_guard.rs')
-rw-r--r--src/cli/protocol_guard.rs110
1 files changed, 110 insertions, 0 deletions
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"
+ )));
+ }
+}