Skip to content

Instantly share code, notes, and snippets.

@ikibalnyi
Last active December 5, 2023 02:54
Show Gist options
  • Save ikibalnyi/d23a3710f810ff0f434bf58c9039d7b3 to your computer and use it in GitHub Desktop.
Save ikibalnyi/d23a3710f810ff0f434bf58c9039d7b3 to your computer and use it in GitHub Desktop.
Nonce for CSP to overcome RemixJs limitation with getLoadContext not being available in `entry.server.tsx`
import { AsyncLocalStorage } from 'node:async_hooks';
interface EntryContext {
nonce: string;
}
const entryContextALS = new AsyncLocalStorage<EntryContext>();
export const entryContext = {
run: entryContextALS.run.bind(entryContextALS);
getStore: (): EntryContext => {
const store = entryContextALS.getStore();
if (!store) throw new Error('Invariant: attempting to entryContext.getStore() outside of entryContext.run()')
return store;
}
}
import { PassThrough } from 'stream';
import { Response } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import isbot from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';
import { entryContext } from './entry-context';
const ABORT_DELAY = 5000;
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext
) {
const callbackName = isbot(request.headers.get('user-agent')) ? 'onAllReady' : 'onShellReady';
return new Promise((resolve, reject) => {
let hasError = false;
// eslint-disable-next-line @typescript-eslint/unbound-method
const { pipe, abort } = renderToPipeableStream(
// pass nonce further into react side of the app to allow isomorphic usage
<NonceProvider nonce={entryContext.getStore().nonce}>
<RemixServer context={remixContext} url={request.url} />
</NonceProvider>,
{
[callbackName]: () => {
const body = new PassThrough();
responseHeaders.set('Content-Type', 'text/html');
resolve(
new Response(body, {
headers: responseHeaders,
status: hasError ? 500 : responseStatusCode,
})
);
pipe(body);
},
onShellError: (err: unknown) => {
reject(err);
},
onError: (error: unknown) => {
hasError = true;
// eslint-disable-next-line no-console
console.error(error);
},
}
);
setTimeout(abort, ABORT_DELAY);
});
}
import * as React from 'react'
const NonceContext = React.createContext<string | undefined>(undefined)
export const NonceProvider = NonceContext.Provider
export const useNonce = () => React.useContext(NonceContext)
import { json } from "@remix-run/node";
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import { entryContext } from './entry-context'
export const loader = async ({ request }: LoaderArgs) => {
// You can retrieve nonce in server side if needed
const nonce = entryContext.getStore().nonce;
return json({ user: await getUser(request) });
};
export default function App() {
// get nonce from react context
const nonce = useNonce();
return (
<html lang="en" className="h-full">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body className="h-full">
<Outlet />
<ScrollRestoration nonce={nonce} />
<Scripts nonce={nonce} />
<LiveReload nonce={nonce} />
</body>
</html>
);
}
import express from 'express';
import * as build from '@remix-run/dev/server-build';
import { createRequestHandler } from '@remix-run/express';
const app = express();
app.use((req, res, next) => {
// pass through context down the execution trace
return entryContext.run(
{
nonce: res.locals.nonce,
},
() => next()
);
});
app.all('*', createRequestHandler({ build }));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment