fix(upload): warn when marked fields cannot travel on a chunked upload - #152
Conversation
Form fields only ever travel on the multipart path. The chunked WebSocket transport carries none — the server builds that action's context from an empty map (mount.go:3116) — so a field marked lvt-upload-with on a Volume/Direct upload never reaches upload_<name>_complete, with nothing in the markup to suggest why. Warn at the point of surprise instead. The mark is an explicit request that the field travel, so a chunked upload from a form that marks fields is worth saying out loud; a form that marks nothing gets no warning, since a message on every chunked upload would train people to ignore it. Not a regression: mount.go:3116 has always passed an empty map, so this path never carried fields. What changed is that #452 made field-carrying an explicit contract, and a contract that quietly doesn't hold on one transport is worth surfacing. Refs livetemplate/livetemplate#508 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ui2cwpeGkrUfRt8rh2FgGG
Review caught a factual error in the first pass: it treated Volume and Direct as the same case. They are not. case "direct" routes to uploadExternal, which PUTs straight to storage and completes with a metadata-only message. It touches neither the chunked nor the multipart path, so marked fields reach a Direct handler in no socket state — while Volume does deliver them on its multipart fallback. Two consequences, both fixed here: The warning claimed fields travel "Volume/Direct when the socket is down", which is a remedy that does not work for Direct. Someone following it would drop the socket and still find empty values — worse than no diagnostic, since a wrong one spends the reader's trust. The text is now per-transport: Volume points at the multipart fallback, Direct points at controller state. Direct had no warning at all, despite being the mode with the more complete failure. uploadExternal now warns too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ui2cwpeGkrUfRt8rh2FgGG
|
Correction pushed — the first pass had a real factual error, caught in review. I treated Volume and Direct as the same case. They are not: Two things were wrong as a result:
Added a fourth test asserting the Direct message names the right transport, recommends controller state, and specifically does not contain "when the socket is down" — the exact claim that was wrong. 35 tests pass. I've also corrected livetemplate/livetemplate#508, which carried the same error. |
Resolves livetemplate/livetemplate#508. Pairs with livetemplate/docs (boundary documented there).
The gap
Form fields only ever travel on the multipart path. The chunked WebSocket transport carries none — the server builds that action's context from an empty map (
mount.go:3116, versusmergedDataatmount.go:1555). So a field markedlvt-upload-withon a Volume/Direct upload never reachesupload_<name>_complete, with nothing in the markup to suggest why.Note the issue was mis-framed, and is now corrected
#508 originally claimed this affected
OnUpload. It does not.OnUploadis Proxied-only (upload.go:41) and Proxied is unconditionally multipart (upload-handler.ts:87-88), so anOnUploadhandler always receives marked fields. I corrected the issue body rather than quietly fixing a different thing than it described.The real scope is narrower: Volume/Direct with the socket up get an empty map; with the socket down they fall back to multipart (#449) and the fields arrive. Same app, same mode, same markup — different behaviour depending on whether the WebSocket happens to be connected.
Why a warning rather than extending the wire format
The investigation shrank this problem.
mount.go:3116has always passed an empty map, so the chunked path never carried fields — this is a long-standing inconsistency, not a regression from #452, and nobody could have depended on it. What #452 changed is the framing: field-carrying went from incidental behaviour to an explicit author-declared contract, and a contract that quietly doesn't hold on one transport is worth surfacing.Carrying fields in the
upload_completemessage would work, but it's a core wire change plus a coordinated dual release — a lot of outward-facing machinery for a never-worked inconsistency on two non-primary modes. The warning turns a silent absence into a diagnosable one at no wire cost. Option 1 is recorded in #508 if a concrete need appears.Behaviour
Warns only when the form actually marks something. A form that marks nothing gets nothing — a message on every chunked upload would train people to ignore it.
Verification
Three tests, and they discriminate — run against
origin/main(no warning present), the results split correctly:The two passing-in-both-directions tests are deliberate: they're the guard against an over-eager warning, which is the failure mode a naive implementation would have. Full suite 34 passed,
tsc --noEmitclean.🤖 Generated with Claude Code
https://claude.ai/code/session_01Ui2cwpeGkrUfRt8rh2FgGG