Skip to content

Commit 970bc19

Browse files
committed
fix: accept the { script, style } CSP nonce in handleRequest
The generated handler passed `options.nonce` unchanged to the client-entry transform, whose `escapeAttribute` calls `.replace` on it, and to `createSSRResponse`, which takes a string, so a `{ script, style }` nonce threw `TypeError: value.replace is not a function`. Project it with `scriptNonce` for both, and declare the option in the `virtual:solid-ssr-handler` types.
1 parent e4cdee4 commit 970bc19

4 files changed

Lines changed: 77 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@solidjs/vite-plugin': patch
3+
---
4+
5+
`handleRequest(request, { nonce })` accepts the `{ script, style }` form of `@solidjs/web`'s `CSPNonce` and uses its `script` value for the scripts the handler writes: the injected client-entry tag and the post-flush redirect fallback. A pair used to throw `TypeError: value.replace is not a function`. The option is now declared in the `virtual:solid-ssr-handler` types.

‎examples/start-ssr/test/run.mjs‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,17 @@ function record(mode, phase, name, ok, detail = '') {
388388
console.log(` [${mode}/${phase}] ${status} ${name}${detail && !ok ? ` — ${detail}` : ''}`);
389389
}
390390

391+
// Reads a handler call's whole body; a rejection becomes the assertion's
392+
// detail instead of aborting the whole run.
393+
async function settledText(call) {
394+
try {
395+
const response = await call();
396+
return { text: await response.text(), error: '' };
397+
} catch (error) {
398+
return { text: '', error: String(error) };
399+
}
400+
}
401+
391402
// Pull the function id for `name` out of the client-transformed module so
392403
// the endpoint can be hit directly.
393404
function extractFunctionId(transformedCode, name) {
@@ -1446,6 +1457,51 @@ async function runProdMode() {
14461457
'client entry carries the escaped CSP nonce',
14471458
nonceHtml.includes('<script type="module" nonce="test&quot;&lt;&amp;" src="'),
14481459
);
1460+
// @solidjs/web's CSPNonce also takes a `{ script, style }` pair. The tags
1461+
// the handler writes are scripts, so they take the `script` value.
1462+
const pairNonce = { script: 'pair-script', style: 'pair-style' };
1463+
const pairPage = await settledText(() =>
1464+
builtHandler.handleRequest(new Request(origin + '/'), { nonce: pairNonce }),
1465+
);
1466+
record(
1467+
mode,
1468+
'build',
1469+
'client entry takes the script nonce of a { script, style } pair',
1470+
pairPage.text.includes('<script type="module" nonce="pair-script" src="') &&
1471+
!/<script\b[^>]*nonce="pair-style"/.test(pairPage.text),
1472+
pairPage.error,
1473+
);
1474+
const pairRedirect = await settledText(() =>
1475+
builtHandler.handleRequest(new Request(origin + '/redirect-post'), { nonce: pairNonce }),
1476+
);
1477+
record(
1478+
mode,
1479+
'build',
1480+
'post-flush redirect fallback takes the script nonce of a { script, style } pair',
1481+
pairRedirect.text.includes('<script nonce="pair-script">window.location='),
1482+
pairRedirect.error,
1483+
);
1484+
const styleOnlyNonce = { script: false, style: 'pair-style' };
1485+
const styleOnlyPage = await settledText(() =>
1486+
builtHandler.handleRequest(new Request(origin + '/'), { nonce: styleOnlyNonce }),
1487+
);
1488+
record(
1489+
mode,
1490+
'build',
1491+
'a pair without a script nonce leaves the client entry un-nonced',
1492+
styleOnlyPage.text.includes('<script type="module" src="'),
1493+
styleOnlyPage.error,
1494+
);
1495+
const styleOnlyRedirect = await settledText(() =>
1496+
builtHandler.handleRequest(new Request(origin + '/redirect-post'), { nonce: styleOnlyNonce }),
1497+
);
1498+
record(
1499+
mode,
1500+
'build',
1501+
'a pair without a script nonce leaves the redirect fallback un-nonced',
1502+
styleOnlyRedirect.text.includes('<script>window.location='),
1503+
styleOnlyRedirect.error,
1504+
);
14491505
record(
14501506
mode,
14511507
'build',

‎src/ssr/index.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,7 +1149,7 @@ export function startServe(
11491149
const composeServerFunctions = internal.serverFunctions;
11501150

11511151
const lines = [
1152-
`import { createRequestEvent, createSSRResponse, commitEventResponse${middlewarePath ? ', composeMiddleware' : ''} } from '@solidjs/web';`,
1152+
`import { createRequestEvent, createSSRResponse, commitEventResponse, scriptNonce${middlewarePath ? ', composeMiddleware' : ''} } from '@solidjs/web';`,
11531153
`import { provideRequestEvent } from ${JSON.stringify(STORAGE_SOURCE)};`,
11541154
`import * as entry from ${JSON.stringify(entryServerSpec())};`,
11551155
...(middlewarePath
@@ -1423,10 +1423,14 @@ export function startServe(
14231423
// The runtime's response-head lifecycle: commit at shell flush,
14241424
// pre-flush Location as a real redirect, post-flush Location as the
14251425
// script fallback; the transform injects the doctype/head pieces.
1426+
// Both write a single script, so a `{ script, style }` nonce
1427+
// contributes its script value, as with @solidjs/web's other
1428+
// single-script surfaces.
1429+
` const nonce = scriptNonce(options.nonce);`,
14261430
` return createSSRResponse(result, event, {`,
14271431
` responseInit: options.responseInit,`,
1428-
` nonce: options.nonce,`,
1429-
` transformChunk: createHtmlChunkTransform(clientEntry, options.devHead, options.nonce),`,
1432+
` nonce,`,
1433+
` transformChunk: createHtmlChunkTransform(clientEntry, options.devHead, nonce),`,
14301434
` });`,
14311435
`}`,
14321436
``,

‎virtual-solid-manifest.d.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ declare module "virtual:solid-server-function-handler" {
4040
// manifest, and — when `serverFunctions` is enabled — serves the
4141
// server-function endpoint ahead of SSR.
4242
declare module "virtual:solid-ssr-handler" {
43+
import type { CSPNonce } from "@solidjs/web";
44+
4345
export function handleRequest(
4446
request: Request,
4547
options?: {
@@ -49,6 +51,13 @@ declare module "virtual:solid-ssr-handler" {
4951
context?: Record<string, unknown>;
5052
/** Status/headers for the HTML response. */
5153
responseInit?: ResponseInit;
54+
/**
55+
* CSP nonce for the scripts the handler writes into the document: the
56+
* injected client-entry tag and the fallback of a redirect decided
57+
* after the shell flushed. A `{ script, style }` pair contributes its
58+
* `script` value.
59+
*/
60+
nonce?: CSPNonce;
5261
/**
5362
* Per-call render mode, overriding `start.renderMode` (static value or
5463
* per-request module alike). `'stream'` flushes the document shell

0 commit comments

Comments
 (0)