Skip to content

feat(start): CSRF middleware#7373

Merged
Sheraff merged 5 commits intomainfrom
feat-start-csrf-middleware
May 9, 2026
Merged

feat(start): CSRF middleware#7373
Sheraff merged 5 commits intomainfrom
feat-start-csrf-middleware

Conversation

@Sheraff
Copy link
Copy Markdown
Contributor

@Sheraff Sheraff commented May 9, 2026

  • Add createCsrfMiddleware() to protect TanStack Start server functions from cross-site requests using Sec-Fetch-Site, Origin, and Referer checks.
  • Auto-install CSRF protection for server functions when no startInstance is defined, and add a dev warning when a custom startInstance omits CSRF middleware.
  • Add plugin config to disable the warning via serverFns.disableCsrfMiddlewareWarning, wired through Vite and Rsbuild.
  • Add docs plus unit, type, plugin config, and e2e coverag
  • createCsrfMiddleware() should be fully removed at build time for the client, including the arguments passed to it
// src/start.ts
import { createCsrfMiddleware, createStart } from '@tanstack/react-start'

const csrfMiddleware = createCsrfMiddleware({
  filter: (ctx) => ctx.handlerType === 'serverFn',
  origin: ['https://app.example.com', 'https://preview.example.com'],
  secFetchSite: ['same-origin', 'same-site'],
})

export const startInstance = createStart(() => ({
  requestMiddleware: [csrfMiddleware],
}))

Summary by CodeRabbit

  • New Features

    • Added CSRF middleware to protect server functions and enforce same-origin requests
    • Middleware is auto-applied when no start configuration is present
    • New option to disable the CSRF-middleware warning
  • Documentation

    • Guides for CSRF middleware setup, configuration, scoping, and same-origin behavior
  • Bug Fixes

    • Improved isomorphic function runtime fallback behavior
  • Tests

    • New unit and e2e tests validating CSRF enforcement and isomorphic-fn behavior

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 9, 2026

📝 Walkthrough

Walkthrough

This PR adds CSRF protection middleware to TanStack Start server functions (Sec-Fetch-Site → Origin → Referer validation), wires automatic injection and dev warning controls, re-exports middleware APIs, adds tests and docs, introduces AsyncLocalStorage plugin context for the start compiler, and implements a runtime callable fallback for isomorphic functions.

Changes

CSRF Middleware Protection

