-
-
Notifications
You must be signed in to change notification settings - Fork 53
feat: streaming support #537
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Bundle Size Analysis
|
app.use('*all', async (req, res) => { | ||
try { | ||
const url = req.originalUrl.replace(base, '') | ||
|
||
/** @type {string} */ | ||
let template | ||
/** @type {import('./src/entry-server.ts').render} */ | ||
let render | ||
if (!isProduction) { | ||
// Always read fresh template in development | ||
template = await fs.readFile('./index.html', 'utf-8') | ||
template = await vite.transformIndexHtml(url, template) | ||
render = (await vite.ssrLoadModule('/src/entry-server.tsx')).render | ||
} else { | ||
template = templateHtml | ||
render = (await import('./dist/server/entry-server.js')).render | ||
} | ||
|
||
let didError = false | ||
|
||
const { stream, head } = render(url, { | ||
onShellError() { | ||
res.status(500) | ||
res.set({ 'Content-Type': 'text/html' }) | ||
res.send('<h1>Something went wrong</h1>') | ||
}, | ||
async onShellReady() { | ||
res.status(didError ? 500 : 200) | ||
res.set({ | ||
'Content-Type': 'text/html', | ||
}) | ||
|
||
const transformStream = new Transform({ | ||
async transform(chunk, encoding, callback) { | ||
res.write(await renderSSRStreamComponents(head, new TextDecoder().decode(chunk))) | ||
callback() | ||
}, | ||
}) | ||
|
||
const [htmlStart, htmlEnd] = template.split(`<!--app-html-->`) | ||
res.write(await renderSSRStreamComponents(head, htmlStart)) | ||
|
||
transformStream.on('finish', async () => { | ||
res.end(await renderSSRStreamComponents(head, htmlEnd)) | ||
}) | ||
|
||
stream.pipe(transformStream) | ||
}, | ||
onError(error) { | ||
didError = true | ||
console.error(error) | ||
}, | ||
}) | ||
|
||
setTimeout(() => { | ||
stream.abort() | ||
}, ABORT_DELAY) | ||
} catch (e) { | ||
vite?.ssrFixStacktrace(e) | ||
console.log(e.stack) | ||
res.status(500).end(e.stack) | ||
} | ||
}) |
Check failure
Code scanning / CodeQL
Missing rate limiting High
a file system access
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
To fix the problem, we need to introduce rate limiting to the Express application to prevent potential DoS attacks. We can use the express-rate-limit
package to achieve this. The rate limiter will be applied to all incoming requests to ensure that the server is protected from excessive requests.
- Install the
express-rate-limit
package. - Import the
express-rate-limit
package in the server file. - Configure the rate limiter with appropriate settings (e.g., maximum number of requests per time window).
- Apply the rate limiter middleware to the Express application.
-
Copy modified lines R5-R11 -
Copy modified lines R27-R29
@@ -4,2 +4,9 @@ | ||
import { renderSSRStreamComponents } from 'unhead/server' | ||
import rateLimit from 'express-rate-limit' | ||
|
||
// Rate limiter configuration | ||
const limiter = rateLimit({ | ||
windowMs: 15 * 60 * 1000, // 15 minutes | ||
max: 100, // limit each IP to 100 requests per windowMs | ||
}) | ||
|
||
@@ -19,2 +26,5 @@ | ||
|
||
// Apply rate limiter to all requests | ||
app.use(limiter) | ||
|
||
// Add Vite or respective production middlewares |
-
Copy modified lines R19-R20
@@ -18,3 +18,4 @@ | ||
"react-dom": "^19.0.0", | ||
"sirv": "^3.0.1" | ||
"sirv": "^3.0.1", | ||
"express-rate-limit": "^7.5.0" | ||
}, |
Package | Version | Security advisories |
express-rate-limit (npm) | 7.5.0 | None |
} catch (e) { | ||
vite?.ssrFixStacktrace(e) | ||
console.log(e.stack) | ||
res.status(500).end(e.stack) |
Check warning
Code scanning / CodeQL
Information exposure through a stack trace Medium
stack trace information
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
To fix the problem, we need to ensure that the stack trace is not sent back to the user. Instead, we should log the stack trace on the server and send a generic error message to the user. This can be achieved by modifying the catch block to log the error and send a generic message.
- Modify the catch block starting at line 96 to log the error stack trace and send a generic error message.
- Ensure that the error stack trace is logged using
console.error
or a similar logging mechanism.
-
Copy modified lines R98-R99
@@ -97,4 +97,4 @@ | ||
vite?.ssrFixStacktrace(e) | ||
console.log(e.stack) | ||
res.status(500).end(e.stack) | ||
console.error(e.stack) | ||
res.status(500).end('An error occurred') | ||
} |
} catch (e) { | ||
vite?.ssrFixStacktrace(e) | ||
console.log(e.stack) | ||
res.status(500).end(e.stack) |
Check warning
Code scanning / CodeQL
Exception text reinterpreted as HTML Medium
Exception text
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 4 months ago
To fix the problem, we need to ensure that any error messages sent in the HTTP response are properly sanitized or escaped to prevent XSS vulnerabilities. The best way to fix this issue is to use a library like he
(HTML entities) to escape the error message before sending it in the response.
- Install the
he
library to handle HTML escaping. - Import the
he
library in the file. - Use the
he.escape
function to escape the error message before sending it in the response.
-
Copy modified line R5 -
Copy modified line R100
@@ -4,2 +4,3 @@ | ||
import { renderSSRStreamComponents } from 'unhead/server' | ||
import he from 'he' | ||
|
||
@@ -98,3 +99,3 @@ | ||
console.log(e.stack) | ||
res.status(500).end(e.stack) | ||
res.status(500).end(he.escape(e.stack)) | ||
} |
-
Copy modified lines R19-R20
@@ -18,3 +18,4 @@ | ||
"react-dom": "^19.0.0", | ||
"sirv": "^3.0.1" | ||
"sirv": "^3.0.1", | ||
"he": "^1.2.0" | ||
}, |
Package | Version | Security advisories |
he (npm) | 1.2.0 | None |
@birkskyum in the works fyi |
@harlan-zw sounds good - looking forward to react and solid support for this, as streaming is a must-have for adoption. |
This is mostly working for react I just don't fully understand how the suspense boundaries are being resolved and can't hook in |
Hi @harlan-zw! |
π Linked issue
#396
β Type of change
π Description
WIP Support for streaming.
<Suspense>
to know how it's meant to work, seems okay in developmentHow it works
We hijack framework stream response prepending and appending the head data in the HTML template.
Outside of the app shell (for suspense) we need to inject code to insert the head tags. When we hydrate our app it should swap out any of the tags without issue.
Both of these are handled by the function
renderSSRStreamComponents(unhead, htmlPartial)
, which can augment Unhead tags into partial HTML.A utility is provided
streamAppWithUnhead
which can take an app stream and augment it for Unhead, this is practical for Vue and TypeScript but likely not for frameworks with more fine-tuned streaming such as React and Solid.js