Bring the Rust crate's idempotency, redirects, Retry-After and two form writes into line with the other SDKs - #201
Merged
Merged
Conversation
…k to the verb The Rust generator decided whether a route may be resent from the spec's explicit x-hey-idempotent.natural alone, and short of one from the verb, so the ten PATCH operations behavior-model.json marks idempotent — UpdateHabit, UpdateCalendarTodo, BulkUpdateClearances, UpdateClearance, UpdateCollection, UpdateContact, UpdateContactClearance, UpdateContactNote, UpdateMyClearance and UpdateSticky — were sent once where their own policy (max: 2 on 429 and 503) says to resend. The generator now reads it as the Kotlin and TypeScript generators do: the explicit override first, so UpdateMessage stays a PUT that is not resent, then the model's readonly or idempotent, and the verb only when the model says neither. Regenerating flips exactly those ten routes. A generator fixture puts a PATCH the model calls idempotent, one it does not and a PUT marked natural: false through the emitter; two client tests send UpdateSticky twice on a 503 then a 200, and UpdateMessage once on a 503.
…as a POST The client turned any request but a GET or HEAD into a bodiless GET on a 301 or 302, so a PUT, PATCH or DELETE answered that way was followed as a read of the target and could report a mutation done that never reached it. RFC 9110 permits that rewrite on those statuses for a POST alone; a 303 applies retrieval to any method; a 307 or 308 keeps everything. The hop is now built by that rule, and the transmit doc says so. Unit tests send each write through a redirect of each status on the canned transport, which now keeps the body it was handed, and check the method, body and content headers of the second hop.
…9 alone The retry loop read Retry-After on a 429 and a 503 but honoured it only on the 429, so a 503 that said how long the outage would last was resent after the backoff instead. RFC 9110 defines the header on a 503 for exactly that, and the TypeScript and Kotlin clients honour it on any status that earns a resend. The wait is now taken as asked on any retryable status, with a header that asks for nothing or cannot be read still leaving the backoff to decide, and the header is reported to the hooks on the same terms. A test answers a 503 with Retry-After: 2 then a 200 and checks the second send waited the two seconds.
…t alone, and an emptied extenzion membership as one blank member An event update filed its reminders under all_day_reminder_durations only when the update itself said all_day: true, and under timed_reminder_durations otherwise. HEY reads the list matching the event's all-day state after the write and unschedules the reminders when that list is absent, so an update that left all_day as None on an existing all-day event cleared its reminders. When all_day is None the durations now go out under both keys, as the Kotlin SDK sends them; the one HEY does not read is ignored. An extenzion update looped over members, so Some(vec![]) sent nothing and could not be told from None. HEY replaces the membership whenever the field is present, and a form carries no empty array, so an explicitly empty list now goes out as one blank extenzion[members][] value, as an empty attendee list already does on a calendar event. Tests check the three all_day cases of an update carrying reminders, and an empty membership against an absent one.
jeremy
approved these changes
Sep 15, 2026
There was a problem hiding this comment.
🟡 Changes recommended
The public Rust README still documents Retry-After as applying only to 429 responses.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
Aligns Rust SDK retries, redirects, idempotency, and form updates with other SDKs.
Changes:
- Corrects redirect and
Retry-Afterbehavior. - Generates behavior-model-aware idempotency.
- Fixes calendar reminders and empty Extenzion memberships, with tests.
[!TIP]
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or rungh pr ready --undo.
Click "Ready for review" or rungh pr readyto reengage.
File summaries
| File | Description |
|---|---|
AGENTS.md |
Documents Rust idempotency generation. |
rust/generator/src/model.rs |
Incorporates behavior-model idempotency. |
rust/generator/src/fixtures.rs |
Tests idempotency precedence. |
rust/hey-sdk/src/generated/routes.rs |
Regenerates idempotent route metadata. |
rust/hey-sdk/src/client.rs |
Fixes redirects and retry waits. |
rust/hey-sdk/src/services/calendar_events.rs |
Sends reminders under appropriate keys. |
rust/hey-sdk/src/services/extenzions.rs |
Encodes empty memberships explicitly. |
rust/hey-sdk/tests/retry_policy.rs |
Covers retry behavior. |
rust/hey-sdk/tests/services_calendar_events.rs |
Covers reminder forms. |
rust/hey-sdk/tests/services_extenzions.rs |
Covers empty memberships. |
Review details
- Files reviewed: 9/10 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
…the policy resends on The retry paragraph still said a Retry-After was honoured on a 429 alone, which the client no longer limits it to: a 503 naming how long the outage will last is waited out the same way. The sentence now says so, and names the 503 alongside the 429.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four defects in the Rust crate, found while reviewing the Kotlin SDK on #198, which already behaves as described here. A Go PR is being opened in parallel for the two defects Go shares (idempotency and the reminders).
Idempotency ignored the behavior model
rust/generatordecided whether a route may be resent from the spec's explicitx-hey-idempotent.naturalalone, and short of one from the verb.behavior-model.jsonmarks ten PATCH operations"idempotent": true— UpdateHabit, UpdateCalendarTodo, BulkUpdateClearances, UpdateClearance, UpdateCollection, UpdateContact, UpdateContactClearance, UpdateContactNote, UpdateMyClearance, UpdateSticky — so those went out once where their own policy (max: 2on 429 and 503) says to resend. The generator now reads it as the Kotlin and TypeScript generators do: the explicit override first (UpdateMessage is a PUT withnatural: falseand stays non-idempotent), then the model'sreadonly || idempotent, and the verb only when the model says neither. Regenerating flips exactly those ten routes inroutes.rsand nothing else. AGENTS.md now states the rule.A 301 or 302 rewrote every non-GET to GET
redirected()turned anything but a GET or HEAD into a bodiless GET on a 301 or 302, so a PUT, PATCH or DELETE answered that way was followed as a read of the target and could report a mutation done that never reached it. RFC 9110 permits the rewrite on those statuses for a POST alone; a 303 applies retrieval to any method; a 307 or 308 keeps everything. The hop is now built by that rule.Retry-After was honoured on a 429 alone
The retry loop read
Retry-Afteron a 429 and a 503 but only waited as asked on the 429; a 503 saying how long the outage would last was resent after the backoff. RFC 9110 defines the header on a 503 for exactly that, and the TypeScript and Kotlin clients honour it on any status that earns a resend. The wait is now taken as asked on any retryable status; a header that asks for nothing or cannot be read still leaves the backoff to decide.Reminders under the wrong list, and an emptied membership that sent nothing
update_event_fieldsfiled reminders underall_day_reminder_durations[]only when the update itself saidall_day: Some(true), else undertimed_reminder_durations[]. HEY reads the list matching the event's all-day state after the write and unschedules the reminders when that list is absent, so an update leavingall_dayasNoneon an all-day event cleared its reminders. Whenall_dayisNonethe durations now go out under both keys, as Kotlin sends them; the one HEY does not read is ignored.Extenzions::updatelooped overmembers, soSome(vec![])sent nothing and could not be told fromNone. HEY replaces the membership whenever the field is present, and a form carries no empty array, so an explicitly empty list now goes out as one blankextenzion[members][], as an empty attendee list already does on a calendar event.Tests
natural: false, checked on the emittedRoute.Retry-After: 2then 200 waits the two seconds and sends twice.all_day: Noneputs every duration under both keys;Some(true)under the all-day key only;Some(false)under the timed key only.Some(vec![])sends one blank member; the existingNonecase still sends nothing.GOWORK=off make rs-check rs-check-driftis green locally: fmt, clippy and tests with all features, clippy and tests with--no-default-features, the examples,cargo denyover both workspaces, the package dry-run and the generator drift check.Summary by cubic
Fixes four Rust SDK behaviors that diverge from the Kotlin and TypeScript SDKs: idempotency, redirects, Retry-After, and two form field writes.
Retry behavior
UpdateMessagekeeps its explicit non-idempotent override.Retry-Afteris now honored on any retryable status instead of only 429, so a 503 gets the wait it asks for; the README says so too.Form writes
all_dayunset now send reminders under both the all-day and timed keys, so editing an all-day event no longer clears its reminders.Written for commit 4ca7b30. Summary will update on new commits.