s3: honor If-Match on DELETE - #1245
Conversation
The deleteObject handler loaded the object manifest -- so it had the current ETag -- and then dispatched kv.Del without looking at If-Match. PUT already enforces the header via validateS3PutPreconditions; DELETE accepted it and deleted anyway. Accepting a precondition and ignoring it is worse than not supporting it. A caller that reads an object, decides it is reclaimable, and sends a conditional delete believes the condition protects it from a concurrent rewrite. It did not: the object was removed whatever its current ETag, so the rewrite was silently lost. snapshotoffload retention depends on exactly this -- DeleteObjectIfUnmodified sends If-Match and maps the 412 to ErrObjectModified -- so against the bundled endpoint its precondition was inert and it could reclaim a payload that had been republished after it was observed. The check runs against the manifest the surrounding transaction already loaded at its read timestamp, so the comparison is against the same version the delete will remove rather than a separately-read one that could have moved in between. If-None-Match is deliberately not honoured on DELETE: "only if absent" is meaningless for an operation whose purpose is to remove something that exists, and honouring it would refuse every delete of a present object. Claude-Session: https://claude.ai/code/session_013rNHooj7NF3giihWVba8QE
|
Warning Review limit reachedNext included review available in 7 minutes. View limit detailsLimit details: You’ve used the included review currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (2)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Fixes the third P2 from #1222 ("Make the bundled S3 endpoint honor delete preconditions"), split out because it changes
adapter/s3.gorather thaninternal/snapshotoffload.Behavior change
DELETE /{bucket}/{key}now honorsIf-Match. A request whoseIf-Matchdoes not equal the object's current ETag gets 412 PreconditionFailed and the object is left in place. Requests without the header are unchanged.If-None-Matchis deliberately not honored on DELETE: "only if absent" is meaningless for an operation whose purpose is to remove something that exists, and honoring it would refuse every delete of a present object.Why this matters beyond spec compliance
validateS3PutPreconditionsalready enforcesIf-Matchon the write path.deleteObjectloaded the object manifest — so it had the current ETag in hand — and then dispatchedkv.Delwithout consulting the header.Accepting a precondition and ignoring it is worse than not supporting it. A caller that reads an object, decides it is reclaimable, and sends a conditional delete believes the condition protects it against a concurrent rewrite. It did not: the object was removed whatever its current ETag, so the rewrite was silently lost.
internal/snapshotoffloadretention depends on this directly —DeleteObjectIfUnmodifiedsendsIf-Matchand maps the 412 toErrObjectModified— so against the bundled endpoint that precondition was inert, and retention could reclaim a payload that had been republished after it was observed.The check runs against the manifest the surrounding transaction already loaded at its read timestamp, so it compares against the same version the delete will remove, not a separately-read one that could have moved in between.
Risk
Low, and confined to requests that send a header the endpoint previously ignored:
A client that was sending
If-Matchand relying on the delete happening regardless would now see a 412 — but that client was already relying on a precondition not being enforced, which is the bug.Test evidence
go test ./adapter/ -race -count=1 -timeout 40m—ok 681.541s(the 600s default is not enough for this package under-race)golangci-lint --config=.golangci.yaml run ./adapter/...— 0 issuesTestS3ConditionalDeleteRefusesAStaleETagFAIL ("a delete conditioned on a superseded ETag must fail with 412"); the file restores byte-exact.The end-to-end tests drive the real HTTP handler — PUT, concurrent rewrite, conditional DELETE, then HEAD to confirm the object survived — rather than calling the predicate directly, so they cover the wiring and not just the decision.
TestValidateS3DeletePreconditionscovers the predicate's cases separately.Self-review
retryS3Mutation, so it cannot compare against a version other than the one being deleted. A rewrite landing after the check is handled by the existing OCC retry, the same as before.readTSandstartTSflow through untouched, so MVCC visibility and OCC validation are unchanged. The new 412 is produced before the dispatch, so no partial mutation is possible.If-None-Matchnon-behavior.deleteObjectneeded the mutation body extracted intodispatchConditionalObjectDeleteto stay inside itscyclopbudget; that helper is pure motion plus the new check.@codex review
@claude review