@tanstack/start-server-core 1.143.6 → 1.143.9

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.
@@ -5,6 +5,11 @@ import { fromJSON, toCrossJSONStream, toCrossJSONAsync } from "seroval";
5
5
  import { getResponse } from "./request-response.js";
6
6
  import { getServerFnById } from "#tanstack-start-server-fn-resolver";
7
7
  let regex = void 0;
8
+ let serovalPlugins = void 0;
9
+ const FORM_DATA_CONTENT_TYPES = [
10
+ "multipart/form-data",
11
+ "application/x-www-form-urlencoded"
12
+ ];
8
13
  const handleServerAction = async ({
9
14
  request,
10
15
  context
@@ -17,33 +22,30 @@ const handleServerAction = async ({
17
22
  regex = new RegExp(`${process.env.TSS_SERVER_FN_BASE}([^/?#]+)`);
18
23
  }
19
24
  const method = request.method;
25
+ const methodLower = method.toLowerCase();
20
26
  const url = new URL(request.url, "http://localhost:3000");
21
27
  const match = url.pathname.match(regex);
22
28
  const serverFnId = match ? match[1] : null;
23
- const search = Object.fromEntries(url.searchParams.entries());
24
- const isCreateServerFn = "createServerFn" in search;
25
29
  if (typeof serverFnId !== "string") {
26
30
  throw new Error("Invalid server action param for serverFnId: " + serverFnId);
27
31
  }
28
32
  const action = await getServerFnById(serverFnId, { fromClient: true });
29
- const formDataContentTypes = [
30
- "multipart/form-data",
31
- "application/x-www-form-urlencoded"
32
- ];
33
+ if (!serovalPlugins) {
34
+ serovalPlugins = getDefaultSerovalPlugins();
35
+ }
33
36
  const contentType = request.headers.get("Content-Type");
34
- const serovalPlugins = getDefaultSerovalPlugins();
35
37
  function parsePayload(payload) {
36
38
  const parsedPayload = fromJSON(payload, { plugins: serovalPlugins });
37
39
  return parsedPayload;
38
40
  }
39
41
  const response = await (async () => {
40
42
  try {
41
- let result = await (async () => {
42
- if (formDataContentTypes.some(
43
+ const result = await (async () => {
44
+ if (FORM_DATA_CONTENT_TYPES.some(
43
45
  (type) => contentType && contentType.includes(type)
44
46
  )) {
45
47
  invariant(
46
- method.toLowerCase() !== "get",
48
+ methodLower !== "get",
47
49
  "GET requests with FormData payloads are not supported"
48
50
  );
49
51
  const formData = await request.formData();
@@ -67,40 +69,27 @@ const handleServerAction = async ({
67
69
  }
68
70
  return await action(params, signal);
69
71
  }
70
- if (method.toLowerCase() === "get") {
71
- invariant(
72
- isCreateServerFn,
73
- "expected GET request to originate from createServerFn"
74
- );
75
- let payload = search.payload;
76
- payload = payload ? parsePayload(JSON.parse(payload)) : {};
77
- payload.context = { ...context, ...payload.context };
78
- return await action(payload, signal);
72
+ if (methodLower === "get") {
73
+ const payloadParam = url.searchParams.get("payload");
74
+ const payload2 = payloadParam ? parsePayload(JSON.parse(payloadParam)) : {};
75
+ payload2.context = { ...context, ...payload2.context };
76
+ return await action(payload2, signal);
79
77
  }
80
- if (method.toLowerCase() !== "post") {
78
+ if (methodLower !== "post") {
81
79
  throw new Error("expected POST method");
82
80
  }
83
81
  let jsonPayload;
84
82
  if (contentType?.includes("application/json")) {
85
83
  jsonPayload = await request.json();
86
84
  }
87
- if (isCreateServerFn) {
88
- const payload = jsonPayload ? parsePayload(jsonPayload) : {};
89
- payload.context = { ...payload.context, ...context };
90
- return await action(payload, signal);
91
- }
92
- return await action(...jsonPayload);
85
+ const payload = jsonPayload ? parsePayload(jsonPayload) : {};
86
+ payload.context = { ...payload.context, ...context };
87
+ return await action(payload, signal);
93
88
  })();
94
89
  if (result.result instanceof Response) {
95
90
  result.result.headers.set(X_TSS_RAW_RESPONSE, "true");
96
91
  return result.result;
97
92
  }
98
- if (!isCreateServerFn) {
99
- result = result.result;
100
- if (result instanceof Response) {
101
- return result;
102
- }
103
- }
104
93
  if (isNotFound(result)) {
105
94
  return isNotFoundResponse(result);
106
95
  }
@@ -1 +1 @@
1
- {"version":3,"file":"server-functions-handler.js","sources":["../../src/server-functions-handler.ts"],"sourcesContent":["import { isNotFound } 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} from '@tanstack/start-client-core'\nimport { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'\nimport { getResponse } from './request-response'\nimport { getServerFnById } from './getServerFnById'\n\nlet regex: RegExp | undefined = undefined\n\nexport const handleServerAction = async ({\n request,\n context,\n}: {\n request: Request\n context: any\n}) => {\n const controller = new AbortController()\n const signal = controller.signal\n const abort = () => controller.abort()\n request.signal.addEventListener('abort', abort)\n\n if (regex === undefined) {\n regex = new RegExp(`${process.env.TSS_SERVER_FN_BASE}([^/?#]+)`)\n }\n\n const method = request.method\n const url = new URL(request.url, 'http://localhost:3000')\n // extract the serverFnId from the url as host/_serverFn/:serverFnId\n // Define a regex to match the path and extract the :thing part\n\n // Execute the regex\n const match = url.pathname.match(regex)\n const serverFnId = match ? match[1] : null\n const search = Object.fromEntries(url.searchParams.entries()) as {\n payload?: any\n createServerFn?: boolean\n }\n\n const isCreateServerFn = 'createServerFn' in search\n\n if (typeof serverFnId !== 'string') {\n throw new Error('Invalid server action param for serverFnId: ' + serverFnId)\n }\n\n const action = await getServerFnById(serverFnId, { fromClient: true })\n\n // Known FormData 'Content-Type' header values\n const formDataContentTypes = [\n 'multipart/form-data',\n 'application/x-www-form-urlencoded',\n ]\n\n const contentType = request.headers.get('Content-Type')\n const serovalPlugins = getDefaultSerovalPlugins()\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 result = await (async () => {\n // FormData\n if (\n formDataContentTypes.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 method.toLowerCase() !== '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 = { ...context, ...deserializedContext }\n }\n } catch {}\n }\n\n return await action(params, signal)\n }\n\n // Get requests use the query string\n if (method.toLowerCase() === 'get') {\n invariant(\n isCreateServerFn,\n 'expected GET request to originate from createServerFn',\n )\n // By default the payload is the search params\n let payload: any = search.payload\n // If there's a payload, we should try to parse it\n payload = payload ? parsePayload(JSON.parse(payload)) : {}\n payload.context = { ...context, ...payload.context }\n // Send it through!\n return await action(payload, signal)\n }\n\n if (method.toLowerCase() !== '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 // If this POST request was created by createServerFn,\n // its payload will be the only argument\n if (isCreateServerFn) {\n const payload = jsonPayload ? parsePayload(jsonPayload) : {}\n payload.context = { ...payload.context, ...context }\n return await action(payload, signal)\n }\n\n // Otherwise, we'll spread the payload. Need to\n // support `use server` functions that take multiple\n // arguments.\n return await action(...jsonPayload)\n })()\n\n // Any time we get a Response back, we should just\n // return it immediately.\n if (result.result instanceof Response) {\n result.result.headers.set(X_TSS_RAW_RESPONSE, 'true')\n return result.result\n }\n\n // If this is a non createServerFn request, we need to\n // pull out the result from the result object\n if (!isCreateServerFn) {\n result = result.result\n\n // The result might again be a response,\n // and if it is, return it.\n if (result instanceof Response) {\n return result\n }\n }\n\n // TODO: RSCs Where are we getting this package?\n // if (isValidElement(result)) {\n // const { renderToPipeableStream } = await import(\n // // @ts-expect-error\n // 'react-server-dom/server'\n // )\n\n // const pipeableStream = renderToPipeableStream(result)\n\n // setHeaders(event, {\n // 'Content-Type': 'text/x-component',\n // } as any)\n\n // sendStream(event, response)\n // event._handled = true\n\n // return new Response(null, { status: 200 })\n // }\n\n if (isNotFound(result)) {\n return isNotFoundResponse(result)\n }\n\n const response = getResponse()\n let nonStreamingBody: any = undefined\n\n if (result !== undefined) {\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(result, {\n refs: new Map(),\n plugins: serovalPlugins,\n onParse(value) {\n callbacks.onParse(value)\n },\n onDone() {\n callbacks.onDone()\n },\n onError: (error) => {\n callbacks.onError(error)\n },\n })\n if (done) {\n return new Response(\n nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,\n {\n status: response.status,\n statusText: response.statusText,\n headers: {\n 'Content-Type': 'application/json',\n [X_TSS_SERIALIZED]: 'true',\n },\n },\n )\n }\n\n // not done yet, we need to stream\n const encoder = new TextEncoder()\n const stream = new ReadableStream({\n start(controller) {\n callbacks.onParse = (value) =>\n controller.enqueue(encoder.encode(JSON.stringify(value) + '\\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 the initial body\n if (nonStreamingBody !== undefined) {\n callbacks.onParse(nonStreamingBody)\n }\n },\n })\n return new Response(stream, {\n status: response.status,\n statusText: response.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: response.status,\n statusText: response.statusText,\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":["response","controller"],"mappings":"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"}
1
+ {"version":3,"file":"server-functions-handler.js","sources":["../../src/server-functions-handler.ts"],"sourcesContent":["import { isNotFound } 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} from '@tanstack/start-client-core'\nimport { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'\nimport { getResponse } from './request-response'\nimport { getServerFnById } from './getServerFnById'\nimport type { Plugin as SerovalPlugin } from 'seroval'\n\nlet regex: RegExp | undefined = undefined\n\n// Cache serovalPlugins at module level to avoid repeated calls\nlet serovalPlugins: Array<SerovalPlugin<any, any>> | undefined = undefined\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\nexport const handleServerAction = async ({\n request,\n context,\n}: {\n request: Request\n context: any\n}) => {\n const controller = new AbortController()\n const signal = controller.signal\n const abort = () => controller.abort()\n request.signal.addEventListener('abort', abort)\n\n if (regex === undefined) {\n regex = new RegExp(`${process.env.TSS_SERVER_FN_BASE}([^/?#]+)`)\n }\n\n const method = request.method\n const methodLower = method.toLowerCase()\n const url = new URL(request.url, 'http://localhost:3000')\n // extract the serverFnId from the url as host/_serverFn/:serverFnId\n // Define a regex to match the path and extract the :thing part\n\n // Execute the regex\n const match = url.pathname.match(regex)\n const serverFnId = match ? match[1] : null\n\n if (typeof serverFnId !== 'string') {\n throw new Error('Invalid server action param for serverFnId: ' + serverFnId)\n }\n\n const action = await getServerFnById(serverFnId, { fromClient: 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 const result = 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 = { ...context, ...deserializedContext }\n }\n } catch {}\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 // 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 = { ...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 = { ...payload.context, ...context }\n return await action(payload, signal)\n })()\n\n // Any time we get a Response back, we should just\n // return it immediately.\n if (result.result instanceof Response) {\n result.result.headers.set(X_TSS_RAW_RESPONSE, 'true')\n return result.result\n }\n\n if (isNotFound(result)) {\n return isNotFoundResponse(result)\n }\n\n const response = getResponse()\n let nonStreamingBody: any = undefined\n\n if (result !== undefined) {\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(result, {\n refs: new Map(),\n plugins: serovalPlugins,\n onParse(value) {\n callbacks.onParse(value)\n },\n onDone() {\n callbacks.onDone()\n },\n onError: (error) => {\n callbacks.onError(error)\n },\n })\n if (done) {\n return new Response(\n nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,\n {\n status: response.status,\n statusText: response.statusText,\n headers: {\n 'Content-Type': 'application/json',\n [X_TSS_SERIALIZED]: 'true',\n },\n },\n )\n }\n\n // not done yet, we need to stream\n const encoder = new TextEncoder()\n const stream = new ReadableStream({\n start(controller) {\n callbacks.onParse = (value) =>\n controller.enqueue(encoder.encode(JSON.stringify(value) + '\\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 the initial body\n if (nonStreamingBody !== undefined) {\n callbacks.onParse(nonStreamingBody)\n }\n },\n })\n return new Response(stream, {\n status: response.status,\n statusText: response.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: response.status,\n statusText: response.statusText,\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":["payload","response","controller"],"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.143.6",
3
+ "version": "1.143.9",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -65,8 +65,8 @@
65
65
  "tiny-invariant": "^1.3.3",
66
66
  "@tanstack/history": "1.141.0",
67
67
  "@tanstack/router-core": "1.143.6",
68
- "@tanstack/start-storage-context": "1.143.6",
69
- "@tanstack/start-client-core": "1.143.6"
68
+ "@tanstack/start-client-core": "1.143.9",
69
+ "@tanstack/start-storage-context": "1.143.6"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@standard-schema/spec": "^1.0.0",
@@ -9,9 +9,19 @@ import {
9
9
  import { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'
10
10
  import { getResponse } from './request-response'
11
11
  import { getServerFnById } from './getServerFnById'
12
+ import type { Plugin as SerovalPlugin } from 'seroval'
12
13
 
13
14
  let regex: RegExp | undefined = undefined
14
15
 
16
+ // Cache serovalPlugins at module level to avoid repeated calls
17
+ let serovalPlugins: Array<SerovalPlugin<any, any>> | undefined = undefined
18
+
19
+ // Known FormData 'Content-Type' header values - module-level constant
20
+ const FORM_DATA_CONTENT_TYPES = [
21
+ 'multipart/form-data',
22
+ 'application/x-www-form-urlencoded',
23
+ ]
24
+
15
25
  export const handleServerAction = async ({
16
26
  request,
17
27
  context,
@@ -29,6 +39,7 @@ export const handleServerAction = async ({
29
39
  }
30
40
 
31
41
  const method = request.method
42
+ const methodLower = method.toLowerCase()
32
43
  const url = new URL(request.url, 'http://localhost:3000')
33
44
  // extract the serverFnId from the url as host/_serverFn/:serverFnId
34
45
  // Define a regex to match the path and extract the :thing part
@@ -36,12 +47,6 @@ export const handleServerAction = async ({
36
47
  // Execute the regex
37
48
  const match = url.pathname.match(regex)
38
49
  const serverFnId = match ? match[1] : null
39
- const search = Object.fromEntries(url.searchParams.entries()) as {
40
- payload?: any
41
- createServerFn?: boolean
42
- }
43
-
44
- const isCreateServerFn = 'createServerFn' in search
45
50
 
46
51
  if (typeof serverFnId !== 'string') {
47
52
  throw new Error('Invalid server action param for serverFnId: ' + serverFnId)
@@ -49,14 +54,12 @@ export const handleServerAction = async ({
49
54
 
50
55
  const action = await getServerFnById(serverFnId, { fromClient: true })
51
56
 
52
- // Known FormData 'Content-Type' header values
53
- const formDataContentTypes = [
54
- 'multipart/form-data',
55
- 'application/x-www-form-urlencoded',
56
- ]
57
+ // Initialize serovalPlugins lazily (cached at module level)
58
+ if (!serovalPlugins) {
59
+ serovalPlugins = getDefaultSerovalPlugins()
60
+ }
57
61
 
58
62
  const contentType = request.headers.get('Content-Type')
59
- const serovalPlugins = getDefaultSerovalPlugins()
60
63
 
61
64
  function parsePayload(payload: any) {
62
65
  const parsedPayload = fromJSON(payload, { plugins: serovalPlugins })
@@ -65,16 +68,16 @@ export const handleServerAction = async ({
65
68
 
66
69
  const response = await (async () => {
67
70
  try {
68
- let result = await (async () => {
71
+ const result = await (async () => {
69
72
  // FormData
70
73
  if (
71
- formDataContentTypes.some(
74
+ FORM_DATA_CONTENT_TYPES.some(
72
75
  (type) => contentType && contentType.includes(type),
73
76
  )
74
77
  ) {
75
78
  // We don't support GET requests with FormData payloads... that seems impossible
76
79
  invariant(
77
- method.toLowerCase() !== 'get',
80
+ methodLower !== 'get',
78
81
  'GET requests with FormData payloads are not supported',
79
82
  )
80
83
  const formData = await request.formData()
@@ -104,21 +107,19 @@ export const handleServerAction = async ({
104
107
  }
105
108
 
106
109
  // Get requests use the query string
107
- if (method.toLowerCase() === 'get') {
108
- invariant(
109
- isCreateServerFn,
110
- 'expected GET request to originate from createServerFn',
111
- )
112
- // By default the payload is the search params
113
- let payload: any = search.payload
110
+ if (methodLower === 'get') {
111
+ // Get payload directly from searchParams
112
+ const payloadParam = url.searchParams.get('payload')
114
113
  // If there's a payload, we should try to parse it
115
- payload = payload ? parsePayload(JSON.parse(payload)) : {}
114
+ const payload: any = payloadParam
115
+ ? parsePayload(JSON.parse(payloadParam))
116
+ : {}
116
117
  payload.context = { ...context, ...payload.context }
117
118
  // Send it through!
118
119
  return await action(payload, signal)
119
120
  }
120
121
 
121
- if (method.toLowerCase() !== 'post') {
122
+ if (methodLower !== 'post') {
122
123
  throw new Error('expected POST method')
123
124
  }
124
125
 
@@ -127,18 +128,9 @@ export const handleServerAction = async ({
127
128
  jsonPayload = await request.json()
128
129
  }
129
130
 
130
- // If this POST request was created by createServerFn,
131
- // its payload will be the only argument
132
- if (isCreateServerFn) {
133
- const payload = jsonPayload ? parsePayload(jsonPayload) : {}
134
- payload.context = { ...payload.context, ...context }
135
- return await action(payload, signal)
136
- }
137
-
138
- // Otherwise, we'll spread the payload. Need to
139
- // support `use server` functions that take multiple
140
- // arguments.
141
- return await action(...jsonPayload)
131
+ const payload = jsonPayload ? parsePayload(jsonPayload) : {}
132
+ payload.context = { ...payload.context, ...context }
133
+ return await action(payload, signal)
142
134
  })()
143
135
 
144
136
  // Any time we get a Response back, we should just
@@ -148,37 +140,6 @@ export const handleServerAction = async ({
148
140
  return result.result
149
141
  }
150
142
 
151
- // If this is a non createServerFn request, we need to
152
- // pull out the result from the result object
153
- if (!isCreateServerFn) {
154
- result = result.result
155
-
156
- // The result might again be a response,
157
- // and if it is, return it.
158
- if (result instanceof Response) {
159
- return result
160
- }
161
- }
162
-
163
- // TODO: RSCs Where are we getting this package?
164
- // if (isValidElement(result)) {
165
- // const { renderToPipeableStream } = await import(
166
- // // @ts-expect-error
167
- // 'react-server-dom/server'
168
- // )
169
-
170
- // const pipeableStream = renderToPipeableStream(result)
171
-
172
- // setHeaders(event, {
173
- // 'Content-Type': 'text/x-component',
174
- // } as any)
175
-
176
- // sendStream(event, response)
177
- // event._handled = true
178
-
179
- // return new Response(null, { status: 200 })
180
- // }
181
-
182
143
  if (isNotFound(result)) {
183
144
  return isNotFoundResponse(result)
184
145
  }