fix(web): stop sending undefined as null before a trailing string - #3622
Conversation
🦋 Changeset detectedLatest commit: cc4a898 The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
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 |
Merging this PR will not alter performance
Comparing Footnotes
|
…; record the solidjs#3622 ruling in MATRIX.md Co-authored-by: Claude via Cursor <noreply@cursor.com>
c7b11b6 to
cc4a898
Compare
|
Summary
A server-function call whose last argument is a string sent an earlier
undefinedargument to the server asnull, so default parameters and=== undefinedchecks broke:initializeResponsehas a fast path for bound calls that end in a natural HTTP encoding (action.with(id)posting a form): the leading arguments go in?args=as JSON and the last argument is the body. It mapsundefinedtonullbefore theisJSONSafecheck, andgetHeadersAndBodycounts a plain string as a natural encoding. A string call can only reach that path when a leading argument is not JSON-safe, so in practice it got there only because of theundefinedmapping. This happened withenableRichArguments()enabled too.The fix keeps strings out of that fast path, so an
undefinedbefore a string goes to the codec. With rich arguments the function now receivesundefinedand the default applies. Without them the call throws the same "sent as JSON by default" error thatsearch(1, undefined)already throws. Bound form actions keep their current shape: before a FormData, URLSearchParams or File body,undefinedis still sent asnull, which matches the no-JS action url the router renders withhashKey(args). The existingserver-functions-body-formatsspec pins that behaviour.Dropping the mapping altogether was not chosen: without rich arguments every bound form action with an
undefinedbinding would start throwing. Limiting the fast path strictly to FormData, URLSearchParams and File was not chosen either: a trailing Blob would then go to the codec, which cannot serialize a Blob, sof("folder", blob)would break.The docs paragraph on argument encoding in
documentation/solid-2.0/10-server-functions.mdnow lists a top-levelundefinedamong the values that need rich arguments, and spells out the one exception (a bound call whose trailing argument is a body-like object). The ruling is recorded intest/server/server-function-matrix/MATRIX.md.Open question resolved: routing
undefinedto the codec is correct. Under the default config it now hits the pre-existing "sent as JSON by default" throw, the same as any otherundefinedargument; the bound fast path was the exception, not the rule.Public API changes
Documented behavior change (default codec):
fn(1, undefined, "str")previously resolved with the server seeingnullfor theundefinedargument. It now rejects client-side, before any request is sent, with the existing error:This is the same error
fn(1, undefined)already threw; only the trailing-string case is new.Rich codec (
enableRichArguments()):fn(1, undefined, "str")previously deliverednull; it now delivers a realundefined, so default parameters apply. No error path changes.Unchanged: bound calls whose trailing argument is FormData, URLSearchParams, File, Blob, ArrayBuffer or Uint8Array keep the
?args=[...,null]wire shape.No exports, options, or signatures added or removed.
How did you test this change?
The new
packages/web/test/server/server-functions-undefined-arguments.spec.tsxcovers:search(1, undefined, "milk")rejects and the function never runs. Before the fix it resolved withlimit=null.search(1, undefined, "milk")returnsid=1 limit=10 q=milkthrough the codec. Before the fix it returnedlimit=null.search(1, undefined)still rejects.search(1, 5, "milk")is still sent as the plain JSON body[1,5,"milk"].?args=[...,null]and their body format tag.Against the unfixed source, 2 of the tests failed, both with
limit=null. With the fix, all 7 pass.From
packages/web, afterpnpm build:scripts/size:npx size-limitshows all 10 entries passing with no size change (the frames entry does not retaininitializeResponse). A bundle importingcreateServerReferencegrows by 24 B minified (21 B gzip, 26 B brotli).