The API refuses a field an edit does not take (refs #330) 5a9a6e9a15bc
PATCH on an issue, proposal, project, milestone, release or drop answered
200 to an unknown field and changed nothing ({"ready": true} on a draft
proposal looked like success). The edit types now deny unknown fields:
422 naming the field and the ones taken. oh proposal ready and PATCH
{"draft": false} already make a draft ready (#313).
- Author
- Andy McCall @andymccall <[email protected]>
- via
- agent
agent:claude@andymccall <[email protected]> · keyless, under your grant for changes,refs,seal · key 7ac8a5e1afbe
- When
- · 2026-10-26 11:58 UTC
- Signature
- ◈ sealed by agent:claude@andymccall
- Hash
5a9a6e9a15bcda9919eae94e7b6a2e5f31530f40ed04688745e3cf21ddc02e94- Depends on
- 09ffa7b600a9 166f6e9bf4f1 27babfa16580 5ea4872f23e8 739a763a558c 76f262e1a666 a7e6e8e6094c b5668108487f ced6d3afc5cf d23464c0ac60 de505df5cf8d ef20fdc14e8b
- Depended on by
- 34388e612963
- In bookmarks
- api-unknown-fields diffusion-pin-bookmark main proposal-list-fast
- In tags
- v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.7 v0.9.8 v0.9.9
- Edits
- 10 atoms, 0 tree operations, 3 files
Browse the tree at this change
modified CHANGELOG.md +2 −0
| … | 3 unchanged lines |
||
| 4 | 4 | ||
| 5 | 5 | ## Unreleased |
|
| 6 | 6 | ||
| 7 | + | - **The API refuses a field an edit does not take (#330).** `PATCH` on an issue, proposal, project, milestone, release or drop answered 200 to a field it does not know and changed nothing (`{"ready": true}` on a draft proposal, say), which looked like success. It now answers 422 naming the field and the ones it takes. |
|
| 7 | 8 | ||
| 9 | + | ||
| 8 | 10 | - **An agent follows its owner's newest identity (oxygen#82).** A keyless certificate's answer now carries `author_chain`: the grant owner's newest linked identity and its predecessors back to the grant's key, so an agent home with `author.for` authors as the owner's current identity, not one several rotations old. |
|
| 9 | 11 | - **The profile page's actions are one tidy group (#328).** Edit profile, Projects and Drops sat on two rows, touching each other and the panel below; they are now one evenly spaced block, Edit profile across the top and the two pages side by side. |
|
| 10 | 12 | ||
| … | 347 unchanged lines |
modified crates/hub-api/src/lib.rs +7 −0
| … | 307 unchanged lines |
||
| 308 | 308 | /// `PATCH /repos/{o}/{n}/milestones/{title}`: only the fields given change |
|
| 309 | 309 | /// (triage). |
|
| 310 | 310 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] |
|
| 311 | + | #[serde(deny_unknown_fields)] |
|
| 311 | 312 | pub struct MilestoneEdit { |
|
| 312 | 313 | #[serde(default)] |
|
| 313 | 314 | pub title: Option<String>, |
|
| … | 476 unchanged lines |
||
| 790 | 791 | /// `PATCH /repos/{o}/{n}/issues/{number}`: only the fields given change. |
|
| 791 | 792 | /// Title and body: the author or triage. The rest: triage. |
|
| 792 | 793 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] |
|
| 794 | + | #[serde(deny_unknown_fields)] |
|
| 793 | 795 | pub struct IssueEdit { |
|
| 794 | 796 | #[serde(default)] |
|
| 795 | 797 | pub title: Option<String>, |
|
| … | 81 unchanged lines |
||
| 877 | 879 | ||
| 878 | 880 | /// `PATCH /projects/{owner}/{n}`: only the fields given change. |
|
| 879 | 881 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] |
|
| 882 | + | #[serde(deny_unknown_fields)] |
|
| 880 | 883 | pub struct ProjectEdit { |
|
| 881 | 884 | /// Transfer the board to this owner (#125): one the caller may make |
|
| 882 | 885 | /// boards under. It takes the next number there. |
|
| … | 209 unchanged lines |
||
| 1092 | 1095 | /// `PATCH /repos/{o}/{n}/proposals/{number}`: what is given changes, |
|
| 1093 | 1096 | /// the rest stays (oxygenhub#76). |
|
| 1094 | 1097 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] |
|
| 1098 | + | #[serde(deny_unknown_fields)] |
|
| 1095 | 1099 | pub struct ProposalEdit { |
|
| 1096 | 1100 | #[serde(default, skip_serializing_if = "Option::is_none")] |
|
| 1097 | 1101 | pub title: Option<String>, |
|
| … | 18 unchanged lines |
||
| 1116 | 1120 | /// `PATCH /repos/{owner}/{name}/releases/{tag}`: what to change; fields |
|
| 1117 | 1121 | /// left out stay as they are. |
|
| 1118 | 1122 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] |
|
| 1123 | + | #[serde(deny_unknown_fields)] |
|
| 1119 | 1124 | pub struct ReleaseEdit { |
|
| 1120 | 1125 | #[serde(default, skip_serializing_if = "Option::is_none")] |
|
| 1121 | 1126 | pub title: Option<String>, |
|
| … | 59 unchanged lines |
||
| 1181 | 1186 | ||
| 1182 | 1187 | /// `PATCH /drops/{id}`: what to change; what is left out stays. |
|
| 1183 | 1188 | #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] |
|
| 1189 | + | #[serde(deny_unknown_fields)] |
|
| 1184 | 1190 | pub struct DropEdit { |
|
| 1185 | 1191 | #[serde(default, skip_serializing_if = "Option::is_none")] |
|
| 1186 | 1192 | pub title: Option<String>, |
|
| … | 10 unchanged lines |
||
| 1197 | 1203 | } |
|
| 1198 | 1204 | ||
| 1199 | 1205 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] |
|
| 1206 | + | #[serde(deny_unknown_fields)] |
|
| 1200 | 1207 | pub struct DropFileEdit { |
|
| 1201 | 1208 | pub name: String, |
|
| 1202 | 1209 | #[serde(default)] |
|
| … | 443 unchanged lines |
modified crates/hub/tests/proposals.rs +5 −0
| … | 694 unchanged lines |
||
| 695 | 695 | assert_eq!(patch(r#"{"state":"open"}"#, Some("site")).await.0, StatusCode::OK); |
|
| 696 | 696 | assert_eq!(state().await, "open"); |
|
| 697 | 697 | assert_eq!(patch(r#"{"state":"open","draft":true}"#, Some("site")).await.0, StatusCode::BAD_REQUEST, "they disagree"); |
|
| 698 | + | // A field PATCH does not take is refused, naming it, rather than a 200 |
|
| 699 | + | // that changes nothing (#330: `{"ready": true}` was taken that way). |
|
| 700 | + | let (st, body) = patch(r#"{"ready":true}"#, Some("site")).await; |
|
| 701 | + | assert!(st.is_client_error() && body.contains("unknown field `ready`"), "{st} {body}"); |
|
| 702 | + | assert_eq!(state().await, "open"); |
|
| 698 | 703 | patch(r#"{"state":"closed"}"#, Some("site")).await; |
|
| 699 | 704 | assert_eq!(patch(r#"{"draft":true}"#, Some("site")).await.0, StatusCode::CONFLICT, "a closed proposal is reopened, not drafted"); |
|
| 700 | 705 | } |
|
| … | 423 unchanged lines |