Layer / File(s) Summary
Type Contracts & Schema
packages/start-client-core/src/createMiddleware.ts, packages/start-client-core/src/index.tsx, packages/start-server-core/src/global.d.ts
RequestServerOptions adds `handlerType: 'serverFn'
CSRF Middleware Implementation
packages/start-client-core/src/createCsrfMiddleware.ts
Adds csrfSymbol, matcher types, CsrfMiddlewareOptions, createCsrfMiddleware, isCsrfRequestAllowed, and getCsrfRequestValidationResult. Middleware validates Sec-Fetch-Site, then Origin, then Referer, supports filters and custom failure responses.
Client Core Exports
packages/start-client-core/src/index.tsx
Re-exports CSRF middleware utilities and types.
Server Handler Integration
packages/start-server-core/src/createStartHandler.ts, e2e start src/start.ts
Uses configured request middleware or falls back to a default CSRF middleware filtered to handlerType === 'serverFn', flattens middleware, detects CSRF middleware presence, emits a one-time dev warning when missing (unless disabled), and passes handlerType into middleware contexts.
Build Configuration
packages/start-plugin-core/src/vite/planning.ts, packages/start-plugin-core/src/vite/plugin.ts, packages/start-plugin-core/src/rsbuild/plugin.ts
Vite and Rsbuild plugins inject TSS_DISABLE_CSRF_MIDDLEWARE_WARNING defines derived from startConfig.serverFns.disableCsrfMiddlewareWarning.
Testing
packages/start-client-core/src/tests/createCsrfMiddleware.test.ts, packages/start-client-core/src/tests/createServerMiddleware.test-d.ts, packages/start-plugin-core/tests/csrf-warning-config.test.ts, e2e/react-start/server-functions/tests/server-functions.spec.ts
Unit tests cover validation results, header precedence, matcher customization, middleware behavior, and custom failure responses. Type tests assert handlerType. Config tests verify define emission. E2E test asserts 403 rejection for cross-site server function requests.
Documentation
docs/start/framework/react/guide/middleware.md, docs/start/framework/react/guide/server-functions.md
Adds CSRF Middleware and Same-Origin Request guide sections showing auto-install, explicit registration, filtering by handlerType, origin overrides, and the disable-warning option.
Release Metadata
.changeset/strong-trains-act.md
Changeset documenting package version bumps and the new CSRF middleware feature.

Start Compiler Plugin Context

Layer / File(s) Summary
Compiler Context Types
packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts
Adds AsyncLocalStorage import and StartCompilerPluginContext type describing environment and plugin-like callbacks.
Compiler Integration
packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts
Creates compilerContextStorage and getCompilerContext, updates loadModule/resolveId to use stored context, and wraps compiler.compile(...) in compilerContextStorage.run(...) to bind context during async compiler callbacks.

Isomorphic Function Runtime Fallback

Layer / File(s) Summary
Runtime Fallback Implementation
packages/start-fn-stubs/src/createIsomorphicFn.ts
Replaces dummy chain-shaped stub with a callable fallback (createRuntimeFn) that tracks .server() and .client() registrations, preferring server implementation when present.
Tests
packages/start-fn-stubs/tests/createIsomorphicFn.test.ts
Verifies callable behavior for server-only, server-preferred, and client-only registration patterns.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant StartHandler
  participant RequestMiddleware
  participant ServerFunction
  Browser->>StartHandler: HTTP request (includes headers)
  StartHandler->>RequestMiddleware: Execute middleware (handlerType added)
  RequestMiddleware->>StartHandler: allow or reject
  alt allowed
    StartHandler->>ServerFunction: invoke serverFn handler
  else rejected
    RequestMiddleware-->>Browser: 403 Forbidden
  end
Loading

Estimated code review effort:
🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs:

  • TanStack/router#6177: touches start-plugin-core compiler/plugin plumbing similar to the compiler context changes here.
  • TanStack/router#5040: related edits to server-function/start plumbing and middleware handling.

"🐰 I hopped through headers, origin checks in tow,
I fenced cross-site mischief where fetch metadata blow;
With runtime fallback ready, both client and server call,
A little rabbit cheers — safe requests for all!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(start): CSRF middleware' clearly and concisely describes the primary change—adding CSRF middleware protection to TanStack Start server functions, which is the main feature introduced across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-start-csrf-middleware

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented May 9, 2026

View your CI Pipeline Execution ↗ for commit 85d7a96

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ❌ Failed 5m 9s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-09 16:15:41 UTC

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

🚀 Changeset Version Preview

4 package(s) bumped directly, 11 bumped as dependents.

🟨 Minor bumps

Package Version Reason
@tanstack/start-client-core 1.168.2 → 1.169.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/start-fn-stubs 1.161.6 → 1.161.7 Changeset
@tanstack/start-plugin-core 1.169.20 → 1.169.21 Changeset
@tanstack/start-server-core 1.167.30 → 1.167.31 Changeset
@tanstack/react-start 1.167.65 → 1.167.66 Dependent
@tanstack/react-start-client 1.166.48 → 1.166.49 Dependent
@tanstack/react-start-rsc 0.0.44 → 0.0.45 Dependent
@tanstack/react-start-server 1.166.52 → 1.166.53 Dependent
@tanstack/solid-start 1.167.62 → 1.167.63 Dependent
@tanstack/solid-start-client 1.166.47 → 1.166.48 Dependent
@tanstack/solid-start-server 1.166.51 → 1.166.52 Dependent
@tanstack/start-static-server-functions 1.166.41 → 1.166.42 Dependent
@tanstack/vue-start 1.167.58 → 1.167.59 Dependent
@tanstack/vue-start-client 1.166.43 → 1.166.44 Dependent
@tanstack/vue-start-server 1.166.47 → 1.166.48 Dependent

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

Bundle Size Benchmarks

  • Commit: 4eed408f127b
  • Measured at: 2026-05-09T14:13:00.873Z
  • Baseline source: history:35e88f04996d
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Initial gzip Raw Brotli Trend
react-router.minimal 87.29 KiB +139 B (+0.16%) 87.15 KiB 274.07 KiB 75.81 KiB ▁▁▁▁▁▁▁▁▁▁▁█
react-router.full 90.82 KiB +141 B (+0.15%) 90.68 KiB 285.58 KiB 78.82 KiB ▁▁▁▁▁▁▁▁▁▁▁█
solid-router.minimal 35.51 KiB +126 B (+0.35%) 35.38 KiB 106.36 KiB 31.91 KiB ▁▁▁▁▁▁▁▁▁▁▁█
solid-router.full 40.23 KiB +127 B (+0.31%) 40.10 KiB 120.58 KiB 36.14 KiB ▁▁▁▁▁▁▁▁▁▁▁█
vue-router.minimal 53.28 KiB +131 B (+0.24%) 53.15 KiB 151.51 KiB 47.83 KiB ▁▁▁▁▁▁▁▁▁▁▁█
vue-router.full 58.41 KiB +133 B (+0.22%) 58.28 KiB 167.68 KiB 52.30 KiB ▁▁▁▁▁▁▁▁▁▁▁█
react-start.minimal 101.97 KiB +141 B (+0.14%) 101.84 KiB 322.51 KiB 88.13 KiB ▁▁▁▁▁▁▁▁▁▁▃█
react-start.full 105.41 KiB +140 B (+0.13%) 105.27 KiB 332.84 KiB 91.10 KiB ▁▁▁▁▁▁▁▁▁▁▄█
react-start.rsbuild.minimal 99.60 KiB +174 B (+0.17%) 99.43 KiB 316.97 KiB 85.65 KiB ▁▁▁▁▁▁▁▁▁▁▄█
react-start.rsbuild.full 102.89 KiB +174 B (+0.17%) 102.72 KiB 327.41 KiB 88.45 KiB ▁▁▁▁▁▁▁▁▁▁▃█
solid-start.minimal 49.61 KiB +131 B (+0.26%) 49.48 KiB 152.48 KiB 43.79 KiB ▁▁▁▁▁▁▁▁▁▁▄█
solid-start.full 55.40 KiB +133 B (+0.24%) 55.27 KiB 169.39 KiB 48.70 KiB ▁▁▁▁▁▁▁▁▁▁▄█

Current gzip tracks all emitted client JS chunks. Initial gzip tracks only the entry/import graph. Trend sparkline is historical current gzip ending with this PR measurement; lower is better.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 9, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/@tanstack/arktype-adapter@7373

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/@tanstack/eslint-plugin-router@7373

@tanstack/eslint-plugin-start

npm i https://pkg.pr.new/@tanstack/eslint-plugin-start@7373

@tanstack/history

npm i https://pkg.pr.new/@tanstack/history@7373

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/@tanstack/nitro-v2-vite-plugin@7373

@tanstack/react-router

npm i https://pkg.pr.new/@tanstack/react-router@7373

@tanstack/react-router-devtools

npm i https://pkg.pr.new/@tanstack/react-router-devtools@7373

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/@tanstack/react-router-ssr-query@7373

@tanstack/react-start

npm i https://pkg.pr.new/@tanstack/react-start@7373

@tanstack/react-start-client

npm i https://pkg.pr.new/@tanstack/react-start-client@7373

@tanstack/react-start-rsc

npm i https://pkg.pr.new/@tanstack/react-start-rsc@7373

@tanstack/react-start-server

npm i https://pkg.pr.new/@tanstack/react-start-server@7373

@tanstack/router-cli

npm i https://pkg.pr.new/@tanstack/router-cli@7373

@tanstack/router-core

npm i https://pkg.pr.new/@tanstack/router-core@7373

@tanstack/router-devtools

npm i https://pkg.pr.new/@tanstack/router-devtools@7373

@tanstack/router-devtools-core

npm i https://pkg.pr.new/@tanstack/router-devtools-core@7373

@tanstack/router-generator

npm i https://pkg.pr.new/@tanstack/router-generator@7373

@tanstack/router-plugin

npm i https://pkg.pr.new/@tanstack/router-plugin@7373

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/@tanstack/router-ssr-query-core@7373

@tanstack/router-utils

npm i https://pkg.pr.new/@tanstack/router-utils@7373

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/@tanstack/router-vite-plugin@7373

@tanstack/solid-router

npm i https://pkg.pr.new/@tanstack/solid-router@7373

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/@tanstack/solid-router-devtools@7373

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/@tanstack/solid-router-ssr-query@7373

@tanstack/solid-start

npm i https://pkg.pr.new/@tanstack/solid-start@7373

@tanstack/solid-start-client

npm i https://pkg.pr.new/@tanstack/solid-start-client@7373

@tanstack/solid-start-server

npm i https://pkg.pr.new/@tanstack/solid-start-server@7373

@tanstack/start-client-core

npm i https://pkg.pr.new/@tanstack/start-client-core@7373

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/@tanstack/start-fn-stubs@7373

@tanstack/start-plugin-core

npm i https://pkg.pr.new/@tanstack/start-plugin-core@7373

@tanstack/start-server-core

npm i https://pkg.pr.new/@tanstack/start-server-core@7373

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/@tanstack/start-static-server-functions@7373

@tanstack/start-storage-context

npm i https://pkg.pr.new/@tanstack/start-storage-context@7373

@tanstack/valibot-adapter

npm i https://pkg.pr.new/@tanstack/valibot-adapter@7373

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/@tanstack/virtual-file-routes@7373

@tanstack/vue-router

npm i https://pkg.pr.new/@tanstack/vue-router@7373

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/@tanstack/vue-router-devtools@7373

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/@tanstack/vue-router-ssr-query@7373

@tanstack/vue-start

npm i https://pkg.pr.new/@tanstack/vue-start@7373

@tanstack/vue-start-client

npm i https://pkg.pr.new/@tanstack/vue-start-client@7373

@tanstack/vue-start-server

npm i https://pkg.pr.new/@tanstack/vue-start-server@7373

@tanstack/zod-adapter

npm i https://pkg.pr.new/@tanstack/zod-adapter@7373

commit: 85d7a96

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
packages/start-fn-stubs/tests/createIsomorphicFn.test.ts (1)

4-24: ⚡ Quick win

Add argument-forwarding and reverse-chain coverage.

These tests validate core behavior, but adding argument passthrough and client().server() order coverage will better protect the new runtime fallback path.

✅ Suggested test additions
 describe('createIsomorphicFn runtime fallback', () => {
@@
   it('returns a callable client-only implementation', () => {
     const fn = createIsomorphicFn().client(() => 'client')
 
     expect(fn()).toBe('client')
   })
+
+  it('forwards call arguments to the active implementation', () => {
+    const fn = createIsomorphicFn().server((a: number, b: number) => a + b)
+
+    expect(fn(2, 3)).toBe(5)
+  })
+
+  it('uses server implementation when client is registered first', () => {
+    const fn = createIsomorphicFn()
+      .client(() => 'client')
+      .server(() => 'server')
+
+    expect(fn()).toBe('server')
+  })
 })
🤖 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-fn-stubs/tests/createIsomorphicFn.test.ts` around lines 4 -
24, Add tests to cover argument forwarding and the reverse registration order:
extend the existing createIsomorphicFn tests to assert that both server and
client implementations receive and return arguments correctly (e.g., call fn(1,
'a') and expect the registered implementation to receive those args and return
based on them), and add a test where .client(...) is chained before .server(...)
to ensure .server still wins at runtime; update tests around createIsomorphicFn,
.server, and .client to include these cases.
packages/start-client-core/src/createCsrfMiddleware.ts (1)

