Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/honest-walls-attack.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@tanstack/start-server-core': patch
'@tanstack/start-storage-context': patch
---

Skip empty route middleware chains and unused middleware bookkeeping. Avoid awaiting absent Start configuration, reuse the parsed request origin and serialization adapters, and return cached manifests directly when asset options are static.

Use the router's decoded pathname for server route handlers and route middleware.
2 changes: 1 addition & 1 deletion packages/start-client-core/src/createServerFn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export async function executeMiddleware(
const startContext = getStartContextServerOnly({ throwIfNotFound: false })
if (startContext?.executedRequestMiddlewares) {
flattenedMiddlewares = flattenedMiddlewares.filter(
(m) => !startContext.executedRequestMiddlewares.has(m),
(m) => !startContext.executedRequestMiddlewares!.has(m),
)
}
}
Expand Down
44 changes: 24 additions & 20 deletions packages/start-server-core/src/createStartHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,6 @@ export function createStartHandler<TRegister = Register>(
// in these cases we would prefer to redirect to the new path
const { url, handledProtocolRelativeURL } = getNormalizedURL(request.url)
const href = url.pathname + url.search + url.hash
const origin = url.origin

if (handledProtocolRelativeURL) {
return Response.redirect(url, 308)
Expand Down Expand Up @@ -579,14 +578,15 @@ export function createStartHandler<TRegister = Register>(
}

// Flatten request middlewares once
const flattenedRequestMiddlewares = requestStartOptions.requestMiddleware
? flattenMiddlewares(requestStartOptions.requestMiddleware)
const requestMiddlewares = requestStartOptions.requestMiddleware
const flattenedRequestMiddlewares = requestMiddlewares?.length
? flattenMiddlewares(requestMiddlewares)
: []

// Create set for deduplication
const executedRequestMiddlewares = new Set<TODO>(
flattenedRequestMiddlewares,
)
const executedRequestMiddlewares = flattenedRequestMiddlewares.length
? new Set<TODO>(flattenedRequestMiddlewares)
: undefined

// Memoized router getter
const getRouter = (): Promise<AnyRouter> => {
Expand All @@ -608,14 +608,17 @@ export function createStartHandler<TRegister = Register>(
history,
isShell,
isPrerendering: IS_PRERENDERING,
origin: requestRouter.options.origin ?? origin,
origin: requestRouter.options.origin ?? url.origin,
// Start-owned options that RouterConstructorOptions omits.
...{
defaultSsr: requestStartOptions.defaultSsr,
serializationAdapters: [
...requestStartOptions.serializationAdapters,
...(requestRouter.options.serializationAdapters || []),
],
serializationAdapters: requestRouter.options.serializationAdapters
?.length
? [
...requestStartOptions.serializationAdapters,
...requestRouter.options.serializationAdapters,
]
: requestStartOptions.serializationAdapters,
},
basepath: ROUTER_BASEPATH,
})
Expand Down Expand Up @@ -938,18 +941,15 @@ async function handleServerRoutes({
matchedRoutes?: ReadonlyArray<AnyRoute>,
) => Promise<SsrResponse>
context: any
executedRequestMiddlewares: Set<AnyRequestMiddleware>
executedRequestMiddlewares: Set<AnyRequestMiddleware> | undefined
}): Promise<SsrResponse> {
const router = await getRouter()
const location = router.latestLocation
// Preserve the encoded pathname exposed to server handlers and middleware.
const pathname = location.href.split(/[?#]/, 1)[0]!
const { pathname } = router.latestLocation
// this will perform a fuzzy match, however for server routes we need an exact match
// if the route is not an exact match, executeRouter will handle rendering the app router
// The cached match avoids another route-tree traversal during the app router render.
const [matchedRoutes, rawParams, foundRoute] = router.getMatchedRoutes(
location.pathname,
)
const [matchedRoutes, rawParams, foundRoute] =
router.getMatchedRoutes(pathname)

const isExactMatch = foundRoute && rawParams['**'] === undefined

Expand All @@ -965,10 +965,10 @@ async function handleServerRoutes({
const serverMiddleware = route.options.server?.middleware as
| Array<AnyRequestMiddleware>
| undefined
if (serverMiddleware) {
if (serverMiddleware?.length) {
const flattened = flattenMiddlewares(serverMiddleware)
for (const m of flattened) {
if (!executedRequestMiddlewares.has(m)) {
if (!executedRequestMiddlewares?.has(m)) {
routeMiddlewares.push(m.options.server)
}
}
Expand Down Expand Up @@ -1019,6 +1019,10 @@ async function handleServerRoutes({
}
}

if (!routeMiddlewares.length && !terminalNext) {
return executeRouter(context, matchedRoutes)
}

const response = await executeMiddleware(
routeMiddlewares,
terminalHandler,
Expand Down
19 changes: 17 additions & 2 deletions packages/start-server-core/src/finalManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,8 +190,23 @@ export function createFinalManifestResolver(
transformResolver.getTransformFn({ warmup: true }),
onError: transformResolver.clearCachedCreateTransform,
}),
resolveCached: (requestOpts) =>
resolveRequest(requestOpts, finalManifestCache),
resolveCached: (requestOpts) => {
if (
opts.transformAssets === undefined &&
handlerDefaultInlineCss !== undefined
) {
const cachedManifest = finalManifestCache.get(
getFinalManifestCacheKey(
requestOpts.requestInlineCss ?? handlerDefaultInlineCss,
),
)
if (cachedManifest) {
return cachedManifest
}
}

return resolveRequest(requestOpts, finalManifestCache)
},
resolveUncached: (requestOpts) => resolveRequest(requestOpts, undefined),
}
}
Expand Down
174 changes: 165 additions & 9 deletions packages/start-server-core/tests/createStartHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
RouterCore,
createNonReactiveMutableStore,
createNonReactiveReadonlyStore,
createSerializationAdapter,
redirect,
} from '@tanstack/router-core'
import {
Expand All @@ -28,7 +29,7 @@ import {
getStaticHandlerInlineCssDefault,
resolveInlineCssForRequest,
} from '../src/inlineCss'
import type { AnyRouter } from '@tanstack/router-core'
import type { AnyRouter, AnySerializationAdapter } from '@tanstack/router-core'

const startMocks = vi.hoisted(() => {
const hadServerFnBase = Object.prototype.hasOwnProperty.call(
Expand All @@ -41,6 +42,7 @@ const startMocks = vi.hoisted(() => {
hadServerFnBase,
previousServerFnBase,
requestMiddleware: [] as Array<any>,
serializationAdapters: [] as Array<AnySerializationAdapter>,
serverFnResult: undefined as undefined | Response | object,
serverFnHandler: undefined as undefined | (() => unknown),
router: undefined as undefined | AnyRouter,
Expand All @@ -56,7 +58,7 @@ vi.mock('#tanstack-start-entry', () => ({
? {
getOptions: () => ({
requestMiddleware: startMocks.requestMiddleware,
serializationAdapters: [],
serializationAdapters: startMocks.serializationAdapters,
}),
}
: undefined
Expand Down Expand Up @@ -173,6 +175,7 @@ function makeCompletingStreamResponse(router: ReturnType<typeof makeRouter>) {

afterEach(() => {
startMocks.requestMiddleware = []
startMocks.serializationAdapters = []
startMocks.serverFnResult = undefined
startMocks.serverFnHandler = undefined
startMocks.router = undefined
Expand Down Expand Up @@ -598,6 +601,138 @@ describe('createStartHandler redirect safety', () => {
})

describe('createStartHandler server-route handling', () => {
it.each([
{ path: '/work', method: 'GET', body: 'app response' },
{ path: '/fallback', method: 'GET', body: 'app response' },
{ path: '/fallback', method: 'HEAD', body: '' },
])(
'renders $method $path alongside API routes',
async ({ path, method, body }) => {
const root = new BaseRootRoute()
startMocks.router = new RouterCore(
{
isServer: true,
routeTree: root.addChildren([
new BaseRoute({
getParentRoute: () => root,
path: '/work',
component: () => null,
}),
new BaseRoute({
getParentRoute: () => root,
path: '/fallback',
component: () => null,
server: {
handlers: ({ createHandlers }) => createHandlers({ GET: {} }),
},
}),
new BaseRoute({
getParentRoute: () => root,
path: '/api',
server: { handlers: { GET: () => new Response('api response') } },
}),
]),
},
getStoreConfig,
)
const render = vi.fn(({ router }: { router: AnyRouter }) => {
expect(router.state.matches.at(-1)?.routeId).toBe(path)
return new Response('app response', {
status: 202,
headers: { 'x-rendered': 'true' },
})
})
const handler = createStartHandler(render)

const response = await handler(
new Request(`http://localhost${path}`, { method }),
{},
)

expect(response.status).toBe(202)
expect(response.headers.get('x-rendered')).toBe('true')
expect(await response.text()).toBe(body)
expect(render).toHaveBeenCalledOnce()
expect(startMocks.router.serverSsr).toBeUndefined()
},
)

it.each([false, true])(
'runs shared route middleware once and preserves its context (global=%s)',
async (global) => {
const runMiddleware = vi.fn(({ next }) =>
next({ context: { message: 'trusted middleware' } }),
)
const middleware = createMiddleware().server(runMiddleware)
startMocks.requestMiddleware = global ? [middleware] : []
const root = new BaseRootRoute({
server: { middleware: [middleware] },
})
startMocks.router = new RouterCore(
{
isServer: true,
routeTree: root.addChildren([
new BaseRoute({
getParentRoute: () => root,
path: '/',
component: () => null,
}),
]),
},
getStoreConfig,
)
const handler = createStartHandler<{
server: { requestContext: { message: string; requestValue: string } }
}>(({ router }) =>
Response.json(router.options.additionalContext?.serverContext),
)

const response = await handler(new Request('http://localhost/'), {
context: { message: 'request context', requestValue: 'retained' },
})

expect(response.status).toBe(200)
expect(await response.json()).toEqual({
message: 'trusted middleware',
requestValue: 'retained',
})
expect(runMiddleware).toHaveBeenCalledOnce()
},
)

it('keeps adapter order and router adapters isolated to their request', async () => {
const adapter = (key: string) =>
createSerializationAdapter({
key,
test: (value: unknown): value is Date => value instanceof Date,
toSerializable: (value: Date) => value.toISOString(),
fromSerializable: (value: string) => new Date(value),
})
const startAdapter = adapter('start')
const routerAdapter = adapter('router')
const startAdapters = [startAdapter]
startMocks.serializationAdapters = startAdapters
let requestCount = 0
startMocks.routerFactory = () => {
const router = makeRouter()
router.options.serializationAdapters =
requestCount++ === 0 ? [routerAdapter] : []
return router
}
const handler = createStartHandler(({ router }) =>
Response.json(
router.options.serializationAdapters?.map(({ key }) => key),
),
)

const first = await handler(new Request('http://localhost/'), {})
const second = await handler(new Request('http://localhost/'), {})

expect(await first.json()).toEqual(['start', '$TSS/serverfn', 'router'])
expect(await second.json()).toEqual(['start', '$TSS/serverfn'])
expect(startAdapters).toEqual([startAdapter])
})

it('keeps default CSRF protection on server function requests', async () => {
startMocks.hasStartInstance = false
startMocks.hasServerRoutes = false
Expand Down Expand Up @@ -800,10 +935,8 @@ describe('createStartHandler request location reuse', () => {
renderApp ? 'app response' : 'server response',
)
expect(input).toHaveBeenCalledOnce()
expect(serverHandler).toHaveBeenCalledExactlyOnceWith(
`/${encodeURIComponent(path)}`,
)
expect(middlewarePathnames).toEqual([`/${encodeURIComponent(path)}`])
expect(serverHandler).toHaveBeenCalledExactlyOnceWith(`/${path}`)
expect(middlewarePathnames).toEqual([`/${path}`])
expect(render).toHaveBeenCalledTimes(renderApp ? 1 : 0)
expect(
getMatchedRoutes.mock.calls.every(
Expand All @@ -816,6 +949,23 @@ describe('createStartHandler request location reuse', () => {
},
)

it('uses the request URL origin regardless of the Origin header', async () => {
startMocks.router = makeRouterWithRouteWork({})
const handler = createStartHandler(
({ router }) => new Response(router.origin),
)

const response = await handler(
new Request('https://public.example:8443/work', {
headers: { Origin: 'https://untrusted.example' },
}),
{},
)

expect(response.status).toBe(200)
expect(await response.text()).toBe('https://public.example:8443')
})

it('uses the configured origin for server route rewrites', async () => {
const input = vi.fn(({ url }: { url: URL }) => {
url.pathname =
Expand Down Expand Up @@ -896,9 +1046,15 @@ describe('createStartHandler request location reuse', () => {
expect(parseSearch).toHaveBeenCalledExactlyOnceWith('?page=2')
})

it.each(['a/b', 'a%b', 'a b', 'a?b', 'a#b'])(
it.each([
['a/b', 'a%2Fb'],
['a%b', 'a%25b'],
['a b', 'a b'],
['a?b', 'a%3Fb'],
['a#b', 'a%23b'],
])(
'preserves encoded params %j when a server handler continues to SSR',
async (value) => {
async (value, pathname) => {
const handlerParams: Array<string> = []
const handlerPathnames: Array<string> = []
const root = new BaseRootRoute()
Expand Down Expand Up @@ -936,7 +1092,7 @@ describe('createStartHandler request location reuse', () => {

expect(response.status).toBe(200)
expect(handlerParams).toEqual([value])
expect(handlerPathnames).toEqual([`/params/${encodeURIComponent(value)}`])
expect(handlerPathnames).toEqual([`/params/${pathname}`])
expect(await response.json()).toEqual({
params: { value },
loaderData: value,
Expand Down
Loading
Loading