Skip to content

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

Closed
lxsmnsyc wants to merge 1 commit into
solidjs:nextfrom
lxsmnsyc:feat/server-function-uploads
Closed

lxsmnsyc wants to merge 1 commit into
solidjs:nextfrom
lxsmnsyc:feat/server-function-uploads

Conversation

@lxsmnsyc

Copy link
Copy Markdown
Member

With enableRichArguments(), a server function can now take files and blobs anywhere in its arguments. This ports solidjs/solid-start#2359 to the core transport.

Problem

A file only reached a server function as the only argument, or as the last argument of a bound call. Anywhere else, the call failed before it was sent:

await upload(file, { tags: ["a"] }); // The value [object File] ... cannot be parsed/serialized
await upload({ file });
await upload(formData, 1);

The codec's default plugin list has no Blob or File plugin.

Changes

  • The rich-args encoder finds each file with a seroval plugin made for the call. It looks wherever the codec meets one: 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 its own file<index> part, without base64.
  • The new BodyFormat.SerializedFormData ("10") marks the request. The Start PR uses "8", which is Json here.
  • The server decodes args with a plugin made for that request, which turns each index back into its part. Any other body decodes without that plugin, so an index in a plain codec body fails to decode.
  • A missing args field, a bad index, or a part that is not a file is refused with 400. bodySizeLimit bounds the whole body, which answers the "no total size limit" note on the Start PR.
  • A file used twice is sent once and arrives as one value. A blob from another frame is found by its shape and copied. A Blob arrives as a File named blob, with its type kept.
  • Response and Request arguments are still refused before sending.
  • Default JSON mode is unchanged. Without enableRichArguments(), these calls still throw, and the message now names files.
  • serializeArgs may return FormData. The core client only checks for it and picks the format.
  • The upload code lives in uploads.ts and ships only in the rich-args entry: +930 B minified there. The core client grows by 97 B minified.
  • The version skew spec moves its unknown tag from "10" to "11".
  • RFC 10 documents the behavior, and a changeset is included.

The Start PR is still open. If its design changes before it lands, this port should follow.

Testing

  • New server-functions-uploads.spec.tsx with 12 cases:
    • a file next to metadata, with raw bytes in the part;
    • files in arrays, objects, maps, sets and FormData;
    • a file used twice;
    • a blob's type;
    • a blob from another frame;
    • a call without files staying as codec text;
    • a refused Response;
    • a 413 over bodySizeLimit;
    • four malformed bodies answered with 400.
  • @solidjs/web unit (1395), server (1746) and hydrate (520) suites pass with JSX_COMPILER=babel.
  • test-types, the types build and Prettier pass.
  • Locally, my native compiler.node predates recent compiler changes, so about 50 unrelated tests fail with it. The same tests fail without this change and pass with Babel.
  • I didn't run the size tool locally, because it needs the native compiler. The sizes above come from minifying the built bundles directly.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Oct 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3a27300

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

@codspeed

codspeed Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Merging this PR will degrade performance by 27.21%

2 warnings

⚠️ 4 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

❌ 2 regressed benchmarks
✅ 194 untouched benchmarks
⏩ 5 skipped benchmarks1

Warning

Please fix the performance issues or acknowledge them on CodSpeed.

Performance Changes

Benchmark BASE HEAD Efficiency
❌ projection derive: write one NESTED field (reference) 881.6 µs 1,486.5 µs -40.69%
❌ build + consume 693.9 µs 776.6 µs -10.65%

Tip

Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.


Comparing lxsmnsyc:feat/server-function-uploads (b745f10) with next (9f64ca9)

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. ↩

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>
@lxsmnsyc
lxsmnsyc force-pushed the feat/server-function-uploads branch from 3a27300 to b745f10 Compare October 21, 2026 11:06
ryansolid added a commit that referenced this pull request Oct 21, 2026
Ports solidjs/solid-start#2359 (from #4177 by @lxsmnsyc), with uploads kept off the core client request path: they keep the Serialized tag and the handler spots them by their multipart content type.

Co-authored-by: Alexis H. Munsayac <alexis.munsayac@gmail.com>

Copy link
Copy Markdown
Member

Thanks! This landed as #4208, which carries your commit (credited as co-author) plus one change on top: uploads keep the Serialized tag and the handler spots them by their multipart/form-data content type. That way the core client's request path has no upload branch, and apps that don't use rich arguments pay 0 B (−21 B minified on the server-function scenarios). Closing in favor of it.

— Claude via Claude Code


Generated by Claude Code

@ryansolid ryansolid closed this Oct 21, 2026
@lxsmnsyc
lxsmnsyc deleted the feat/server-function-uploads branch October 22, 2026 00:13
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.

2 participants