Skip to content

fix(start): use setHeader in preview server for HTTP/2 compatibility …#6996

Open
sotasan wants to merge 1 commit intoTanStack:mainfrom
sotasan:main
Open

fix(start): use setHeader in preview server for HTTP/2 compatibility …#6996
sotasan wants to merge 1 commit intoTanStack:mainfrom
sotasan:main

Conversation

@sotasan
Copy link

@sotasan sotasan commented Mar 20, 2026

Fixes #6294

Http2ServerResponse (used by Vite when HTTPS is enabled) doesn't have setHeaders, which is only available on http.ServerResponse. This causes the preview server to crash when using HTTPS (e.g. via vite-plugin-mkcert).

Summary by CodeRabbit

  • Refactor
    • Improved internal header processing in the preview server for better reliability.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

A method call that batch-copies response headers to a Node response object was replaced with explicit iteration over header entries, setting each header individually via separate calls before writeHead operations.

Changes

Cohort / File(s) Summary
Header Processing in Preview Server Plugin
packages/start-plugin-core/src/preview-server-plugin/plugin.ts
Replaced res.setHeaders(webRes.headers) with explicit loop iterating over webRes.headers entries, calling res.setHeader(key, value) individually before res.writeHead() and sendNodeResponse() execution.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 Each header now gets its own gentle hop,
Instead of bundling them all in one swap,
Explicit and clear, like carrots in rows,
Before the response to the client goes! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: replacing setHeaders with setHeader for HTTP/2 compatibility in the preview server, which matches the core fix described in the PR objectives.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Tip

You can validate your CodeRabbit configuration file in your editor.

If your editor has YAML language server, you can enable auto-completion and validation by adding # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json at the top of your CodeRabbit configuration file.

@github-actions
Copy link
Contributor

Bundle Size Benchmarks

  • Commit: 21e39bde0832
  • Measured at: 2026-03-20T17:24:56.573Z
  • Baseline source: history:91cc62899b75
  • Dashboard: bundle-size history
Scenario Current (gzip) Delta vs baseline Raw Brotli Trend
react-router.minimal 88.56 KiB 0 B (0.00%) 279.34 KiB 76.84 KiB ▁▁▁▁▁▁▁▁███
react-router.full 91.73 KiB 0 B (0.00%) 290.07 KiB 79.48 KiB ▁▁▁▁▁▁▁▁███
solid-router.minimal 36.13 KiB 0 B (0.00%) 108.98 KiB 32.41 KiB ████████▁▁▁
solid-router.full 40.47 KiB 0 B (0.00%) 122.08 KiB 36.26 KiB ████████▁▁▁
vue-router.minimal 54.11 KiB 0 B (0.00%) 155.27 KiB 48.48 KiB ▁▁▁▁▁▁▁▁███
vue-router.full 58.89 KiB 0 B (0.00%) 170.42 KiB 52.63 KiB ▁▁▁▁▁▁▁▁███
react-start.minimal 103.00 KiB 0 B (0.00%) 327.41 KiB 89.02 KiB ▁▁▁▁▁▁▁▁███
react-start.full 106.37 KiB 0 B (0.00%) 337.72 KiB 91.83 KiB ▁▁▁▁▁▁▁▁███
solid-start.minimal 50.32 KiB 0 B (0.00%) 155.47 KiB 44.32 KiB ████████▁▁▁
solid-start.full 55.72 KiB 0 B (0.00%) 171.27 KiB 48.90 KiB ████████▁▁▁

Trend sparkline is historical gzip bytes ending with this PR measurement; lower is better.

Copy link
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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/start-plugin-core/src/preview-server-plugin/plugin.ts`:
- Line 57: The current header-copying loop using webRes.headers.forEach(...) can
merge multiple Set-Cookie values into one string; instead, use
webRes.getSetCookie() to retrieve the Set-Cookie array and call
res.setHeader('Set-Cookie', cookieArray) so Node emits multiple Set-Cookie
headers; keep the existing loop for other headers but ensure you set the
Set-Cookie header from webRes.getSetCookie() before writeHead()/end() (refer to
webRes.getSetCookie(), res.setHeader('Set-Cookie', ...) and the existing
webRes.headers.forEach usage).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8aa42569-0f1d-480e-be9f-7ecc9e0d2229

📥 Commits

Reviewing files that changed from the base of the PR and between 21e39bd and a648cf9.

📒 Files selected for processing (1)
  • packages/start-plugin-core/src/preview-server-plugin/plugin.ts

// Vite preview's compression middleware doesn't support flattened array headers that srvx sets
// Call writeHead() before srvx to avoid corruption
res.setHeaders(webRes.headers)
webRes.headers.forEach((value, key) => res.setHeader(key, value));
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

For Node.js Fetch Headers, does headers.forEachpreserve multipleSet-Cookievalues distinctly, and what is the recommended way to forward them tohttp.ServerResponse/Http2ServerResponse without losing cookies?

💡 Result:

No, Node.js Fetch Headers.forEach does not preserve multiple Set-Cookie values distinctly. The Fetch Headers object follows the WHATWG spec where multiple values for the same header (including Set-Cookie) are combined into a single comma-separated string. Thus, headers.forEach((value, name) => ...) will iterate once over 'set-cookie' with value as the concatenated string (e.g., "cookie1=val1, cookie2=val2"), not once per cookie. This makes parsing unreliable due to commas in cookie attributes like Expires. The recommended way to access distinct Set-Cookie values and forward them to http.ServerResponse or Http2ServerResponse without loss: 1. Use response.headers.getSetCookie: Returns an array of individual Set-Cookie strings (Node.js 18.10.0+, undici-based Fetch). const cookies = response.headers.getSetCookie; 2. Set on ServerResponse: res.setHeader('Set-Cookie', cookies); // Array accepted, sends multiple distinct headers. Example proxy code: const { fetch } = require('undici'); // or global fetch in Node 18+ async function proxy(req, res) { const upstreamRes = await fetch(upstreamUrl); // Copy other headers... for (const [name, value] of upstreamRes.headers) { if (name.toLowerCase !== 'set-cookie') { res.setHeader(name, value); } } // Handle Set-Cookie distinctly const setCookies = upstreamRes.headers.getSetCookie; if (setCookies.length > 0) { res.setHeader('Set-Cookie', setCookies); } res.statusCode = upstreamRes.status; // Pipe body... } This ensures multiple Set-Cookie headers are forwarded as separate lines, per RFC 6265 recommendation against comma-folding Set-Cookie. Fallback for older node-fetch (non-standard): response.headers.raw['set-cookie'] returns array of strings. Node.js http.ServerResponse.setHeader('Set-Cookie', array) correctly emits multiple headers.

Citations:


🏁 Script executed:

# Check the context around line 57 and determine the type of webRes
sed -n '40,75p' packages/start-plugin-core/src/preview-server-plugin/plugin.ts

Repository: TanStack/router

Length of output: 1123


🏁 Script executed:

# Check Node.js version requirement in package.json or similar
cat packages/start-plugin-core/package.json | grep -A 5 -B 5 "engines\|node"

Repository: TanStack/router

Length of output: 1084


🏁 Script executed:

# Search for getSetCookie usage elsewhere in the codebase
rg "getSetCookie" --type ts --type tsx -A 2 -B 2

Repository: TanStack/router

Length of output: 86


🏁 Script executed:

# Check if Response or webRes is typed somewhere
ast-grep --pattern 'let $_ = $$$' packages/start-plugin-core/src/preview-server-plugin/plugin.ts

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Check what sendNodeResponse does with headers/body
rg "sendNodeResponse" --type ts -A 10 -B 2 | head -50

Repository: TanStack/router

Length of output: 4516


🏁 Script executed:

# Look for the sendNodeResponse implementation
fd "sendNodeResponse\|send-node-response" --type ts --type tsx

Repository: TanStack/router

Length of output: 228


🏁 Script executed:

# Check if there's a common pattern for forwarding headers in the codebase
rg "headers\.forEach\|getSetCookie" --type ts -C 3

Repository: TanStack/router

Length of output: 41


🏁 Script executed:

# Get full view of the function containing line 57 to understand complete flow
sed -n '1,100p' packages/start-plugin-core/src/preview-server-plugin/plugin.ts | tail -60

Repository: TanStack/router

Length of output: 2551


🏁 Script executed:

# Check srvx dependency version and see if there's any documentation
cat packages/start-plugin-core/package.json | grep -i srvx

Repository: TanStack/router

Length of output: 81


🏁 Script executed:

# Search for any other patterns of header forwarding or Set-Cookie handling in the codebase
rg "Set-Cookie|set-cookie" --type-add 'ts:*.{ts,tsx}' --type ts -i -C 2

Repository: TanStack/router

Length of output: 8518


🌐 Web query:

What is the srvx library for Node.js and how does sendNodeResponse handle headers from Fetch Response objects, particularly multi-value headers like Set-Cookie?

💡 Result:

srvx is a universal server library for Node.js, Deno, and Bun that implements web standard APIs (Request/Response/fetch handlers) with a full-featured CLI for development (watcher, logger, static serving). It provides seamless runtime integration and Node.js compatibility with near-native performance using lightweight proxies to wrap node:IncomingMessage as Request and convert Response to node:ServerResponse. sendNodeResponse is an internal utility in srvx's Node.js adapter (src/adapters/_node/send.ts) that converts a web-standard Fetch Response object to a Node.js ServerResponse. It sets the status, statusText, and headers on the Node.js response using res.writeHead. For headers, it processes the Response.headers using .flat multiple times to handle multi-value headers correctly, passing them as a raw array of [name, value] pairs (which Node.js accepts natively for efficiency). Specifically for multi-value headers like Set-Cookie, it properly splits and preserves all values (using cookie-es internally), ensuring all cookies are sent as separate header lines in the HTTP response. Past issues with header formatting (e.g., invalid output) were resolved and unrelated to srvx core (due to middleware like morgan), confirming robust handling in current versions (v0.11+ as of 2026).

Citations:


Use getSetCookie() to preserve multiple Set-Cookie headers.

Line 57 iterates headers with forEach(), which combines multiple Set-Cookie values into a single comma-separated string. This loses distinct cookies before writeHead() locks them in. Call getSetCookie() to retrieve them as an array and pass the array to res.setHeader('Set-Cookie', ...), which Node.js correctly emits as multiple headers. This matters for session/auth cookies and aligns with existing multi-cookie preservation logic in the codebase.

Proposed fix
- webRes.headers.forEach((value, key) => res.setHeader(key, value));
+ webRes.headers.forEach((value, key) => {
+   if (key.toLowerCase() !== 'set-cookie') {
+     res.setHeader(key, value)
+   }
+ })
+
+ const setCookies = webRes.headers.getSetCookie()
+ if (setCookies.length > 0) {
+   res.setHeader('set-cookie', setCookies)
+ }
📝 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
webRes.headers.forEach((value, key) => res.setHeader(key, value));
webRes.headers.forEach((value, key) => {
if (key.toLowerCase() !== 'set-cookie') {
res.setHeader(key, value)
}
})
const setCookies = webRes.headers.getSetCookie()
if (setCookies.length > 0) {
res.setHeader('set-cookie', setCookies)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/start-plugin-core/src/preview-server-plugin/plugin.ts` at line 57,
The current header-copying loop using webRes.headers.forEach(...) can merge
multiple Set-Cookie values into one string; instead, use webRes.getSetCookie()
to retrieve the Set-Cookie array and call res.setHeader('Set-Cookie',
cookieArray) so Node emits multiple Set-Cookie headers; keep the existing loop
for other headers but ensure you set the Set-Cookie header from
webRes.getSetCookie() before writeHead()/end() (refer to webRes.getSetCookie(),
res.setHeader('Set-Cookie', ...) and the existing webRes.headers.forEach usage).

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.

Start + https preview server fails on res.setHeaders is not a function

1 participant