OXYGENHUB

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