Conversation
Bundle Size Analysis
|
| app.use('/{*path}', async (req, res) => { | ||
| try { | ||
| const url = req.originalUrl | ||
|
|
||
| let template, render | ||
| if (!isProd) { | ||
| template = fs.readFileSync(resolve('index.html'), 'utf-8') | ||
| template = await vite.transformIndexHtml(url, template) | ||
| render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render | ||
| } | ||
| else { | ||
| template = indexProd | ||
| render = (await import('./dist/server/entry-server.js')).render | ||
| } | ||
|
|
||
| const stream = render(url, template) | ||
|
|
||
| res.status(200).set({ 'Content-Type': 'text/html; charset=utf-8' }) | ||
| const reader = stream.getReader() | ||
|
|
||
| while (true) { | ||
| const { done, value } = await reader.read() | ||
| if (done) break | ||
| if (res.closed) break | ||
| res.write(value) | ||
| } | ||
| res.end() | ||
| } | ||
| catch (e) { | ||
| vite && vite.ssrFixStacktrace(e) | ||
| console.log(e.stack) | ||
| res.status(500).end(e.stack) | ||
| } | ||
| }) |
Check failure
Code scanning / CodeQL
Missing rate limiting
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
In general, the fix is to introduce rate limiting middleware using a well‑known library (e.g., express-rate-limit) and apply it to the route that performs file system access and SSR work (or globally to all routes). This limits how many requests a client can make within a time window, mitigating DoS risk from that endpoint while preserving existing functionality.
For this specific file (examples/vite-ssr-solidjs-streaming/server.js), the minimal, non‑disruptive change is:
- Import
express-rate-limitat the top of the file. - Create a limiter instance, for example allowing a reasonable number of requests per IP per time window (e.g., 100 per 15 minutes as in the provided pattern).
- Apply this limiter to the path that currently has the expensive handler:
app.use('/{*path}', limiter, async (req, res) => { ... }).
This keeps the handler logic unchanged and only adds a middleware step before it. Concretely:
- At the top of the file, after the existing imports, add:
import rateLimit from 'express-rate-limit'
- After
const app = express(), define:const ssrLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100, })
- Modify the route at line 54 from
app.use('/{*path}', async (req, res) => { ... })toapp.use('/{*path}', ssrLimiter, async (req, res) => { ... }).
No other behavior needs to change; Vite dev middleware and static file serving remain unaffected.
| @@ -3,6 +3,7 @@ | ||
| import path from 'node:path' | ||
| import { fileURLToPath } from 'node:url' | ||
| import express from 'express' | ||
| import rateLimit from 'express-rate-limit' | ||
|
|
||
| const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD | ||
|
|
||
| @@ -19,6 +20,10 @@ | ||
| : '' | ||
|
|
||
| const app = express() | ||
| const ssrLimiter = rateLimit({ | ||
| windowMs: 15 * 60 * 1000, | ||
| max: 100, | ||
| }) | ||
|
|
||
| /** @type {import('vite').ViteDevServer} */ | ||
| let vite | ||
| @@ -51,7 +56,7 @@ | ||
| ) | ||
| } | ||
|
|
||
| app.use('/{*path}', async (req, res) => { | ||
| app.use('/{*path}', ssrLimiter, async (req, res) => { | ||
| try { | ||
| const url = req.originalUrl | ||
|
|
| @@ -16,7 +16,8 @@ | ||
| "@unhead/solid-js": "workspace:*", | ||
| "compression": "^1.8.1", | ||
| "express": "^5.2.1", | ||
| "solid-js": "^1.9.11" | ||
| "solid-js": "^1.9.11", | ||
| "express-rate-limit": "^8.3.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@playwright/test": "^1.58.2", |
| Package | Version | Security advisories |
| express-rate-limit (npm) | 8.3.1 | None |
| app.use('/{*path}', async (req, res) => { | ||
| try { | ||
| const url = req.originalUrl | ||
|
|
||
| let template, render | ||
| if (!isProd) { | ||
| template = fs.readFileSync(resolve('index.html'), 'utf-8') | ||
| template = await vite.transformIndexHtml(url, template) | ||
| render = (await vite.ssrLoadModule('/src/entry-server.ts')).render | ||
| } | ||
| else { | ||
| template = indexProd | ||
| render = (await import('./dist/server/entry-server.js')).render | ||
| } | ||
|
|
||
| const stream = await render(url, template) | ||
|
|
||
| res.status(200).set({ 'Content-Type': 'text/html; charset=utf-8' }) | ||
| const reader = stream.getReader() | ||
|
|
||
| while (true) { | ||
| const { done, value } = await reader.read() | ||
| if (done) break | ||
| if (res.closed) break | ||
| res.write(value) | ||
| } | ||
| res.end() | ||
| } | ||
| catch (e) { | ||
| vite && vite.ssrFixStacktrace(e) | ||
| console.log(e.stack) | ||
| res.status(500).end(e.stack) | ||
| } | ||
| }) |
Check failure
Code scanning / CodeQL
Missing rate limiting
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
In general, we should introduce a rate-limiting middleware and apply it to the expensive route handler so that any client can only make a bounded number of requests in a given time window. In Express, the standard approach is to use a library such as express-rate-limit, configure a limiter (e.g., N requests per IP per time window), and attach it either globally (app.use(limiter)) or specifically to the route (app.use('/{*path}', limiter, handler)).
For this specific file, the least invasive fix is:
- Import
express-rate-limitat the top ofexamples/vite-ssr-svelte-streaming/server.js. - Inside
createServer, afterconst app = express(), define a limiter instance with a reasonable window and max request count. - Attach that limiter to the SSR route used at line 54 so that only this expensive handler is affected and existing functional behavior is preserved aside from enforcing limits.
Concretely:
- Add
import rateLimit from 'express-rate-limit'near the other imports. - After
const app = express(), defineconst ssrLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 })(values can mirror the example in the background). - Change
app.use('/{*path}', async (req, res) => { ... })intoapp.use('/{*path}', ssrLimiter, async (req, res) => { ... }).
No other logic should be changed.
| @@ -3,6 +3,7 @@ | ||
| import path from 'node:path' | ||
| import { fileURLToPath } from 'node:url' | ||
| import express from 'express' | ||
| import rateLimit from 'express-rate-limit' | ||
|
|
||
| const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD | ||
|
|
||
| @@ -19,6 +20,10 @@ | ||
| : '' | ||
|
|
||
| const app = express() | ||
| const ssrLimiter = rateLimit({ | ||
| windowMs: 15 * 60 * 1000, | ||
| max: 100, | ||
| }) | ||
|
|
||
| /** @type {import('vite').ViteDevServer} */ | ||
| let vite | ||
| @@ -51,7 +56,7 @@ | ||
| ) | ||
| } | ||
|
|
||
| app.use('/{*path}', async (req, res) => { | ||
| app.use('/{*path}', ssrLimiter, async (req, res) => { | ||
| try { | ||
| const url = req.originalUrl | ||
|
|
| @@ -15,7 +15,8 @@ | ||
| "@unhead/svelte": "workspace:*", | ||
| "compression": "^1.8.1", | ||
| "express": "^5.2.1", | ||
| "sirv": "^3.0.2" | ||
| "sirv": "^3.0.2", | ||
| "express-rate-limit": "^8.3.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@playwright/test": "^1.58.2", |
| Package | Version | Security advisories |
| express-rate-limit (npm) | 8.3.1 | None |
| catch (e) { | ||
| vite && vite.ssrFixStacktrace(e) | ||
| console.log(e.stack) | ||
| res.status(500).end(e.stack) |
Check warning
Code scanning / CodeQL
Exception text reinterpreted as HTML
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, never send raw exception messages or stack traces directly to the browser as HTML. For user-facing responses, either (a) send a generic error page/message that does not include user input, or (b) if you must include details, escape/encode the text appropriately (for example, HTML-escape it, or serve it with a safe content type like text/plain).
For this specific code, the safest and simplest change without altering core functionality is:
- Stop writing
e.stackdirectly as HTML. - Instead, respond with a generic error message to the client (for example,
Internal Server Error) and continue logging the full stack trace to the server console. - Optionally, if you still want to expose some detail (e.g., in dev), you can encode the stack trace before sending it or change the content type to
text/plain. Since we should not change surrounding behavior more than necessary and avoid adding new configuration toggles, the minimal secure fix is to replaceres.status(500).end(e.stack)with a generic constant string response such asres.status(500).end('Internal Server Error').
Concretely:
- In
examples/vite-ssr-solidjs-streaming/server.js, within thecatch (e)block around line 82, keepvite && vite.ssrFixStacktrace(e)andconsole.log(e.stack)as-is, but change the final response so it no longer includese.stack. No new imports or helper functions are required.
| @@ -82,7 +82,7 @@ | ||
| catch (e) { | ||
| vite && vite.ssrFixStacktrace(e) | ||
| console.log(e.stack) | ||
| res.status(500).end(e.stack) | ||
| res.status(500).end('Internal Server Error') | ||
| } | ||
| }) | ||
|
|
| catch (e) { | ||
| vite && vite.ssrFixStacktrace(e) | ||
| console.log(e.stack) | ||
| res.status(500).end(e.stack) |
Check warning
Code scanning / CodeQL
Information exposure through a stack trace
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, the fix is to avoid sending the stack trace (or detailed error object) to the client, and instead log it on the server while returning a generic error message and status code to the user. This preserves debuggability without exposing internal implementation details.
Specifically for this file, we should:
- Keep server-side logging (
console.logor similar) of the full error/stack trace. - Replace
res.status(500).end(e.stack)with a generic message likeres.status(500).end('Internal Server Error')(or localized/alternative wording) that does not depend oneore.stack. - Optionally slightly improve logging by logging both the error and its stack, but without changing behavior observed by the client.
All required functionality can be implemented within examples/vite-ssr-solidjs-streaming/server.js without new imports. The only change is in the catch block of the app.use('/{*path}', ...) handler around lines 82–86.
| @@ -82,7 +82,7 @@ | ||
| catch (e) { | ||
| vite && vite.ssrFixStacktrace(e) | ||
| console.log(e.stack) | ||
| res.status(500).end(e.stack) | ||
| res.status(500).end('Internal Server Error') | ||
| } | ||
| }) | ||
|
|
| catch (e) { | ||
| vite && vite.ssrFixStacktrace(e) | ||
| console.log(e.stack) | ||
| res.status(500).end(e.stack) |
Check warning
Code scanning / CodeQL
Exception text reinterpreted as HTML
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, to fix this class of issue you should avoid sending raw exception messages or stack traces directly in an HTML response. For production, it’s safer to send a generic error page or message that contains no user-controllable data. For development, if you really need to expose the error content to the browser, you must HTML‑escape (encode) it so that any <, >, &, quotes, etc. are rendered as text instead of being interpreted as HTML or JavaScript.
The best fix here without changing existing functionality too much is:
- In the
catchblock, keep logging the full stack trace to the server console (console.log(e.stack)). - Change the HTTP response so that it no longer sends
e.stackdirectly. Instead:- In production (
isProd === true), send a generic error message (e.g.,"Internal Server Error"). - In non‑production, either:
- send an HTML‑escaped version of
e.stack, or - for maximum simplicity and safety, also send a generic message.
- send an HTML‑escaped version of
- In production (
- Implement a small HTML-escaping helper within this file if you want to preserve error content in dev. This avoids adding new dependencies and keeps the change localized.
Concretely, in examples/vite-ssr-svelte-streaming/server.js:
- Add a simple
escapeHtmlhelper function near the top of the file. - Replace
res.status(500).end(e.stack)with something like:const msg = isProd ? 'Internal Server Error' : escapeHtml(String(e.stack || e));res.status(500).set({ 'Content-Type': 'text/html; charset=utf-8' }).end(msg);
- Make sure the catch block doesn’t introduce any new tainted pathway (i.e., only escapes or discards exception content).
| @@ -6,6 +6,15 @@ | ||
|
|
||
| const isTest = process.env.NODE_ENV === 'test' || !!process.env.VITE_TEST_BUILD | ||
|
|
||
| function escapeHtml(str) { | ||
| return String(str) | ||
| .replace(/&/g, '&') | ||
| .replace(/</g, '<') | ||
| .replace(/>/g, '>') | ||
| .replace(/"/g, '"') | ||
| .replace(/'/g, ''') | ||
| } | ||
|
|
||
| export async function createServer( | ||
| root = process.cwd(), | ||
| isProd = process.env.NODE_ENV === 'production', | ||
| @@ -81,8 +90,14 @@ | ||
| } | ||
| catch (e) { | ||
| vite && vite.ssrFixStacktrace(e) | ||
| console.log(e.stack) | ||
| res.status(500).end(e.stack) | ||
| console.log(e && e.stack ? e.stack : e) | ||
| const message = isProd | ||
| ? 'Internal Server Error' | ||
| : escapeHtml(e && e.stack ? e.stack : String(e)) | ||
| res | ||
| .status(500) | ||
| .set({ 'Content-Type': 'text/html; charset=utf-8' }) | ||
| .end(message) | ||
| } | ||
| }) | ||
|
|
| catch (e) { | ||
| vite && vite.ssrFixStacktrace(e) | ||
| console.log(e.stack) | ||
| res.status(500).end(e.stack) |
Check warning
Code scanning / CodeQL
Information exposure through a stack trace
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 6 months ago
In general, the fix is to stop sending full stack traces to the client and instead send a generic error message while logging the stack trace on the server. The server logs retain the detail needed for debugging without exposing it over HTTP.
For this specific file, the best minimal change is inside the catch (e) block of the app.use('/{*path}', ...) handler. We should:
- Keep
vite && vite.ssrFixStacktrace(e)as-is (it adjusts stack traces for server-side logging and debugging). - Keep logging the error stack to the server console (or potentially improve logging later), but not include it in the HTTP response.
- Replace
res.status(500).end(e.stack)with a response that sends a generic message such as"Internal Server Error"or"An error occurred"without any stack/implementation details.
No new imports or external libraries are strictly required. All changes are localized to the existing catch block in examples/vite-ssr-svelte-streaming/server.js, around lines 82–86.
| @@ -82,7 +82,7 @@ | ||
| catch (e) { | ||
| vite && vite.ssrFixStacktrace(e) | ||
| console.log(e.stack) | ||
| res.status(500).end(e.stack) | ||
| res.status(500).end('Internal Server Error') | ||
| } | ||
| }) | ||
|
|
|
Important Review skippedToo many files! This PR contains 299 files, which is 149 over the limit of 150. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (299)
You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Branch to track Unhead v3 that will introduce support for streaming and numerous performance and DX improvmeents.
PRs
useHead()type narrowing #627resolveTags()#622renderDOMHead()#628renderSSRHead()#629render()function #630Performance
Bundle
✨ 11.2% smaller client
Bench
e2e benchmark
simple benchmark
✨ ~32% faster for mid-size site