Skip to content

Prevent caching handle-less 409 must-refetch responses#4638

Open
KyleAMathews wants to merge 2 commits into
mainfrom
investigate-409-handleless-loop
Open

Prevent caching handle-less 409 must-refetch responses#4638
KyleAMathews wants to merge 2 commits into
mainfrom
investigate-409-handleless-loop

Conversation

@KyleAMathews

Copy link
Copy Markdown
Contributor

Handle-less 409 must-refetch responses are no longer cacheable; only 409s with an electric-handle keep the short-lived redirect cache behavior. This prevents shared caches from replaying a non-actionable 409 that gives clients no replacement handle to follow.

Root Cause

409 must-refetch responses serve two subtly different roles:

  • With an electric-handle, the response acts like a shape redirect and can safely be cached briefly to coalesce refetch storms.
  • Without an electric-handle, the response only tells the client to renegotiate from scratch. Caching that response can replay a stale, non-actionable retry signal and keep clients from converging on a fresh shape handle.

The previous cache-header logic cached both cases, using max-age=1 for handle-less 409s.

Approach

The sync service now only special-cases 409 responses when they include a handle:

defp put_cache_headers(conn, %{status: 409, handle: handle}) when not is_nil(handle) do
  # cacheable redirect
end

Handle-less 409s fall through to the existing generic status >= 400 handling, which sets both cache-control: no-store and surrogate-control: no-store.

Key Invariants

  • 409 responses with electric-handle remain cacheable as redirect/coalescing responses.
  • 409 responses without electric-handle are not stored by downstream or surrogate caches.
  • The response body still carries the must-refetch control message in both cases.

Non-goals

  • No client retry behavior changes.
  • No change to shape rotation semantics.
  • No change to cache behavior for successful shape responses or other error statuses.

Trade-offs

This gives up the previous 1-second cache coalescing for handle-less 409s. That is preferable because a handle-less 409 is not a useful redirect target; caching it can amplify the stuck-loop failure mode described in the bug report.

Verification

cd packages/sync-service
mix test test/electric/plug/serve_shape_plug_test.exs:680
mix test test/electric/plug/serve_shape_plug_test.exs

Also verified the changeset coverage:

GITHUB_BASE_REF=main node scripts/check-changeset.mjs

Files changed

  • packages/sync-service/lib/electric/shapes/api/response.ex — restrict cacheable 409 handling to responses with a shape handle, allowing handle-less 409s to use the generic no-store error path.
  • packages/sync-service/test/electric/plug/serve_shape_plug_test.exs — assert handle-less shape-rotation 409s have no handle and are marked no-store.
  • .changeset/handleless-409-no-store.md — add patch changeset for @core/sync-service.

@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 59.45%. Comparing base (ee0da19) to head (364276b).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4638      +/-   ##
==========================================
- Coverage   59.46%   59.45%   -0.01%     
==========================================
  Files         385      385              
  Lines       43039    43039              
  Branches    12383    12380       -3     
==========================================
- Hits        25591    25588       -3     
- Misses      17371    17375       +4     
+ Partials       77       76       -1     
Flag Coverage Δ
packages/agents 72.64% <ø> (ø)
packages/agents-mcp 77.70% <ø> (ø)
packages/agents-mobile 80.67% <ø> (ø)
packages/agents-runtime 83.45% <ø> (-0.02%) ⬇️
packages/agents-server 75.45% <ø> (-0.03%) ⬇️
packages/agents-server-ui 7.51% <ø> (ø)
packages/electric-ax 51.06% <ø> (ø)
packages/experimental 87.73% <ø> (ø)
packages/react-hooks 86.48% <ø> (ø)
packages/start 82.83% <ø> (ø)
packages/typescript-client 91.83% <ø> (+0.11%) ⬆️
packages/y-electric 56.05% <ø> (ø)
typescript 59.45% <ø> (-0.01%) ⬇️
unit-tests 59.45% <ø> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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