80-83: ⚡ Quick win

Remove any cast from CSRF middleware context typing.

Line 82 currently drops type safety with RequestServerOptions<any, any>. Since the function is typed as CreateCsrfMiddleware (a generic type) but the implementation doesn't declare explicit type parameters, TRegister and TMiddlewares aren't in scope—forcing the unsafe cast. Add explicit generics to the implementation to match the type signature and eliminate this.

♻️ Proposed type-safe refactor
-const innerCreateCsrfMiddleware: CreateCsrfMiddleware = (opts = {}) => {
+const innerCreateCsrfMiddleware: CreateCsrfMiddleware = <
+  TRegister,
+  TMiddlewares,
+>(
+  opts: CsrfMiddlewareOptions<TRegister, TMiddlewares> = {},
+) => {
   const middleware = createMiddleware().server(async (ctx) => {
-    const csrfCtx = ctx as RequestServerOptions<any, any> & typeof ctx
+    const csrfCtx = ctx as RequestServerOptions<TRegister, TMiddlewares> &
+      typeof ctx

As per coding guidelines, **/*.{ts,tsx}: Use TypeScript strict mode with extensive type safety. This middleware is security-sensitive and should not use any types.

🤖 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-client-core/src/createCsrfMiddleware.ts` around lines 80 - 83,
The implementation of innerCreateCsrfMiddleware drops type safety by casting ctx
to RequestServerOptions<any, any>; instead add explicit generic parameters to
innerCreateCsrfMiddleware so TRegister and TMiddlewares are in scope and use
RequestServerOptions<TRegister, TMiddlewares> (or the correct generic aliases)
for the csrfCtx typing; update the function signature for
innerCreateCsrfMiddleware to declare the same generics as CreateCsrfMiddleware
and remove the `any` cast where createMiddleware().server(async (ctx) => { const
csrfCtx = ... }) is typed, ensuring the middleware uses the proper generic types
throughout.
docs/start/framework/react/guide/middleware.md (2)

462-462: ⚡ Quick win

Clarify what "different public origin" means.

The phrase "different public origin" is ambiguous. Consider clarifying that this is for cases where your application's public-facing URL differs from the server's internal origin (e.g., when behind a reverse proxy, CDN, or load balancer).

Suggested clarification
-By default, `Origin` and `Referer` checks compare against the incoming request URL origin. If your deployment needs to allow a different public origin, configure it on the CSRF middleware with `createCsrfMiddleware({ origin: 'https://app.example.com' })`.
+By default, `Origin` and `Referer` checks compare against the incoming request URL origin. If your application's public-facing origin differs from the server origin (e.g., when behind a reverse proxy or CDN), configure the expected public origin with `createCsrfMiddleware({ origin: 'https://app.example.com' })`.
🤖 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 `@docs/start/framework/react/guide/middleware.md` at line 462, Update the
sentence explaining "different public origin" to explicitly state it refers to
situations where the public-facing URL differs from the server's internal origin
(for example when behind a reverse proxy, CDN, or load balancer) and show how to
set that with createCsrfMiddleware({ origin: 'https://app.example.com' }); also
mention that Origin and Referer checks compare against the configured origin
instead of the incoming request URL when this option is provided.

477-486: 💤 Low value

Consider strengthening the security message.

The current text explains how to disable the warning but could more explicitly emphasize that disabling the warning does not provide CSRF protection. Consider adding a note that users must still implement CSRF protection through another mechanism.

Optional enhancement
 If you define `src/start.ts` without the CSRF middleware, Start shows a development warning for server function requests. If you intentionally handle CSRF another way, disable the warning:
+
+> [!WARNING]
+> Disabling this warning does not provide CSRF protection. You must implement CSRF protection through the middleware shown above or an alternative mechanism.
 
 ```tsx
 // vite.config.ts or rsbuild.config.ts
🤖 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 `@docs/start/framework/react/guide/middleware.md` around lines 477 - 486,
Update the docs text around the CSRF middleware warning to explicitly state that
setting serverFns.disableCsrfMiddlewareWarning (used in tanstackStart when you
omit src/start.ts) only suppresses the development warning and does not provide
CSRF protection; add a clear note that you must implement an alternative CSRF
mitigation (e.g., same-site cookies, custom CSRF tokens, or other server-side
checks) if you disable the warning and reference the
disableCsrfMiddlewareWarning option so readers can locate the exact
configuration.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@docs/start/framework/react/guide/middleware.md`:
- Around line 466-475: The wording "use the same middleware" is inconsistent
with the example which calls createCsrfMiddleware() anew; either change the
sentence to "You can also use CSRF middleware to protect any other route" or
update the example to reuse the earlier csrfMiddleware variable (e.g. replace
createCsrfMiddleware() with csrfMiddleware in the createFileRoute call). Ensure
references to createCsrfMiddleware, csrfMiddleware and the Route/createFileRoute
example are updated so text and code match.

In `@packages/start-client-core/src/tests/createCsrfMiddleware.test.ts`:
- Around line 124-128: The test incorrectly treats `${requestOrigin}:443/path`
as cross-origin; update the it.each cases in createCsrfMiddleware.test.ts so
`${requestOrigin}:443/path` is moved from the rejection list into the allowed
Referer cases (same-origin) and replace the rejected port case with a
non-default port (e.g. `${requestOrigin}:444/path`) to assert true cross-origin
rejection; adjust the corresponding test descriptions (e.g., 'rejects
cross-origin Referer fallback') if needed so the examples and expectations
match.

In `@packages/start-fn-stubs/src/createIsomorphicFn.ts`:
- Around line 47-66: The current createRuntimeFn and RuntimeFallbackFn use loose
any types, an as any cast and mutate the input function via Object.assign;
change RuntimeFallbackFn to be generic (e.g., RuntimeFallbackFn<T extends
(...args:any[])=>any>) so the base function and server/client implementations
keep proper parameter/return types, update createRuntimeFn signature to
createRuntimeFn<T>(fn: T, serverImpl?: T): RuntimeFallbackFn<T>, and implement
it by returning a new wrapper object/function (not mutating the passed fn) that
forwards to fn and exposes server and client methods which return new typed
wrapper instances; ensure server and client accept implementations of type T and
remove the as any cast.

---

Nitpick comments:
In `@docs/start/framework/react/guide/middleware.md`:
- Line 462: Update the sentence explaining "different public origin" to
explicitly state it refers to situations where the public-facing URL differs
from the server's internal origin (for example when behind a reverse proxy, CDN,
or load balancer) and show how to set that with createCsrfMiddleware({ origin:
'https://app.example.com' }); also mention that Origin and Referer checks
compare against the configured origin instead of the incoming request URL when
this option is provided.
- Around line 477-486: Update the docs text around the CSRF middleware warning
to explicitly state that setting serverFns.disableCsrfMiddlewareWarning (used in
tanstackStart when you omit src/start.ts) only suppresses the development
warning and does not provide CSRF protection; add a clear note that you must
implement an alternative CSRF mitigation (e.g., same-site cookies, custom CSRF
tokens, or other server-side checks) if you disable the warning and reference
the disableCsrfMiddlewareWarning option so readers can locate the exact
configuration.

In `@packages/start-client-core/src/createCsrfMiddleware.ts`:
- Around line 80-83: The implementation of innerCreateCsrfMiddleware drops type
safety by casting ctx to RequestServerOptions<any, any>; instead add explicit
generic parameters to innerCreateCsrfMiddleware so TRegister and TMiddlewares
are in scope and use RequestServerOptions<TRegister, TMiddlewares> (or the
correct generic aliases) for the csrfCtx typing; update the function signature
for innerCreateCsrfMiddleware to declare the same generics as
CreateCsrfMiddleware and remove the `any` cast where
createMiddleware().server(async (ctx) => { const csrfCtx = ... }) is typed,
ensuring the middleware uses the proper generic types throughout.

In `@packages/start-fn-stubs/tests/createIsomorphicFn.test.ts`:
- Around line 4-24: Add tests to cover argument forwarding and the reverse
registration order: extend the existing createIsomorphicFn tests to assert that
both server and client implementations receive and return arguments correctly
(e.g., call fn(1, 'a') and expect the registered implementation to receive those
args and return based on them), and add a test where .client(...) is chained
before .server(...) to ensure .server still wins at runtime; update tests around
createIsomorphicFn, .server, and .client to include these cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f73d3d49-153d-4b5f-a94a-4547aae1e707

📥 Commits

Reviewing files that changed from the base of the PR and between 4eed408 and 9f96e93.

📒 Files selected for processing (19)
  • .changeset/strong-trains-act.md
  • docs/start/framework/react/guide/middleware.md
  • docs/start/framework/react/guide/server-functions.md
  • e2e/react-start/server-functions/src/start.ts
  • e2e/react-start/server-functions/tests/server-functions.spec.ts
  • packages/start-client-core/src/createCsrfMiddleware.ts
  • packages/start-client-core/src/createMiddleware.ts
  • packages/start-client-core/src/index.tsx
  • packages/start-client-core/src/tests/createCsrfMiddleware.test.ts
  • packages/start-client-core/src/tests/createServerMiddleware.test-d.ts
  • packages/start-fn-stubs/src/createIsomorphicFn.ts
  • packages/start-fn-stubs/tests/createIsomorphicFn.test.ts
  • packages/start-plugin-core/src/rsbuild/plugin.ts
  • packages/start-plugin-core/src/schema.ts
  • packages/start-plugin-core/src/vite/planning.ts
  • packages/start-plugin-core/src/vite/plugin.ts
  • packages/start-plugin-core/tests/csrf-warning-config.test.ts
  • packages/start-server-core/src/createStartHandler.ts
  • packages/start-server-core/src/global.d.ts

Comment on lines +466 to +475
You can also use the same middleware to protect any other route.

```tsx
export const Route = createFileRoute('/api/foo')({
server: {
middleware: [createCsrfMiddleware()],
handlers: { GET: () => {...} }
}
})
```
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 | 🟡 Minor | ⚡ Quick win

Clarify whether to reuse or create a new middleware instance.

Line 466 says "use the same middleware" but the code example creates a new instance with createCsrfMiddleware(). Either update the text to say "You can also use CSRF middleware to protect any other route" or update the code to reuse the csrfMiddleware variable from the earlier example.

Option 1: Update text to match code (creates new instance)
-You can also use the same middleware to protect any other route.
+You can also use CSRF middleware to protect any other route.
Option 2: Update code to reuse existing instance
 export const Route = createFileRoute('/api/foo')({
   server: {
-    middleware: [createCsrfMiddleware()],
+    middleware: [csrfMiddleware],
     handlers: { GET: () => {...} }
   }
 })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
You can also use the same middleware to protect any other route.
```tsx
export const Route = createFileRoute('/api/foo')({
server: {
middleware: [createCsrfMiddleware()],
handlers: { GET: () => {...} }
}
})
```
You can also use CSRF middleware to protect any other route.
Suggested change
You can also use the same middleware to protect any other route.
```tsx
export const Route = createFileRoute('/api/foo')({
server: {
middleware: [createCsrfMiddleware()],
handlers: { GET: () => {...} }
}
})
```
You can also use the same middleware to protect any other route.
🤖 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 `@docs/start/framework/react/guide/middleware.md` around lines 466 - 475, The
wording "use the same middleware" is inconsistent with the example which calls
createCsrfMiddleware() anew; either change the sentence to "You can also use
CSRF middleware to protect any other route" or update the example to reuse the
earlier csrfMiddleware variable (e.g. replace createCsrfMiddleware() with
csrfMiddleware in the createFileRoute call). Ensure references to
createCsrfMiddleware, csrfMiddleware and the Route/createFileRoute example are
updated so text and code match.

Comment on lines +124 to +128
it.each([
'https://evil.example.com/path',
`${requestOrigin}.evil/path`,
`${requestOrigin}:443/path`,
])('rejects cross-origin Referer fallback: %s', async (referer) => {
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

🌐 Web query:

Under the browser same-origin policy, are https://app.example.com` and https://app.example.com:443 considered the same origin?`

💡 Result:

Yes. Under the browser same-origin policy, https://app.example.com and https://app.example.com:443 are considered the same origin. Reason: the origin is defined by the scheme, hostname, and port tuple, and the port is only included in the origin if it is not the default for the scheme; for HTTPS the default port is 443, so omitting:443 is equivalent to explicitly specifying it. Therefore both URLs have the same scheme (https), same host (app.example.com), and effectively the same port (443). [1][2][3]

Citations:


Default HTTPS port is same-origin; move this case to the allowed list.

Line 127 treats ${requestOrigin}:443/path as cross-origin, but https://app.example.com and https://app.example.com:443 are same-origin equivalents per the browser same-origin policy—the default port 443 for HTTPS is implicit when omitted. This test expectation can push the implementation toward false CSRF denials for valid same-origin requests.

Move ${requestOrigin}:443/path from the rejection list to the allowed list and replace it with a non-default port like 444 in the rejection cases.

Suggested test adjustment
   it.each([
     requestOrigin,
     `${requestOrigin}/path`,
     `${requestOrigin}?query=1`,
     `${requestOrigin}#hash`,
+    `${requestOrigin}:443/path`,
   ])('allows same-origin Referer fallback: %s', async (referer) => {
     const ctx = createContext({ headers: { Referer: referer } })

     await expect(getCsrfRequestValidationResult({}, ctx)).resolves.toBe(true)
   })

   it.each([
     'https://evil.example.com/path',
     `${requestOrigin}.evil/path`,
-    `${requestOrigin}:443/path`,
+    'https://app.example.com:444/path',
   ])('rejects cross-origin Referer fallback: %s', async (referer) => {
🤖 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-client-core/src/tests/createCsrfMiddleware.test.ts` around
lines 124 - 128, The test incorrectly treats `${requestOrigin}:443/path` as
cross-origin; update the it.each cases in createCsrfMiddleware.test.ts so
`${requestOrigin}:443/path` is moved from the rejection list into the allowed
Referer cases (same-origin) and replace the rejected port case with a
non-default port (e.g. `${requestOrigin}:444/path`) to assert true cross-origin
rejection; adjust the corresponding test descriptions (e.g., 'rejects
cross-origin Referer fallback') if needed so the examples and expectations
match.

Comment thread packages/start-fn-stubs/src/createIsomorphicFn.ts
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 9, 2026

Merging this PR will not alter performance

✅ 5 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing feat-start-csrf-middleware (85d7a96) with main (a04d5e4)2

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

  2. No successful run was found on main (4eed408) during the generation of this report, so a04d5e4 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts (1)

334-336: 💤 Low value

Consider adding a brief inline comment explaining the cast.

The as unknown as StartCompilerPluginContext cast bypasses TypeScript's type checking, which is a pragmatic approach for plugin contexts. However, a brief comment would clarify why this is safe and what subset of Vite's context is being captured.

📝 Suggested comment
          const result = await compilerContextStorage.run(
+            // Cast to our context subset - Vite's transform handler provides these methods
             this as unknown as StartCompilerPluginContext,
             () =>

As per coding guidelines: "Use TypeScript strict mode with extensive type safety" - while this cast is necessary, documenting it improves maintainability.

🤖 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-plugin-core/src/vite/start-compiler-plugin/plugin.ts` around
lines 334 - 336, Add a short inline comment above the cast in the
compilerContextStorage.run call explaining why the `as unknown as
StartCompilerPluginContext` cast is used and what subset of Vite's context is
being captured; mention that the cast is intentional to adapt Vite's broader
context to the plugin's narrower StartCompilerPluginContext and note any
invariants or assumptions that make this conversion safe (reference:
compilerContextStorage.run and StartCompilerPluginContext).
🤖 Prompt for all review comments with 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.

Nitpick comments:
In `@packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts`:
- Around line 334-336: Add a short inline comment above the cast in the
compilerContextStorage.run call explaining why the `as unknown as
StartCompilerPluginContext` cast is used and what subset of Vite's context is
being captured; mention that the cast is intentional to adapt Vite's broader
context to the plugin's narrower StartCompilerPluginContext and note any
invariants or assumptions that make this conversion safe (reference:
compilerContextStorage.run and StartCompilerPluginContext).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d69083d0-3052-4c33-ad54-a251d1231e5c

📥 Commits

Reviewing files that changed from the base of the PR and between 9f96e93 and 85d7a96.

📒 Files selected for processing (2)
  • packages/react-start-client/src/tests/createServerFn.test-d.tsx
  • packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts

Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud Bot left a comment

Choose a reason for hiding this comment

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

Nx Cloud is proposing a fix for your failed CI:

We fix the Start compiler Vite context is unavailable error by wrapping the hotUpdate handler's finishHotUpdate() call with compilerContextStorage.run(), mirroring the same pattern already used in transform.handler. Without this, the AsyncLocalStorage store was empty during HMR, causing getCompilerContext() to throw when compiler.getTransitiveImporters() internally called the resolveId callback.

Tip

We verified this fix by re-running tanstack-react-start-e2e-hmr:test:e2e--vite-ssr.

Suggested Fix changes
diff --git a/packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts b/packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts
index d4237985..dcc26ad6 100644
--- a/packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts
+++ b/packages/start-plugin-core/src/vite/start-compiler-plugin/plugin.ts
@@ -198,6 +198,14 @@ export function startCompilerPlugin(
   const compilers = new Map<string, ReturnType<typeof createStartCompiler>>()
   const compilerContextStorage =
     new AsyncLocalStorage<StartCompilerPluginContext>()
+  // Stores the most recent full PluginContext from transform.handler per
+  // environment. hotUpdate runs with MinimalPluginContext (no resolve/load),
+  // so we fall back to this when setting up the AsyncLocalStorage context for
+  // getTransitiveImporters calls during HMR.
+  const lastTransformContextByEnv = new Map<
+    string,
+    StartCompilerPluginContext
+  >()
 
   const getCompilerContext = () => {
     const context = compilerContextStorage.getStore()
@@ -331,14 +339,15 @@ export function startCompilerPlugin(
             compilerTransforms,
           })
 
-          const result = await compilerContextStorage.run(
-            this as unknown as StartCompilerPluginContext,
-            () =>
-              compiler.compile({
-                id,
-                code,
-                detectedKinds,
-              }),
+          const pluginContext = this as unknown as StartCompilerPluginContext
+          lastTransformContextByEnv.set(this.environment.name, pluginContext)
+
+          const result = await compilerContextStorage.run(pluginContext, () =>
+            compiler.compile({
+              id,
+              code,
+              detectedKinds,
+            }),
           )
           return result
         },
@@ -424,6 +433,13 @@ export function startCompilerPlugin(
           return mergeHotUpdateModules(ctx.modules, providerModules)
         }
 
+        const storedContext = lastTransformContextByEnv.get(
+          this.environment.name,
+        )
+        if (storedContext) {
+          return compilerContextStorage.run(storedContext, finishHotUpdate)
+        }
+
         return finishHotUpdate()
       },
     }

Apply fix via Nx Cloud  Reject fix via Nx Cloud


Or Apply changes locally with:

npx nx-cloud apply-locally NHaT-0fiz

Apply fix locally with your editor ↗   View interactive diff ↗



🎓 Learn more about Self-Healing CI on nx.dev

@Sheraff Sheraff merged commit 5ae2ae5 into main May 9, 2026
18 of 20 checks passed
@Sheraff Sheraff deleted the feat-start-csrf-middleware branch May 9, 2026 16:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant