-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
[Start] Dev server breaks on IP/Safari: Nitro intercepts $id route modules when Sec-Fetch-Dest header is missing #7095
Description
Description
When accessing a TanStack Start app via IP address (e.g. http://192.168.0.64:3000) or Safari, route files containing $ (like $id.edit.tsx) fail to load as JS modules. Nitro's dev middleware intercepts these requests and interprets $id as a route parameter instead of letting Vite serve them.
This effectively breaks Vite's server.host: true option for TanStack Start projects.
Reproduction
- Create a TanStack Start project with dynamic route files (e.g.
routes/_authed/manager/$id.edit.tsx) - Set
server.host: trueinvite.config.ts - Access the app via IP address or Safari
Expected
Modules load correctly, same as Chrome on localhost.
Actual
GET http://192.168.0.64:3000/app/routes/_authed/manager/$id.edit.tsx
→ 307 redirect to /app/routes/_authed/manager/undefined
→ 404
The app fails to hydrate because the route modules can't be loaded.
Root cause
Nitro's nitroDevMiddlewarePre middleware decides whether to intercept a request based on the Sec-Fetch-Dest header:
// nitro/dist/_chunks/plugin.mjs
if (!fetchDest || /^(document|iframe|frame|empty)$/.test(fetchDest)) {
nitroDevMiddleware(req, res, next); // Nitro handles
} else {
next(); // Vite handles
}Browsers don't always send this header:
| Browser | localhost | IP address |
|---|---|---|
| Chrome | script ✓ |
missing ✗ |
| Safari | missing ✗ | missing ✗ |
Verified by logging incoming headers on a standalone HTTP server.
When the header is missing, Nitro intercepts the module request. Since the URL contains $id (TanStack Router's filename convention for dynamic params), Nitro's routing resolves $id to undefined and returns a 307 redirect.
On localhost with Chrome, Sec-Fetch-Dest: script is sent, so Nitro skips the request and Vite serves the module correctly.
Additional issue: Secure cookie on HTTP
useSession() sets the Secure flag on the session cookie by default. This prevents the cookie from being stored on HTTP connections (IP access). Chrome makes an exception for localhost, but Safari and IP access don't work.
This means even after fixing the module loading issue, login doesn't work on IP/Safari because the session cookie is rejected by the browser.
Workaround:
useSession<SessionData>({
password: process.env.SESSION_SECRET!,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
},
})Workaround for module loading
A Vite plugin that injects the missing header before Nitro's middleware:
import { type Plugin } from 'vite'
function patchSecFetchDest(): Plugin {
return {
name: 'patch-sec-fetch-dest',
configureServer(server) {
server.middlewares.use((req, _res, next) => {
if (!req.headers['sec-fetch-dest'] && req.url && /\.[mc]?[jt]sx?(\?|$)/.test(req.url)) {
req.headers['sec-fetch-dest'] = 'script'
}
next()
})
},
}
}Environment
- TanStack Start: v1.167+
- Nitro: v3 (via
nitro/vite) - Vite: v8.0.3
- macOS, tested with Chrome, Safari, Playwright (Chromium headless + WebKit)