remix icon indicating copy to clipboard operation
remix copied to clipboard

Allow a nonce to be set on single fetch stream transfer inline scripts

Open haines opened this issue 1 year ago • 2 comments

Closes: #9359

  • [x] Docs
  • [ ] Tests - not sure of the best approach to add tests for this. I looked at adding one to packages/remix-react/__tests__/components-test.tsx but it requires setting a context.serverHandoffStream which seems non-trivial? Is there a better approach?

Testing Strategy:

I patched @remix-run/react in our application with these changes and saw that it fixed our CSP violations.

haines avatar May 03 '24 09:05 haines

🦋 Changeset detected

Latest commit: 1087c22cc39556af9b9b3f9b8a39bd3968c51a15

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@remix-run/react Patch
@remix-run/testing Patch
@remix-run/dev Patch
create-remix Patch
remix Patch
@remix-run/architect Patch
@remix-run/cloudflare Patch
@remix-run/cloudflare-pages Patch
@remix-run/cloudflare-workers Patch
@remix-run/css-bundle Patch
@remix-run/deno Patch
@remix-run/eslint-config Patch
@remix-run/express Patch
@remix-run/node Patch
@remix-run/serve Patch
@remix-run/server-runtime Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

changeset-bot[bot] avatar May 03 '24 09:05 changeset-bot[bot]

Thanks!

Yeah this stuff is best tested through our E2E tests - want to try something like this in integration/single-fetch-test.ts?

You can run it with pnpm test:integration single-fetch --project chromium

  test("supports nonce on streaming script tags", async ({ page }) => {
    let fixture = await createFixture({
      config: {
        future: {
          unstable_singleFetch: true,
        },
      },
      files: {
        ...files,
        "app/entry.server.tsx": js`
          import { PassThrough } from "node:stream";

          import type { EntryContext } from "@remix-run/node";
          import { createReadableStreamFromReadable } from "@remix-run/node";
          import { RemixServer } from "@remix-run/react";
          import { renderToPipeableStream } from "react-dom/server";

          export default function handleRequest(
            request: Request,
            responseStatusCode: number,
            responseHeaders: Headers,
            remixContext: EntryContext
          ) {
            return new Promise((resolve, reject) => {
              const { pipe } = renderToPipeableStream(
                <RemixServer context={remixContext} url={request.url} nonce="the-nonce" />,
                {
                  onShellReady() {
                    const body = new PassThrough();
                    const stream = createReadableStreamFromReadable(body);
                    responseHeaders.set("Content-Type", "text/html");
                    resolve(
                      new Response(stream, {
                        headers: responseHeaders,
                        status: responseStatusCode,
                      })
                    );
                    pipe(body);
                  },
                  onShellError(error: unknown) {
                    reject(error);
                  },
                  onError(error: unknown) {
                    responseStatusCode = 500;
                  },
                }
              );
            });
          }
        `,
      },
    });
    let appFixture = await createAppFixture(fixture);
    let app = new PlaywrightFixture(appFixture, page);
    await app.goto("/data", true);
    let scripts = await page.$$("script");
    expect(scripts.length).toBe(6);
    for (let s of scripts) {
      // Do nonce expect() calls here for the scripts that should have it - which I think might just be
      // any with `window.__remixContext.streamController` in the innterHTML?
      console.log(await s.getAttribute("nonce"), await s.innerHTML());
    }
  });

brophdawg11 avatar May 03 '24 14:05 brophdawg11

@brophdawg11 thanks a lot for helping with the test case!

I found that <Scripts> also injects an inline script that references window.__remixContext.streamController. I opted to add the nonce to that as well, rather than trying to distinguish it from the scripts added by <StreamTransfer>, which I felt would probably make the test too brittle.

haines avatar May 06 '24 20:05 haines