From 446fb92ba3316e2003c7e966da17aec984074842 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Sat, 29 Aug 2026 06:13:18 -0400 Subject: [PATCH] Refuse a request field the server does not know A misspelled field was accepted and dropped. Sending `permission_mode` instead of `permissionMode` produced a 200 and a session running in the default permission mode -- so the caller's setting was gone, and nothing anywhere said so. That is the expensive shape: indistinguishable from success at the place you are looking. It cost an hour here, chasing a "startup race" that was a key serde had silently discarded; with the name spelled the way the API asks, a bypassPermissions session runs a `sleep` loop with no prompt at all. So every request body refuses unknown fields, not just the one that bit. Axum's message names the offending field and lists what was expected, which is the whole of what the caller needs. Query strings are deliberately left permissive: a stale link carrying an extra parameter is not a mistake worth failing a request over. 53 tests, clippy and rustfmt clean; verified against the running server that the misspelling is now a 422 naming the field and the correct spelling still spawns. Co-Authored-By: Claude Opus 5 --- server/src/routes.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/server/src/routes.rs b/server/src/routes.rs index 48df13b..50a3d9a 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -186,8 +186,19 @@ fn info_for(setup: crate::config::SetupConfig) -> SetupInfo { /// Note what is absent: nothing here names a program. Providers are found /// by asking the machine (`crate::setups`), never sent, so the enrolled /// token cannot introduce something to run. +/// Every request body below refuses fields it does not know. +/// +/// Silently ignoring one is the worst available answer: a caller that +/// misspells `permissionMode` gets a session in the default permission mode +/// and a 200 saying it worked, which is indistinguishable from success at +/// the place they are looking. It cost an hour of chasing a "race" that was +/// a snake_case key serde had dropped on the floor. A 400 naming the field +/// is the whole fix, and it belongs on all of them rather than on the one +/// that bit -- query strings are deliberately excluded, since a stale link +/// carrying an extra parameter is not a mistake worth failing. #[derive(Deserialize)] #[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] struct SshRequest { address: String, #[serde(default)] @@ -225,6 +236,7 @@ impl SshRequest { #[derive(Deserialize)] #[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] struct AddSetupRequest { name: String, /// Absent means this machine. @@ -239,6 +251,7 @@ struct AddSetupRequest { /// form that caused it, rather than at the first spawn. #[derive(Deserialize)] #[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] struct ProbeRequest { #[serde(default)] ssh: Option, @@ -320,6 +333,7 @@ async fn read_setup( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] struct UpdateSetupRequest { #[serde(default)] name: Option, @@ -365,6 +379,7 @@ async fn delete_setup( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] struct SpawnRequest { /// Which machine, and which of the things it offers. setup: String, @@ -576,6 +591,7 @@ async fn delete_session( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] struct MessageRequest { text: String, /// Ids from `POST /attachments`, uploaded before the message that @@ -599,6 +615,7 @@ async fn message( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] struct AnswerRequest { question_id: String, answer: String, @@ -658,6 +675,7 @@ async fn usage( } #[derive(Deserialize)] +#[serde(deny_unknown_fields)] struct ModelRequest { model: String, } @@ -675,6 +693,7 @@ async fn set_model( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] struct PermissionModeRequest { mode: String, }