Skip to content

fix(upload): warn when marked fields cannot travel on a chunked upload - #152

Merged
adnaan merged 2 commits into
mainfrom
fix/chunked-upload-field-warning
Jul 20, 2026
Merged

fix(upload): warn when marked fields cannot travel on a chunked upload#152
adnaan merged 2 commits into
mainfrom
fix/chunked-upload-field-warning

Conversation

@adnaan

@adnaan adnaan commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

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, versus mergedData at mount.go:1555). 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.

Note the issue was mis-framed, and is now corrected

#508 originally claimed this affected OnUpload. It does not. OnUpload is Proxied-only (upload.go:41) and Proxied is unconditionally multipart (upload-handler.ts:87-88), so an OnUpload handler 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:3116 has 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_complete message 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:

✕ warns that marked fields cannot travel on a chunked upload (#508)
✓ stays quiet on a chunked upload when no field is marked
✓ stays quiet on a proxied upload, where marked fields do travel

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 --noEmit clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ui2cwpeGkrUfRt8rh2FgGG

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
@adnaan

adnaan commented Jul 20, 2026

Copy link
Copy Markdown
Contributor Author

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: case "direct" routes to uploadExternal (upload-handler.ts:90), a presigned PUT to storage followed by a metadata-only completion. It touches neither the chunked nor the multipart path.

Two things were wrong as a result:

  1. The warning's remedy was actively misleading. It said fields travel "Volume/Direct when the socket is down." That's true for Volume, false for Direct — someone following it would drop their socket and still find empty values. A wrong diagnostic is worse than none, because it spends the reader's trust. The text is now per-transport: Volume points at the multipart fallback, Direct points at controller state.

  2. Direct had no warning at all — despite being the mode with the more complete failure (Volume at least delivers on the fallback; Direct never does). uploadExternal now warns too, with its own wording.

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.

@adnaan
adnaan merged commit cdf1c3f into main Jul 20, 2026
6 of 7 checks passed
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.

upload_<name>_complete action sees marked fields on multipart but not chunked uploads

1 participant