Skip to content

Fileless multipart forms submit urlencoded, dodging Safari's empty-body bug (v7.206.1) - #1254

Merged
wintermeyer merged 2 commits into
mainfrom
safari-empty-multipart-1227
Jul 31, 2026
Merged

Fileless multipart forms submit urlencoded, dodging Safari's empty-body bug (v7.206.1)#1254
wintermeyer merged 2 commits into
mainfrom
safari-empty-multipart-1227

Conversation

@wintermeyer

Copy link
Copy Markdown
Owner

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 precommit green (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

…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
@wintermeyer
wintermeyer merged commit 92b4518 into main Jul 31, 2026
1 check passed
@wintermeyer
wintermeyer deleted the safari-empty-multipart-1227 branch July 31, 2026 06:41
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.

Profile save can die on a bare 400 "Bad Request" page

1 participant