Skip to content

fix(admin): stop autosave from retrying a payload the server rejected - #2614

Open
danielmlr wants to merge 2 commits into
emdash-cms:mainfrom
danielmlr:fix/admin-autosave-rejected-payload
Open

fix(admin): stop autosave from retrying a payload the server rejected#2614
danielmlr wants to merge 2 commits into
emdash-cms:mainfrom
danielmlr:fix/admin-autosave-rejected-payload

Conversation

@danielmlr

@danielmlr danielmlr commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

When the server rejects an autosave with a 400, the editor resends the same payload every two seconds until the tab is closed, each attempt adding another failed-save toast. #2417 records 443 identical PUTs for one entry over 23 minutes; the trigger there was a value past a maxLength that nothing in the editor marks while typing.

The editor schedules an autosave whenever the form is dirty and nothing is in flight; a failed one leaves it dirty and clears the flag, so the next timer starts at once.

isTerminalRequestError reads the status off the ApiResponseError that throwResponseError already throws. A client error is terminal unless it is in RETRYABLE_CLIENT_ERROR_STATUSES — 408, 421, 425, 429 — which judge the connection or the rate limit, not the payload. A terminal error advances an entry-scoped autosaveRejectionToken, and the editor schedules no autosave while the form still matches the rejected state. Manual Save keeps working; the next content change autosaves again. 5xx and network failures keep retrying.

Part of #2417

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (n/a: no new strings)
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/... (n/a: bug fix)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Fable 5, Claude Opus 5

Screenshots / test output

packages/admin/tests/components/ContentEditor.test.tsx — after a rejection token, no further autosave within 10 s; Save stays enabled; the next content change autosaves again
packages/admin/tests/router.test.tsx — a 400 on the autosave PUT advances the rejection token for that entry; a 500 does not
packages/admin/tests/lib/api-client.test.ts — 400, 401, 404, 409 and 413 are terminal, 408, 421, 425, 429, 500, 504 and a network TypeError are not
e2e/tests/autosave.spec.ts — real server: one 400 PUT, no further PUT in the following 7 s, the corrected value saves with 200

pnpm --filter @emdash-cms/admin test    137 files, 1733 passed
red/green: with packages/admin/src at main, the editor test sees 2 autosave calls instead of 1, the router test a token of 0, and api-client.test.ts no longer loads
pnpm exec playwright test               274 passed, 21 skipped (node); red without the fix: 4 PUTs with 400 inside the 7 s window
pnpm typecheck / oxlint --type-aware --deny-warnings / oxfmt --check / prettier --check    clean, 0 diagnostics

Revisions

  • ac24d3f — the retryable statuses are a named list instead of a single 429 exception, with 421 alongside 408 and 425. 409 and 401 stay terminal: a conflict repeats with the same body, and the admin client has no session refresh.
  • Rebased onto main. feat(media): add flat folder UI #2586 landed an ApiResponseError carrying the status, so the ApiError class this PR added is dropped and isTerminalRequestError narrows on that type. Its status test goes too — preserves status, code, and details on API response errors covers it.

@changeset-bot

changeset-bot Bot commented Aug 23, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c5d3ac1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added area/admin size/L review/needs-review No maintainer or bot review yet labels Aug 23, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a focused, well-reasoned bug fix for #2417. The approach is sound: it mirrors the existing autosaveCompletionToken pattern with a new entry-scoped autosaveRejectionToken, pushes HTTP-status awareness into ApiError/isTerminalRequestError, and lets ContentEditor remember the exact serialized state that the server rejected so it doesn't schedule another autosave while the form is unchanged. The split (terminal 4xx, retryable 429/5xx/network) matches the issue's proposal, and manual Save continues to work.

I read the diff and the full changed files, traced the new token through router.tsx, verified the serialization comparison in ContentEditor.tsx, and checked the test coverage at the API-client, router, component, and e2e levels. All changed hunks have a meaningful test, and the test in packages/admin/tests/components/ContentEditor.test.tsx fails without the source change (it asserts one autosave call instead of two). No AGENTS.md conventions are violated: no new user-facing strings, no new logged-out queries, no SQL, no authorization changes, no new indexes, no stale or reviewer-facing comments, and the changeset is a concise, user-facing patch entry that names the affected behavior.

I found no logic bugs, regressions, or gaps. The code is clean and ready to merge.

@pkg-pr-new

pkg-pr-new Bot commented Aug 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/admin@2614

@emdash-cms/auth

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth@2614

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/auth-atproto@2614

@emdash-cms/blocks

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/blocks@2614

@emdash-cms/cloudflare

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/cloudflare@2614

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/contentful-to-portable-text@2614

emdash

npm i https://pkg.pr.new/emdash-cms/emdash@2614

create-emdash

npm i https://pkg.pr.new/emdash-cms/emdash/create-emdash@2614

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/gutenberg-to-portable-text@2614

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-cli@2614

@emdash-cms/plugin-types

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-types@2614

@emdash-cms/registry-client

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-client@2614

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-lexicons@2614

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-moderation@2614

@emdash-cms/registry-verification

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-verification@2614

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/sandbox-workerd@2614

@emdash-cms/x402

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/x402@2614

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-ai-moderation@2614

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-atproto@2614

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-audit-log@2614

@emdash-cms/plugin-color

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-color@2614

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-embeds@2614

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-field-kit@2614

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-forms@2614

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/plugin-webhook-notifier@2614

commit: c5d3ac1

@github-actions

Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-review No maintainer or bot review yet labels Aug 24, 2026
Comment thread packages/admin/src/lib/api/client.ts Outdated
/** Whether retrying the same request unchanged can never succeed. */
export function isTerminalRequestError(error: unknown): boolean {
return (
error instanceof ApiError && error.status >= 400 && error.status < 500 && error.status !== 429

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

429 isn't the only retryable response. e.g. 408 and 425 should be retried. Can you create a list of retryable responses and exclude them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. RETRYABLE_CLIENT_ERROR_STATUSES in packages/admin/src/lib/api/client.ts now holds 408, 421, 425 and 429, and isTerminalRequestError excludes that list instead of special-casing 429.

I put 421 in it for the same reason as your two: it judges the connection rather than the body, and RFC 9110 says the client may retry over a different one.

409 and 401 I left terminal. A conflict repeats with the same body, and the admin client has no session refresh, so a resend cannot establish one. If you would rather have either of them retried, say which and I will move it.

@ascorbic

Copy link
Copy Markdown
Collaborator

Thanks. Could you resolve the conflicts, then we can get this merged

@danielmlr
danielmlr force-pushed the fix/admin-autosave-rejected-payload branch from 0c444ad to ac24d3f Compare August 27, 2026 20:53
A failed autosave left the form dirty and cleared the in-flight flag, so
the editor scheduled the next attempt two seconds later and resent the
same payload until the tab was closed. For a validation error that can
never succeed, each attempt added another error toast.

The edit page now signals a 4xx other than 429 to the editor through an
entry-scoped rejection token, the counterpart of the completion token.
The editor remembers the rejected state and schedules the next autosave
only once the content differs from it. 5xx responses and network
failures keep retrying as before.
Treating every 4xx except 429 as terminal was too broad. A 408 and a 425
say the request never reached the handler, and a 421 says it reached the
wrong one; all three can succeed unchanged on the next attempt.

The classification now works off a named list of retryable client error
statuses instead of a single exception, so adding one is a list entry.
@danielmlr
danielmlr force-pushed the fix/admin-autosave-rejected-payload branch from ac24d3f to c5d3ac1 Compare August 28, 2026 18:22
@danielmlr
danielmlr requested a review from ascorbic August 28, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/admin overlap review/needs-rereview Author pushed changes since the last review size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants