@tanstack/start-server-core 1.143.9 → 1.143.12
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.
- package/dist/esm/createStartHandler.js +268 -257
- package/dist/esm/createStartHandler.js.map +1 -1
- package/dist/esm/server-functions-handler.d.ts +2 -1
- package/dist/esm/server-functions-handler.js +106 -93
- package/dist/esm/server-functions-handler.js.map +1 -1
- package/package.json +4 -4
- package/src/createStartHandler.ts +386 -343
- package/src/server-functions-handler.ts +120 -105
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import { isNotFound } from '@tanstack/router-core'
|
|
1
|
+
import { isNotFound, isRedirect } from '@tanstack/router-core'
|
|
2
2
|
import invariant from 'tiny-invariant'
|
|
3
3
|
import {
|
|
4
4
|
TSS_FORMDATA_CONTEXT,
|
|
5
5
|
X_TSS_RAW_RESPONSE,
|
|
6
6
|
X_TSS_SERIALIZED,
|
|
7
7
|
getDefaultSerovalPlugins,
|
|
8
|
+
safeObjectMerge,
|
|
8
9
|
} from '@tanstack/start-client-core'
|
|
9
10
|
import { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'
|
|
10
11
|
import { getResponse } from './request-response'
|
|
11
12
|
import { getServerFnById } from './getServerFnById'
|
|
12
13
|
import type { Plugin as SerovalPlugin } from 'seroval'
|
|
13
14
|
|
|
14
|
-
let regex: RegExp | undefined = undefined
|
|
15
|
-
|
|
16
15
|
// Cache serovalPlugins at module level to avoid repeated calls
|
|
17
16
|
let serovalPlugins: Array<SerovalPlugin<any, any>> | undefined = undefined
|
|
18
17
|
|
|
@@ -22,38 +21,31 @@ const FORM_DATA_CONTENT_TYPES = [
|
|
|
22
21
|
'application/x-www-form-urlencoded',
|
|
23
22
|
]
|
|
24
23
|
|
|
24
|
+
// Maximum payload size for GET requests (1MB)
|
|
25
|
+
const MAX_PAYLOAD_SIZE = 1_000_000
|
|
26
|
+
|
|
25
27
|
export const handleServerAction = async ({
|
|
26
28
|
request,
|
|
27
29
|
context,
|
|
30
|
+
serverFnId,
|
|
28
31
|
}: {
|
|
29
32
|
request: Request
|
|
30
33
|
context: any
|
|
34
|
+
serverFnId: string
|
|
31
35
|
}) => {
|
|
32
36
|
const controller = new AbortController()
|
|
33
37
|
const signal = controller.signal
|
|
34
38
|
const abort = () => controller.abort()
|
|
35
39
|
request.signal.addEventListener('abort', abort)
|
|
36
40
|
|
|
37
|
-
if (regex === undefined) {
|
|
38
|
-
regex = new RegExp(`${process.env.TSS_SERVER_FN_BASE}([^/?#]+)`)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
41
|
const method = request.method
|
|
42
42
|
const methodLower = method.toLowerCase()
|
|
43
|
-
const url = new URL(request.url
|
|
44
|
-
// extract the serverFnId from the url as host/_serverFn/:serverFnId
|
|
45
|
-
// Define a regex to match the path and extract the :thing part
|
|
46
|
-
|
|
47
|
-
// Execute the regex
|
|
48
|
-
const match = url.pathname.match(regex)
|
|
49
|
-
const serverFnId = match ? match[1] : null
|
|
50
|
-
|
|
51
|
-
if (typeof serverFnId !== 'string') {
|
|
52
|
-
throw new Error('Invalid server action param for serverFnId: ' + serverFnId)
|
|
53
|
-
}
|
|
43
|
+
const url = new URL(request.url)
|
|
54
44
|
|
|
55
45
|
const action = await getServerFnById(serverFnId, { fromClient: true })
|
|
56
46
|
|
|
47
|
+
const isServerFn = request.headers.get('x-tsr-serverFn') === 'true'
|
|
48
|
+
|
|
57
49
|
// Initialize serovalPlugins lazily (cached at module level)
|
|
58
50
|
if (!serovalPlugins) {
|
|
59
51
|
serovalPlugins = getDefaultSerovalPlugins()
|
|
@@ -68,7 +60,7 @@ export const handleServerAction = async ({
|
|
|
68
60
|
|
|
69
61
|
const response = await (async () => {
|
|
70
62
|
try {
|
|
71
|
-
|
|
63
|
+
let res = await (async () => {
|
|
72
64
|
// FormData
|
|
73
65
|
if (
|
|
74
66
|
FORM_DATA_CONTENT_TYPES.some(
|
|
@@ -98,9 +90,17 @@ export const handleServerAction = async ({
|
|
|
98
90
|
typeof deserializedContext === 'object' &&
|
|
99
91
|
deserializedContext
|
|
100
92
|
) {
|
|
101
|
-
params.context =
|
|
93
|
+
params.context = safeObjectMerge(
|
|
94
|
+
context,
|
|
95
|
+
deserializedContext as Record<string, unknown>,
|
|
96
|
+
)
|
|
102
97
|
}
|
|
103
|
-
} catch {
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// Log warning for debugging but don't expose to client
|
|
100
|
+
if (process.env.NODE_ENV === 'development') {
|
|
101
|
+
console.warn('Failed to parse FormData context:', e)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
return await action(params, signal)
|
|
@@ -110,11 +110,15 @@ export const handleServerAction = async ({
|
|
|
110
110
|
if (methodLower === 'get') {
|
|
111
111
|
// Get payload directly from searchParams
|
|
112
112
|
const payloadParam = url.searchParams.get('payload')
|
|
113
|
+
// Reject oversized payloads to prevent DoS
|
|
114
|
+
if (payloadParam && payloadParam.length > MAX_PAYLOAD_SIZE) {
|
|
115
|
+
throw new Error('Payload too large')
|
|
116
|
+
}
|
|
113
117
|
// If there's a payload, we should try to parse it
|
|
114
118
|
const payload: any = payloadParam
|
|
115
119
|
? parsePayload(JSON.parse(payloadParam))
|
|
116
120
|
: {}
|
|
117
|
-
payload.context =
|
|
121
|
+
payload.context = safeObjectMerge(context, payload.context)
|
|
118
122
|
// Send it through!
|
|
119
123
|
return await action(payload, signal)
|
|
120
124
|
}
|
|
@@ -129,103 +133,114 @@ export const handleServerAction = async ({
|
|
|
129
133
|
}
|
|
130
134
|
|
|
131
135
|
const payload = jsonPayload ? parsePayload(jsonPayload) : {}
|
|
132
|
-
payload.context =
|
|
136
|
+
payload.context = safeObjectMerge(payload.context, context)
|
|
133
137
|
return await action(payload, signal)
|
|
134
138
|
})()
|
|
135
139
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
return result.result
|
|
140
|
+
const unwrapped = res.result || res.error
|
|
141
|
+
|
|
142
|
+
if (isNotFound(res)) {
|
|
143
|
+
res = isNotFoundResponse(res)
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
if (
|
|
144
|
-
return
|
|
146
|
+
if (!isServerFn) {
|
|
147
|
+
return unwrapped
|
|
145
148
|
}
|
|
146
149
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (result !== undefined) {
|
|
151
|
-
// first run without the stream in case `result` does not need streaming
|
|
152
|
-
let done = false as boolean
|
|
153
|
-
const callbacks: {
|
|
154
|
-
onParse: (value: any) => void
|
|
155
|
-
onDone: () => void
|
|
156
|
-
onError: (error: any) => void
|
|
157
|
-
} = {
|
|
158
|
-
onParse: (value) => {
|
|
159
|
-
nonStreamingBody = value
|
|
160
|
-
},
|
|
161
|
-
onDone: () => {
|
|
162
|
-
done = true
|
|
163
|
-
},
|
|
164
|
-
onError: (error) => {
|
|
165
|
-
throw error
|
|
166
|
-
},
|
|
150
|
+
if (unwrapped instanceof Response) {
|
|
151
|
+
if (isRedirect(unwrapped)) {
|
|
152
|
+
return unwrapped
|
|
167
153
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
154
|
+
unwrapped.headers.set(X_TSS_RAW_RESPONSE, 'true')
|
|
155
|
+
return unwrapped
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return serializeResult(res)
|
|
159
|
+
|
|
160
|
+
function serializeResult(res: unknown): Response {
|
|
161
|
+
let nonStreamingBody: any = undefined
|
|
162
|
+
|
|
163
|
+
const alsResponse = getResponse()
|
|
164
|
+
if (res !== undefined) {
|
|
165
|
+
// first run without the stream in case `result` does not need streaming
|
|
166
|
+
let done = false as boolean
|
|
167
|
+
const callbacks: {
|
|
168
|
+
onParse: (value: any) => void
|
|
169
|
+
onDone: () => void
|
|
170
|
+
onError: (error: any) => void
|
|
171
|
+
} = {
|
|
172
|
+
onParse: (value) => {
|
|
173
|
+
nonStreamingBody = value
|
|
174
|
+
},
|
|
175
|
+
onDone: () => {
|
|
176
|
+
done = true
|
|
177
|
+
},
|
|
178
|
+
onError: (error) => {
|
|
179
|
+
throw error
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
toCrossJSONStream(res, {
|
|
183
|
+
refs: new Map(),
|
|
184
|
+
plugins: serovalPlugins,
|
|
185
|
+
onParse(value) {
|
|
186
|
+
callbacks.onParse(value)
|
|
187
|
+
},
|
|
188
|
+
onDone() {
|
|
189
|
+
callbacks.onDone()
|
|
190
|
+
},
|
|
191
|
+
onError: (error) => {
|
|
192
|
+
callbacks.onError(error)
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
if (done) {
|
|
196
|
+
return new Response(
|
|
197
|
+
nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,
|
|
198
|
+
{
|
|
199
|
+
status: alsResponse.status,
|
|
200
|
+
statusText: alsResponse.statusText,
|
|
201
|
+
headers: {
|
|
202
|
+
'Content-Type': 'application/json',
|
|
203
|
+
[X_TSS_SERIALIZED]: 'true',
|
|
204
|
+
},
|
|
190
205
|
},
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// not done yet, we need to stream
|
|
210
|
+
const encoder = new TextEncoder()
|
|
211
|
+
const stream = new ReadableStream({
|
|
212
|
+
start(controller) {
|
|
213
|
+
callbacks.onParse = (value) =>
|
|
214
|
+
controller.enqueue(encoder.encode(JSON.stringify(value) + '\n'))
|
|
215
|
+
callbacks.onDone = () => {
|
|
216
|
+
try {
|
|
217
|
+
controller.close()
|
|
218
|
+
} catch (error) {
|
|
219
|
+
controller.error(error)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
callbacks.onError = (error) => controller.error(error)
|
|
223
|
+
// stream the initial body
|
|
224
|
+
if (nonStreamingBody !== undefined) {
|
|
225
|
+
callbacks.onParse(nonStreamingBody)
|
|
226
|
+
}
|
|
191
227
|
},
|
|
192
|
-
)
|
|
228
|
+
})
|
|
229
|
+
return new Response(stream, {
|
|
230
|
+
status: alsResponse.status,
|
|
231
|
+
statusText: alsResponse.statusText,
|
|
232
|
+
headers: {
|
|
233
|
+
'Content-Type': 'application/x-ndjson',
|
|
234
|
+
[X_TSS_SERIALIZED]: 'true',
|
|
235
|
+
},
|
|
236
|
+
})
|
|
193
237
|
}
|
|
194
238
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
start(controller) {
|
|
199
|
-
callbacks.onParse = (value) =>
|
|
200
|
-
controller.enqueue(encoder.encode(JSON.stringify(value) + '\n'))
|
|
201
|
-
callbacks.onDone = () => {
|
|
202
|
-
try {
|
|
203
|
-
controller.close()
|
|
204
|
-
} catch (error) {
|
|
205
|
-
controller.error(error)
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
callbacks.onError = (error) => controller.error(error)
|
|
209
|
-
// stream the initial body
|
|
210
|
-
if (nonStreamingBody !== undefined) {
|
|
211
|
-
callbacks.onParse(nonStreamingBody)
|
|
212
|
-
}
|
|
213
|
-
},
|
|
214
|
-
})
|
|
215
|
-
return new Response(stream, {
|
|
216
|
-
status: response.status,
|
|
217
|
-
statusText: response.statusText,
|
|
218
|
-
headers: {
|
|
219
|
-
'Content-Type': 'application/x-ndjson',
|
|
220
|
-
[X_TSS_SERIALIZED]: 'true',
|
|
221
|
-
},
|
|
239
|
+
return new Response(undefined, {
|
|
240
|
+
status: alsResponse.status,
|
|
241
|
+
statusText: alsResponse.statusText,
|
|
222
242
|
})
|
|
223
243
|
}
|
|
224
|
-
|
|
225
|
-
return new Response(undefined, {
|
|
226
|
-
status: response.status,
|
|
227
|
-
statusText: response.statusText,
|
|
228
|
-
})
|
|
229
244
|
} catch (error: any) {
|
|
230
245
|
if (error instanceof Response) {
|
|
231
246
|
return error
|