Skip to content
Open
9 changes: 9 additions & 0 deletions .changeset/thick-sloths-write.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@tanstack/start-client-core': patch
'@tanstack/start-server-core': patch
'@tanstack/react-start': patch
'@tanstack/solid-start': patch
'@tanstack/vue-start': patch
---

fix(start): align request handler types with registered server context
4 changes: 2 additions & 2 deletions docs/start/framework/react/guide/server-entry-point.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default createServerEntry({

When your server needs to pass additional, typed data into request handlers (for example, authenticated user info, a database connection, or per-request flags), register a request context type via TypeScript module augmentation. The registered context is delivered as the second argument to the server `fetch` handler and is available throughout the server-side middleware chain — including global middleware, request/function middleware, server routes, server functions, and the router itself.

To add types for your request context, augment the `Register` interface from `@tanstack/react-router` with a `server.requestContext` property. The runtime `context` you pass to `handler.fetch` will then match that type. Example:
To add types for your request context, augment the `Register` interface from `@tanstack/react-start` with a `server.requestContext` property. The runtime `context` you pass to `handler.fetch` will then match that type. Example:

```tsx
import handler, { createServerEntry } from '@tanstack/react-start/server-entry'
Expand All @@ -74,7 +74,7 @@ type MyRequestContext = {
foo: number
}

declare module '@tanstack/react-router' {
declare module '@tanstack/react-start' {
interface Register {
server: {
requestContext: MyRequestContext
Expand Down
6 changes: 1 addition & 5 deletions docs/start/framework/solid/guide/server-entry-point.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
---
ref: docs/start/framework/react/guide/server-entry-point.md
replace:
{
'@tanstack/react-start': '@tanstack/solid-start',
'@tanstack/react-router': '@tanstack/solid-router',
'React': 'SolidJS',
}
{ '@tanstack/react-start': '@tanstack/solid-start', 'React': 'SolidJS' }
---
15 changes: 14 additions & 1 deletion e2e/react-start/basic/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,21 @@ import handler from '@tanstack/react-start/server-entry'

console.log("[server-entry]: using custom server entry in 'src/server.ts'")

declare module '@tanstack/react-start' {
interface Register {
server: {
/**
* This is just a test to make sure that the typing of the request context is working correctly in the custom server entry.
*/
requestContext: {
foo: string
}
}
}
}

export default {
fetch(request: Request) {
return handler.fetch(request)
return handler.fetch(request, { context: { foo: 'bar' } })
},
}
15 changes: 14 additions & 1 deletion e2e/solid-start/basic/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,21 @@ import handler from '@tanstack/solid-start/server-entry'

console.log("[server-entry]: using custom server entry in 'src/server.ts'")

declare module '@tanstack/solid-start' {
interface Register {
server: {
/**
* This is just a test to make sure that the typing of the request context is working correctly in the custom server entry.
*/
requestContext: {
foo: string
}
}
}
}

export default {
fetch(request: Request) {
return handler.fetch(request)
return handler.fetch(request, { context: { foo: 'bar' } })
},
}
15 changes: 14 additions & 1 deletion e2e/vue-start/basic/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,21 @@ import handler from '@tanstack/vue-start/server-entry'

console.log("[server-entry]: using custom server entry in 'src/server.ts'")

declare module '@tanstack/vue-start' {
interface Register {
server: {
/**
* This is just a test to make sure that the typing of the request context is working correctly in the custom server entry.
*/
requestContext: {
foo: string
}
}
}
}

export default {
fetch(request: Request) {
return handler.fetch(request)
return handler.fetch(request, { context: { foo: 'bar' } })
},
}
16 changes: 10 additions & 6 deletions packages/react-start/src/default-entry/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/react-start/server'
import type { Register } from '@tanstack/react-router'
import type { RequestHandler } from '@tanstack/react-start/server'
import type { Register } from '@tanstack/react-start'
import type { RequestOptions } from '@tanstack/react-start/server'

const fetch = createStartHandler(defaultStreamHandler)

// Providing `RequestHandler` from `@tanstack/react-start/server` is required so that the output types don't import it from `@tanstack/start-server-core`
export type ServerEntry = { fetch: RequestHandler<Register> }
export type ServerEntry = {
fetch: (
request: Request,
opts?: RequestOptions<Register>,
) => Promise<Response> | Response
}

export function createServerEntry(entry: ServerEntry): ServerEntry {
return {
async fetch(...args) {
return await entry.fetch(...args)
async fetch(request, opts) {
return await entry.fetch(request, opts)
},
}
}
Expand Down
16 changes: 10 additions & 6 deletions packages/solid-start/src/default-entry/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/solid-start/server'
import type { Register } from '@tanstack/solid-router'
import type { RequestHandler } from '@tanstack/solid-start/server'
import type { Register } from '@tanstack/solid-start'
import type { RequestOptions } from '@tanstack/solid-start/server'

const fetch = createStartHandler(defaultStreamHandler)

// Providing `RequestHandler` from `@tanstack/solid-start/server` is required so that the output types don't import it from `@tanstack/start-server-core`
export type ServerEntry = { fetch: RequestHandler<Register> }
export type ServerEntry = {
fetch: (
request: Request,
opts?: RequestOptions<Register>,
) => Promise<Response> | Response
}

export function createServerEntry(entry: ServerEntry): ServerEntry {
return {
async fetch(...args) {
return await entry.fetch(...args)
async fetch(request, opts) {
return await entry.fetch(request, opts)
},
}
}
Expand Down
12 changes: 6 additions & 6 deletions packages/start-server-core/src/createStartHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ import type {
AnyFunctionMiddleware,
AnyRequestMiddleware,
AnyStartInstanceOptions,
Register,
RouteMethod,
RouteMethodHandlerFn,
RouterEntry,
StartEntry,
} from '@tanstack/start-client-core'
import type { RequestHandler } from './request-handler'
import type { RequestHandler, RequestOptions } from './request-handler'
import type {
AnyRoute,
AnyRouter,
AnySerializationAdapter,
Register,
} from '@tanstack/router-core'
import type { HandlerCallback } from '@tanstack/router-core/ssr/server'
import type { FinalManifestOptions } from './finalManifest'
Expand Down Expand Up @@ -300,7 +300,7 @@ function handlerToMiddleware(
* })
* ```
*/
export function createStartHandler<TRegister = Register>(
export function createStartHandler<TRegister extends Register = Register>(
cbOrOptions: HandlerCallback<AnyRouter> | CreateStartHandlerOptions,
): RequestHandler<TRegister> {
const handlerOptions: FinalManifestOptions =
Expand All @@ -323,8 +323,8 @@ export function createStartHandler<TRegister = Register>(
}

const startRequestResolver: RequestHandler<Register> = async (
request,
requestOpts,
request: Request,
requestOpts?: RequestOptions<Register>,
Comment on lines 325 to +327
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify generic erasure/cast pattern in this file
rg -n -C2 'RequestHandler<Register>|RequestOptions<Register>|as RequestHandler<TRegister>' packages/start-server-core/src/createStartHandler.ts

Repository: TanStack/router

Length of output: 398


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/start-server-core/src/createStartHandler.ts"

echo "== Context around startRequestResolver (lines ~280-360) =="
nl -ba "$FILE" | sed -n '280,360p'

echo "== Context around return requestHandler cast (lines ~540-620) =="
nl -ba "$FILE" | sed -n '540,620p'

echo "== Find TRegister/Register/RequestOptions usage in file =="
rg -n "TRegister|Register|RequestOptions<|requestHandler<|requestHandler\(" "$FILE"

echo "== Find type of requestHandler helper usage (look for its generic signature or import) =="
rg -n "function requestHandler|const requestHandler|requestHandler\s*<" "$FILE"

Repository: TanStack/router

Length of output: 179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/start-server-core/src/createStartHandler.ts"

echo "== Context around startRequestResolver (rough lines ~280-360) =="
cat -n "$FILE" | sed -n '280,360p'

echo "== Context around return requestHandler cast (rough lines ~540-620) =="
cat -n "$FILE" | sed -n '540,620p'

echo "== Search TRegister/Register/RequestOptions/requestHandler in file =="
rg -n "TRegister|Register|RequestOptions<|requestHandler<|requestHandler\s*\(" "$FILE"

echo "== Search for definition/signature of requestHandler helper (generic signature) =="
rg -n "function requestHandler|const requestHandler|requestHandler\s*<" "$FILE"

Repository: TanStack/router

Length of output: 7191


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="packages/start-server-core/src/createStartHandler.ts"

echo "== Top of file (imports/types) =="
cat -n "$FILE" | sed -n '1,140p'

echo "== Search for RequestOptions type/import usage =="
rg -n "RequestOptions" "$FILE"

echo "== Search for RequestHandlerParameters or requestHandler helper import/identifier =="
rg -n "RequestHandlerParameters|requestHandler\b" "$FILE"

Repository: TanStack/router

Length of output: 5904


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate likely files
rg -n "export type RequestHandler|export interface RequestHandler|type RequestHandler" packages/start-server-core/src -S
rg -n "export type RequestOptions|interface RequestOptions|type RequestOptions" packages/start-server-core/src -S
rg -n "export function requestHandler|const requestHandler|function requestHandler" packages/start-server-core/src -S

# Print the relevant files (if found); fall back to targeted cat by resolving known paths
echo "== Try cat request-handler.ts =="
ls -1 packages/start-server-core/src | rg "request-handler"
if [ -f packages/start-server-core/src/request-handler.ts ]; then
  cat -n packages/start-server-core/src/request-handler.ts | sed -n '1,220p'
fi

echo "== Try cat request-response.ts =="
ls -1 packages/start-server-core/src | rg "request-response"
if [ -f packages/start-server-core/src/request-response.ts ]; then
  cat -n packages/start-server-core/src/request-response.ts | sed -n '1,260p'
fi

# If filenames differ, show a short list of matches
echo "== Show matches for request-handler import path =="
rg -n "from '\\./request-handler'|from \"\\./request-handler\"" packages/start-server-core/src/createStartHandler.ts

Repository: TanStack/router

Length of output: 13081


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find the definition of Register in start-client-core (in-repo)
rg -n "export (type|interface) Register" packages/start-client-core -S
rg -n "type Register|interface Register" packages/start-client-core -S

# Also inspect server.requestContext typing if nearby
rg -n "requestContext" packages/start-client-core -S

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search repo for Register definition =="
rg -n "export (type|interface) Register|type Register\s*=" . -S

echo "== Search repo for requestContext typing =="
rg -n "requestContext" . -S

Repository: TanStack/router

Length of output: 1991


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect Register type
FILE1="packages/router-core/src/router.ts"
echo "== router-core Register (around its definition) =="
rg -n "export interface Register|export type Register" "$FILE1"
start=$(rg -n "export interface Register" "$FILE1" | head -n1 | cut -d: -f1)
end=$((start+60))
sed -n "${start},${end}p" "$FILE1" | cat -n

# Inspect createStartHandler around executeMiddleware and requestOpts?.context usage
FILE2="packages/start-server-core/src/createStartHandler.ts"
echo "== createStartHandler executeMiddleware context usage (around ~510-590) =="
cat -n "$FILE2" | sed -n '500,590p'

Repository: TanStack/router

Length of output: 6008


Avoid casting RequestHandler<Register> to RequestHandler<TRegister> in createStartHandler

startRequestResolver is typed as RequestHandler<Register> with requestOpts?: RequestOptions<Register> (325-327), but RequestOptions<TRegister> can require context when TRegister defines server.requestContext. Casting the result to RequestHandler<TRegister> (588) masks that constraint from callers.

Proposed type-safe fix
-  const startRequestResolver: RequestHandler<Register> = async (
-    request: Request,
-    requestOpts?: RequestOptions<Register>,
-  ) => {
+  const startRequestResolver: RequestHandler<TRegister> = async (
+    ...args: RequestHandlerParameters<TRegister>
+  ) => {
+    const [request, requestOpts] = args
     let router: AnyRouter | null = null as AnyRouter | null
     let cbWillCleanup = false as boolean
@@
-  return requestHandler(startRequestResolver) as RequestHandler<TRegister>
+  return requestHandler<TRegister>(startRequestResolver)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-server-core/src/createStartHandler.ts` around lines 325 - 327,
startRequestResolver is declared as RequestHandler<Register> with requestOpts?:
RequestOptions<Register>, but the code later casts it to
RequestHandler<TRegister>, which hides RequestOptions<TRegister> constraints
such as required server.requestContext; instead make startRequestResolver
generic over TRegister (or accept RequestOptions<TRegister>) within
createStartHandler so its signature matches RequestHandler<TRegister> without
casting—update the function/type parameters (startRequestResolver,
RequestOptions, RequestHandler, createStartHandler, TRegister, Register) so the
resolver uses the generic TRegister request option types (including
requestContext when present) and remove the unsafe cast.

) => {
let router: AnyRouter | null = null as AnyRouter | null
let cbWillCleanup = false as boolean
Expand Down Expand Up @@ -585,7 +585,7 @@ export function createStartHandler<TRegister = Register>(
}
}

return requestHandler(startRequestResolver)
return requestHandler(startRequestResolver) as RequestHandler<TRegister>
}

async function handleRedirectResponse(
Expand Down
5 changes: 4 additions & 1 deletion packages/start-server-core/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ export * from './virtual-modules'

export { HEADERS } from './constants'

export type { RequestHandler, RequestOptions } from './request-handler'
export type {
RequestHandler,
RequestOptions,
} from './request-handler'

export type { SessionConfig } from './session'

Expand Down
26 changes: 8 additions & 18 deletions packages/start-server-core/src/request-handler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { OnEarlyHints, ResponseLinkHeaderOptions } from './early-hints'
import type { Register } from '@tanstack/start-client-core'

type BaseContext = {
nonce?: string
Expand Down Expand Up @@ -66,22 +67,11 @@ export type RequestOptions<TRegister> = EarlyHintsOptions &
: { context: TRequestContext & BaseContext }
: { context?: BaseContext })

// Utility type: true if T has any required keys, else false
type HasRequired<T> = keyof T extends never
? false
: {
[K in keyof T]-?: undefined extends T[K] ? never : K
}[keyof T] extends never
? false
: true
export type RequestHandlerParameters<TRegister> =
{} extends RequestOptions<TRegister>
? [request: Request, opts?: RequestOptions<TRegister>]
: [request: Request, opts: RequestOptions<TRegister>]

export type RequestHandler<TRegister> =
HasRequired<RequestOptions<TRegister>> extends true
? (
request: Request,
opts: RequestOptions<TRegister>,
) => Promise<Response> | Response
: (
request: Request,
opts?: RequestOptions<TRegister>,
) => Promise<Response> | Response
export type RequestHandler<TRegister extends Register = Register> = (
...args: RequestHandlerParameters<TRegister>
) => Promise<Response> | Response
10 changes: 7 additions & 3 deletions packages/start-server-core/src/request-response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import type {
SessionUpdate,
} from './session'
import type { StandardSchemaV1 } from '@standard-schema/spec'
import type { Register } from '@tanstack/start-client-core'
import type { RequestHandler } from './request-handler'

interface StartEvent {
Expand Down Expand Up @@ -118,10 +119,13 @@ function attachResponseHeaders<T>(
return value
}

export function requestHandler<TRegister = unknown>(
export function requestHandler<TRegister extends Register = Register>(
handler: RequestHandler<TRegister>,
) {
return (request: Request, requestOpts: any): Promise<Response> | Response => {
return ((
request: Request,
requestOpts?: any,
): Promise<Response> | Response => {
Comment on lines +125 to +128
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify unsafe typing in wrapper signatures
rg -n -C2 'requestOpts\?: any|as RequestHandler<|RequestHandlerParameters<' packages/start-server-core/src/request-response.ts

Repository: TanStack/router

Length of output: 342


requestOpts?: any bypasses the handler type guarantees (and is masked by a cast).

In packages/start-server-core/src/request-response.ts, the wrapper takes requestOpts?: any (line 127) and then relies on as RequestHandler<TRegister> (line 146) to restore typing. This defeats strict type safety for the TRegister-specific opts contract. Replace any with the correct typed parameters for TRegister and remove/avoid the cast by aligning the wrapper’s parameter/return types with RequestHandler<TRegister] directly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/start-server-core/src/request-response.ts` around lines 125 - 128,
The wrapper currently declares requestOpts?: any which bypasses the
RequestHandler<TRegister> contract and then re-establishes types via a cast;
change the wrapper signature to accept the strongly-typed options instead of any
so the wrapper itself matches RequestHandler<TRegister> (i.e., replace
requestOpts?: any with the correct generic opts type for TRegister and adjust
the wrapper return type to Promise<Response> | Response as needed), remove the
subsequent as RequestHandler<TRegister> cast, and ensure the internal call sites
use the typed opts variable (references: the wrapper function taking (request:
Request, requestOpts), the RequestHandler<TRegister> type and the TRegister
generic).

let h3Event: H3Event
try {
h3Event = new H3Event(request)
Expand All @@ -139,7 +143,7 @@ export function requestHandler<TRegister = unknown>(
handler(request, requestOpts),
)
return h3_toResponse(attachResponseHeaders(response, h3Event), h3Event)
}
}) as RequestHandler<TRegister>
}

function getH3Event() {
Expand Down
16 changes: 10 additions & 6 deletions packages/vue-start/src/default-entry/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@ import {
createStartHandler,
defaultStreamHandler,
} from '@tanstack/vue-start/server'
import type { Register } from '@tanstack/vue-router'
import type { RequestHandler } from '@tanstack/vue-start/server'
import type { Register } from '@tanstack/vue-start'
import type { RequestOptions } from '@tanstack/vue-start/server'

const fetch = createStartHandler(defaultStreamHandler)

// Providing `RequestHandler` from `@tanstack/vue-start/server` is required so that the output types don't import it from `@tanstack/start-server-core`
export type ServerEntry = { fetch: RequestHandler<Register> }
export type ServerEntry = {
fetch: (
request: Request,
opts?: RequestOptions<Register>,
) => Promise<Response> | Response
}

export function createServerEntry(entry: ServerEntry): ServerEntry {
return {
async fetch(...args) {
return await entry.fetch(...args)
async fetch(request, opts) {
return await entry.fetch(request, opts)
},
}
}
Expand Down
Loading