From 77643e41c87b9e2a9dd896ad3e90311bef224ef5 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 08:53:42 +0800 Subject: [PATCH 01/20] add seroval json mode --- packages/start/src/config/index.ts | 5 + packages/start/src/server/serialization.ts | 253 ++++++++++++++++++ .../src/server/server-functions-handler.ts | 155 ++++------- packages/start/src/server/server-runtime.ts | 183 +++---------- 4 files changed, 345 insertions(+), 251 deletions(-) create mode 100644 packages/start/src/server/serialization.ts diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 4b1c82179..869aef0cf 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -21,6 +21,10 @@ export interface SolidStartOptions { routeDir?: string; extensions?: string[]; middleware?: string; + serialization?: { + // This only matters for server function responses + mode?: 'js' | 'json'; + }; } const absolute = (path: string, root: string) => @@ -131,6 +135,7 @@ export function solidStart(options?: SolidStartOptions): Array { "import.meta.env.START_APP_ENTRY": JSON.stringify(appEntryPath), "import.meta.env.START_CLIENT_ENTRY": JSON.stringify(handlers.client), "import.meta.env.START_DEV_OVERLAY": JSON.stringify(start.devOverlay), + "import.meta.env.SEROVAL_MODE": JSON.stringify(start.serialization?.mode || 'json'), }, builder: { sharedPlugins: true, diff --git a/packages/start/src/server/serialization.ts b/packages/start/src/server/serialization.ts new file mode 100644 index 000000000..c157d157e --- /dev/null +++ b/packages/start/src/server/serialization.ts @@ -0,0 +1,253 @@ +import { + crossSerializeStream, + deserialize, + Feature, + fromCrossJSON, + getCrossReferenceHeader, + type SerovalNode, + toCrossJSONStream, +} from "seroval"; +import { + AbortSignalPlugin, + CustomEventPlugin, + DOMExceptionPlugin, + EventPlugin, + FormDataPlugin, + HeadersPlugin, + ReadableStreamPlugin, + RequestPlugin, + ResponsePlugin, + URLPlugin, + URLSearchParamsPlugin, +} from "seroval-plugins/web"; + +// TODO(Alexis): if we can, allow providing an option to extend these. +const DEFAULT_PLUGINS = [ + AbortSignalPlugin, + CustomEventPlugin, + DOMExceptionPlugin, + EventPlugin, + FormDataPlugin, + HeadersPlugin, + ReadableStreamPlugin, + RequestPlugin, + ResponsePlugin, + URLSearchParamsPlugin, + URLPlugin, +]; +const MAX_SERIALIZATION_DEPTH_LIMIT = 64; +const DISABLED_FEATURES = Feature.RegExp; + +/** + * Alexis: + * + * A "chunk" is a piece of data emitted by the streaming serializer. + * Each chunk is represented by a 32-bit value (encoded in hexadecimal), + * followed by the encoded string (8-bit representation). This format + * is important so we know how much of the chunk being streamed we + * are expecting before parsing the entire string data. + * + * This is sort of a bootleg "multipart/form-data" except it's bad at + * handling File/Blob LOL + * + * The format is as follows: + * ;0xFFFFFFFF; + */ +function createChunk(data: string): Uint8Array { + const encodeData = new TextEncoder().encode(data); + const bytes = encodeData.length; + const baseHex = bytes.toString(16); + const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit + const head = new TextEncoder().encode(`;0x${totalHex};`); + + const chunk = new Uint8Array(12 + bytes); + chunk.set(head); + chunk.set(encodeData, 12); + return chunk; +} + +export function serializeToJSStream(id: string, value: any) { + return new ReadableStream({ + start(controller) { + crossSerializeStream(value, { + scopeId: id, + plugins: DEFAULT_PLUGINS, + onSerialize(data: string, initial: boolean) { + controller.enqueue( + createChunk( + initial ? `(${getCrossReferenceHeader(id)},${data})` : data, + ), + ); + }, + onDone() { + controller.close(); + }, + onError(error: any) { + controller.error(error); + }, + }); + }, + }); +} + +export function serializeToJSONStream(value: any) { + return new ReadableStream({ + start(controller) { + toCrossJSONStream(value, { + disabledFeatures: DISABLED_FEATURES, + depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT, + plugins: DEFAULT_PLUGINS, + onParse(node) { + controller.enqueue(createChunk(JSON.stringify(node))); + }, + onDone() { + controller.close(); + }, + onError(error) { + controller.error(error); + }, + }); + }, + }); +} + +class SerovalChunkReader { + reader: ReadableStreamDefaultReader; + buffer: Uint8Array; + done: boolean; + constructor(stream: ReadableStream) { + this.reader = stream.getReader(); + this.buffer = new Uint8Array(0); + this.done = false; + } + + async readChunk() { + // if there's no chunk, read again + const chunk = await this.reader.read(); + if (!chunk.done) { + // repopulate the buffer + const newBuffer = new Uint8Array(this.buffer.length + chunk.value.length); + newBuffer.set(this.buffer); + newBuffer.set(chunk.value, this.buffer.length); + this.buffer = newBuffer; + } else { + this.done = true; + } + } + + async next(): Promise< + { done: true; value: undefined } | { done: false; value: string } + > { + // Check if the buffer is empty + if (this.buffer.length === 0) { + // if we are already done... + if (this.done) { + return { + done: true, + value: undefined, + }; + } + // Otherwise, read a new chunk + await this.readChunk(); + return await this.next(); + } + // Read the "byte header" + // The byte header tells us how big the expected data is + // so we know how much data we should wait before we + // deserialize the data + const head = new TextDecoder().decode(this.buffer.subarray(1, 11)); + const bytes = Number.parseInt(head, 16); // ;0x00000000; + // Check if the buffer has enough bytes to be parsed + while (bytes > this.buffer.length - 12) { + // If it's not enough, and the reader is done + // then the chunk is invalid. + if (this.done) { + throw new Error("Malformed server function stream."); + } + // Otherwise, we read more chunks + await this.readChunk(); + } + // Extract the exact chunk as defined by the byte header + const partial = new TextDecoder().decode( + this.buffer.subarray(12, 12 + bytes), + ); + // The rest goes to the buffer + this.buffer = this.buffer.subarray(12 + bytes); + + // Deserialize the chunk + return { + done: false, + value: partial, + }; + } + + async drain(interpret: (chunk: string) => void) { + while (true) { + const result = await this.next(); + if (result.done) { + break; + } else { + interpret(result.value); + } + } + } +} + +export async function serializeToJSONString(value: any) { + const response = new Response(serializeToJSONStream(value)); + return await response.text(); +} + +export async function deserializeFromJSONString(json: string) { + const blob = new Response(json); + return await deserializeJSONStream(blob); +} + +export async function deserializeJSONStream(response: Response | Request) { + if (!response.body) { + throw new Error("missing body"); + } + const reader = new SerovalChunkReader(response.body); + const result = await reader.next(); + if (!result.done) { + const refs = new Map(); + + function interpretChunk(chunk: string): unknown { + const value = fromCrossJSON(JSON.parse(chunk) as SerovalNode, { + refs, + disabledFeatures: DISABLED_FEATURES, + depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT, + plugins: DEFAULT_PLUGINS, + }); + return value; + } + + void reader.drain(interpretChunk); + + return interpretChunk(result.value); + } + return undefined; +} + +export async function deserializeJSStream(id: string, response: Response) { + if (!response.body) { + throw new Error("missing body"); + } + const reader = new SerovalChunkReader(response.body); + + const result = await reader.next(); + + if (!result.done) { + reader.drain(deserialize).then( + () => { + // @ts-ignore + delete $R[id]; + }, + () => { + // no-op + }, + ); + return deserialize(result.value); + } + return undefined; +} diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index 160672684..e5912aaa1 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -1,74 +1,21 @@ -import { getServerFnById } from "solidstart:server-fn-manifest"; import { parseSetCookie } from "cookie-es"; import { type H3Event, parseCookies } from "h3"; -import { crossSerializeStream, fromJSON, getCrossReferenceHeader } from "seroval"; -import { - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLPlugin, - URLSearchParamsPlugin, -} from "seroval-plugins/web"; import { sharedConfig } from "solid-js"; import { renderToString } from "solid-js/web"; import { provideRequestEvent } from "solid-js/web/storage"; +import { getServerFnById } from "solidstart:server-fn-manifest"; import { getFetchEvent, mergeResponseHeaders } from "./fetchEvent.ts"; import { createPageEvent } from "./handler.ts"; +import { + deserializeFromJSONString, + deserializeJSONStream, + serializeToJSONStream, + serializeToJSStream, +} from "./serialization.ts"; import type { FetchEvent, PageEvent } from "./types.ts"; import { getExpectedRedirectStatus } from "./util.ts"; -function createChunk(data: string) { - const encodeData = new TextEncoder().encode(data); - const bytes = encodeData.length; - const baseHex = bytes.toString(16); - const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit - const head = new TextEncoder().encode(`;0x${totalHex};`); - - const chunk = new Uint8Array(12 + bytes); - chunk.set(head); - chunk.set(encodeData, 12); - return chunk; -} - -function serializeToStream(id: string, value: any) { - return new ReadableStream({ - start(controller) { - crossSerializeStream(value, { - scopeId: id, - plugins: [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin, - ], - onSerialize(data: string, initial: boolean) { - controller.enqueue( - createChunk(initial ? `(${getCrossReferenceHeader(id)},${data})` : data), - ); - }, - onDone() { - controller.close(); - }, - onError(error: any) { - controller.error(error); - }, - }); - }, - }); -} - export async function handleServerFunction(h3Event: H3Event) { const event = getFetchEvent(h3Event); const request = event.request; @@ -99,26 +46,10 @@ export async function handleServerFunction(h3Event: H3Event) { if (!instance || h3Event.method === "GET") { const args = url.searchParams.get("args"); if (args) { - const json = JSON.parse(args); - (json.t - ? (fromJSON(json, { - plugins: [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin, - ], - }) as any) - : json - ).forEach((arg: any) => { + const result = (await deserializeFromJSONString(args)) as any[]; + for (const arg of result) { parsed.push(arg); - }); + } } } if (h3Event.method === "POST") { @@ -129,21 +60,8 @@ export async function handleServerFunction(h3Event: H3Event) { contentType?.startsWith("application/x-www-form-urlencoded") ) { parsed.push(await event.request.formData()); - } else if (contentType?.startsWith("application/json")) { - parsed = fromJSON(await event.request.json(), { - plugins: [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin, - ], - }); + } else { + parsed = (await deserializeJSONStream(event.request.clone())) as any[]; } } try { @@ -178,9 +96,12 @@ export async function handleServerFunction(h3Event: H3Event) { // handle no JS success case if (!instance) return handleNoJS(result, request, parsed); - h3Event.res.headers.set("content-type", "text/javascript"); - - return serializeToStream(instance, result); + if (import.meta.env.SEROVAL_MODE === "js") { + h3Event.res.headers.set("content-type", "text/javascript"); + return serializeToJSStream(instance, result); + } + h3Event.res.headers.set("content-type", "text/plain"); + return serializeToJSONStream(result); } catch (x) { if (x instanceof Response) { if (singleFlight && instance) { @@ -189,28 +110,41 @@ export async function handleServerFunction(h3Event: H3Event) { // forward headers if ((x as any).headers) mergeResponseHeaders(h3Event, (x as any).headers); // forward non-redirect statuses - if ((x as any).status && (!instance || (x as any).status < 300 || (x as any).status >= 400)) + if ( + (x as any).status && + (!instance || (x as any).status < 300 || (x as any).status >= 400) + ) h3Event.res.status = (x as any).status; if ((x as any).customBody) { x = (x as any).customBody(); } else if ((x as any).body === undefined) x = null; h3Event.res.headers.set("X-Error", "true"); } else if (instance) { - const error = x instanceof Error ? x.message : typeof x === "string" ? x : "true"; + const error = + x instanceof Error ? x.message : typeof x === "string" ? x : "true"; h3Event.res.headers.set("X-Error", error.replace(/[\r\n]+/g, "")); } else { x = handleNoJS(x, request, parsed, true); } if (instance) { - h3Event.res.headers.set("content-type", "text/javascript"); - return serializeToStream(instance, x); + if (import.meta.env.SEROVAL_MODE === "js") { + h3Event.res.headers.set("content-type", "text/javascript"); + return serializeToJSStream(instance, x); + } + h3Event.res.headers.set("content-type", "text/plain"); + return serializeToJSONStream(x); } return x; } } -function handleNoJS(result: any, request: Request, parsed: any[], thrown?: boolean) { +function handleNoJS( + result: any, + request: Request, + parsed: any[], + thrown?: boolean, +) { const url = new URL(request.url); const isError = result instanceof Error; let statusCode = 302; @@ -220,7 +154,10 @@ function handleNoJS(result: any, request: Request, parsed: any[], thrown?: boole if (result.headers.has("Location")) { headers.set( `Location`, - new URL(result.headers.get("Location")!, url.origin + import.meta.env.BASE_URL).toString(), + new URL( + result.headers.get("Location")!, + url.origin + import.meta.env.BASE_URL, + ).toString(), ); statusCode = getExpectedRedirectStatus(result); } @@ -237,7 +174,10 @@ function handleNoJS(result: any, request: Request, parsed: any[], thrown?: boole result: isError ? result.message : result, thrown: thrown, error: isError, - input: [...parsed.slice(0, -1), [...parsed[parsed.length - 1].entries()]], + input: [ + ...parsed.slice(0, -1), + [...parsed[parsed.length - 1].entries()], + ], }), )}; Secure; HttpOnly;`, ); @@ -263,7 +203,7 @@ function createSingleFlightHeaders(sourceEvent: FetchEvent) { // useH3Internals = true; // sourceEvent.nativeEvent.node.req.headers.cookie = ""; // } - SetCookies.forEach(cookie => { + SetCookies.forEach((cookie) => { if (!cookie) return; const { maxAge, expires, name, value } = parseSetCookie(cookie); if (maxAge != null && maxAge <= 0) { @@ -284,7 +224,10 @@ function createSingleFlightHeaders(sourceEvent: FetchEvent) { return headers; } -async function handleSingleFlight(sourceEvent: FetchEvent, result: any): Promise { +async function handleSingleFlight( + sourceEvent: FetchEvent, + result: any, +): Promise { let revalidate: string[]; let url = new URL(sourceEvent.request.headers.get("referer")!).toString(); if (result instanceof Response) { diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index 8bbabe40b..d2be79ef9 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -1,122 +1,20 @@ // @ts-ignore - seroval exports issue with NodeNext -import { join } from "pathe"; -import { deserialize, toJSONAsync } from "seroval"; -import { - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLPlugin, - URLSearchParamsPlugin, -} from "seroval-plugins/web"; import { type Component } from "solid-js"; - -class SerovalChunkReader { - reader: ReadableStreamDefaultReader; - buffer: Uint8Array; - done: boolean; - constructor(stream: ReadableStream) { - this.reader = stream.getReader(); - this.buffer = new Uint8Array(0); - this.done = false; - } - - async readChunk() { - // if there's no chunk, read again - const chunk = await this.reader.read(); - if (!chunk.done) { - // repopulate the buffer - let newBuffer = new Uint8Array(this.buffer.length + chunk.value.length); - newBuffer.set(this.buffer); - newBuffer.set(chunk.value, this.buffer.length); - this.buffer = newBuffer; - } else { - this.done = true; - } - } - - async next(): Promise { - // Check if the buffer is empty - if (this.buffer.length === 0) { - // if we are already done... - if (this.done) { - return { - done: true, - value: undefined, - }; - } - // Otherwise, read a new chunk - await this.readChunk(); - return await this.next(); - } - // Read the "byte header" - // The byte header tells us how big the expected data is - // so we know how much data we should wait before we - // deserialize the data - const head = new TextDecoder().decode(this.buffer.subarray(1, 11)); - const bytes = Number.parseInt(head, 16); // ;0x00000000; - // Check if the buffer has enough bytes to be parsed - while (bytes > this.buffer.length - 12) { - // If it's not enough, and the reader is done - // then the chunk is invalid. - if (this.done) { - throw new Error("Malformed server function stream."); - } - // Otherwise, we read more chunks - await this.readChunk(); - } - // Extract the exact chunk as defined by the byte header - const partial = new TextDecoder().decode(this.buffer.subarray(12, 12 + bytes)); - // The rest goes to the buffer - this.buffer = this.buffer.subarray(12 + bytes); - - // Deserialize the chunk - return { - done: false, - value: deserialize(partial), - }; - } - - async drain() { - while (true) { - const result = await this.next(); - if (result.done) { - break; - } - } - } -} - -async function deserializeStream(id: string, response: Response) { - if (!response.body) { - throw new Error("missing body"); - } - const reader = new SerovalChunkReader(response.body); - - const result = await reader.next(); - - if (!result.done) { - reader.drain().then( - () => { - // @ts-ignore - delete $R[id]; - }, - () => { - // no-op - }, - ); - } - - return result.value; -} +import { + deserializeJSONStream, + deserializeJSStream, + serializeToJSONStream, + serializeToJSONString, +} from "./serialization"; let INSTANCE = 0; -function createRequest(base: string, id: string, instance: string, options: RequestInit) { +function createRequest( + base: string, + id: string, + instance: string, + options: RequestInit, +) { return fetch(base, { method: "POST", ...options, @@ -127,20 +25,6 @@ function createRequest(base: string, id: string, instance: string, options: Requ }, }); } - -const plugins = [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin, -]; - async function fetchServerFunction( base: string, id: string, @@ -154,15 +38,18 @@ async function fetchServerFunction( ? createRequest(base, id, instance, { ...options, body: args[0] }) : args.length === 1 && args[0] instanceof URLSearchParams ? createRequest(base, id, instance, { - ...options, - body: args[0], - headers: { ...options.headers, "Content-Type": "application/x-www-form-urlencoded" }, - }) + ...options, + body: args[0], + headers: { + ...options.headers, + "Content-Type": "application/x-www-form-urlencoded", + }, + }) : createRequest(base, id, instance, { - ...options, - body: JSON.stringify(await Promise.resolve(toJSONAsync(args, { plugins }))), - headers: { ...options.headers, "Content-Type": "application/json" }, - })); + ...options, + body: serializeToJSONStream(args), + headers: { ...options.headers, "Content-Type": "application/json" }, + })); if ( response.headers.has("Location") || @@ -172,7 +59,8 @@ async function fetchServerFunction( if (response.body) { /* @ts-ignore-next-line */ response.customBody = () => { - return deserializeStream(instance, response); + // TODO check for serialization mode + return deserializeJSStream(instance, response); }; } return response; @@ -184,8 +72,11 @@ async function fetchServerFunction( result = await response.text(); } else if (contentType && contentType.startsWith("application/json")) { result = await response.json(); + } else if (import.meta.env.SEROVAL_MODE === "js") { + // TODO check for serialization mode + result = await deserializeJSStream(instance, response); } else { - result = await deserializeStream(instance, response); + result = await deserializeJSONStream(response); } if (response.headers.has("X-Error")) { throw result; @@ -197,7 +88,8 @@ export function createServerReference(id: string) { let baseURL = import.meta.env.BASE_URL ?? "/"; if (!baseURL.endsWith("/")) baseURL += "/"; - const fn = (...args: any[]) => fetchServerFunction(`${baseURL}_server`, id, {}, args); + const fn = (...args: any[]) => + fetchServerFunction(`${baseURL}_server`, id, {}, args); return new Proxy(fn, { get(target, prop, receiver) { @@ -211,15 +103,16 @@ export function createServerReference(id: string) { const url = `${baseURL}_server?id=${encodeURIComponent(id)}`; return (options: RequestInit) => { const fn = async (...args: any[]) => { - const encodeArgs = options.method && options.method.toUpperCase() === "GET"; + const encodeArgs = + options.method && options.method.toUpperCase() === "GET"; return fetchServerFunction( encodeArgs ? url + - (args.length - ? `&args=${encodeURIComponent( - JSON.stringify(await Promise.resolve(toJSONAsync(args, { plugins }))), - )}` - : "") + (args.length + ? `&args=${encodeURIComponent( + await serializeToJSONString(args), + )}` + : "") : `${baseURL}_server`, id, options, From 05fde61c36a21775945626ec9c36eb04f9112440 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 09:00:33 +0800 Subject: [PATCH 02/20] Update server-runtime.ts --- packages/start/src/server/server-runtime.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index d2be79ef9..d2a51f2a7 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -59,8 +59,10 @@ async function fetchServerFunction( if (response.body) { /* @ts-ignore-next-line */ response.customBody = () => { - // TODO check for serialization mode - return deserializeJSStream(instance, response); + if (import.meta.env.SEROVAL_MODE === "js") { + return deserializeJSStream(instance, response); + } + return deserializeJSONStream(response); }; } return response; @@ -73,7 +75,6 @@ async function fetchServerFunction( } else if (contentType && contentType.startsWith("application/json")) { result = await response.json(); } else if (import.meta.env.SEROVAL_MODE === "js") { - // TODO check for serialization mode result = await deserializeJSStream(instance, response); } else { result = await deserializeJSONStream(response); From c06b5d0df8bdc3009aaa1caeb5ae230112531b58 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 09:06:24 +0800 Subject: [PATCH 03/20] Create plenty-geese-enter.md --- .changeset/plenty-geese-enter.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/plenty-geese-enter.md diff --git a/.changeset/plenty-geese-enter.md b/.changeset/plenty-geese-enter.md new file mode 100644 index 000000000..27c7c5457 --- /dev/null +++ b/.changeset/plenty-geese-enter.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": minor +--- + +seroval json mode From 90f5706a339dfd0f3a6a04ad086e6c1c7d43ed36 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 09:43:12 +0800 Subject: [PATCH 04/20] Fix client-server comms --- .../src/server/server-functions-handler.ts | 4 ++-- packages/start/src/server/server-runtime.ts | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index e5912aaa1..c536b940f 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -60,7 +60,7 @@ export async function handleServerFunction(h3Event: H3Event) { contentType?.startsWith("application/x-www-form-urlencoded") ) { parsed.push(await event.request.formData()); - } else { + } else if (contentType?.startsWith('text/plain')) { parsed = (await deserializeJSONStream(event.request.clone())) as any[]; } } @@ -96,11 +96,11 @@ export async function handleServerFunction(h3Event: H3Event) { // handle no JS success case if (!instance) return handleNoJS(result, request, parsed); + h3Event.res.headers.set("x-serialized", "true"); if (import.meta.env.SEROVAL_MODE === "js") { h3Event.res.headers.set("content-type", "text/javascript"); return serializeToJSStream(instance, result); } - h3Event.res.headers.set("content-type", "text/plain"); return serializeToJSONStream(result); } catch (x) { if (x instanceof Response) { diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index d2a51f2a7..3c0f2a201 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -47,8 +47,11 @@ async function fetchServerFunction( }) : createRequest(base, id, instance, { ...options, - body: serializeToJSONStream(args), - headers: { ...options.headers, "Content-Type": "application/json" }, + // TODO(Alexis): move to serializeToJSONStream + body: await serializeToJSONString(args), + // duplex: 'half', + // body: serializeToJSONStream(args), + headers: { ...options.headers, "Content-Type": "text/plain" }, })); if ( @@ -70,14 +73,16 @@ async function fetchServerFunction( const contentType = response.headers.get("Content-Type"); let result; - if (contentType && contentType.startsWith("text/plain")) { + if (contentType?.startsWith("text/plain")) { result = await response.text(); - } else if (contentType && contentType.startsWith("application/json")) { + } else if (contentType?.startsWith("application/json")) { result = await response.json(); - } else if (import.meta.env.SEROVAL_MODE === "js") { - result = await deserializeJSStream(instance, response); - } else { - result = await deserializeJSONStream(response); + } else if (response.headers.get('x-serialized')) { + if (import.meta.env.SEROVAL_MODE === "js") { + result = await deserializeJSStream(instance, response); + } else { + result = await deserializeJSONStream(response); + } } if (response.headers.has("X-Error")) { throw result; From 22ba63f716c05cf8de3b3f9fdba44053edc89fd3 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 09:43:17 +0800 Subject: [PATCH 05/20] Add tests --- apps/tests/src/e2e/server-function.test.ts | 5 ++++ .../tests/src/routes/server-function-ping.tsx | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 apps/tests/src/routes/server-function-ping.tsx diff --git a/apps/tests/src/e2e/server-function.test.ts b/apps/tests/src/e2e/server-function.test.ts index 7a8131be9..edba86fcc 100644 --- a/apps/tests/src/e2e/server-function.test.ts +++ b/apps/tests/src/e2e/server-function.test.ts @@ -67,4 +67,9 @@ test.describe("server-function", () => { await page.goto("http://localhost:3000/generator-server-function"); await expect(page.locator("#server-fn-test")).toContainText("¡Hola, Mundo!"); }); + + test("should build with a server function ping", async ({ page }) => { + await page.goto("http://localhost:3000/server-function-ping"); + await expect(page.locator("#server-fn-test")).toContainText('{"result":true}'); + }); }); diff --git a/apps/tests/src/routes/server-function-ping.tsx b/apps/tests/src/routes/server-function-ping.tsx new file mode 100644 index 000000000..70f9d2617 --- /dev/null +++ b/apps/tests/src/routes/server-function-ping.tsx @@ -0,0 +1,23 @@ +import { createEffect, createSignal } from "solid-js"; + +async function ping(value: string) { + "use server"; + + return await Promise.resolve(value); +} + +export default function App() { + const [output, setOutput] = createSignal<{ result?: boolean }>({}); + + createEffect(async () => { + const value = `${Math.random() * 1000}`; + const result = await ping(value); + setOutput(prev => ({ ...prev, result: value === result })); + }); + + return ( +
+ {JSON.stringify(output())} +
+ ); +} From 450b7d0f924660816adb4baa47a6da291ceb69c1 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 10:06:47 +0800 Subject: [PATCH 06/20] Add new header --- packages/start/src/server/server-functions-handler.ts | 4 +++- packages/start/src/server/server-runtime.ts | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index c536b940f..4881e8bbc 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -60,7 +60,9 @@ export async function handleServerFunction(h3Event: H3Event) { contentType?.startsWith("application/x-www-form-urlencoded") ) { parsed.push(await event.request.formData()); - } else if (contentType?.startsWith('text/plain')) { + } else if (contentType?.startsWith('application/json')) { + parsed = await event.request.json() as any[]; + } else if (request.headers.has('x-serialized')) { parsed = (await deserializeJSONStream(event.request.clone())) as any[]; } } diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index 3c0f2a201..723542a05 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -1,9 +1,9 @@ -// @ts-ignore - seroval exports issue with NodeNext + import { type Component } from "solid-js"; import { deserializeJSONStream, deserializeJSStream, - serializeToJSONStream, + // serializeToJSONStream, serializeToJSONString, } from "./serialization"; @@ -51,7 +51,11 @@ async function fetchServerFunction( body: await serializeToJSONString(args), // duplex: 'half', // body: serializeToJSONStream(args), - headers: { ...options.headers, "Content-Type": "text/plain" }, + headers: { + ...options.headers, + "x-serialized": "true", + "Content-Type": "text/plain" + }, })); if ( From abe6ec7f00b8eef0a74ab44ae5535e89e48d2c44 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 12:13:06 +0800 Subject: [PATCH 07/20] Update server-functions-handler.ts --- packages/start/src/server/server-functions-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index 4881e8bbc..6152fc8bd 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -130,11 +130,11 @@ export async function handleServerFunction(h3Event: H3Event) { x = handleNoJS(x, request, parsed, true); } if (instance) { + h3Event.res.headers.set("x-serialized", "true"); if (import.meta.env.SEROVAL_MODE === "js") { h3Event.res.headers.set("content-type", "text/javascript"); return serializeToJSStream(instance, x); } - h3Event.res.headers.set("content-type", "text/plain"); return serializeToJSONStream(x); } return x; From a80bfbb27f06eb1e68de42c4d655f0ec79f16de8 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Mon, 5 Jan 2026 13:10:35 +0800 Subject: [PATCH 08/20] Clone requests/responses --- .../start/src/server/server-functions-handler.ts | 9 +++++---- packages/start/src/server/server-runtime.ts | 13 +++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index 6152fc8bd..62aa27f69 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -52,18 +52,19 @@ export async function handleServerFunction(h3Event: H3Event) { } } } - if (h3Event.method === "POST") { + if (request.method === "POST") { const contentType = request.headers.get("content-type"); + const clone = request.clone(); if ( contentType?.startsWith("multipart/form-data") || contentType?.startsWith("application/x-www-form-urlencoded") ) { - parsed.push(await event.request.formData()); + parsed.push(await clone.formData()); } else if (contentType?.startsWith('application/json')) { - parsed = await event.request.json() as any[]; + parsed = await clone.json() as any[]; } else if (request.headers.has('x-serialized')) { - parsed = (await deserializeJSONStream(event.request.clone())) as any[]; + parsed = (await deserializeJSONStream(clone)) as any[]; } } try { diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index 723542a05..3a31cc2f7 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -67,25 +67,26 @@ async function fetchServerFunction( /* @ts-ignore-next-line */ response.customBody = () => { if (import.meta.env.SEROVAL_MODE === "js") { - return deserializeJSStream(instance, response); + return deserializeJSStream(instance, response.clone()); } - return deserializeJSONStream(response); + return deserializeJSONStream(response.clone()); }; } return response; } const contentType = response.headers.get("Content-Type"); + const clone = response.clone(); let result; if (contentType?.startsWith("text/plain")) { - result = await response.text(); + result = await clone.text(); } else if (contentType?.startsWith("application/json")) { - result = await response.json(); + result = await clone.json(); } else if (response.headers.get('x-serialized')) { if (import.meta.env.SEROVAL_MODE === "js") { - result = await deserializeJSStream(instance, response); + result = await deserializeJSStream(instance, clone); } else { - result = await deserializeJSONStream(response); + result = await deserializeJSONStream(clone); } } if (response.headers.has("X-Error")) { From a39958a9bbb66454b5d63e73f0b6450b413e4d2f Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Thu, 15 Jan 2026 02:15:25 +0800 Subject: [PATCH 09/20] Add more formats --- .../src/server/server-functions-handler.ts | 46 ++++-- .../src/server/server-functions-shared.ts | 15 ++ packages/start/src/server/server-runtime.ts | 138 ++++++++++++++---- 3 files changed, 160 insertions(+), 39 deletions(-) create mode 100644 packages/start/src/server/server-functions-shared.ts diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index 62aa27f69..0f7c22ce3 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -13,6 +13,7 @@ import { serializeToJSONStream, serializeToJSStream, } from "./serialization.ts"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared.ts"; import type { FetchEvent, PageEvent } from "./types.ts"; import { getExpectedRedirectStatus } from "./util.ts"; @@ -43,7 +44,7 @@ export async function handleServerFunction(h3Event: H3Event) { let parsed: any[] = []; // grab bound arguments from url when no JS - if (!instance || h3Event.method === "GET") { + if (!instance || request.method === "GET") { const args = url.searchParams.get("args"); if (args) { const result = (await deserializeFromJSONString(args)) as any[]; @@ -54,17 +55,38 @@ export async function handleServerFunction(h3Event: H3Event) { } if (request.method === "POST") { const contentType = request.headers.get("content-type"); + const startType = request.headers.get(BODY_FORMAT_KEY); const clone = request.clone(); - if ( - contentType?.startsWith("multipart/form-data") || - contentType?.startsWith("application/x-www-form-urlencoded") - ) { - parsed.push(await clone.formData()); - } else if (contentType?.startsWith('application/json')) { - parsed = await clone.json() as any[]; - } else if (request.headers.has('x-serialized')) { - parsed = (await deserializeJSONStream(clone)) as any[]; + switch (true) { + case startType === BodyFormat.Seroval: + parsed = (await deserializeJSONStream(clone)) as any[]; + break; + case startType === BodyFormat.String: + parsed.push(await clone.text()); + break; + case startType === BodyFormat.File: { + const formData = await clone.formData(); + parsed.push(formData.get(BODY_FORMAL_FILE)); + break; + } + case startType === BodyFormat.FormData: + case contentType?.startsWith("multipart/form-data"): + parsed.push(await clone.formData()); + break; + case startType === BodyFormat.URLSearchParams: + case contentType?.startsWith("application/x-www-form-urlencoded"): + parsed.push(new URLSearchParams(await clone.text())); + break; + case startType === BodyFormat.Blob: + parsed.push(await clone.blob()); + break; + case startType === BodyFormat.ArrayBuffer: + parsed.push(await clone.arrayBuffer()); + break; + case startType === BodyFormat.Uint8Array: + parsed.push(await clone.bytes()); + break; } } try { @@ -92,14 +114,14 @@ export async function handleServerFunction(h3Event: H3Event) { h3Event.res.status = result.status; if ((result as any).customBody) { result = await (result as any).customBody(); - } else if (result.body == undefined) result = null; + } else if (result.body == null) result = null; } } // handle no JS success case if (!instance) return handleNoJS(result, request, parsed); - h3Event.res.headers.set("x-serialized", "true"); + h3Event.res.headers.set(BODY_FORMAT_KEY, "true"); if (import.meta.env.SEROVAL_MODE === "js") { h3Event.res.headers.set("content-type", "text/javascript"); return serializeToJSStream(instance, result); diff --git a/packages/start/src/server/server-functions-shared.ts b/packages/start/src/server/server-functions-shared.ts new file mode 100644 index 000000000..f7c253dd8 --- /dev/null +++ b/packages/start/src/server/server-functions-shared.ts @@ -0,0 +1,15 @@ + +export const BODY_FORMAT_KEY = "X-Start-Type"; + +export const BODY_FORMAL_FILE = "__START__"; + +export const enum BodyFormat { + Seroval = "0", + String = "1", + FormData = "2", + URLSearchParams = "3", + Blob = "4", + File = "5", + ArrayBuffer = "6", + Uint8Array = "7", +} diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index 3a31cc2f7..b1496fd31 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -1,4 +1,3 @@ - import { type Component } from "solid-js"; import { deserializeJSONStream, @@ -6,6 +5,7 @@ import { // serializeToJSONStream, serializeToJSONString, } from "./serialization"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared"; let INSTANCE = 0; @@ -25,6 +25,113 @@ function createRequest( }, }); } + +function getHeadersAndBody(body: any): { + headers?: HeadersInit; + body: BodyInit; +} | undefined { + switch (true) { + case typeof body === "string": + return { + headers: { + "Content-Type": "text/plain", + [BODY_FORMAT_KEY]: BodyFormat.String, + }, + body, + }; + case body instanceof FormData: + return { + headers: { + "Content-Type": "multipart/form-data", + [BODY_FORMAT_KEY]: BodyFormat.FormData, + }, + body, + }; + case body instanceof URLSearchParams: + return { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + [BODY_FORMAT_KEY]: BodyFormat.URLSearchParams, + }, + body, + }; + case body instanceof Blob: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.Blob, + }, + body, + }; + case body instanceof File: { + const formData = new FormData(); + formData.append(BODY_FORMAL_FILE, body, body.name); + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.File, + }, + body: new FormData(), + }; + } + case body instanceof ArrayBuffer: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.ArrayBuffer, + }, + body, + }; + case body instanceof Uint8Array: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.Uint8Array, + }, + body: new Uint8Array(body), + }; + default: + return undefined; + } +} + +async function initializeResponse( + base: string, + id: string, + instance: string, + options: RequestInit, + args: any[], +) { + // No args, skip serialization + if (args.length === 0) { + return createRequest(base, id, instance, options); + } + // For single arguments, we can directly encode as body + if (args.length === 1) { + const body = args[0]; + const result = getHeadersAndBody(body); + if (result) { + return createRequest(base, id, instance, { + ...options, + body: result.body, + headers: { + ...options.headers, + ...result.headers, + }, + }); + } + } + // Fallback to seroval + return createRequest(base, id, instance, { + ...options, + // TODO(Alexis): move to serializeToJSONStream + body: await serializeToJSONString(args), + // duplex: 'half', + // body: serializeToJSONStream(args), + headers: { + ...options.headers, + "Content-Type": "text/plain", + [BODY_FORMAT_KEY]: BodyFormat.Seroval, + }, + }); +} + async function fetchServerFunction( base: string, id: string, @@ -32,31 +139,8 @@ async function fetchServerFunction( args: any[], ) { const instance = `server-fn:${INSTANCE++}`; - const response = await (args.length === 0 - ? createRequest(base, id, instance, options) - : args.length === 1 && args[0] instanceof FormData - ? createRequest(base, id, instance, { ...options, body: args[0] }) - : args.length === 1 && args[0] instanceof URLSearchParams - ? createRequest(base, id, instance, { - ...options, - body: args[0], - headers: { - ...options.headers, - "Content-Type": "application/x-www-form-urlencoded", - }, - }) - : createRequest(base, id, instance, { - ...options, - // TODO(Alexis): move to serializeToJSONStream - body: await serializeToJSONString(args), - // duplex: 'half', - // body: serializeToJSONStream(args), - headers: { - ...options.headers, - "x-serialized": "true", - "Content-Type": "text/plain" - }, - })); + + const response = await initializeResponse(base, id, instance, options, args); if ( response.headers.has("Location") || @@ -82,7 +166,7 @@ async function fetchServerFunction( result = await clone.text(); } else if (contentType?.startsWith("application/json")) { result = await clone.json(); - } else if (response.headers.get('x-serialized')) { + } else if (response.headers.get(BODY_FORMAT_KEY)) { if (import.meta.env.SEROVAL_MODE === "js") { result = await deserializeJSStream(instance, clone); } else { From a44176d1af00828d263ec0d99d0c3e429f843795 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Thu, 15 Jan 2026 02:17:41 +0800 Subject: [PATCH 10/20] Fix `File` encoding --- packages/start/src/server/server-runtime.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index b1496fd31..46d07808c 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -55,13 +55,6 @@ function getHeadersAndBody(body: any): { }, body, }; - case body instanceof Blob: - return { - headers: { - [BODY_FORMAT_KEY]: BodyFormat.Blob, - }, - body, - }; case body instanceof File: { const formData = new FormData(); formData.append(BODY_FORMAL_FILE, body, body.name); @@ -69,9 +62,16 @@ function getHeadersAndBody(body: any): { headers: { [BODY_FORMAT_KEY]: BodyFormat.File, }, - body: new FormData(), + body: formData, }; } + case body instanceof Blob: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.Blob, + }, + body, + }; case body instanceof ArrayBuffer: return { headers: { From 8488a5ec9614aec10b764cf8c548ca2d38a67deb Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Thu, 15 Jan 2026 11:16:00 +0800 Subject: [PATCH 11/20] Update server-functions-handler.ts --- packages/start/src/server/server-functions-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index 0f7c22ce3..c03340dae 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -153,7 +153,7 @@ export async function handleServerFunction(h3Event: H3Event) { x = handleNoJS(x, request, parsed, true); } if (instance) { - h3Event.res.headers.set("x-serialized", "true"); + h3Event.res.headers.set(BODY_FORMAT_KEY, "true"); if (import.meta.env.SEROVAL_MODE === "js") { h3Event.res.headers.set("content-type", "text/javascript"); return serializeToJSStream(instance, x); From 1b2464bde1846d261015b65b1ab5bb5a286cb7ba Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Thu, 15 Jan 2026 11:17:48 +0800 Subject: [PATCH 12/20] Update server-runtime.ts --- packages/start/src/server/server-runtime.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index 46d07808c..c0f4ec4d6 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -4,8 +4,8 @@ import { deserializeJSStream, // serializeToJSONStream, serializeToJSONString, -} from "./serialization"; -import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared"; +} from "./serialization.ts"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared.ts"; let INSTANCE = 0; From db926e9c50b9305f838032a571e377170df21300 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 16 Jan 2026 08:05:01 +0800 Subject: [PATCH 13/20] Fix `FormData` missing boundary --- packages/start/src/server/server-runtime.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index c0f4ec4d6..cb2b889f2 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -42,7 +42,6 @@ function getHeadersAndBody(body: any): { case body instanceof FormData: return { headers: { - "Content-Type": "multipart/form-data", [BODY_FORMAT_KEY]: BodyFormat.FormData, }, body, From 4bbe94bbed08a8df42e1814507b8fdd4548d3640 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 16 Jan 2026 16:42:23 +0800 Subject: [PATCH 14/20] Add form-data test --- apps/tests/src/e2e/server-function.test.ts | 5 ++++ .../src/routes/server-function-form-data.tsx | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 apps/tests/src/routes/server-function-form-data.tsx diff --git a/apps/tests/src/e2e/server-function.test.ts b/apps/tests/src/e2e/server-function.test.ts index edba86fcc..a4dbc3746 100644 --- a/apps/tests/src/e2e/server-function.test.ts +++ b/apps/tests/src/e2e/server-function.test.ts @@ -72,4 +72,9 @@ test.describe("server-function", () => { await page.goto("http://localhost:3000/server-function-ping"); await expect(page.locator("#server-fn-test")).toContainText('{"result":true}'); }); + + test("should build with a server function w/ form data", async ({ page }) => { + await page.goto("http://localhost:3000/server-form-data"); + await expect(page.locator("#server-fn-test")).toContainText('{"result":true}'); + }); }); diff --git a/apps/tests/src/routes/server-function-form-data.tsx b/apps/tests/src/routes/server-function-form-data.tsx new file mode 100644 index 000000000..d54ad095a --- /dev/null +++ b/apps/tests/src/routes/server-function-form-data.tsx @@ -0,0 +1,26 @@ +import { createEffect, createSignal } from "solid-js"; + +async function ping(value: FormData) { + "use server"; + const file = value.get('example') as File; + return await file.text(); +} + +export default function App() { + const [output, setOutput] = createSignal<{ result?: boolean }>({}); + + createEffect(async () => { + const file = new File(['Hello, World!'], 'hello-world.txt'); + const formData = new FormData(); + formData.append('example', file); + const result = await ping(formData); + const value = await file.text(); + setOutput(prev => ({ ...prev, result: value === result })); + }); + + return ( +
+ {JSON.stringify(output())} +
+ ); +} From 28064a6620e1dd4dc6f3c72b5db7fc25aeb3651e Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 16 Jan 2026 16:46:36 +0800 Subject: [PATCH 15/20] fix tests --- apps/tests/src/e2e/server-function.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tests/src/e2e/server-function.test.ts b/apps/tests/src/e2e/server-function.test.ts index a4dbc3746..861495e3b 100644 --- a/apps/tests/src/e2e/server-function.test.ts +++ b/apps/tests/src/e2e/server-function.test.ts @@ -74,7 +74,7 @@ test.describe("server-function", () => { }); test("should build with a server function w/ form data", async ({ page }) => { - await page.goto("http://localhost:3000/server-form-data"); + await page.goto("http://localhost:3000/server-function-form-data"); await expect(page.locator("#server-fn-test")).toContainText('{"result":true}'); }); }); From 4ddcff31a6ff167efcf645dcf6a81710ba08e79e Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 23 Jan 2026 15:02:13 +0800 Subject: [PATCH 16/20] Bump `seroval` to `1.5.0` --- packages/start/package.json | 2 +- pnpm-lock.yaml | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/start/package.json b/packages/start/package.json index 9170468d3..af2bbca6c 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -56,7 +56,7 @@ "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", - "seroval": "^1.4.1", + "seroval": "^1.5.0", "seroval-plugins": "^1.4.0", "shiki": "^1.26.1", "solid-js": "^1.9.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d00c64b91..2d030c9f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -368,11 +368,11 @@ importers: specifier: ^1.1.2 version: 1.1.2 seroval: - specifier: ^1.4.1 - version: 1.4.1 + specifier: ^1.5.0 + version: 1.5.0 seroval-plugins: specifier: ^1.4.0 - version: 1.4.0(seroval@1.4.1) + version: 1.4.0(seroval@1.5.0) shiki: specifier: ^1.26.1 version: 1.26.1 @@ -4851,8 +4851,8 @@ packages: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} - seroval@1.4.1: - resolution: {integrity: sha512-9GOc+8T6LN4aByLN75uRvMbrwY5RDBW6lSlknsY4LEa9ZmWcxKcRe1G/Q3HZXjltxMHTrStnvrwAICxZrhldtg==} + seroval@1.5.0: + resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} engines: {node: '>=10'} serve-placeholder@2.0.2: @@ -10519,13 +10519,13 @@ snapshots: dependencies: seroval: 1.3.2 - seroval-plugins@1.4.0(seroval@1.4.1): + seroval-plugins@1.4.0(seroval@1.5.0): dependencies: - seroval: 1.4.1 + seroval: 1.5.0 seroval@1.3.2: {} - seroval@1.4.1: {} + seroval@1.5.0: {} serve-placeholder@2.0.2: dependencies: From 1fa3f7ca0ec523232608e7f9e955f8e4638e145e Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sat, 24 Jan 2026 14:20:03 +0800 Subject: [PATCH 17/20] Bump `seroval-plugins` --- packages/start/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/start/package.json b/packages/start/package.json index af2bbca6c..40ea19875 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -57,7 +57,7 @@ "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.5.0", - "seroval-plugins": "^1.4.0", + "seroval-plugins": "^1.5.0", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d030c9f8..508daf5c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -371,8 +371,8 @@ importers: specifier: ^1.5.0 version: 1.5.0 seroval-plugins: - specifier: ^1.4.0 - version: 1.4.0(seroval@1.5.0) + specifier: ^1.5.0 + version: 1.5.0(seroval@1.5.0) shiki: specifier: ^1.26.1 version: 1.26.1 @@ -4841,8 +4841,8 @@ packages: peerDependencies: seroval: ^1.0 - seroval-plugins@1.4.0: - resolution: {integrity: sha512-zir1aWzoiax6pbBVjoYVd0O1QQXgIL3eVGBMsBsNmM8Ukq90yGaWlfx0AB9dTS8GPqrOrbXn79vmItCUP9U3BQ==} + seroval-plugins@1.5.0: + resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 @@ -10519,7 +10519,7 @@ snapshots: dependencies: seroval: 1.3.2 - seroval-plugins@1.4.0(seroval@1.5.0): + seroval-plugins@1.5.0(seroval@1.5.0): dependencies: seroval: 1.5.0 From 5e08e6259e45b160112658b27afb84df39b895d8 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Mon, 2 Feb 2026 00:15:24 +0800 Subject: [PATCH 18/20] Add bidirectional encoding --- .../tests/src/routes/server-function-blob.tsx | 28 +++++ packages/start/src/server/serialization.ts | 2 +- .../src/server/server-functions-handler.ts | 64 ++++------- .../src/server/server-functions-shared.ts | 101 ++++++++++++++++++ packages/start/src/server/server-runtime.ts | 80 +------------- 5 files changed, 153 insertions(+), 122 deletions(-) create mode 100644 apps/tests/src/routes/server-function-blob.tsx diff --git a/apps/tests/src/routes/server-function-blob.tsx b/apps/tests/src/routes/server-function-blob.tsx new file mode 100644 index 000000000..31de2cb38 --- /dev/null +++ b/apps/tests/src/routes/server-function-blob.tsx @@ -0,0 +1,28 @@ +import { createEffect, createSignal } from "solid-js"; + +async function ping(value: Blob) { + "use server"; + return value; +} + +const blobURI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAxNjYgMTU1LjMnPjxwYXRoIGQ9J00xNjMgMzVTMTEwLTQgNjkgNWwtMyAxYy02IDItMTEgNS0xNCA5bC0yIDMtMTUgMjYgMjYgNWMxMSA3IDI1IDEwIDM4IDdsNDYgOSAxOC0zMHonIGZpbGw9JyM3NmIzZTEnLz48bGluZWFyR3JhZGllbnQgaWQ9J2EnIGdyYWRpZW50VW5pdHM9J3VzZXJTcGFjZU9uVXNlJyB4MT0nMjcuNScgeTE9JzMnIHgyPScxNTInIHkyPSc2My41Jz48c3RvcCBvZmZzZXQ9Jy4xJyBzdG9wLWNvbG9yPScjNzZiM2UxJy8+PHN0b3Agb2Zmc2V0PScuMycgc3RvcC1jb2xvcj0nI2RjZjJmZCcvPjxzdG9wIG9mZnNldD0nMScgc3RvcC1jb2xvcj0nIzc2YjNlMScvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0nTTE2MyAzNVMxMTAtNCA2OSA1bC0zIDFjLTYgMi0xMSA1LTE0IDlsLTIgMy0xNSAyNiAyNiA1YzExIDcgMjUgMTAgMzggN2w0NiA5IDE4LTMweicgb3BhY2l0eT0nLjMnIGZpbGw9J3VybCgjYSknLz48cGF0aCBkPSdNNTIgMzVsLTQgMWMtMTcgNS0yMiAyMS0xMyAzNSAxMCAxMyAzMSAyMCA0OCAxNWw2Mi0yMVM5MiAyNiA1MiAzNXonIGZpbGw9JyM1MThhYzgnLz48bGluZWFyR3JhZGllbnQgaWQ9J2InIGdyYWRpZW50VW5pdHM9J3VzZXJTcGFjZU9uVXNlJyB4MT0nOTUuOCcgeTE9JzMyLjYnIHgyPSc3NCcgeTI9JzEwNS4yJz48c3RvcCBvZmZzZXQ9JzAnIHN0b3AtY29sb3I9JyM3NmIzZTEnLz48c3RvcCBvZmZzZXQ9Jy41JyBzdG9wLWNvbG9yPScjNDM3N2JiJy8+PHN0b3Agb2Zmc2V0PScxJyBzdG9wLWNvbG9yPScjMWYzYjc3Jy8+PC9saW5lYXJHcmFkaWVudD48cGF0aCBkPSdNNTIgMzVsLTQgMWMtMTcgNS0yMiAyMS0xMyAzNSAxMCAxMyAzMSAyMCA0OCAxNWw2Mi0yMVM5MiAyNiA1MiAzNXonIG9wYWNpdHk9Jy4zJyBmaWxsPSd1cmwoI2IpJy8+PGxpbmVhckdyYWRpZW50IGlkPSdjJyBncmFkaWVudFVuaXRzPSd1c2VyU3BhY2VPblVzZScgeDE9JzE4LjQnIHkxPSc2NC4yJyB4Mj0nMTQ0LjMnIHkyPScxNDkuOCc+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjMzE1YWE5Jy8+PHN0b3Agb2Zmc2V0PScuNScgc3RvcC1jb2xvcj0nIzUxOGFjOCcvPjxzdG9wIG9mZnNldD0nMScgc3RvcC1jb2xvcj0nIzMxNWFhOScvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0nTTEzNCA4MGE0NSA0NSAwIDAwLTQ4LTE1TDI0IDg1IDQgMTIwbDExMiAxOSAyMC0zNmM0LTcgMy0xNS0yLTIzeicgZmlsbD0ndXJsKCNjKScvPjxsaW5lYXJHcmFkaWVudCBpZD0nZCcgZ3JhZGllbnRVbml0cz0ndXNlclNwYWNlT25Vc2UnIHgxPSc3NS4yJyB5MT0nNzQuNScgeDI9JzI0LjQnIHkyPScyNjAuOCc+PHN0b3Agb2Zmc2V0PScwJyBzdG9wLWNvbG9yPScjNDM3N2JiJy8+PHN0b3Agb2Zmc2V0PScuNScgc3RvcC1jb2xvcj0nIzFhMzM2YicvPjxzdG9wIG9mZnNldD0nMScgc3RvcC1jb2xvcj0nIzFhMzM2YicvPjwvbGluZWFyR3JhZGllbnQ+PHBhdGggZD0nTTExNCAxMTVhNDUgNDUgMCAwMC00OC0xNUw0IDEyMHM1MyA0MCA5NCAzMGwzLTFjMTctNSAyMy0yMSAxMy0zNHonIGZpbGw9J3VybCgjZCknLz48L3N2Zz4='; + +export default function App() { + const [output, setOutput] = createSignal<{ result?: boolean }>({}); + + createEffect(async () => { + const request = await fetch(blobURI); + const blob = await request.blob(); + const result = await ping(blob); + const value = await blob.text(); + const test = await result.text(); + + setOutput(prev => ({ ...prev, result: value === test })); + }); + + return ( +
+ {JSON.stringify(output())} +
+ ); +} diff --git a/packages/start/src/server/serialization.ts b/packages/start/src/server/serialization.ts index c157d157e..a0c269505 100644 --- a/packages/start/src/server/serialization.ts +++ b/packages/start/src/server/serialization.ts @@ -229,7 +229,7 @@ export async function deserializeJSONStream(response: Response | Request) { return undefined; } -export async function deserializeJSStream(id: string, response: Response) { +export async function deserializeJSStream(id: string, response: Request | Response) { if (!response.body) { throw new Error("missing body"); } diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index c03340dae..b8e95888a 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -9,11 +9,10 @@ import { getFetchEvent, mergeResponseHeaders } from "./fetchEvent.ts"; import { createPageEvent } from "./handler.ts"; import { deserializeFromJSONString, - deserializeJSONStream, serializeToJSONStream, serializeToJSStream, } from "./serialization.ts"; -import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared.ts"; +import { BODY_FORMAT_KEY, BodyFormat, extractBody, getHeadersAndBody } from "./server-functions-shared.ts"; import type { FetchEvent, PageEvent } from "./types.ts"; import { getExpectedRedirectStatus } from "./util.ts"; @@ -54,40 +53,7 @@ export async function handleServerFunction(h3Event: H3Event) { } } if (request.method === "POST") { - const contentType = request.headers.get("content-type"); - const startType = request.headers.get(BODY_FORMAT_KEY); - const clone = request.clone(); - - switch (true) { - case startType === BodyFormat.Seroval: - parsed = (await deserializeJSONStream(clone)) as any[]; - break; - case startType === BodyFormat.String: - parsed.push(await clone.text()); - break; - case startType === BodyFormat.File: { - const formData = await clone.formData(); - parsed.push(formData.get(BODY_FORMAL_FILE)); - break; - } - case startType === BodyFormat.FormData: - case contentType?.startsWith("multipart/form-data"): - parsed.push(await clone.formData()); - break; - case startType === BodyFormat.URLSearchParams: - case contentType?.startsWith("application/x-www-form-urlencoded"): - parsed.push(new URLSearchParams(await clone.text())); - break; - case startType === BodyFormat.Blob: - parsed.push(await clone.blob()); - break; - case startType === BodyFormat.ArrayBuffer: - parsed.push(await clone.arrayBuffer()); - break; - case startType === BodyFormat.Uint8Array: - parsed.push(await clone.bytes()); - break; - } + parsed.push(await extractBody('', false, request.clone())); } try { let result = await provideRequestEvent(event, async () => { @@ -121,12 +87,18 @@ export async function handleServerFunction(h3Event: H3Event) { // handle no JS success case if (!instance) return handleNoJS(result, request, parsed); - h3Event.res.headers.set(BODY_FORMAT_KEY, "true"); - if (import.meta.env.SEROVAL_MODE === "js") { - h3Event.res.headers.set("content-type", "text/javascript"); - return serializeToJSStream(instance, result); - } - return serializeToJSONStream(result); + const body = getHeadersAndBody(result); + if (body) { + return new Response(body.body, { + headers: body.headers, + }); + } + h3Event.res.headers.set(BODY_FORMAT_KEY, BodyFormat.Seroval); + if (import.meta.env.SEROVAL_MODE === "js") { + h3Event.res.headers.set("content-type", "text/javascript"); + return serializeToJSStream(instance, result); + } + return serializeToJSONStream(result); } catch (x) { if (x instanceof Response) { if (singleFlight && instance) { @@ -153,7 +125,13 @@ export async function handleServerFunction(h3Event: H3Event) { x = handleNoJS(x, request, parsed, true); } if (instance) { - h3Event.res.headers.set(BODY_FORMAT_KEY, "true"); + const body = getHeadersAndBody(x); + if (body) { + return new Response(body.body, { + headers: body.headers, + }); + } + h3Event.res.headers.set(BODY_FORMAT_KEY, BodyFormat.Seroval); if (import.meta.env.SEROVAL_MODE === "js") { h3Event.res.headers.set("content-type", "text/javascript"); return serializeToJSStream(instance, x); diff --git a/packages/start/src/server/server-functions-shared.ts b/packages/start/src/server/server-functions-shared.ts index f7c253dd8..5a6f6624a 100644 --- a/packages/start/src/server/server-functions-shared.ts +++ b/packages/start/src/server/server-functions-shared.ts @@ -1,3 +1,4 @@ +import { deserializeJSONStream, deserializeJSStream } from "./serialization"; export const BODY_FORMAT_KEY = "X-Start-Type"; @@ -13,3 +14,103 @@ export const enum BodyFormat { ArrayBuffer = "6", Uint8Array = "7", } + +export function getHeadersAndBody(body: any): + | { + headers?: HeadersInit; + body: BodyInit; + } + | undefined { + switch (true) { + case typeof body === "string": + return { + headers: { + "Content-Type": "text/plain", + [BODY_FORMAT_KEY]: BodyFormat.String, + }, + body, + }; + case body instanceof FormData: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.FormData, + }, + body, + }; + case body instanceof URLSearchParams: + return { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + [BODY_FORMAT_KEY]: BodyFormat.URLSearchParams, + }, + body, + }; + case body instanceof File: { + const formData = new FormData(); + formData.append(BODY_FORMAL_FILE, body, body.name); + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.File, + }, + body: formData, + }; + } + case body instanceof Blob: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.Blob, + }, + body, + }; + case body instanceof ArrayBuffer: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.ArrayBuffer, + }, + body, + }; + case body instanceof Uint8Array: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.Uint8Array, + }, + body: new Uint8Array(body), + }; + default: + return undefined; + } +} + +export async function extractBody(instance: string, client: boolean, source: Request | Response) { + const contentType = source.headers.get("content-type"); + const startType = source.headers.get(BODY_FORMAT_KEY); + const clone = source.clone(); + + switch (true) { + case startType === BodyFormat.Seroval: + if (client && import.meta.env.SEROVAL_MODE === "js") { + return await deserializeJSStream(instance, clone); + } + return await deserializeJSONStream(clone); + case startType === BodyFormat.String: + return await clone.text(); + case startType === BodyFormat.File: { + const formData = await clone.formData(); + return formData.get(BODY_FORMAL_FILE); + } + case startType === BodyFormat.FormData: + case contentType?.startsWith("multipart/form-data"): + return await clone.formData(); + case startType === BodyFormat.URLSearchParams: + case contentType?.startsWith("application/x-www-form-urlencoded"): + return new URLSearchParams(await clone.text()); + case startType === BodyFormat.Blob: + return await clone.blob(); + case startType === BodyFormat.ArrayBuffer: + return await clone.arrayBuffer(); + case startType === BodyFormat.Uint8Array: + return await clone.bytes(); + } + + throw new Error("Unknown body format"); +} diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index cb2b889f2..d31a13942 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -5,7 +5,7 @@ import { // serializeToJSONStream, serializeToJSONString, } from "./serialization.ts"; -import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared.ts"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat, extractBody, getHeadersAndBody } from "./server-functions-shared.ts"; let INSTANCE = 0; @@ -26,70 +26,6 @@ function createRequest( }); } -function getHeadersAndBody(body: any): { - headers?: HeadersInit; - body: BodyInit; -} | undefined { - switch (true) { - case typeof body === "string": - return { - headers: { - "Content-Type": "text/plain", - [BODY_FORMAT_KEY]: BodyFormat.String, - }, - body, - }; - case body instanceof FormData: - return { - headers: { - [BODY_FORMAT_KEY]: BodyFormat.FormData, - }, - body, - }; - case body instanceof URLSearchParams: - return { - headers: { - "Content-Type": "application/x-www-form-urlencoded", - [BODY_FORMAT_KEY]: BodyFormat.URLSearchParams, - }, - body, - }; - case body instanceof File: { - const formData = new FormData(); - formData.append(BODY_FORMAL_FILE, body, body.name); - return { - headers: { - [BODY_FORMAT_KEY]: BodyFormat.File, - }, - body: formData, - }; - } - case body instanceof Blob: - return { - headers: { - [BODY_FORMAT_KEY]: BodyFormat.Blob, - }, - body, - }; - case body instanceof ArrayBuffer: - return { - headers: { - [BODY_FORMAT_KEY]: BodyFormat.ArrayBuffer, - }, - body, - }; - case body instanceof Uint8Array: - return { - headers: { - [BODY_FORMAT_KEY]: BodyFormat.Uint8Array, - }, - body: new Uint8Array(body), - }; - default: - return undefined; - } -} - async function initializeResponse( base: string, id: string, @@ -158,20 +94,8 @@ async function fetchServerFunction( return response; } - const contentType = response.headers.get("Content-Type"); const clone = response.clone(); - let result; - if (contentType?.startsWith("text/plain")) { - result = await clone.text(); - } else if (contentType?.startsWith("application/json")) { - result = await clone.json(); - } else if (response.headers.get(BODY_FORMAT_KEY)) { - if (import.meta.env.SEROVAL_MODE === "js") { - result = await deserializeJSStream(instance, clone); - } else { - result = await deserializeJSONStream(clone); - } - } + const result = await extractBody(instance, true, clone); if (response.headers.has("X-Error")) { throw result; } From db9eaf527ef91904ab3b151f3a0acf9e29bea53d Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Mon, 2 Feb 2026 00:22:01 +0800 Subject: [PATCH 19/20] Fix error --- packages/start/src/server/server-functions-shared.ts | 2 +- packages/start/src/server/server-runtime.ts | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/start/src/server/server-functions-shared.ts b/packages/start/src/server/server-functions-shared.ts index 5a6f6624a..023e5f413 100644 --- a/packages/start/src/server/server-functions-shared.ts +++ b/packages/start/src/server/server-functions-shared.ts @@ -112,5 +112,5 @@ export async function extractBody(instance: string, client: boolean, source: Req return await clone.bytes(); } - throw new Error("Unknown body format"); + return undefined; } diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index d31a13942..9a9e91dc5 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -84,18 +84,14 @@ async function fetchServerFunction( ) { if (response.body) { /* @ts-ignore-next-line */ - response.customBody = () => { - if (import.meta.env.SEROVAL_MODE === "js") { - return deserializeJSStream(instance, response.clone()); - } - return deserializeJSONStream(response.clone()); + response.customBody = async () => { + return await extractBody(instance, true, response.clone()) }; } return response; } - const clone = response.clone(); - const result = await extractBody(instance, true, clone); + const result = await extractBody(instance, true, response.clone()); if (response.headers.has("X-Error")) { throw result; } From db436df86b70c7fea2165b67f3d4363e81ae9827 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Mon, 9 Feb 2026 16:51:34 +0800 Subject: [PATCH 20/20] Update server-functions-shared.ts --- packages/start/src/server/server-functions-shared.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/start/src/server/server-functions-shared.ts b/packages/start/src/server/server-functions-shared.ts index 023e5f413..c399fef5a 100644 --- a/packages/start/src/server/server-functions-shared.ts +++ b/packages/start/src/server/server-functions-shared.ts @@ -1,4 +1,4 @@ -import { deserializeJSONStream, deserializeJSStream } from "./serialization"; +import { deserializeJSONStream, deserializeJSStream } from "./serialization.ts"; export const BODY_FORMAT_KEY = "X-Start-Type";