Skip to content

Conversation

@sneako
Copy link
Member

@sneako sneako commented Jan 13, 2026

  • add optional download_if_changed/2 to Fetcher behaviour and wire update loop to use it when available
  • implement conditional GETs in the S3 fetcher via If-None-Match and 304 handling
  • replace S3 version checks to use HEAD (ETag) instead of LISTing object versions
  • add S3 tests covering conditional GET and header usage

Note

Introduces an optional fast-path update flow and optimizes the S3 fetcher to reduce redundant downloads.

  • Core: Add optional download_if_changed/2 to RemotePersistentTerm.Fetcher; update update_term/3 to prefer it when implemented; extract store_term/4; unify error logging via log_update_error/2.
  • S3: Use HEAD to read ETag for current_version/1; implement conditional GET via If-None-Match and handle 304 Not Modified; normalize/quote ETags; adjust failover to not fail over on 304; add Req-based ExAws HTTP client (RemotePersistentTerm.Fetcher.S3.HttpClient).
  • Tests: Update S3 tests and add coverage for conditional requests and failover behavior.
  • Build: Bump version to 0.13.0; switch to Req for ExAws HTTP; update lockfile; add AGENTS.md contributor guidelines.

Written by Cursor Bugbot for commit 3df3517. This will update automatically on new commits. Configure here.


Summary by cubic

Adds conditional GETs to the S3 fetcher and an optional download_if_changed callback to avoid re-downloading unchanged terms and cut S3/API usage. S3 version checks now use HEAD/ETag for faster, simpler change detection.

  • New Features

    • Added download_if_changed/2 to the Fetcher behaviour; the update loop uses it when available.
    • S3 fetcher now sends If-None-Match and handles 304 Not Modified; uses ETag from HEAD for change detection.
    • Adjusted failover to skip on 304; added tests covering conditional requests and header usage.
  • Dependencies

    • Bumped library to 0.13.0.
    • Switched ExAws HTTP client to Req and removed hackney; updated ex_aws_s3 and related deps.

Written for commit 3df3517. Summary will update on new commits.

… hook

  - add optional download_if_changed/2 to Fetcher behaviour and wire update loop to use it when available
  - implement conditional GETs in the S3 fetcher via If-None-Match and 304 handling
  - switch S3 version checks to ETag-only and document versioned/non‑versioned behavior
  - add S3 tests covering conditional GET and header usage
Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

No issues found across 7 files

message: "Failed to fetch from primary bucket, attempting failover buckets"
})

try_failover_buckets(op, failover_buckets, opts, state)
Copy link

Choose a reason for hiding this comment

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

Failover buckets ignore 304 handling during conditional GETs

Medium Severity

The failover_on_error? predicate is used to prevent treating 304 responses as failures for the primary bucket, but it's not passed to try_failover_buckets. When download_if_changed triggers failover (e.g., primary returns 500) and a failover bucket returns 304 (not modified), the response is incorrectly treated as an error. This causes unnecessary requests to all remaining failover buckets and can result in "All buckets failed" errors with misleading log messages, even when the content is actually unchanged across all failover buckets.

Additional Locations (1)

Fix in Cursor Fix in Web

Copy link
Member Author

Choose a reason for hiding this comment

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

planning to work on the failover logic in a separate PR

Copy link

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="lib/remote_persistent_term/fetcher/s3.ex">

<violation number="1" location="lib/remote_persistent_term/fetcher/s3.ex:212">
P1: This case-sensitive comparison will fail to find HTTP headers returned by AWS S3. S3 returns headers like `"ETag"` (mixed case), but this code searches for `"etag"` (lowercase). HTTP headers are case-insensitive per RFC 7230. The previous implementation correctly used `String.downcase()` for comparison. This will cause `extract_version/1` to always return `{:error, :not_found}` in production.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

defp header_value(headers, name) do
Enum.find_value(headers, fn
{key, value} when is_binary(key) and is_binary(value) ->
if key == name, do: value, else: nil
Copy link

@cubic-dev-ai cubic-dev-ai bot Jan 13, 2026

Choose a reason for hiding this comment

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

P1: This case-sensitive comparison will fail to find HTTP headers returned by AWS S3. S3 returns headers like "ETag" (mixed case), but this code searches for "etag" (lowercase). HTTP headers are case-insensitive per RFC 7230. The previous implementation correctly used String.downcase() for comparison. This will cause extract_version/1 to always return {:error, :not_found} in production.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/remote_persistent_term/fetcher/s3.ex, line 212:

<comment>This case-sensitive comparison will fail to find HTTP headers returned by AWS S3. S3 returns headers like `"ETag"` (mixed case), but this code searches for `"etag"` (lowercase). HTTP headers are case-insensitive per RFC 7230. The previous implementation correctly used `String.downcase()` for comparison. This will cause `extract_version/1` to always return `{:error, :not_found}` in production.</comment>

<file context>
@@ -207,14 +207,9 @@ defmodule RemotePersistentTerm.Fetcher.S3 do
-
-      {key, value} when is_atom(key) and is_binary(value) ->
-        if String.downcase(Atom.to_string(key)) == downcased, do: value, else: nil
+        if key == name, do: value, else: nil
 
       _ ->
</file context>
Suggested change
if key == name, do: value, else: nil
if String.downcase(key) == String.downcase(name), do: value, else: nil

✅ Addressed in 3df3517

@sneako sneako merged commit 3213dce into main Jan 14, 2026
3 checks passed
value
|> String.trim()
|> String.trim("\"")
end
Copy link

Choose a reason for hiding this comment

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

Weak ETags are incorrectly normalized causing match failures

Low Severity

The normalize_etag function uses String.trim("\"") which only removes quotes from both ends if present. For weak ETags like W/"abc123", this removes only the trailing quote (since the first char is W), producing the malformed value W/"abc123 (missing closing quote). Subsequently, quote_etag wraps this as "W/\"abc123" instead of the correct W/"abc123", causing conditional GET requests to never match for S3-compatible services using weak validators.

Additional Locations (1)

Fix in Cursor Fix in Web

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.

2 participants