Skip to content

feat(web): send files anywhere in rich server function arguments - #4208

Merged
ryansolid merged 2 commits into
nextfrom
claude/server-function-uploads
Oct 21, 2026
Merged

ryansolid merged 2 commits into
nextfrom
claude/server-function-uploads

Conversation

@ryansolid

Copy link
Copy Markdown
Member

Summary

Before: with enableRichArguments(), #4177 lets a server function take files anywhere in its arguments, but its core client request path gained an instanceof FormData branch, a conditional Content-Type header and a new BodyFormat.SerializedFormData tag. That cost +95 B minified for every app that uses server functions, rich arguments or not.

After: the same feature, with apps that don't use rich arguments paying nothing. The server-function scenarios come in at −21 B minified against next.

This carries @lxsmnsyc's commit from #4177 unchanged, plus one commit on top:

  • The core client no longer sets Content-Type: text/plain on the codec path. Fetch already gives a string body text/plain and a FormData body its multipart boundary.
  • Uploads keep the Serialized tag ("0"). The handler treats a Serialized body with a multipart/form-data content type as an upload. SerializedFormData ("10") is gone, and the version-skew spec's future tag is back to "10".
  • The uploads comment and RFC 10 say the content type marks an upload instead of the tag.

How did you test this change?

  • @solidjs/web unit: 1395/1395. Hydrate: 520 passed. Server: 1745 passed, 1 failed. The failure is server-functions-adapter-request, which also fails on next under local Node 22.
  • The upload spec now asserts the "0" tag plus the multipart content type. The malformed-body cases (no args field, a missing part, a non-file part, a file index in a plain codec body) still answer 400.
  • scripts/size against next (9f64ca9): −21 B minified on all six server-function page scenarios. Brotli moves −64 to +70 B per scenario from compression noise; none of them crosses a gate.

Supersedes #4177.


Generated by Claude Code

lxsmnsyc and others added 2 commits October 21, 2026 19:04
With enableRichArguments(), a call whose arguments hold files or blobs
used to fail before sending, unless the file was the only argument or
the last one. Ports solidjs/solid-start#2359 to the core transport.

- The rich-args encoder finds each file with a seroval plugin made for
  the call, wherever the codec meets it: arguments, arrays, objects,
  maps, sets and FormData entries.
- A call with files sends multipart form data. The codec output goes in
  the `args` field and each file goes as it is in a `file<index>` part.
- The new BodyFormat.SerializedFormData ("10") marks the request.
- The server decodes `args` with a plugin made for that request, which
  turns each index back into its part. Other bodies decode without it.
- A missing `args` field, a bad index or a missing part is refused with
  400. bodySizeLimit bounds the whole body.
- Default JSON mode is unchanged. Upload code ships only in the
  rich-args entry.
- The version skew spec moves its unknown tag from "10" to "11".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The codec request path no longer sets `Content-Type: text/plain` or picks a
body-format tag per body. Fetch gives a string body `text/plain` and a
`FormData` body its multipart boundary, so uploads keep the `Serialized` tag
and the handler tells them apart by the multipart content type. That drops
`BodyFormat.SerializedFormData` and the `instanceof FormData` branch, so apps
that don't use rich arguments pay nothing for uploads (−21 B minified on the
server-function scenarios against the PR's base).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1
@changeset-bot

changeset-bot Bot commented Oct 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e543ec9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 14 packages
Name Type
@solidjs/web Patch
@solidjs/babel-plugin Patch
@solidjs/diagnostics Patch
@solidjs/element Patch
@solidjs/h Patch
@solidjs/html Patch
test-integration Patch
board-example Patch
server-dbmon-example Patch
todos-server-example Patch
@solidjs/compiler Patch
@solidjs/signals Patch
solid-js Patch
@solidjs/universal Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown

Size (brotli, eager entry chunk)

scenario head vs base minified vs base minified vs recorded cap lazy chunks (not counted)
signals: core floor (createSignal/Memo/Effect/Root/flush) 7.36 KB 0 B 0 B +8 B 7.39 KB ✅
signals: + createStore 14.94 KB 0 B 0 B +8 B 14.95 KB ✅
signals: + isPending/latest 10.17 KB 0 B 0 B +8 B 10.19 KB ✅
signals: + until/refresh (truth readers, no optimism) 7.97 KB 0 B 0 B +8 B 7.98 KB ✅
app: render + one signal (the simple-app floor) 9.68 KB 0 B 0 B +8 B 9.70 KB ✅
app: hydrating (no stores) with Show/For/Loading/Errored/lazy 18.32 KB 0 B 0 B +8 B 18.36 KB ✅ lazy-page.js 0.04 KB
app: hydrating + every store primitive family 30.36 KB 0 B 0 B +8 B 30.41 KB ✅ lazy-page.js 0.04 KB
app: CSR with Show/For/Loading/Errored/lazy 13.25 KB 0 B 0 B +8 B 13.28 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier (same app on the observe artifacts) 14.98 KB 0 B 0 B +8 B 15.04 KB ✅ lazy-page.js 0.04 KB
app: CSR, observe tier + attribution engine enabled 29.59 KB 0 B 0 B +8 B 29.63 KB ✅ lazy-page.js 0.04 KB
app: compiled floor (one template, one text hole, one delegated click) 9.87 KB 0 B 0 B +8 B 9.89 KB ✅
app: compiled CSR (JSX todo app: spread/merge/omit, events, class/style, keyed For, Show, Loading + lazy, store) 26.08 KB 0 B 0 B +8 B 26.11 KB ✅ stats.js 0.18 KB
app: compiled hydrating (the same JSX todo app through hydrate(), compiled hydratable) 32.11 KB 0 B 0 B +8 B 32.20 KB ✅ stats.js 0.20 KB
frames: eager client consumer (frames client + transport, lazy codec) 11.75 KB 0 B 0 B 0 B 11.76 KB ✅
page: base server components (hydrating + dynamic + frames + sf reference) 33.20 KB −3 B (−0.0%) −21 B −21 B 33.20 KB ✅ assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, trace.js 9.02 KB, wire.js 2.78 KB
page: live server components (base + live/GET + action + isPending/latest) 37.42 KB −83 B (−0.2%) −21 B −161 B 37.53 KB ✅ assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, trace.js 9.06 KB, wire.js 2.78 KB
page: compiled base server components (the base page as JSX: templates with class/style/attributes/events, For/Show; no spread) 35.25 KB +41 B (+0.1%) −21 B −21 B 35.30 KB ✅ assets.js 0.78 KB, decode.js 6.25 KB, regions.js 1.06 KB, sc-comments.js 0.20 KB, trace.js 9.06 KB, wire.js 2.77 KB
page: compiled live server components (the compiled base page + live/GET + action + isPending/latest) 41.31 KB +21 B (+0.1%) −21 B −161 B 41.42 KB ✅ eager (counted): web.js 21.99 KB; assets.js 0.78 KB, decode.js 6.25 KB, regions.js 1.06 KB, sc-comments.js 0.19 KB, trace.js 9.08 KB, wire.js 2.77 KB
page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate) 40.73 KB +22 B (+0.1%) −21 B −21 B 40.72 KB ⚠️ over by 11 B, 41 B minified headroom assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.06 KB, server.js 1.02 KB, serverForms.js 3.59 KB, trace.js 9.13 KB, wire.js 2.77 KB
page: live + router (live page + @solidjs/router: createRouter, two routes, preload, useNavigate) 46.91 KB +25 B (+0.1%) −21 B −161 B 46.91 KB ✅ eager (counted): client.js 27.10 KB; assets.js 0.78 KB, decode.js 6.25 KB, lazy-page.js 0.04 KB, regions.js 1.07 KB, server.js 1.02 KB, serverForms.js 3.30 KB, trace.js 9.06 KB, wire.js 2.80 KB
server: floor (getRequestEvent + isServer) 1.33 KB 0 B 0 B 0 B 1.34 KB ✅
server: renderToString (the server-render floor) 20.62 KB 0 B 0 B −30 B 20.59 KB ⚠️ over by 31 B, 50 B minified headroom

⚠️ Over the brotli cap within the minified allowance (passes)

  • page: base + router (base page + @solidjs/router: createRouter, two routes, preload, useNavigate): over brotli cap by 11 B; minified 125,231 B vs 125,252 B recorded with the cap (−21 B) — 41 B of the 20 B minified allowance left; −21 B minified over this PR's base
  • server: renderToString (the server-render floor): over brotli cap by 31 B; minified 72,124 B vs 72,154 B recorded with the cap (−30 B) — 50 B of the 20 B minified allowance left; +0 B minified over this PR's base

Bundled with Rolldown (what Vite ships), brotli q11, decimal KB. A scenario fails only when it is over its brotli cap and its minified size is more than 20 B over the minified recorded with the cap; over the cap within that allowance is brotli layout noise and passes with a warning. Caps and their recorded minified in scripts/size/scenarios.js; the floor and page caps in floor-caps.json are frozen (lower only, or Size-Exception: in the PR body). npm run ratchet lowers caps per RC; it never raises one (scripts/size/README.md).

@github-actions

Copy link
Copy Markdown

Coverage Report for CI Build 39662771674

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Coverage remained the same at 77.662%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1349
Covered Lines: 1105
Line Coverage: 81.91%
Relevant Branches: 1055
Covered Branches: 762
Branch Coverage: 72.23%
Branches in Coverage %: Yes
Coverage Strength: 27.54 hits per line

💛 - Coveralls

@ryansolid
ryansolid merged commit 0222d68 into next Oct 21, 2026
7 checks passed
@codspeed

codspeed Bot commented Oct 21, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 12.01%

2 warnings

⚠️ 3 benchmarks are dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ Base and head ran on different runner types

Some benchmarks with significant performance changes were compared across different runner types, so the changes might not come from the code. Run base and head on the same runner type, or use CodSpeed Macro Runners for more stable results.

Learn more about Macro Runners

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 195 untouched benchmarks
⏩ 5 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ build + consume 777.5 µs 694.1 µs +12.01%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/server-function-uploads (e543ec9) with next (6cca8b5)

Open in CodSpeed

Footnotes

  1. 5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

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.

3 participants