@tanstack/start-client-core 1.143.9 → 1.144.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.
@@ -1,10 +1,10 @@
1
- import { isNotFound, isRedirect } from '@tanstack/router-core'
2
1
  import { mergeHeaders } from '@tanstack/router-core/ssr/client'
3
2
 
3
+ import { isRedirect, parseRedirect } from '@tanstack/router-core'
4
4
  import { TSS_SERVER_FUNCTION_FACTORY } from './constants'
5
5
  import { getStartOptions } from './getStartOptions'
6
6
  import { getStartContextServerOnly } from './getStartContextServerOnly'
7
- import type { TSS_SERVER_FUNCTION } from './constants'
7
+ import { createNullProtoObject, safeObjectMerge } from './safeObjectMerge'
8
8
  import type {
9
9
  AnyValidator,
10
10
  Constrain,
@@ -16,6 +16,7 @@ import type {
16
16
  ValidateSerializableInput,
17
17
  Validator,
18
18
  } from '@tanstack/router-core'
19
+ import type { TSS_SERVER_FUNCTION } from './constants'
19
20
  import type {
20
21
  AnyFunctionMiddleware,
21
22
  AnyRequestMiddleware,
@@ -112,17 +113,22 @@ export const createServerFn: CreateServerFn<Register> = (options, __opts) => {
112
113
  return Object.assign(
113
114
  async (opts?: CompiledFetcherFnOptions) => {
114
115
  // Start by executing the client-side middleware chain
115
- return executeMiddleware(resolvedMiddleware, 'client', {
116
+ const result = await executeMiddleware(resolvedMiddleware, 'client', {
116
117
  ...extractedFn,
117
118
  ...newOptions,
118
119
  data: opts?.data as any,
119
120
  headers: opts?.headers,
120
121
  signal: opts?.signal,
121
- context: {},
122
- }).then((d) => {
123
- if (d.error) throw d.error
124
- return d.result
122
+ context: createNullProtoObject(),
125
123
  })
124
+
125
+ const redirect = parseRedirect(result.error)
126
+ if (redirect) {
127
+ throw redirect
128
+ }
129
+
130
+ if (result.error) throw result.error
131
+ return result.result
126
132
  },
127
133
  {
128
134
  // This copies over the URL, function ID
@@ -133,25 +139,30 @@ export const createServerFn: CreateServerFn<Register> = (options, __opts) => {
133
139
  const startContext = getStartContextServerOnly()
134
140
  const serverContextAfterGlobalMiddlewares =
135
141
  startContext.contextAfterGlobalMiddlewares
142
+ // Use safeObjectMerge for opts.context which comes from client
136
143
  const ctx = {
137
144
  ...extractedFn,
138
145
  ...opts,
139
- context: {
140
- ...serverContextAfterGlobalMiddlewares,
141
- ...opts.context,
142
- },
146
+ context: safeObjectMerge(
147
+ serverContextAfterGlobalMiddlewares,
148
+ opts.context,
149
+ ),
143
150
  signal,
144
151
  request: startContext.request,
145
152
  }
146
153
 
147
- return executeMiddleware(resolvedMiddleware, 'server', ctx).then(
148
- (d) => ({
149
- // Only send the result and sendContext back to the client
150
- result: d.result,
151
- error: d.error,
152
- context: d.sendContext,
153
- }),
154
- )
154
+ const result = await executeMiddleware(
155
+ resolvedMiddleware,
156
+ 'server',
157
+ ctx,
158
+ ).then((d) => ({
159
+ // Only send the result and sendContext back to the client
160
+ result: d.result,
161
+ error: d.error,
162
+ context: d.sendContext,
163
+ }))
164
+
165
+ return result
155
166
  },
156
167
  },
157
168
  ) as any
@@ -173,12 +184,23 @@ export async function executeMiddleware(
173
184
  opts: ServerFnMiddlewareOptions,
174
185
  ): Promise<ServerFnMiddlewareResult> {
175
186
  const globalMiddlewares = getStartOptions()?.functionMiddleware || []
176
- const flattenedMiddlewares = flattenMiddlewares([
187
+ let flattenedMiddlewares = flattenMiddlewares([
177
188
  ...globalMiddlewares,
178
189
  ...middlewares,
179
190
  ])
180
191
 
181
- const next: NextFn = async (ctx) => {
192
+ // On server, filter out middlewares that already executed in the request phase
193
+ // to prevent duplicate execution (issue #5239)
194
+ if (env === 'server') {
195
+ const startContext = getStartContextServerOnly({ throwIfNotFound: false })
196
+ if (startContext?.executedRequestMiddlewares) {
197
+ flattenedMiddlewares = flattenedMiddlewares.filter(
198
+ (m) => !startContext.executedRequestMiddlewares.has(m),
199
+ )
200
+ }
201
+ }
202
+
203
+ const callNextMiddleware: NextFn = async (ctx) => {
182
204
  // Get the next middleware
183
205
  const nextMiddleware = flattenedMiddlewares.shift()
184
206
 
@@ -187,54 +209,110 @@ export async function executeMiddleware(
187
209
  return ctx
188
210
  }
189
211
 
190
- if (
191
- 'inputValidator' in nextMiddleware.options &&
192
- nextMiddleware.options.inputValidator &&
193
- env === 'server'
194
- ) {
195
- // Execute the middleware's input function
196
- ctx.data = await execValidator(
197
- nextMiddleware.options.inputValidator,
198
- ctx.data,
199
- )
200
- }
212
+ // Execute the middleware
213
+ try {
214
+ if (
215
+ 'inputValidator' in nextMiddleware.options &&
216
+ nextMiddleware.options.inputValidator &&
217
+ env === 'server'
218
+ ) {
219
+ // Execute the middleware's input function
220
+ ctx.data = await execValidator(
221
+ nextMiddleware.options.inputValidator,
222
+ ctx.data,
223
+ )
224
+ }
201
225
 
202
- let middlewareFn: MiddlewareFn | undefined = undefined
203
- if (env === 'client') {
204
- if ('client' in nextMiddleware.options) {
205
- middlewareFn = nextMiddleware.options.client as MiddlewareFn | undefined
226
+ let middlewareFn: MiddlewareFn | undefined = undefined
227
+ if (env === 'client') {
228
+ if ('client' in nextMiddleware.options) {
229
+ middlewareFn = nextMiddleware.options.client as
230
+ | MiddlewareFn
231
+ | undefined
232
+ }
233
+ }
234
+ // env === 'server'
235
+ else if ('server' in nextMiddleware.options) {
236
+ middlewareFn = nextMiddleware.options.server as MiddlewareFn | undefined
206
237
  }
207
- }
208
- // env === 'server'
209
- else if ('server' in nextMiddleware.options) {
210
- middlewareFn = nextMiddleware.options.server as MiddlewareFn | undefined
211
- }
212
238
 
213
- if (middlewareFn) {
214
- // Execute the middleware
215
- return applyMiddleware(middlewareFn, ctx, async (newCtx) => {
216
- return next(newCtx).catch((error: any) => {
217
- if (isRedirect(error) || isNotFound(error)) {
239
+ if (middlewareFn) {
240
+ const userNext = async (
241
+ userCtx: ServerFnMiddlewareResult | undefined = {} as any,
242
+ ) => {
243
+ // Return the next middleware
244
+ // Use safeObjectMerge for context objects to prevent prototype pollution
245
+ const nextCtx = {
246
+ ...ctx,
247
+ ...userCtx,
248
+ context: safeObjectMerge(ctx.context, userCtx.context),
249
+ sendContext: safeObjectMerge(ctx.sendContext, userCtx.sendContext),
250
+ headers: mergeHeaders(ctx.headers, userCtx.headers),
251
+ result:
252
+ userCtx.result !== undefined
253
+ ? userCtx.result
254
+ : userCtx instanceof Response
255
+ ? userCtx
256
+ : (ctx as any).result,
257
+ error: userCtx.error ?? (ctx as any).error,
258
+ }
259
+
260
+ try {
261
+ return await callNextMiddleware(nextCtx)
262
+ } catch (error: any) {
218
263
  return {
219
- ...newCtx,
264
+ ...nextCtx,
220
265
  error,
221
266
  }
222
267
  }
268
+ }
223
269
 
224
- throw error
225
- })
226
- })
227
- }
270
+ // Execute the middleware
271
+ const result = await middlewareFn({
272
+ ...ctx,
273
+ next: userNext as any,
274
+ } as any)
275
+
276
+ // If result is NOT a ctx object, we need to return it as
277
+ // the { result }
278
+ if (isRedirect(result)) {
279
+ return {
280
+ ...ctx,
281
+ error: result,
282
+ }
283
+ }
284
+
285
+ if (result instanceof Response) {
286
+ return {
287
+ ...ctx,
288
+ result,
289
+ }
290
+ }
228
291
 
229
- return next(ctx)
292
+ if (!(result as any)) {
293
+ throw new Error(
294
+ 'User middleware returned undefined. You must call next() or return a result in your middlewares.',
295
+ )
296
+ }
297
+
298
+ return result
299
+ }
300
+
301
+ return callNextMiddleware(ctx)
302
+ } catch (error: any) {
303
+ return {
304
+ ...ctx,
305
+ error,
306
+ }
307
+ }
230
308
  }
231
309
 
232
310
  // Start the middleware chain
233
- return next({
311
+ return callNextMiddleware({
234
312
  ...opts,
235
313
  headers: opts.headers || {},
236
314
  sendContext: opts.sendContext || {},
237
- context: opts.context || {},
315
+ context: opts.context || createNullProtoObject(),
238
316
  })
239
317
  }
240
318
 
@@ -571,18 +649,21 @@ export interface ServerFnTypes<
571
649
  allOutput: IntersectAllValidatorOutputs<TMiddlewares, TInputValidator>
572
650
  }
573
651
 
574
- export function flattenMiddlewares(
575
- middlewares: Array<AnyFunctionMiddleware | AnyRequestMiddleware>,
576
- ): Array<AnyFunctionMiddleware | AnyRequestMiddleware> {
577
- const seen = new Set<AnyFunctionMiddleware | AnyRequestMiddleware>()
578
- const flattened: Array<AnyFunctionMiddleware | AnyRequestMiddleware> = []
652
+ export function flattenMiddlewares<
653
+ T extends AnyFunctionMiddleware | AnyRequestMiddleware,
654
+ >(middlewares: Array<T>, maxDepth: number = 100): Array<T> {
655
+ const seen = new Set<T>()
656
+ const flattened: Array<T> = []
579
657
 
580
- const recurse = (
581
- middleware: Array<AnyFunctionMiddleware | AnyRequestMiddleware>,
582
- ) => {
658
+ const recurse = (middleware: Array<T>, depth: number) => {
659
+ if (depth > maxDepth) {
660
+ throw new Error(
661
+ `Middleware nesting depth exceeded maximum of ${maxDepth}. Check for circular references.`,
662
+ )
663
+ }
583
664
  middleware.forEach((m) => {
584
665
  if (m.options.middleware) {
585
- recurse(m.options.middleware)
666
+ recurse(m.options.middleware as Array<T>, depth + 1)
586
667
  }
587
668
 
588
669
  if (!seen.has(m)) {
@@ -592,7 +673,7 @@ export function flattenMiddlewares(
592
673
  })
593
674
  }
594
675
 
595
- recurse(middlewares)
676
+ recurse(middlewares, 0)
596
677
 
597
678
  return flattened
598
679
  }
@@ -622,41 +703,6 @@ export type MiddlewareFn = (
622
703
  },
623
704
  ) => Promise<ServerFnMiddlewareResult>
624
705
 
625
- export const applyMiddleware = async (
626
- middlewareFn: MiddlewareFn,
627
- ctx: ServerFnMiddlewareOptions,
628
- nextFn: NextFn,
629
- ) => {
630
- return middlewareFn({
631
- ...ctx,
632
- next: (async (
633
- userCtx: ServerFnMiddlewareResult | undefined = {} as any,
634
- ) => {
635
- // Return the next middleware
636
- return nextFn({
637
- ...ctx,
638
- ...userCtx,
639
- context: {
640
- ...ctx.context,
641
- ...userCtx.context,
642
- },
643
- sendContext: {
644
- ...ctx.sendContext,
645
- ...(userCtx.sendContext ?? {}),
646
- },
647
- headers: mergeHeaders(ctx.headers, userCtx.headers),
648
- result:
649
- userCtx.result !== undefined
650
- ? userCtx.result
651
- : userCtx instanceof Response
652
- ? userCtx
653
- : (ctx as any).result,
654
- error: userCtx.error ?? (ctx as any).error,
655
- })
656
- }) as any,
657
- } as any)
658
- }
659
-
660
706
  export function execValidator(
661
707
  validator: AnyValidator,
662
708
  input: unknown,
package/src/index.tsx CHANGED
@@ -2,6 +2,9 @@ export type { JsonResponse } from '@tanstack/router-core/ssr/client'
2
2
 
3
3
  export { hydrate, json, mergeHeaders } from '@tanstack/router-core/ssr/client'
4
4
 
5
+ export { RawStream } from '@tanstack/router-core'
6
+ export type { OnRawStreamCallback } from '@tanstack/router-core'
7
+
5
8
  export {
6
9
  createIsomorphicFn,
7
10
  createServerOnlyFn,
@@ -72,7 +75,6 @@ export type {
72
75
  RequiredFetcher,
73
76
  } from './createServerFn'
74
77
  export {
75
- applyMiddleware,
76
78
  execValidator,
77
79
  flattenMiddlewares,
78
80
  executeMiddleware,
@@ -81,9 +83,17 @@ export {
81
83
  export {
82
84
  TSS_FORMDATA_CONTEXT,
83
85
  TSS_SERVER_FUNCTION,
86
+ TSS_CONTENT_TYPE_FRAMED,
87
+ TSS_CONTENT_TYPE_FRAMED_VERSIONED,
88
+ TSS_FRAMED_PROTOCOL_VERSION,
89
+ FrameType,
90
+ FRAME_HEADER_SIZE,
84
91
  X_TSS_SERIALIZED,
85
92
  X_TSS_RAW_RESPONSE,
93
+ X_TSS_CONTEXT,
94
+ validateFramedProtocolVersion,
86
95
  } from './constants'
96
+ export type { FrameType as FrameTypeValue } from './constants'
87
97
 
88
98
  export type * from './serverRoute'
89
99
 
@@ -100,3 +110,4 @@ export type { Register } from '@tanstack/router-core'
100
110
  export { getRouterInstance } from './getRouterInstance'
101
111
  export { getDefaultSerovalPlugins } from './getDefaultSerovalPlugins'
102
112
  export { getGlobalStartContext } from './getGlobalStartContext'
113
+ export { safeObjectMerge, createNullProtoObject } from './safeObjectMerge'
@@ -0,0 +1,38 @@
1
+ function isSafeKey(key: string): boolean {
2
+ return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'
3
+ }
4
+
5
+ /**
6
+ * Merge target and source into a new null-proto object, filtering dangerous keys.
7
+ */
8
+ export function safeObjectMerge<T extends Record<string, unknown>>(
9
+ target: T | undefined,
10
+ source: Record<string, unknown> | null | undefined,
11
+ ): T {
12
+ const result = Object.create(null) as T
13
+ if (target) {
14
+ for (const key of Object.keys(target)) {
15
+ if (isSafeKey(key)) result[key as keyof T] = target[key] as T[keyof T]
16
+ }
17
+ }
18
+ if (source && typeof source === 'object') {
19
+ for (const key of Object.keys(source)) {
20
+ if (isSafeKey(key)) result[key as keyof T] = source[key] as T[keyof T]
21
+ }
22
+ }
23
+ return result
24
+ }
25
+
26
+ /**
27
+ * Create a null-prototype object, optionally copying from source.
28
+ */
29
+ export function createNullProtoObject<T extends object>(
30
+ source?: T,
31
+ ): { [K in keyof T]: T[K] } {
32
+ if (!source) return Object.create(null)
33
+ const obj = Object.create(null)
34
+ for (const key of Object.keys(source)) {
35
+ if (isSafeKey(key)) obj[key] = (source as Record<string, unknown>)[key]
36
+ }
37
+ return obj
38
+ }