Skip to content

reflect: preserve oneof members on decode errors#303

Merged
iainmcgin merged 2 commits into
anthropics:mainfrom
fallintoplace:agent/preserve-oneof-on-decode-error
Jul 16, 2026
Merged

reflect: preserve oneof members on decode errors#303
iainmcgin merged 2 commits into
anthropics:mainfrom
fallintoplace:agent/preserve-oneof-on-decode-error

Conversation

@fallintoplace

Copy link
Copy Markdown
Contributor

What this does

DynamicMessage now decodes a singular oneof replacement into a temporary value before clearing sibling members. If decoding fails, the previously valid oneof member remains set. Existing singular message values are cloned before wire merging as well, so malformed repeated message occurrences do not mutate the stored value.

Coverage

  • truncated replacement varints
  • truncated nested-message payloads
  • mismatched group terminators
  • invalid UTF-8 in a replacement string

Validation

  • cargo fmt --all -- --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test --workspace
  • cargo check --workspace --all-features

@github-actions

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

…f clear

The branch predated the fix for dropping an existing singular message on a
decode error, which reworked the same function this change rewrote. Resolve in
favour of main's in-place merge: it keeps a partially merged value on error,
which is what the owned decoder leaves behind, whereas decoding into a clone
and discarding it on error restores the pre-merge value and reintroduces the
divergence that fix removed. The clone also cost a deep copy on every repeated
occurrence of a message field, on the success path.

The oneof bug does not need that path at all. Reaching the merge branch means
this member already holds the oneof, so no sibling is set and the clear has
nothing to do. Only the fresh-decode path could destroy a live member, so the
clear moves after the decode there and the rest stands as main has it.

Both sets of tests are kept and both hold: the four oneof cases fail against
main without the reorder, and the five message-merge cases still pass, so the
resolution preserves the earlier fix rather than reverting it.

@iainmcgin iainmcgin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[claude code] The bug here is real and your four tests are good — I measured each one against the merge-base and all four fail without the fix, which is exactly what you want from a regression test.

The complication is timing: #299 landed earlier today and reworked the same function this PR rewrites, so I've resolved that maintainer-side in 718b044 rather than hand you a conflict.

Why the resolution went main's way on the merge path. #299 wrapped the in-place merge in a closure so the taken value is always reinserted, deliberately keeping the partially merged value on error — that matches what the owned decoder leaves behind, and its changelog fragment says so explicitly. This PR's decode_merged_message clones the existing message, merges into the clone, and discards it on error, which restores the pre-merge value. That's the opposite semantic, and it would have re-opened the owned/reflective divergence #299 had just closed — silently, one commit later. A naive "take this PR's side" rebase would have reverted #299 with nothing failing to catch it. The clone also cost a deep copy on every repeated occurrence of a message field, on the success path, where #299 mutates in place with none.

The good news: the oneof bug never needed that path. Reaching the merge-into-existing branch means the field is already set, so for a oneof that member is already the active one and no sibling can be lost — the clear there is a no-op. Only the fresh-decode path could destroy a live member. So the fix reduces to moving the clear after decode_element on that path, leaving #299's function untouched. Same bug fixed, no conflict, no clone, no divergence.

Both test suites are kept and both hold, which is the check that matters here:

vs main (no reorder)
your 4 oneof tests fail — they guard the reorder
#299's 5 merge tests pass — the resolution preserves #299

Also added a Fixed changelog fragment: this is user-visible and belongs in the 0.9.0 notes. (check-changelog only verifies CHANGELOG.md is in sync with existing fragments, so its passing wasn't evidence one wasn't needed.)

Approving. Thanks for finding this — clearing oneof siblings before the replacement decoded was a genuine data-loss path, and the four error shapes you picked cover it well.

@iainmcgin
iainmcgin added this pull request to the merge queue Jul 16, 2026
Merged via the queue into anthropics:main with commit 27521fe Jul 16, 2026
9 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 16, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants