Skip to content

feat: add ETag support with conditional request handling#327

Closed
alecthomas wants to merge 4 commits into
mainfrom
etag-support
Closed

feat: add ETag support with conditional request handling#327
alecthomas wants to merge 4 commits into
mainfrom
etag-support

Conversation

@alecthomas

Copy link
Copy Markdown
Collaborator

Add content-hash (SHA-256) based ETags to all cache backends, with full
If-Match and If-None-Match support threaded from the HTTP serving layer
through to storage backends.

ETag generation:

  • All backends (disk, memory, S3) compute SHA-256 content hashes during
    Create() via a shared HashingWriter, producing consistent ETags across
    tiers (same content = same ETag regardless of backend).
  • S3 backend does a server-side copy-to-self after upload to update
    stored metadata with the computed ETag.

Cache interface changes:

  • Open() now accepts variadic Precondition options (WithIfMatch,
    WithIfNoneMatch) for atomic precondition checking, eliminating
    TOCTOU races between Stat() and Open() that would cause data
    corruption with future range request support.
  • Returns ErrNotModified or ErrPreconditionFailed when preconditions
    are not met, along with stored headers (including ETag).
  • CheckPreconditions() helper for use by Stat/PUT/DELETE paths.

APIV1 HTTP handler:

  • GET/HEAD: If-None-Match → 304, If-Match → 412
  • POST: If-Match/If-None-Match → 412, returns new ETag in response
  • DELETE: If-Match/If-None-Match → 412
  • 304 responses include full stored headers per RFC 7232 §4.1

Client:

  • Precondition type with WithIfMatch/WithIfNoneMatch options
  • Open, Stat, Create, Delete all accept preconditions (variadic,
    backward compatible)
  • ErrNotModified and ErrPreconditionFailed sentinels
  • 304 and 412 responses return headers (including ETag)

RFC compliance:

  • If-Match uses strong comparison (W/ prefix ETags don't match)
  • If-None-Match uses weak comparison per RFC 7232 §2.3.2
  • Conditional request headers stripped from stored object metadata

@alecthomas alecthomas requested a review from a team as a code owner June 18, 2026 05:25
@alecthomas alecthomas requested review from worstell and removed request for a team June 18, 2026 05:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a2f9b13bb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread internal/strategy/apiv1.go
Comment thread internal/cache/etag.go

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1ea37c2822

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread internal/strategy/apiv1.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 74883f9da7

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread client/client.go
Comment on lines +264 to +265
if resp.StatusCode == http.StatusPreconditionFailed {
return nil, ErrPreconditionFailed

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Return 412 headers from Stat

When a conditional HEAD fails, the APIV1 handler includes the current ETag on the 412 response, and Open() already returns those headers on the same status. Dropping resp.Header here means callers using Stat(ctx, key, WithIfMatch(staleETag)) cannot read the server's current ETag from the failed validation and must issue another request, despite the response carrying it.

Useful? React with 👍 / 👎.

Add content-hash (SHA-256) based ETags to all cache backends, with full
If-Match and If-None-Match support threaded from the HTTP serving layer
through to storage backends.

ETag generation:
- All backends (disk, memory, S3) compute SHA-256 content hashes during
  Create() via a shared HashingWriter, producing consistent ETags across
  tiers (same content = same ETag regardless of backend).
- S3 backend does a server-side copy-to-self after upload to update
  stored metadata with the computed ETag.

Cache interface changes:
- Open() now accepts variadic Precondition options (WithIfMatch,
  WithIfNoneMatch) for atomic precondition checking, eliminating
  TOCTOU races between Stat() and Open() that would cause data
  corruption with future range request support.
- Returns ErrNotModified or ErrPreconditionFailed when preconditions
  are not met, along with stored headers (including ETag).
- CheckPreconditions() helper for use by Stat/PUT/DELETE paths.

APIV1 HTTP handler:
- GET/HEAD: If-None-Match → 304, If-Match → 412
- POST: If-Match/If-None-Match → 412, returns new ETag in response
- DELETE: If-Match/If-None-Match → 412
- 304 responses include full stored headers per RFC 7232 §4.1

Client:
- Precondition type with WithIfMatch/WithIfNoneMatch options
- Open, Stat, Create, Delete all accept preconditions (variadic,
  backward compatible)
- ErrNotModified and ErrPreconditionFailed sentinels
- 304 and 412 responses return headers (including ETag)

RFC compliance:
- If-Match uses strong comparison (W/ prefix ETags don't match)
- If-None-Match uses weak comparison per RFC 7232 §2.3.2
- Conditional request headers stripped from stored object metadata
Per RFC 7232, the '*' wildcard tests for resource existence, not for
the presence of an ETag. An existing object without an ETag should
still match '*' for both If-Match (§3.1) and If-None-Match (§3.2).

This ensures backward compatibility with pre-ETag cache entries.
When Stat returns ErrNotExist in putObject, If-Match (including *)
must fail (nothing to match), and If-None-Match (including *) must
pass (nothing to conflict with). Previously, empty headers were passed
to CheckPreconditions which couldn't distinguish non-existent from
existing-without-ETag.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d1c8b07fe2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +112 to +116
if v := r.Header.Get("If-None-Match"); v != "" {
conds = append(conds, cache.WithIfNoneMatch(v))
}
if v := r.Header.Get("If-Match"); v != "" {
conds = append(conds, cache.WithIfMatch(v))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor all repeated conditional header values

When a client or proxy sends the same list-valued precondition header more than once, Header.Get only evaluates the first field value and ignores the rest. For example, a GET with two If-None-Match fields where the current ETag is in the second field will stream a 200 response instead of returning 304; the same extraction pattern is used by HEAD/POST/DELETE. Combine r.Header.Values(...) before passing the value to the ETag parser.

Useful? React with 👍 / 👎.

The type now carries more than just preconditions (will include byte
ranges for range request support). Rename to reflect the broader scope:

- Preconditions -> OpenOptions
- Precondition -> OpenOption
- ResolvePreconditions -> ResolveOpenOptions
- applyPreconditions -> applyOpenOptions

CheckPreconditions and ErrPreconditionFailed retain their names since
they specifically deal with precondition semantics.
@alecthomas alecthomas closed this Jun 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant