@tanstack/start-server-core 1.150.0 → 1.151.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,7 @@ const handleServerAction = async ({
22
22
  const abort = () => controller.abort();
23
23
  request.signal.addEventListener("abort", abort);
24
24
  const method = request.method;
25
+ const methodUpper = method.toUpperCase();
25
26
  const methodLower = method.toLowerCase();
26
27
  const url = new URL(request.url);
27
28
  const action = await getServerFnById(serverFnId, { fromClient: true });
@@ -161,7 +162,8 @@ const handleServerAction = async ({
161
162
  formData.delete(TSS_FORMDATA_CONTEXT);
162
163
  const params = {
163
164
  context,
164
- data: formData
165
+ data: formData,
166
+ method: methodUpper
165
167
  };
166
168
  if (typeof serializedContext === "string") {
167
169
  try {
@@ -190,6 +192,7 @@ const handleServerAction = async ({
190
192
  }
191
193
  const payload2 = payloadParam ? parsePayload(JSON.parse(payloadParam)) : {};
192
194
  payload2.context = safeObjectMerge(context, payload2.context);
195
+ payload2.method = methodUpper;
193
196
  return await action(payload2, signal);
194
197
  }
195
198
  if (methodLower !== "post") {
@@ -201,6 +204,7 @@ const handleServerAction = async ({
201
204
  }
202
205
  const payload = jsonPayload ? parsePayload(jsonPayload) : {};
203
206
  payload.context = safeObjectMerge(payload.context, context);
207
+ payload.method = methodUpper;
204
208
  return await action(payload, signal);
205
209
  })();
206
210
  const unwrapped = res.result || res.error;
@@ -1 +1 @@
1
- {"version":3,"file":"server-functions-handler.js","sources":["../../src/server-functions-handler.ts"],"sourcesContent":["import {\n createRawStreamRPCPlugin,\n isNotFound,\n isRedirect,\n} from '@tanstack/router-core'\nimport invariant from 'tiny-invariant'\nimport {\n TSS_FORMDATA_CONTEXT,\n X_TSS_RAW_RESPONSE,\n X_TSS_SERIALIZED,\n getDefaultSerovalPlugins,\n safeObjectMerge,\n} from '@tanstack/start-client-core'\nimport { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'\nimport { getResponse } from './request-response'\nimport { getServerFnById } from './getServerFnById'\nimport {\n TSS_CONTENT_TYPE_FRAMED_VERSIONED,\n createMultiplexedStream,\n} from './frame-protocol'\nimport type { Plugin as SerovalPlugin } from 'seroval'\n\n// Cache serovalPlugins at module level to avoid repeated calls\nlet serovalPlugins: Array<SerovalPlugin<any, any>> | undefined = undefined\n\n// Cache TextEncoder for NDJSON serialization\nconst textEncoder = new TextEncoder()\n\n// Known FormData 'Content-Type' header values - module-level constant\nconst FORM_DATA_CONTENT_TYPES = [\n 'multipart/form-data',\n 'application/x-www-form-urlencoded',\n]\n\n// Maximum payload size for GET requests (1MB)\nconst MAX_PAYLOAD_SIZE = 1_000_000\n\nexport const handleServerAction = async ({\n request,\n context,\n serverFnId,\n}: {\n request: Request\n context: any\n serverFnId: string\n}) => {\n const controller = new AbortController()\n const signal = controller.signal\n const abort = () => controller.abort()\n request.signal.addEventListener('abort', abort)\n\n const method = request.method\n const methodLower = method.toLowerCase()\n const url = new URL(request.url)\n\n const action = await getServerFnById(serverFnId, { fromClient: true })\n\n const isServerFn = request.headers.get('x-tsr-serverFn') === 'true'\n\n // Initialize serovalPlugins lazily (cached at module level)\n if (!serovalPlugins) {\n serovalPlugins = getDefaultSerovalPlugins()\n }\n\n const contentType = request.headers.get('Content-Type')\n\n function parsePayload(payload: any) {\n const parsedPayload = fromJSON(payload, { plugins: serovalPlugins })\n return parsedPayload as any\n }\n\n const response = await (async () => {\n try {\n let res = await (async () => {\n // FormData\n if (\n FORM_DATA_CONTENT_TYPES.some(\n (type) => contentType && contentType.includes(type),\n )\n ) {\n // We don't support GET requests with FormData payloads... that seems impossible\n invariant(\n methodLower !== 'get',\n 'GET requests with FormData payloads are not supported',\n )\n const formData = await request.formData()\n const serializedContext = formData.get(TSS_FORMDATA_CONTEXT)\n formData.delete(TSS_FORMDATA_CONTEXT)\n\n const params = {\n context,\n data: formData,\n }\n if (typeof serializedContext === 'string') {\n try {\n const parsedContext = JSON.parse(serializedContext)\n const deserializedContext = fromJSON(parsedContext, {\n plugins: serovalPlugins,\n })\n if (\n typeof deserializedContext === 'object' &&\n deserializedContext\n ) {\n params.context = safeObjectMerge(\n context,\n deserializedContext as Record<string, unknown>,\n )\n }\n } catch (e) {\n // Log warning for debugging but don't expose to client\n if (process.env.NODE_ENV === 'development') {\n console.warn('Failed to parse FormData context:', e)\n }\n }\n }\n\n return await action(params, signal)\n }\n\n // Get requests use the query string\n if (methodLower === 'get') {\n // Get payload directly from searchParams\n const payloadParam = url.searchParams.get('payload')\n // Reject oversized payloads to prevent DoS\n if (payloadParam && payloadParam.length > MAX_PAYLOAD_SIZE) {\n throw new Error('Payload too large')\n }\n // If there's a payload, we should try to parse it\n const payload: any = payloadParam\n ? parsePayload(JSON.parse(payloadParam))\n : {}\n payload.context = safeObjectMerge(context, payload.context)\n // Send it through!\n return await action(payload, signal)\n }\n\n if (methodLower !== 'post') {\n throw new Error('expected POST method')\n }\n\n let jsonPayload\n if (contentType?.includes('application/json')) {\n jsonPayload = await request.json()\n }\n\n const payload = jsonPayload ? parsePayload(jsonPayload) : {}\n payload.context = safeObjectMerge(payload.context, context)\n return await action(payload, signal)\n })()\n\n const unwrapped = res.result || res.error\n\n if (isNotFound(res)) {\n res = isNotFoundResponse(res)\n }\n\n if (!isServerFn) {\n return unwrapped\n }\n\n if (unwrapped instanceof Response) {\n if (isRedirect(unwrapped)) {\n return unwrapped\n }\n unwrapped.headers.set(X_TSS_RAW_RESPONSE, 'true')\n return unwrapped\n }\n\n return serializeResult(res)\n\n function serializeResult(res: unknown): Response {\n let nonStreamingBody: any = undefined\n\n const alsResponse = getResponse()\n if (res !== undefined) {\n // Collect raw streams encountered during serialization\n const rawStreams = new Map<number, ReadableStream<Uint8Array>>()\n const rawStreamPlugin = createRawStreamRPCPlugin(\n (id: number, stream: ReadableStream<Uint8Array>) => {\n rawStreams.set(id, stream)\n },\n )\n\n // Build plugins with RawStreamRPCPlugin first (before default SSR plugin)\n const plugins = [rawStreamPlugin, ...(serovalPlugins || [])]\n\n // first run without the stream in case `result` does not need streaming\n let done = false as boolean\n const callbacks: {\n onParse: (value: any) => void\n onDone: () => void\n onError: (error: any) => void\n } = {\n onParse: (value) => {\n nonStreamingBody = value\n },\n onDone: () => {\n done = true\n },\n onError: (error) => {\n throw error\n },\n }\n toCrossJSONStream(res, {\n refs: new Map(),\n plugins,\n onParse(value) {\n callbacks.onParse(value)\n },\n onDone() {\n callbacks.onDone()\n },\n onError: (error) => {\n callbacks.onError(error)\n },\n })\n\n // If no raw streams and done synchronously, return simple JSON\n if (done && rawStreams.size === 0) {\n return new Response(\n nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,\n {\n status: alsResponse.status,\n statusText: alsResponse.statusText,\n headers: {\n 'Content-Type': 'application/json',\n [X_TSS_SERIALIZED]: 'true',\n },\n },\n )\n }\n\n // If we have raw streams, use framed protocol\n if (rawStreams.size > 0) {\n // Create a stream of JSON chunks (NDJSON style)\n const jsonStream = new ReadableStream<string>({\n start(controller) {\n callbacks.onParse = (value) => {\n controller.enqueue(JSON.stringify(value) + '\\n')\n }\n callbacks.onDone = () => {\n try {\n controller.close()\n } catch {\n // Already closed\n }\n }\n callbacks.onError = (error) => controller.error(error)\n // Emit initial body if we have one\n if (nonStreamingBody !== undefined) {\n callbacks.onParse(nonStreamingBody)\n }\n },\n })\n\n // Create multiplexed stream with JSON and raw streams\n const multiplexedStream = createMultiplexedStream(\n jsonStream,\n rawStreams,\n )\n\n return new Response(multiplexedStream, {\n status: alsResponse.status,\n statusText: alsResponse.statusText,\n headers: {\n 'Content-Type': TSS_CONTENT_TYPE_FRAMED_VERSIONED,\n [X_TSS_SERIALIZED]: 'true',\n },\n })\n }\n\n // No raw streams but not done yet - use standard NDJSON streaming\n const stream = new ReadableStream({\n start(controller) {\n callbacks.onParse = (value) =>\n controller.enqueue(\n textEncoder.encode(JSON.stringify(value) + '\\n'),\n )\n callbacks.onDone = () => {\n try {\n controller.close()\n } catch (error) {\n controller.error(error)\n }\n }\n callbacks.onError = (error) => controller.error(error)\n // stream initial body\n if (nonStreamingBody !== undefined) {\n callbacks.onParse(nonStreamingBody)\n }\n },\n })\n return new Response(stream, {\n status: alsResponse.status,\n statusText: alsResponse.statusText,\n headers: {\n 'Content-Type': 'application/x-ndjson',\n [X_TSS_SERIALIZED]: 'true',\n },\n })\n }\n\n return new Response(undefined, {\n status: alsResponse.status,\n statusText: alsResponse.statusText,\n })\n }\n } catch (error: any) {\n if (error instanceof Response) {\n return error\n }\n // else if (\n // isPlainObject(error) &&\n // 'result' in error &&\n // error.result instanceof Response\n // ) {\n // return error.result\n // }\n\n // Currently this server-side context has no idea how to\n // build final URLs, so we need to defer that to the client.\n // The client will check for __redirect and __notFound keys,\n // and if they exist, it will handle them appropriately.\n\n if (isNotFound(error)) {\n return isNotFoundResponse(error)\n }\n\n console.info()\n console.info('Server Fn Error!')\n console.info()\n console.error(error)\n console.info()\n\n const serializedError = JSON.stringify(\n await Promise.resolve(\n toCrossJSONAsync(error, {\n refs: new Map(),\n plugins: serovalPlugins,\n }),\n ),\n )\n const response = getResponse()\n return new Response(serializedError, {\n status: response.status ?? 500,\n statusText: response.statusText,\n headers: {\n 'Content-Type': 'application/json',\n [X_TSS_SERIALIZED]: 'true',\n },\n })\n }\n })()\n\n request.signal.removeEventListener('abort', abort)\n\n return response\n}\n\nfunction isNotFoundResponse(error: any) {\n const { headers, ...rest } = error\n\n return new Response(JSON.stringify(rest), {\n status: 404,\n headers: {\n 'Content-Type': 'application/json',\n ...(headers || {}),\n },\n })\n}\n"],"names":["res","stream","controller","payload","response"],"mappings":"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"}
1
+ {"version":3,"file":"server-functions-handler.js","sources":["../../src/server-functions-handler.ts"],"sourcesContent":["import {\n createRawStreamRPCPlugin,\n isNotFound,\n isRedirect,\n} from '@tanstack/router-core'\nimport invariant from 'tiny-invariant'\nimport {\n TSS_FORMDATA_CONTEXT,\n X_TSS_RAW_RESPONSE,\n X_TSS_SERIALIZED,\n getDefaultSerovalPlugins,\n safeObjectMerge,\n} from '@tanstack/start-client-core'\nimport { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'\nimport { getResponse } from './request-response'\nimport { getServerFnById } from './getServerFnById'\nimport {\n TSS_CONTENT_TYPE_FRAMED_VERSIONED,\n createMultiplexedStream,\n} from './frame-protocol'\nimport type { Plugin as SerovalPlugin } from 'seroval'\n\n// Cache serovalPlugins at module level to avoid repeated calls\nlet serovalPlugins: Array<SerovalPlugin<any, any>> | undefined = undefined\n\n// Cache TextEncoder for NDJSON serialization\nconst textEncoder = new TextEncoder()\n\n// Known FormData 'Content-Type' header values - module-level constant\nconst FORM_DATA_CONTENT_TYPES = [\n 'multipart/form-data',\n 'application/x-www-form-urlencoded',\n]\n\n// Maximum payload size for GET requests (1MB)\nconst MAX_PAYLOAD_SIZE = 1_000_000\n\nexport const handleServerAction = async ({\n request,\n context,\n serverFnId,\n}: {\n request: Request\n context: any\n serverFnId: string\n}) => {\n const controller = new AbortController()\n const signal = controller.signal\n const abort = () => controller.abort()\n request.signal.addEventListener('abort', abort)\n\n const method = request.method\n const methodUpper = method.toUpperCase()\n const methodLower = method.toLowerCase()\n const url = new URL(request.url)\n\n const action = await getServerFnById(serverFnId, { fromClient: true })\n\n const isServerFn = request.headers.get('x-tsr-serverFn') === 'true'\n\n // Initialize serovalPlugins lazily (cached at module level)\n if (!serovalPlugins) {\n serovalPlugins = getDefaultSerovalPlugins()\n }\n\n const contentType = request.headers.get('Content-Type')\n\n function parsePayload(payload: any) {\n const parsedPayload = fromJSON(payload, { plugins: serovalPlugins })\n return parsedPayload as any\n }\n\n const response = await (async () => {\n try {\n let res = await (async () => {\n // FormData\n if (\n FORM_DATA_CONTENT_TYPES.some(\n (type) => contentType && contentType.includes(type),\n )\n ) {\n // We don't support GET requests with FormData payloads... that seems impossible\n invariant(\n methodLower !== 'get',\n 'GET requests with FormData payloads are not supported',\n )\n const formData = await request.formData()\n const serializedContext = formData.get(TSS_FORMDATA_CONTEXT)\n formData.delete(TSS_FORMDATA_CONTEXT)\n\n const params = {\n context,\n data: formData,\n method: methodUpper,\n }\n if (typeof serializedContext === 'string') {\n try {\n const parsedContext = JSON.parse(serializedContext)\n const deserializedContext = fromJSON(parsedContext, {\n plugins: serovalPlugins,\n })\n if (\n typeof deserializedContext === 'object' &&\n deserializedContext\n ) {\n params.context = safeObjectMerge(\n context,\n deserializedContext as Record<string, unknown>,\n )\n }\n } catch (e) {\n // Log warning for debugging but don't expose to client\n if (process.env.NODE_ENV === 'development') {\n console.warn('Failed to parse FormData context:', e)\n }\n }\n }\n\n return await action(params, signal)\n }\n\n // Get requests use the query string\n if (methodLower === 'get') {\n // Get payload directly from searchParams\n const payloadParam = url.searchParams.get('payload')\n // Reject oversized payloads to prevent DoS\n if (payloadParam && payloadParam.length > MAX_PAYLOAD_SIZE) {\n throw new Error('Payload too large')\n }\n // If there's a payload, we should try to parse it\n const payload: any = payloadParam\n ? parsePayload(JSON.parse(payloadParam))\n : {}\n payload.context = safeObjectMerge(context, payload.context)\n payload.method = methodUpper\n // Send it through!\n return await action(payload, signal)\n }\n\n if (methodLower !== 'post') {\n throw new Error('expected POST method')\n }\n\n let jsonPayload\n if (contentType?.includes('application/json')) {\n jsonPayload = await request.json()\n }\n\n const payload = jsonPayload ? parsePayload(jsonPayload) : {}\n payload.context = safeObjectMerge(payload.context, context)\n payload.method = methodUpper\n return await action(payload, signal)\n })()\n\n const unwrapped = res.result || res.error\n\n if (isNotFound(res)) {\n res = isNotFoundResponse(res)\n }\n\n if (!isServerFn) {\n return unwrapped\n }\n\n if (unwrapped instanceof Response) {\n if (isRedirect(unwrapped)) {\n return unwrapped\n }\n unwrapped.headers.set(X_TSS_RAW_RESPONSE, 'true')\n return unwrapped\n }\n\n return serializeResult(res)\n\n function serializeResult(res: unknown): Response {\n let nonStreamingBody: any = undefined\n\n const alsResponse = getResponse()\n if (res !== undefined) {\n // Collect raw streams encountered during serialization\n const rawStreams = new Map<number, ReadableStream<Uint8Array>>()\n const rawStreamPlugin = createRawStreamRPCPlugin(\n (id: number, stream: ReadableStream<Uint8Array>) => {\n rawStreams.set(id, stream)\n },\n )\n\n // Build plugins with RawStreamRPCPlugin first (before default SSR plugin)\n const plugins = [rawStreamPlugin, ...(serovalPlugins || [])]\n\n // first run without the stream in case `result` does not need streaming\n let done = false as boolean\n const callbacks: {\n onParse: (value: any) => void\n onDone: () => void\n onError: (error: any) => void\n } = {\n onParse: (value) => {\n nonStreamingBody = value\n },\n onDone: () => {\n done = true\n },\n onError: (error) => {\n throw error\n },\n }\n toCrossJSONStream(res, {\n refs: new Map(),\n plugins,\n onParse(value) {\n callbacks.onParse(value)\n },\n onDone() {\n callbacks.onDone()\n },\n onError: (error) => {\n callbacks.onError(error)\n },\n })\n\n // If no raw streams and done synchronously, return simple JSON\n if (done && rawStreams.size === 0) {\n return new Response(\n nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,\n {\n status: alsResponse.status,\n statusText: alsResponse.statusText,\n headers: {\n 'Content-Type': 'application/json',\n [X_TSS_SERIALIZED]: 'true',\n },\n },\n )\n }\n\n // If we have raw streams, use framed protocol\n if (rawStreams.size > 0) {\n // Create a stream of JSON chunks (NDJSON style)\n const jsonStream = new ReadableStream<string>({\n start(controller) {\n callbacks.onParse = (value) => {\n controller.enqueue(JSON.stringify(value) + '\\n')\n }\n callbacks.onDone = () => {\n try {\n controller.close()\n } catch {\n // Already closed\n }\n }\n callbacks.onError = (error) => controller.error(error)\n // Emit initial body if we have one\n if (nonStreamingBody !== undefined) {\n callbacks.onParse(nonStreamingBody)\n }\n },\n })\n\n // Create multiplexed stream with JSON and raw streams\n const multiplexedStream = createMultiplexedStream(\n jsonStream,\n rawStreams,\n )\n\n return new Response(multiplexedStream, {\n status: alsResponse.status,\n statusText: alsResponse.statusText,\n headers: {\n 'Content-Type': TSS_CONTENT_TYPE_FRAMED_VERSIONED,\n [X_TSS_SERIALIZED]: 'true',\n },\n })\n }\n\n // No raw streams but not done yet - use standard NDJSON streaming\n const stream = new ReadableStream({\n start(controller) {\n callbacks.onParse = (value) =>\n controller.enqueue(\n textEncoder.encode(JSON.stringify(value) + '\\n'),\n )\n callbacks.onDone = () => {\n try {\n controller.close()\n } catch (error) {\n controller.error(error)\n }\n }\n callbacks.onError = (error) => controller.error(error)\n // stream initial body\n if (nonStreamingBody !== undefined) {\n callbacks.onParse(nonStreamingBody)\n }\n },\n })\n return new Response(stream, {\n status: alsResponse.status,\n statusText: alsResponse.statusText,\n headers: {\n 'Content-Type': 'application/x-ndjson',\n [X_TSS_SERIALIZED]: 'true',\n },\n })\n }\n\n return new Response(undefined, {\n status: alsResponse.status,\n statusText: alsResponse.statusText,\n })\n }\n } catch (error: any) {\n if (error instanceof Response) {\n return error\n }\n // else if (\n // isPlainObject(error) &&\n // 'result' in error &&\n // error.result instanceof Response\n // ) {\n // return error.result\n // }\n\n // Currently this server-side context has no idea how to\n // build final URLs, so we need to defer that to the client.\n // The client will check for __redirect and __notFound keys,\n // and if they exist, it will handle them appropriately.\n\n if (isNotFound(error)) {\n return isNotFoundResponse(error)\n }\n\n console.info()\n console.info('Server Fn Error!')\n console.info()\n console.error(error)\n console.info()\n\n const serializedError = JSON.stringify(\n await Promise.resolve(\n toCrossJSONAsync(error, {\n refs: new Map(),\n plugins: serovalPlugins,\n }),\n ),\n )\n const response = getResponse()\n return new Response(serializedError, {\n status: response.status ?? 500,\n statusText: response.statusText,\n headers: {\n 'Content-Type': 'application/json',\n [X_TSS_SERIALIZED]: 'true',\n },\n })\n }\n })()\n\n request.signal.removeEventListener('abort', abort)\n\n return response\n}\n\nfunction isNotFoundResponse(error: any) {\n const { headers, ...rest } = error\n\n return new Response(JSON.stringify(rest), {\n status: 404,\n headers: {\n 'Content-Type': 'application/json',\n ...(headers || {}),\n },\n })\n}\n"],"names":["res","stream","controller","payload","response"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/start-server-core",
3
- "version": "1.150.0",
3
+ "version": "1.151.0",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -64,9 +64,9 @@
64
64
  "seroval": "^1.4.1",
65
65
  "tiny-invariant": "^1.3.3",
66
66
  "@tanstack/history": "1.145.7",
67
- "@tanstack/router-core": "1.150.0",
68
- "@tanstack/start-storage-context": "1.150.0",
69
- "@tanstack/start-client-core": "1.150.0"
67
+ "@tanstack/router-core": "1.151.0",
68
+ "@tanstack/start-client-core": "1.151.0",
69
+ "@tanstack/start-storage-context": "1.151.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@standard-schema/spec": "^1.0.0",
@@ -50,6 +50,7 @@ export const handleServerAction = async ({
50
50
  request.signal.addEventListener('abort', abort)
51
51
 
52
52
  const method = request.method
53
+ const methodUpper = method.toUpperCase()
53
54
  const methodLower = method.toLowerCase()
54
55
  const url = new URL(request.url)
55
56
 
@@ -90,6 +91,7 @@ export const handleServerAction = async ({
90
91
  const params = {
91
92
  context,
92
93
  data: formData,
94
+ method: methodUpper,
93
95
  }
94
96
  if (typeof serializedContext === 'string') {
95
97
  try {
@@ -130,6 +132,7 @@ export const handleServerAction = async ({
130
132
  ? parsePayload(JSON.parse(payloadParam))
131
133
  : {}
132
134
  payload.context = safeObjectMerge(context, payload.context)
135
+ payload.method = methodUpper
133
136
  // Send it through!
134
137
  return await action(payload, signal)
135
138
  }
@@ -145,6 +148,7 @@ export const handleServerAction = async ({
145
148
 
146
149
  const payload = jsonPayload ? parsePayload(jsonPayload) : {}
147
150
  payload.context = safeObjectMerge(payload.context, context)
151
+ payload.method = methodUpper
148
152
  return await action(payload, signal)
149
153
  })()
150
154