Fileless multipart forms submit urlencoded, dodging Safari's empty-body bug (v7.206.1) - #1254
Merged
Conversation
…dy bug Issue #1227: one member's Safari 26.5.2 answered every save of the multipart Basics & photos form with a 400. The nginx body capture (2026-07-31 05:41/05:42 UTC) showed why: the browser declared a multipart boundary but sent Content-Length: 0 - a zero-byte body, so the form data never left his machine. Every urlencoded form from the same browser worked. WebKit builds multipart bodies as a stream it cannot always replay (a stale keep-alive retry goes out body-less), while urlencoded bodies are in-memory and survive. Multipart only buys file transport, so app.js now downgrades any non-LiveView multipart form to urlencoded at submit time when no file is actually selected, disabling the empty file inputs for that one submission so the request carries exactly the params a fileless multipart submit produces. With a file chosen, multipart stays. The server side needed nothing: file fields are not in the changeset cast list and only a %Plug.Upload{} is ever stored, which the new tests pin down (an empty-string avatar param can never clear a stored avatar). Smoke-tested in a real browser: fileless save arrives urlencoded with no file params and persists; a DataTransfer-injected file flips the form back to multipart. An AI agent wrote this text in my name, unreviewed by me. The work behind it is mine; I only delegated the writing. Claude-Session: https://claude.ai/code/session_014ksBm1P9qsazixAHFeHwMc
An AI agent wrote this text in my name, unreviewed by me. The work behind it is mine; I only delegated the writing. Claude-Session: https://claude.ai/code/session_014ksBm1P9qsazixAHFeHwMc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1227.
The diagnosis. The nginx body capture set up yesterday caught the member's two retries this morning (2026-07-31 05:41/05:42 UTC): his Safari 26.5.2 declares a multipart boundary but sends
Content-Length: 0— a zero-byte body. The form data never leaves his browser, so no server-side handling can recover it; the 400 is the only honest answer to an empty request. Every urlencoded form from the same browser works (his whole session log confirms it), and a healthy Chrome save captured the full multipart body fine. WebKit builds multipart bodies as a stream it cannot always replay (a retry on a stale keep-alive connection goes out body-less); urlencoded bodies are in-memory and survive.The fix. Multipart only buys file transport. app.js now downgrades any non-LiveView multipart form to urlencoded at submit time when no file is actually selected, disabling the empty file inputs for that one submission so the request carries exactly the params a fileless multipart submit produces. With a file chosen, multipart stays. The server needed no change: file fields are not in the changeset cast list and only a
%Plug.Upload{}is ever stored — the new tests pin that contract (including that an empty-string avatar param can never clear a stored avatar).Verified end to end in a real browser against a dev server: a fileless save arrives as urlencoded PUT with no file params and persists (server log + DB checked); a DataTransfer-injected file flips the form back to multipart; the disabled inputs re-enable after a cancelled submit.
mix precommitgreen (6239 tests).An AI agent wrote this text in my name, unreviewed by me. The work behind it is mine; I only delegated the writing.
https://claude.ai/code/session_014ksBm1P9qsazixAHFeHwMc