Skip to content

Enforce conditional-write preconditions (If-Match / If-None-Match) on PutObject #120

Description

@alukach

What's needed

Honor conditional-write preconditions (If-Match / If-None-Match, and the x-amz-copy-source-if-* variants) on PutObject so a write with a stale/wrong ETag fails with 412 Precondition Failed instead of silently succeeding.

Motivation

Reported by a Source Coop contributor writing an Icechunk Zarr store natively through the proxy. Icechunk uses conditional writes as the compare-and-swap that protects refs from concurrent writers. Today a PUT with a deliberately wrong ETag succeeds where S3 returns 412:

If-Match preconditions are silently ignored. A PUT with a deliberately wrong ETag succeeds where S3 returns 412 Precondition Failed. Without enforcement, concurrent commits can silently clobber each other.

Current behavior

Conditional headers are forwarded on reads but dropped on writes:

  • GET forwards if-match/if-none-match/if-modified-since/if-unmodified-since: crates/core/src/proxy.rs:769-775.
  • HEAD forwards the same set: crates/core/src/proxy.rs:789-795.
  • PutObject forward whitelist (crates/core/src/proxy.rs:818-827) is only content-*, cache-control, expiresif-match/if-none-match are absent, so the precondition is silently dropped and the write always succeeds.

The 412 machinery already exists but is only wired for the read/backend-executed paths:

  • ProxyError::PreconditionFailed → S3 code PreconditionFailed / HTTP 412: crates/core/src/error.rs:79,112,131.
  • Produced from object_store::Error::Precondition in from_object_store_error(): crates/core/src/error.rs:162.

ETags themselves are never computed/stored by the proxy — they originate from the backend and are only passed through (crates/core/src/api/response.rs:100-101,128-130,228-230; crates/core/src/api/list.rs:200).

Why it isn't a trivial header pass-through

Same constraint as CopyObject: PUT forwards via a presigned URL carrying only signed, whitelisted headers (crates/core/src/proxy.rs:810-817). if-match/if-none-match must be part of the signature for the backend to evaluate them — they can't be added unsigned. So enforcement means including the precondition headers in the presign/signing so the backend (S3) returns 412 directly, then mapping that response through cleanly.

Scope / proposed approach

  • Add if-match / if-none-match (and x-amz-copy-source-if-* for the copy case) to the PutObject signed-forward set so the backend evaluates the precondition.
  • Confirm the backend's 412 is surfaced to the client unchanged (presigned passthrough) and, for proxy-executed paths, maps via from_object_store_error() (error.rs:162).
  • Failing-test-first (per CLAUDE.md): a PUT with a wrong If-Match must return 412; If-None-Match: * must fail when the object exists.
  • Update docs/ for the newly honored preconditions.

Pairs with CopyObject support (#119) — Icechunk needs both for native transactional commits.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions