Repository navigation
Field constraints are invisible until the server rejects the save, and autosave then retries the doomed request indefinitely #2417
Description
Description
A collection field can declare validation constraints (maxLength, min, minItems, pattern). These are enforced server-side by the generated Zod schema, but nothing in the admin surfaces them to the person typing. The editor's first and only signal is a failed save.
Since #1838 that failure message is at least specific — the toast reads excerpt: Too big: expected string to have <=160 characters rather than the old generic "Invalid request data", which is a real improvement. But it arrives after the fact, names the field by slug rather than by its label, uses validator-internal phrasing, and — the substantive part of this report — autosave keeps retrying a request that can never succeed.
In one editing session on 0.32.0 we recorded 443 failed PUTs to the same entry over 23.4 minutes, a median of 3.0 seconds apart (~19/min), every one a 400 with the identical body. Each produced its own toast. The author had added a few words to a summary field capped at 160 characters; nothing in the UI had indicated a cap existed.
Steps to reproduce
- Declare a text field with
validation: { maxLength: 160 }in a collection. - Open an entry in the admin and type past 160 characters in that field.
- Observe: no counter, no
maxlength, no help text — nothing marks the limit while typing. - Wait. Autosave fires, fails 400, and toasts
excerpt: Too big: expected string to have <=160 characters. - Leave the tab open. Autosave retries every ~3 seconds indefinitely, toasting each time, with no back-off and no terminal state.
Observed
- The constraint is unknowable from the UI until it is violated.
- The message identifies the field by slug (
excerpt), not by the label the editor actually sees ("Summary"). - The message is the raw validator string ("Too big: expected string to have <=160 characters") rather than a sentence aimed at an author.
- Autosave treats a permanent 400 as retryable. 443 attempts, 23.4 minutes, no escalation, no stop, no back-off.
Expected
- Constraints declared in the schema are visible while editing — at minimum a character counter or
maxlengthon length-limited fields, and help text derived fromvalidationrather than hand-written into the label. - Validation errors name the field by its label.
- Autosave treats a 4xx other than 429 as terminal: stop retrying, keep the buffer dirty, surface the problem once, and retry only when the content changes again.
Why this matters beyond one field
We audited our own schema after hitting this: 16 of 17 declared constraints never reach the editor's screen. The single exception is the field in this report, and only because a developer had previously hand-typed "max 160 chars" into its label — after an earlier incident. That workaround is unchecked: change maxLength to 200 and the label still says 160.
The options constraints are effectively surfaced, because a select renders the allowed set. Everything expressed as a number or a pattern — maxLength, min, minItems, pattern — is invisible. A non-technical author cannot discover any of it except by tripping it.
Related
- Bug: Validation Errors must be shown to the End User as Taxonomy Creation Endpoint #255 — validation errors not shown to end users. Fixed by fix(admin): surface field-level validation messages instead of a generic error #1838 (the message in our toast is that fix working). This report is about the constraint being undiscoverable before the error, and about the retry behaviour after it.
- Repeater min/max and required subfields are not enforced by runtime content validation #2378 — repeater
min/maxand required subfields not enforced by runtime content validation. Same family: constraints declared in the schema that do not take effect where an author would meet them. - Publish changes races the autosave debounce: unsaved edits silently dropped, and a stale pending draft can regress live content (0.29.0) #2123 — a different autosave defect (publish racing the debounce).
Environment
- emdash 0.32.0
- Astro node adapter, PostgreSQL 18 adapter
- Reproduced against a production-shaped content set; server responses captured from load-balancer access logs, so the retry counts above are measured rather than estimated.
Notes on possible fixes
Roughly in increasing order of effort:
- Stop the retry storm. Classify non-429 4xx as terminal in the autosave client. This alone removes 442 of the 443 requests and the toast pile.
- Use the label in the message. The field descriptor already carries both slug and label.
- Render the constraint. Pass
validationthrough to the field renderer:maxlengthplus a live counter for length-limited strings,min/maxon numerics, and generated help text. This is the fix that would have prevented the situation entirely.
Happy to open a PR for (1) and (2) if that would be useful — they look self-contained, and (1) is the one that turns a papercut into a non-event.
Activity
virafb commentedon Aug 14, 2026
A concrete, small fix for the "constraints are invisible" half — the data is already at the render site.
Checked against
@emdash-cms/admin@0.32.0and0.33.0,src/components/ContentEditor.tsx:case "string"(line 1170 in 0.32.0) renders a bare<Input>— nomaxLengthattribute and no character counter:The
fieldobject in that same switch already carriesvalidation(declared line 93), and the component reads it elsewhere —validation.allowedMimeTypesfor media fields (1326, 1350) andvalidation.minItems/maxItemsfor repeaters (1376–1377). Only the string case ignores it.So passing
maxLength={field.validation?.maxLength}to theInput, and/or rendering a live count against it, needs no new plumbing. A nativemaxLengthalone would prevent the doomed save entirely, which also sidesteps the retry loop reported above.Worth noting the same gap for
textfields, and that the author has no way to measure themselves against a limit even when they know the number: we worked around this by generating the limit into the field's own label fromvalidation, and it only half helps — you are told "max 160 characters" with nothing counting.Plugin widgets can't fill the gap either.
field.widgetresolves to a plugin field component, but the props passed to it arevalue, onChange, label, id, required, options, minimal—validationis not among them. An integrator writing a counting widget has to hardcode the number, which then drifts from the one the server enforces.danielmlr commentedon Aug 25, 2026
Thanks for numbering the fixes. You had offered to take (1) and (2) — hope you don't mind that I picked up (1). Do you still want (2)? If so I will leave it with you, and I would look at (3) only if the sketch in your comment above is not already headed for a PR of its own. Say the word and I stay off both.
#2614 covers (1). A client error is now terminal unless it is 408, 421, 425 or 429, and the editor schedules no further autosave while the form still matches the rejected payload. Manual Save keeps working and the next content change resumes autosaving; 5xx and network failures still retry.
ascorbic commentedon Sep 2, 2026
Thanks both. #2614 completes item 1.
@virafb, let me know if you still want to take item 2: map validation failures to field labels and editor-facing messages.
@danielmlr, please go ahead with item 3: surface field constraints in the editor.
Please let me know if either of you won't have time. These are 1.0 blockers, so otherwise somebody else will handle them.
feat(admin): show field constraints in the content editor
feat(admin): show field constraints in the content editor
CacheMeOwside commentedon Sep 7, 2026
@virafb 👋
This (2) is still yours if you want it. I'm offering to pick it up only because it's on the 1.0 milestone (and a blocker). If you're already partway in, I'll leave it alone.
danielmlr commentedon Sep 12, 2026
@CacheMeOwside for context: virafb can't sign the CLA (they said so when closing #2770 on 29 August), so a PR for item 2 from them couldn't be merged.
danielmlr commentedon Sep 15, 2026
Hi @CacheMeOwside 👋 Are you still up for item 2? If your plate's full right now, I'd be happy to pick it up.
It would take #2426 along: core returns the per-field issues with their codes, and the admin turns them into localized messages that name the field by its label.
feat(admin): show field constraints in the content editor (#2880)
danielmlr commentedon Sep 19, 2026
Hi @CacheMeOwside 👋 Thanks for offering to take item 2. Since it's a 1.0 blocker, I went ahead and opened #3205 for it, which also closes #2426. If you'd already started on it, just let me know.
ascorbic commentedon Sep 21, 2026
I think all parts of this are now fixed. Thanks all!