@tanstack/start-server-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,5 +1,10 @@
1
1
  import { createMemoryHistory } from '@tanstack/history'
2
- import { flattenMiddlewares, mergeHeaders } from '@tanstack/start-client-core'
2
+ import {
3
+ createNullProtoObject,
4
+ flattenMiddlewares,
5
+ mergeHeaders,
6
+ safeObjectMerge,
7
+ } from '@tanstack/start-client-core'
3
8
  import {
4
9
  executeRewriteInput,
5
10
  isRedirect,
@@ -17,6 +22,8 @@ import { handleServerAction } from './server-functions-handler'
17
22
  import { HEADERS } from './constants'
18
23
  import { ServerFunctionSerializationAdapter } from './serializer/ServerFunctionSerializationAdapter'
19
24
  import type {
25
+ AnyFunctionMiddleware,
26
+ AnyRequestMiddleware,
20
27
  AnyStartInstanceOptions,
21
28
  RouteMethod,
22
29
  RouteMethodHandlerFn,
@@ -27,7 +34,6 @@ import type { RequestHandler } from './request-handler'
27
34
  import type {
28
35
  AnyRoute,
29
36
  AnyRouter,
30
- Awaitable,
31
37
  Manifest,
32
38
  Register,
33
39
  } from '@tanstack/router-core'
@@ -35,6 +41,10 @@ import type { HandlerCallback } from '@tanstack/router-core/ssr/server'
35
41
 
36
42
  type TODO = any
37
43
 
44
+ type AnyMiddlewareServerFn =
45
+ | AnyRequestMiddleware['options']['server']
46
+ | AnyFunctionMiddleware['options']['server']
47
+
38
48
  function getStartResponseHeaders(opts: { router: AnyRouter }) {
39
49
  const headers = mergeHeaders(
40
50
  {
@@ -47,46 +57,165 @@ function getStartResponseHeaders(opts: { router: AnyRouter }) {
47
57
  return headers
48
58
  }
49
59
 
50
- export function createStartHandler<TRegister = Register>(
51
- cb: HandlerCallback<AnyRouter>,
52
- ): RequestHandler<TRegister> {
53
- const ROUTER_BASEPATH = process.env.TSS_ROUTER_BASEPATH || '/'
54
- let startRoutesManifest: Manifest | null = null
55
- let startEntry: StartEntry | null = null
56
- let routerEntry: RouterEntry | null = null
57
- const getEntries = async (): Promise<{
58
- startEntry: StartEntry
59
- routerEntry: RouterEntry
60
- }> => {
61
- if (routerEntry === null) {
62
- // @ts-ignore when building, we currently don't respect tsconfig.ts' `include` so we are not picking up the .d.ts from start-client-core
63
- routerEntry = await import('#tanstack-router-entry')
60
+ // Cached entries - loaded once per process
61
+ let cachedStartEntry: StartEntry | null = null
62
+ let cachedRouterEntry: RouterEntry | null = null
63
+ let cachedManifest: Manifest | null = null
64
+
65
+ async function getEntries(): Promise<{
66
+ startEntry: StartEntry
67
+ routerEntry: RouterEntry
68
+ }> {
69
+ if (cachedRouterEntry === null) {
70
+ // @ts-ignore when building, we currently don't respect tsconfig.ts' `include` so we are not picking up the .d.ts from start-client-core
71
+ cachedRouterEntry = await import('#tanstack-router-entry')
72
+ }
73
+ if (cachedStartEntry === null) {
74
+ // @ts-ignore when building, we currently don't respect tsconfig.ts' `include` so we are not picking up the .d.ts from start-client-core
75
+ cachedStartEntry = await import('#tanstack-start-entry')
76
+ }
77
+ return {
78
+ startEntry: cachedStartEntry as unknown as StartEntry,
79
+ routerEntry: cachedRouterEntry as unknown as RouterEntry,
80
+ }
81
+ }
82
+
83
+ async function getManifest(): Promise<Manifest> {
84
+ if (cachedManifest === null) {
85
+ cachedManifest = await getStartManifest()
86
+ }
87
+ return cachedManifest
88
+ }
89
+
90
+ // Pre-computed constants
91
+ const ROUTER_BASEPATH = process.env.TSS_ROUTER_BASEPATH || '/'
92
+ const SERVER_FN_BASE = process.env.TSS_SERVER_FN_BASE
93
+ const IS_PRERENDERING = process.env.TSS_PRERENDERING === 'true'
94
+ const IS_SHELL_ENV = process.env.TSS_SHELL === 'true'
95
+ const IS_DEV = process.env.NODE_ENV === 'development'
96
+
97
+ // Reusable error messages
98
+ const ERR_NO_RESPONSE = IS_DEV
99
+ ? `It looks like you forgot to return a response from your server route handler. If you want to defer to the app router, make sure to have a component set in this route.`
100
+ : 'Internal Server Error'
101
+
102
+ const ERR_NO_DEFER = IS_DEV
103
+ ? `You cannot defer to the app router if there is no component defined on this route.`
104
+ : 'Internal Server Error'
105
+
106
+ function throwRouteHandlerError(): never {
107
+ throw new Error(ERR_NO_RESPONSE)
108
+ }
109
+
110
+ function throwIfMayNotDefer(): never {
111
+ throw new Error(ERR_NO_DEFER)
112
+ }
113
+
114
+ /**
115
+ * Check if a value is a special response (Response or Redirect)
116
+ */
117
+ function isSpecialResponse(value: unknown): value is Response {
118
+ return value instanceof Response || isRedirect(value)
119
+ }
120
+
121
+ /**
122
+ * Normalize middleware result to context shape
123
+ */
124
+ function handleCtxResult(result: TODO) {
125
+ if (isSpecialResponse(result)) {
126
+ return { response: result }
127
+ }
128
+ return result
129
+ }
130
+
131
+ /**
132
+ * Execute a middleware chain
133
+ */
134
+ function executeMiddleware(middlewares: Array<TODO>, ctx: TODO): Promise<TODO> {
135
+ let index = -1
136
+
137
+ const next = async (nextCtx?: TODO): Promise<TODO> => {
138
+ // Merge context if provided using safeObjectMerge for prototype pollution prevention
139
+ if (nextCtx) {
140
+ if (nextCtx.context) {
141
+ ctx.context = safeObjectMerge(ctx.context, nextCtx.context)
142
+ }
143
+ // Copy own properties except context (Object.keys returns only own enumerable properties)
144
+ for (const key of Object.keys(nextCtx)) {
145
+ if (key !== 'context') {
146
+ ctx[key] = nextCtx[key]
147
+ }
148
+ }
64
149
  }
65
- if (startEntry === null) {
66
- // @ts-ignore when building, we currently don't respect tsconfig.ts' `include` so we are not picking up the .d.ts from start-client-core
67
- startEntry = await import('#tanstack-start-entry')
150
+
151
+ index++
152
+ const middleware = middlewares[index]
153
+ if (!middleware) return ctx
154
+
155
+ let result: TODO
156
+ try {
157
+ result = await middleware({ ...ctx, next })
158
+ } catch (err) {
159
+ if (isSpecialResponse(err)) {
160
+ ctx.response = err
161
+ return ctx
162
+ }
163
+ throw err
68
164
  }
69
- return {
70
- startEntry: startEntry as unknown as StartEntry,
71
- routerEntry: routerEntry as unknown as RouterEntry,
165
+
166
+ const normalized = handleCtxResult(result)
167
+ if (normalized) {
168
+ if (normalized.response !== undefined) {
169
+ ctx.response = normalized.response
170
+ }
171
+ if (normalized.context) {
172
+ ctx.context = safeObjectMerge(ctx.context, normalized.context)
173
+ }
72
174
  }
175
+
176
+ return ctx
73
177
  }
74
178
 
179
+ return next()
180
+ }
181
+
182
+ /**
183
+ * Wrap a route handler as middleware
184
+ */
185
+ function handlerToMiddleware(
186
+ handler: RouteMethodHandlerFn<any, AnyRoute, any, any, any, any, any>,
187
+ mayDefer: boolean = false,
188
+ ): TODO {
189
+ if (mayDefer) {
190
+ return handler
191
+ }
192
+ return async (ctx: TODO) => {
193
+ const response = await handler({ ...ctx, next: throwIfMayNotDefer })
194
+ if (!response) {
195
+ throwRouteHandlerError()
196
+ }
197
+ return response
198
+ }
199
+ }
200
+
201
+ export function createStartHandler<TRegister = Register>(
202
+ cb: HandlerCallback<AnyRouter>,
203
+ ): RequestHandler<TRegister> {
75
204
  const startRequestResolver: RequestHandler<Register> = async (
76
205
  request,
77
206
  requestOpts,
78
207
  ) => {
79
208
  let router: AnyRouter | null = null as AnyRouter | null
80
- // Track whether the callback will handle cleanup
81
209
  let cbWillCleanup = false as boolean
82
- try {
83
- const origin = getOrigin(request)
84
210
 
211
+ try {
85
212
  const url = new URL(request.url)
86
213
  const href = url.href.replace(url.origin, '')
214
+ const origin = getOrigin(request)
87
215
 
216
+ const entries = await getEntries()
88
217
  const startOptions: AnyStartInstanceOptions =
89
- (await (await getEntries()).startEntry.startInstance?.getOptions()) ||
218
+ (await entries.startEntry.startInstance?.getOptions()) ||
90
219
  ({} as AnyStartInstanceOptions)
91
220
 
92
221
  const serializationAdapters = [
@@ -99,22 +228,27 @@ export function createStartHandler<TRegister = Register>(
99
228
  serializationAdapters,
100
229
  }
101
230
 
102
- const getRouter = async () => {
231
+ // Flatten request middlewares once
232
+ const flattenedRequestMiddlewares = startOptions.requestMiddleware
233
+ ? flattenMiddlewares(startOptions.requestMiddleware)
234
+ : []
235
+
236
+ // Create set for deduplication
237
+ const executedRequestMiddlewares = new Set<TODO>(
238
+ flattenedRequestMiddlewares,
239
+ )
240
+
241
+ // Memoized router getter
242
+ const getRouter = async (): Promise<AnyRouter> => {
103
243
  if (router) return router
104
- router = await (await getEntries()).routerEntry.getRouter()
105
-
106
- // Update the client-side router with the history
107
- const isPrerendering = process.env.TSS_PRERENDERING === 'true'
108
- // env var is set during dev is SPA mode is enabled
109
- let isShell = process.env.TSS_SHELL === 'true'
110
- if (isPrerendering && !isShell) {
111
- // only read the shell header if we are prerendering
112
- // to avoid runtime behavior changes by injecting this header
113
- // the header is set by the prerender plugin
244
+
245
+ router = await entries.routerEntry.getRouter()
246
+
247
+ let isShell = IS_SHELL_ENV
248
+ if (IS_PRERENDERING && !isShell) {
114
249
  isShell = request.headers.get(HEADERS.TSS_SHELL) === 'true'
115
250
  }
116
251
 
117
- // Create a history for the client-side router
118
252
  const history = createMemoryHistory({
119
253
  initialEntries: [href],
120
254
  })
@@ -122,7 +256,7 @@ export function createStartHandler<TRegister = Register>(
122
256
  router.update({
123
257
  history,
124
258
  isShell,
125
- isPrerendering,
259
+ isPrerendering: IS_PRERENDERING,
126
260
  origin: router.options.origin ?? origin,
127
261
  ...{
128
262
  defaultSsr: requestStartOptions.defaultSsr,
@@ -133,184 +267,134 @@ export function createStartHandler<TRegister = Register>(
133
267
  },
134
268
  basepath: ROUTER_BASEPATH,
135
269
  })
270
+
136
271
  return router
137
272
  }
138
273
 
139
- const requestHandlerMiddleware = handlerToMiddleware(
140
- async ({ context }) => {
141
- const response = await runWithStartContext(
274
+ // Check for server function requests first (early exit)
275
+ if (SERVER_FN_BASE && url.pathname.startsWith(SERVER_FN_BASE)) {
276
+ const serverFnId = url.pathname
277
+ .slice(SERVER_FN_BASE.length)
278
+ .split('/')[0]
279
+
280
+ if (!serverFnId) {
281
+ throw new Error('Invalid server action param for serverFnId')
282
+ }
283
+
284
+ const serverFnHandler = async ({ context }: TODO) => {
285
+ return runWithStartContext(
142
286
  {
143
287
  getRouter,
144
288
  startOptions: requestStartOptions,
145
289
  contextAfterGlobalMiddlewares: context,
146
290
  request,
291
+ executedRequestMiddlewares,
147
292
  },
148
- async () => {
149
- try {
150
- // First, let's attempt to handle server functions
151
- if (href.startsWith(process.env.TSS_SERVER_FN_BASE)) {
152
- return await handleServerAction({
153
- request,
154
- context: requestOpts?.context,
155
- })
156
- }
157
-
158
- const executeRouter = async ({
159
- serverContext,
160
- }: {
161
- serverContext: any
162
- }) => {
163
- const requestAcceptHeader =
164
- request.headers.get('Accept') || '*/*'
165
- const splitRequestAcceptHeader =
166
- requestAcceptHeader.split(',')
167
-
168
- const supportedMimeTypes = ['*/*', 'text/html']
169
- const isRouterAcceptSupported = supportedMimeTypes.some(
170
- (mimeType) =>
171
- splitRequestAcceptHeader.some((acceptedMimeType) =>
172
- acceptedMimeType.trim().startsWith(mimeType),
173
- ),
174
- )
175
-
176
- if (!isRouterAcceptSupported) {
177
- return Response.json(
178
- {
179
- error: 'Only HTML requests are supported here',
180
- },
181
- {
182
- status: 500,
183
- },
184
- )
185
- }
186
-
187
- // if the startRoutesManifest is not loaded yet, load it once
188
- if (startRoutesManifest === null) {
189
- startRoutesManifest = await getStartManifest()
190
- }
191
- const router = await getRouter()
192
- attachRouterServerSsrUtils({
193
- router,
194
- manifest: startRoutesManifest,
195
- })
196
-
197
- router.update({ additionalContext: { serverContext } })
198
- await router.load()
199
-
200
- // If there was a redirect, skip rendering the page at all
201
- if (router.state.redirect) {
202
- return router.state.redirect
203
- }
204
-
205
- await router.serverSsr!.dehydrate()
206
-
207
- const responseHeaders = getStartResponseHeaders({ router })
208
- // Mark that the callback will handle cleanup
209
- cbWillCleanup = true
210
- const response = await cb({
211
- request,
212
- router,
213
- responseHeaders,
214
- })
215
-
216
- return response
217
- }
218
-
219
- const response = await handleServerRoutes({
220
- getRouter,
221
- request,
222
- executeRouter,
223
- context,
224
- })
225
-
226
- return response
227
- } catch (err) {
228
- if (err instanceof Response) {
229
- return err
230
- }
231
-
232
- throw err
233
- }
234
- },
293
+ () =>
294
+ handleServerAction({
295
+ request,
296
+ context: requestOpts?.context,
297
+ serverFnId,
298
+ }),
235
299
  )
236
- return response
237
- },
238
- )
300
+ }
239
301
 
240
- const flattenedMiddlewares = startOptions.requestMiddleware
241
- ? flattenMiddlewares(startOptions.requestMiddleware)
242
- : []
243
- const middlewares = flattenedMiddlewares.map((d) => d.options.server)
244
- const ctx = await executeMiddleware(
245
- [...middlewares, requestHandlerMiddleware],
246
- {
302
+ const middlewares = flattenedRequestMiddlewares.map(
303
+ (d) => d.options.server,
304
+ )
305
+ const ctx = await executeMiddleware([...middlewares, serverFnHandler], {
247
306
  request,
307
+ context: createNullProtoObject(requestOpts?.context),
308
+ })
248
309
 
249
- context: requestOpts?.context || {},
250
- },
251
- )
310
+ return handleRedirectResponse(ctx.response, request, getRouter)
311
+ }
252
312
 
253
- const response: Response = ctx.response
254
-
255
- if (isRedirect(response)) {
256
- if (isResolvedRedirect(response)) {
257
- if (request.headers.get('x-tsr-redirect') === 'manual') {
258
- return Response.json(
259
- {
260
- ...response.options,
261
- isSerializedRedirect: true,
262
- },
263
- {
264
- headers: response.headers,
265
- },
266
- )
267
- }
268
- return response
269
- }
270
- if (
271
- response.options.to &&
272
- typeof response.options.to === 'string' &&
273
- !response.options.to.startsWith('/')
274
- ) {
275
- throw new Error(
276
- `Server side redirects must use absolute paths via the 'href' or 'to' options. The redirect() method's "to" property accepts an internal path only. Use the "href" property to provide an external URL. Received: ${JSON.stringify(response.options)}`,
277
- )
278
- }
313
+ // Router execution function
314
+ const executeRouter = async (serverContext: TODO): Promise<Response> => {
315
+ const acceptHeader = request.headers.get('Accept') || '*/*'
316
+ const acceptParts = acceptHeader.split(',')
317
+ const supportedMimeTypes = ['*/*', 'text/html']
279
318
 
280
- if (
281
- ['params', 'search', 'hash'].some(
282
- (d) => typeof (response.options as any)[d] === 'function',
283
- )
284
- ) {
285
- throw new Error(
286
- `Server side redirects must use static search, params, and hash values and do not support functional values. Received functional values for: ${Object.keys(
287
- response.options,
288
- )
289
- .filter((d) => typeof (response.options as any)[d] === 'function')
290
- .map((d) => `"${d}"`)
291
- .join(', ')}`,
319
+ const isSupported = supportedMimeTypes.some((mimeType) =>
320
+ acceptParts.some((part) => part.trim().startsWith(mimeType)),
321
+ )
322
+
323
+ if (!isSupported) {
324
+ return Response.json(
325
+ { error: 'Only HTML requests are supported here' },
326
+ { status: 500 },
292
327
  )
293
328
  }
294
329
 
295
- const router = await getRouter()
296
- const redirect = router.resolveRedirect(response)
330
+ const manifest = await getManifest()
331
+ const routerInstance = await getRouter()
297
332
 
298
- if (request.headers.get('x-tsr-redirect') === 'manual') {
299
- return Response.json(
300
- {
301
- ...response.options,
302
- isSerializedRedirect: true,
303
- },
304
- {
305
- headers: response.headers,
306
- },
307
- )
333
+ attachRouterServerSsrUtils({
334
+ router: routerInstance,
335
+ manifest,
336
+ })
337
+
338
+ routerInstance.update({ additionalContext: { serverContext } })
339
+ await routerInstance.load()
340
+
341
+ if (routerInstance.state.redirect) {
342
+ return routerInstance.state.redirect
308
343
  }
309
344
 
310
- return redirect
345
+ await routerInstance.serverSsr!.dehydrate()
346
+
347
+ const responseHeaders = getStartResponseHeaders({
348
+ router: routerInstance,
349
+ })
350
+ cbWillCleanup = true
351
+
352
+ return cb({
353
+ request,
354
+ router: routerInstance,
355
+ responseHeaders,
356
+ })
311
357
  }
312
358
 
313
- return response
359
+ // Main request handler
360
+ const requestHandlerMiddleware = async ({ context }: TODO) => {
361
+ return runWithStartContext(
362
+ {
363
+ getRouter,
364
+ startOptions: requestStartOptions,
365
+ contextAfterGlobalMiddlewares: context,
366
+ request,
367
+ executedRequestMiddlewares,
368
+ },
369
+ async () => {
370
+ try {
371
+ return await handleServerRoutes({
372
+ getRouter,
373
+ request,
374
+ url,
375
+ executeRouter,
376
+ context,
377
+ executedRequestMiddlewares,
378
+ })
379
+ } catch (err) {
380
+ if (err instanceof Response) {
381
+ return err
382
+ }
383
+ throw err
384
+ }
385
+ },
386
+ )
387
+ }
388
+
389
+ const middlewares = flattenedRequestMiddlewares.map(
390
+ (d) => d.options.server,
391
+ )
392
+ const ctx = await executeMiddleware(
393
+ [...middlewares, requestHandlerMiddleware],
394
+ { request, context: createNullProtoObject(requestOpts?.context) },
395
+ )
396
+
397
+ return handleRedirectResponse(ctx.response, request, getRouter)
314
398
  } finally {
315
399
  if (router && !cbWillCleanup) {
316
400
  // Clean up router SSR state if it was set up but won't be cleaned up by the callback
@@ -326,25 +410,78 @@ export function createStartHandler<TRegister = Register>(
326
410
  return requestHandler(startRequestResolver)
327
411
  }
328
412
 
413
+ async function handleRedirectResponse(
414
+ response: Response,
415
+ request: Request,
416
+ getRouter: () => Promise<AnyRouter>,
417
+ ): Promise<Response> {
418
+ if (!isRedirect(response)) {
419
+ return response
420
+ }
421
+
422
+ if (isResolvedRedirect(response)) {
423
+ if (request.headers.get('x-tsr-serverFn') === 'true') {
424
+ return Response.json(
425
+ { ...response.options, isSerializedRedirect: true },
426
+ { headers: response.headers },
427
+ )
428
+ }
429
+ return response
430
+ }
431
+
432
+ const opts = response.options
433
+ if (opts.to && typeof opts.to === 'string' && !opts.to.startsWith('/')) {
434
+ throw new Error(
435
+ `Server side redirects must use absolute paths via the 'href' or 'to' options. The redirect() method's "to" property accepts an internal path only. Use the "href" property to provide an external URL. Received: ${JSON.stringify(opts)}`,
436
+ )
437
+ }
438
+
439
+ if (
440
+ ['params', 'search', 'hash'].some(
441
+ (d) => typeof (opts as TODO)[d] === 'function',
442
+ )
443
+ ) {
444
+ throw new Error(
445
+ `Server side redirects must use static search, params, and hash values and do not support functional values. Received functional values for: ${Object.keys(
446
+ opts,
447
+ )
448
+ .filter((d) => typeof (opts as TODO)[d] === 'function')
449
+ .map((d) => `"${d}"`)
450
+ .join(', ')}`,
451
+ )
452
+ }
453
+
454
+ const router = await getRouter()
455
+ const redirect = router.resolveRedirect(response)
456
+
457
+ if (request.headers.get('x-tsr-serverFn') === 'true') {
458
+ return Response.json(
459
+ { ...response.options, isSerializedRedirect: true },
460
+ { headers: response.headers },
461
+ )
462
+ }
463
+
464
+ return redirect
465
+ }
466
+
329
467
  async function handleServerRoutes({
330
468
  getRouter,
331
469
  request,
470
+ url,
332
471
  executeRouter,
333
472
  context,
473
+ executedRequestMiddlewares,
334
474
  }: {
335
- getRouter: () => Awaitable<AnyRouter>
475
+ getRouter: () => Promise<AnyRouter>
336
476
  request: Request
337
- executeRouter: ({
338
- serverContext,
339
- }: {
340
- serverContext: any
341
- }) => Promise<Response>
477
+ url: URL
478
+ executeRouter: (serverContext: any) => Promise<Response>
342
479
  context: any
343
- }) {
480
+ executedRequestMiddlewares: Set<AnyRequestMiddleware>
481
+ }): Promise<Response> {
344
482
  const router = await getRouter()
345
- let url = new URL(request.url)
346
- url = executeRewriteInput(router.rewrite, url)
347
- const pathname = url.pathname
483
+ const rewrittenUrl = executeRewriteInput(router.rewrite, url)
484
+ const pathname = rewrittenUrl.pathname
348
485
  // this will perform a fuzzy match, however for server routes we need an exact match
349
486
  // if the route is not an exact match, executeRouter will handle rendering the app router
350
487
  // the match will be cached internally, so no extra work is done during the app router render
@@ -353,158 +490,64 @@ async function handleServerRoutes({
353
490
 
354
491
  const isExactMatch = foundRoute && routeParams['**'] === undefined
355
492
 
356
- // TODO: Error handling? What happens when its `throw redirect()` vs `throw new Error()`?
357
-
358
- const middlewares = flattenMiddlewares(
359
- matchedRoutes.flatMap((r) => r.options.server?.middleware).filter(Boolean),
360
- ).map((d) => d.options.server)
361
-
362
- const server = foundRoute?.options.server
363
- if (server && isExactMatch) {
364
- if (server.handlers) {
365
- const handlers =
366
- typeof server.handlers === 'function'
367
- ? server.handlers({
368
- createHandlers: (d: any) => d,
369
- })
370
- : server.handlers
371
-
372
- const requestMethod = request.method.toUpperCase() as RouteMethod
373
-
374
- // Attempt to find the method in the handlers
375
- const handler = handlers[requestMethod] ?? handlers['ANY']
376
-
377
- // If a method is found, execute the handler
378
- if (handler) {
379
- const mayDefer = !!foundRoute.options.component
380
- if (typeof handler === 'function') {
381
- middlewares.push(handlerToMiddleware(handler, mayDefer))
382
- } else {
383
- const { middleware } = handler
384
- if (middleware && middleware.length) {
385
- middlewares.push(
386
- ...flattenMiddlewares(middleware).map((d) => d.options.server),
387
- )
388
- }
389
- if (handler.handler) {
390
- middlewares.push(handlerToMiddleware(handler.handler, mayDefer))
391
- }
493
+ // Collect and dedupe route middlewares
494
+ const routeMiddlewares: Array<AnyMiddlewareServerFn> = []
495
+
496
+ // Collect middleware from matched routes, filtering out those already executed
497
+ // in the request phase
498
+ for (const route of matchedRoutes) {
499
+ const serverMiddleware = route.options.server?.middleware as
500
+ | Array<AnyRequestMiddleware>
501
+ | undefined
502
+ if (serverMiddleware) {
503
+ const flattened = flattenMiddlewares(serverMiddleware)
504
+ for (const m of flattened) {
505
+ if (!executedRequestMiddlewares.has(m)) {
506
+ routeMiddlewares.push(m.options.server)
392
507
  }
393
508
  }
394
509
  }
395
510
  }
396
511
 
397
- // eventually, execute the router
398
- middlewares.push(
399
- handlerToMiddleware((ctx) => executeRouter({ serverContext: ctx.context })),
400
- )
401
-
402
- const ctx = await executeMiddleware(middlewares, {
403
- request,
404
- context,
405
- params: routeParams,
406
- pathname,
407
- })
408
-
409
- const response: Response = ctx.response
410
-
411
- return response
412
- }
413
-
414
- function throwRouteHandlerError() {
415
- if (process.env.NODE_ENV === 'development') {
416
- throw new Error(
417
- `It looks like you forgot to return a response from your server route handler. If you want to defer to the app router, make sure to have a component set in this route.`,
418
- )
419
- }
420
- throw new Error('Internal Server Error')
421
- }
422
-
423
- function throwIfMayNotDefer() {
424
- if (process.env.NODE_ENV === 'development') {
425
- throw new Error(
426
- `You cannot defer to the app router if there is no component defined on this route.`,
427
- )
428
- }
429
- throw new Error('Internal Server Error')
430
- }
431
- function handlerToMiddleware(
432
- handler: RouteMethodHandlerFn<any, AnyRoute, any, any, any, any, any>,
433
- mayDefer: boolean = false,
434
- ) {
435
- if (mayDefer) {
436
- return handler as TODO
437
- }
438
- return async ({ next: _next, ...rest }: TODO) => {
439
- const response = await handler({ ...rest, next: throwIfMayNotDefer })
440
- if (!response) {
441
- throwRouteHandlerError()
442
- }
443
- return response
444
- }
445
- }
512
+ // Add handler middleware if exact match
513
+ const server = foundRoute?.options.server
514
+ if (server?.handlers && isExactMatch) {
515
+ const handlers =
516
+ typeof server.handlers === 'function'
517
+ ? server.handlers({ createHandlers: (d: any) => d })
518
+ : server.handlers
446
519
 
447
- function executeMiddleware(middlewares: TODO, ctx: TODO) {
448
- let index = -1
520
+ const requestMethod = request.method.toUpperCase() as RouteMethod
521
+ const handler = handlers[requestMethod] ?? handlers['ANY']
449
522
 
450
- const next = async (ctx: TODO) => {
451
- index++
452
- const middleware = middlewares[index]
453
- if (!middleware) return ctx
523
+ if (handler) {
524
+ const mayDefer = !!foundRoute.options.component
454
525
 
455
- let result
456
- try {
457
- result = await middleware({
458
- ...ctx,
459
- // Allow the middleware to call the next middleware in the chain
460
- next: async (nextCtx: TODO) => {
461
- // Allow the caller to extend the context for the next middleware
462
- const nextResult = await next({
463
- ...ctx,
464
- ...nextCtx,
465
- context: {
466
- ...ctx.context,
467
- ...(nextCtx?.context || {}),
468
- },
469
- })
470
-
471
- // Merge the result into the context\
472
- return Object.assign(ctx, handleCtxResult(nextResult))
473
- },
474
- // Allow the middleware result to extend the return context
475
- })
476
- } catch (err: TODO) {
477
- if (isSpecialResponse(err)) {
478
- result = {
479
- response: err,
480
- }
526
+ if (typeof handler === 'function') {
527
+ routeMiddlewares.push(handlerToMiddleware(handler, mayDefer))
481
528
  } else {
482
- throw err
529
+ if (handler.middleware?.length) {
530
+ const handlerMiddlewares = flattenMiddlewares(handler.middleware)
531
+ for (const m of handlerMiddlewares) {
532
+ routeMiddlewares.push(m.options.server)
533
+ }
534
+ }
535
+ if (handler.handler) {
536
+ routeMiddlewares.push(handlerToMiddleware(handler.handler, mayDefer))
537
+ }
483
538
  }
484
539
  }
485
-
486
- // Merge the middleware result into the context, just in case it
487
- // returns a partial context
488
- return Object.assign(ctx, handleCtxResult(result))
489
- }
490
-
491
- return handleCtxResult(next(ctx))
492
- }
493
-
494
- function handleCtxResult(result: TODO) {
495
- if (isSpecialResponse(result)) {
496
- return {
497
- response: result,
498
- }
499
540
  }
500
541
 
501
- return result
502
- }
542
+ // Final middleware: execute router
543
+ routeMiddlewares.push((ctx: TODO) => executeRouter(ctx.context))
503
544
 
504
- function isSpecialResponse(err: TODO) {
505
- return isResponse(err) || isRedirect(err)
506
- }
545
+ const ctx = await executeMiddleware(routeMiddlewares, {
546
+ request,
547
+ context,
548
+ params: routeParams,
549
+ pathname,
550
+ })
507
551
 
508
- function isResponse(response: Response): response is Response {
509
- return response instanceof Response
552
+ return ctx.response
510
553
  }