Skip to content

plugin-rsc: cross-origin stylesheet resources omit crossOrigin and can poison browser CORS cache #1464

Closed

Description

@jaden-chen

Summary

With plugin-rsc 0.5.34, both generated Resources stylesheet links and SSR ReactDOM.preinit(href, { as: 'style' }) omit crossOrigin. Cross-origin CSS can consequently be cached from a non-CORS request and fail a later CORS fetch of the same URL.

The omission also appears in main at df9f995 (src/plugin.ts generateResourcesCode and src/ssr/shared.ts preloadDeps). I did not find a supported option controlling these attributes; cssLinkPrecedence does not address request mode.

Runnable minimal reproduction

https://gist.github.com/jaden-chen/64f2ea68397a9d5844c28da4abb71a74

Node 26, Vinext 1.0.0-beta.9, plugin-rsc 0.5.34, Vite 8.3.0, React 19.3.0. The fixture builds a tiny real App Router application and serves its generated assets from a second local origin. No service worker or application recovery code is present. It models an asset origin that returns ACAO and Vary: Origin only when an Origin header is sent (observed with R2), with immutable cache headers.

  1. Initial server-imported and client-component stylesheets load through links without crossorigin.
  2. Fetch those same URLs from JavaScript with default CORS mode.
  3. Chromium reuses the non-CORS response and rejects it because ACAO is missing.
  4. A cache-reload CORS fetch succeeds. This is not a missing allowlist or missing asset.

Expected / proposed correction

Consistent anonymous CORS for CSS resource generation, matching Vite's ordinary HTML stylesheet generation and RSC's adjacent module preload behavior:

  • crossOrigin: 'anonymous' in the generated Resources link props.
  • crossOrigin: 'anonymous' in SSR CSS preinit options.

A version-checked local patch to these paths passes initial rendering, navigation and warm-cache checks without a worker. The fixture also uncovered a separate Vinext next/dynamic preload omission, which is being reported there independently.

Happy to adjust the proposed correction if the intended contract is a configurable crossOrigin policy rather than a default. The reproduction intentionally distinguishes origin behavior from framework request consistency.

Activity

  1. hi-ogawa commented on Sep 24, 2026

    @hi-ogawa
    Contributor

    It sounds reasonable to have crossOrigin: 'anonymous' but I thought React may not support it (or it was so at that time). I think aligning crossOrigin is good if possible.

  2. added theissue type on Sep 24, 2026
  3. hi-ogawa commented on Sep 24, 2026

    @hi-ogawa
    Contributor

    On second thought, I have no idea how Vite/RSC app would realistically try to fetch 2nd stylesheet. From what I understand, Vite client doesn't try to fetch if link is already there regardless of crossOrigin.

    I'm inclined to fix regardless, but would be nice to have realistic repro other than surgically crafting fetch(link).

  4. github-actions commented on Sep 24, 2026

    @github-actions

    Hello @jaden-chen. Please provide a minimal reproduction using a GitHub repository or StackBlitz. Issues marked with need reproduction will be closed if they have no activity within 3 days.

  5. jaden-chen commented on Sep 24, 2026

    @jaden-chen
    Author

    Thanks — you're right that the original gist's explicit fetch(link.href) did not demonstrate a normal application trigger. I reduced this to a full-document navigation followed by a click-triggered lazy import:

    https://github.com/jaden-chen/vite-rsc-css-cors-reproduction

    1. / server-renders a client component that imports card.css. The generated stylesheet link has no crossorigin; Chromium caches the non-CORS response.
    2. Follow a plain <a href="/second"> link. This creates a new document, initially without that component or a stylesheet link.
    3. Click Show card, which renders React.lazy(() => import('../card')). Vite's dynamic-import helper creates an anonymous-CORS stylesheet link for the same CSS URL. The previous document's link is gone, so the existing-link check cannot deduplicate it.
    4. Chromium reuses the cached response without ACAO, reports a CSS CORS failure, and the page error boundary receives Unable to preload CSS.

    No application CSS fetch(), service worker, next/dynamic, or manual stylesheet insertion/removal is involved. The asset fixture retains the original report's condition: immutable caching, ACAO + Vary: Origin only when the request carries Origin.

    Verified with the pinned original versions in Chromium:

    The automated check asserts matching CSS URLs and only one CSS request reaching the asset server in the warm sequence. npm test asserts the unpatched failure; npm run test:fixed asserts success with those two changes. Manual steps are in the README.

    This establishes a realistic second request across documents; I haven't established that this exact sequence caused the original application incident, and I'm not claiming same-document navigation with an existing link fails.

  6. jaden-chen commented on Sep 24, 2026

    @jaden-chen
    Author

    Clarifying the navigation condition: this reproduction crosses a document boundary, rather than using ordinary same-document client-side navigation. Your point about Vite skipping a stylesheet link that already exists in the current document applies; this example deliberately exercises a new document where that link is absent, while the browser HTTP cache remains available.

    I've updated the repository to keep two tested variants:

    • /second: <Link href="/second" target="_blank" prefetch={false}> from next/link, opening a new tab. The first tab remains open with its original stylesheet.
    • /third: a plain <a href="/third">, loading a new document in the same tab.

    Both destinations initially omit the card and use the same click-triggered React.lazy loader. Both variants reproduce the CSS CORS failure on the unpatched versions and pass with only #1469's two runtime changes. Cold direct visits to either destination pass. The tests also assert an actual document request for each navigation and that the second CSS attempt does not reach the asset server.

    So bypassing same-document client-side navigation is a prerequisite of these demonstrated scenarios. This is not evidence that ordinary client-side navigation with an existing stylesheet link fails, nor a claim that these are the only possible triggers. Opening a standard framework Link in a new tab provides a normal user action that reaches the same condition without requiring a plain-anchor-only application.

    Updated reproduction: jaden-chen/vite-rsc-css-cors-reproduction@a688ed5

  7. locked and limited conversation to collaborators on Oct 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions