Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/handle-request-nonce-pair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/vite-plugin': patch
---

`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.
56 changes: 56 additions & 0 deletions examples/start-ssr/test/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,17 @@ function record(mode, phase, name, ok, detail = '') {
console.log(` [${mode}/${phase}] ${status} ${name}${detail && !ok ? ` — ${detail}` : ''}`);
}

// Reads a handler call's whole body; a rejection becomes the assertion's
// detail instead of aborting the whole run.
async function settledText(call) {
try {
const response = await call();
return { text: await response.text(), error: '' };
} catch (error) {
return { text: '', error: String(error) };
}
}

// Pull the function id for `name` out of the client-transformed module so
// the endpoint can be hit directly.
function extractFunctionId(transformedCode, name) {
Expand Down Expand Up @@ -1446,6 +1457,51 @@ async function runProdMode() {
'client entry carries the escaped CSP nonce',
nonceHtml.includes('<script type="module" nonce="test&quot;&lt;&amp;" src="'),
);
// @solidjs/web's CSPNonce also takes a `{ script, style }` pair. The tags
// the handler writes are scripts, so they take the `script` value.
const pairNonce = { script: 'pair-script', style: 'pair-style' };
const pairPage = await settledText(() =>
builtHandler.handleRequest(new Request(origin + '/'), { nonce: pairNonce }),
);
record(
mode,
'build',
'client entry takes the script nonce of a { script, style } pair',
pairPage.text.includes('<script type="module" nonce="pair-script" src="') &&
!/<script\b[^>]*nonce="pair-style"/.test(pairPage.text),
pairPage.error,
);
const pairRedirect = await settledText(() =>
builtHandler.handleRequest(new Request(origin + '/redirect-post'), { nonce: pairNonce }),
);
record(
mode,
'build',
'post-flush redirect fallback takes the script nonce of a { script, style } pair',
pairRedirect.text.includes('<script nonce="pair-script">window.location='),
pairRedirect.error,
);
const styleOnlyNonce = { script: false, style: 'pair-style' };
const styleOnlyPage = await settledText(() =>
builtHandler.handleRequest(new Request(origin + '/'), { nonce: styleOnlyNonce }),
);
record(
mode,
'build',
'a pair without a script nonce leaves the client entry un-nonced',
styleOnlyPage.text.includes('<script type="module" src="'),
styleOnlyPage.error,
);
const styleOnlyRedirect = await settledText(() =>
builtHandler.handleRequest(new Request(origin + '/redirect-post'), { nonce: styleOnlyNonce }),
);
record(
mode,
'build',
'a pair without a script nonce leaves the redirect fallback un-nonced',
styleOnlyRedirect.text.includes('<script>window.location='),
styleOnlyRedirect.error,
);
record(
mode,
'build',
Expand Down
10 changes: 7 additions & 3 deletions src/ssr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1149,7 +1149,7 @@ export function startServe(
const composeServerFunctions = internal.serverFunctions;

const lines = [
`import { createRequestEvent, createSSRResponse, commitEventResponse${middlewarePath ? ', composeMiddleware' : ''} } from '@solidjs/web';`,
`import { createRequestEvent, createSSRResponse, commitEventResponse, scriptNonce${middlewarePath ? ', composeMiddleware' : ''} } from '@solidjs/web';`,
`import { provideRequestEvent } from ${JSON.stringify(STORAGE_SOURCE)};`,
`import * as entry from ${JSON.stringify(entryServerSpec())};`,
...(middlewarePath
Expand Down Expand Up @@ -1423,10 +1423,14 @@ export function startServe(
// The runtime's response-head lifecycle: commit at shell flush,
// pre-flush Location as a real redirect, post-flush Location as the
// script fallback; the transform injects the doctype/head pieces.
// Both write a single script, so a `{ script, style }` nonce
// contributes its script value, as with @solidjs/web's other
// single-script surfaces.
` const nonce = scriptNonce(options.nonce);`,
` return createSSRResponse(result, event, {`,
` responseInit: options.responseInit,`,
` nonce: options.nonce,`,
` transformChunk: createHtmlChunkTransform(clientEntry, options.devHead, options.nonce),`,
` nonce,`,
` transformChunk: createHtmlChunkTransform(clientEntry, options.devHead, nonce),`,
` });`,
`}`,
``,
Expand Down
9 changes: 9 additions & 0 deletions virtual-solid-manifest.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ declare module "virtual:solid-server-function-handler" {
// manifest, and — when `serverFunctions` is enabled — serves the
// server-function endpoint ahead of SSR.
declare module "virtual:solid-ssr-handler" {
import type { CSPNonce } from "@solidjs/web";

export function handleRequest(
request: Request,
options?: {
Expand All @@ -49,6 +51,13 @@ declare module "virtual:solid-ssr-handler" {
context?: Record<string, unknown>;
/** Status/headers for the HTML response. */
responseInit?: ResponseInit;
/**
* CSP nonce for the scripts the handler writes into the document: the
* injected client-entry tag and the fallback of a redirect decided
* after the shell flushed. A `{ script, style }` pair contributes its
* `script` value.
*/
nonce?: CSPNonce;
/**
* Per-call render mode, overriding `start.renderMode` (static value or
* per-request module alike). `'stream'` flushes the document shell
Expand Down