Skip to content

renderBuiltUrl appending a query string to CSS modulepreload deps breaks the runtime .css classification #23178

Description

@emiliosheinz

Describe the bug

experimental.renderBuiltUrl is the documented hook for rewriting emitted asset URLs (e.g. Vercel Skew Protection, which appends ?dpl=<deploymentId> so a long-lived tab keeps loading its original deploy's assets across a new deploy).

When renderBuiltUrl returns a query-suffixed URL for a CSS asset, that URL also lands in the __vite__mapDeps array of any chunk that dynamically imports code-split CSS (host type js). At runtime, the injected preload helper classifies each dep with:

const isCss = dep.endsWith(".css");
...
link.rel = isCss ? "stylesheet" : scriptRel; // scriptRel === "modulepreload"
if (!isCss) link.as = "script";

Because the dep now ends in ?dpl=… rather than .css, isCss is false. The stylesheet gets rel="modulepreload" + as="script", so the browser fetches it expecting a JS module and rejects it:

Failed to load module script: Expected a JavaScript-or-Wasm module script but
the server responded with a MIME type of "text/css". Strict MIME type checking
is enforced for module scripts per HTML spec.

The bytes are served correctly; Vite just requests them as a script. Result: the code-split CSS never applies, and every affected navigation logs the error.

So Vite ships an official API (renderBuiltUrl) that can produce URLs its own runtime preload helper cannot classify.

Expected behavior

A CSS dep should be classified as a stylesheet (rel="stylesheet") regardless of a trailing query string, so renderBuiltUrl-rewritten CSS URLs preload correctly.

Related

Reproduction

https://github.com/emiliosheinz/vite-renderbuilturl-css-preload-repro

Steps to reproduce

vite.config.ts:

import { defineConfig } from 'vite'

export default defineConfig({
  experimental: {
    renderBuiltUrl(filename, { type }) {
      if (type !== 'asset') return undefined
      return `/${filename}?dpl=deploy123`
    },
  },
})

A route that dynamically imports a component with its own CSS:

// main.ts
document.querySelector('#go')!.addEventListener('click', () => {
  import('./lazy.ts') // lazy.ts imports './lazy.css'
})

Build and serve dist/. Trigger the dynamic import. The __vite__mapDeps entry for lazy-*.css?dpl=deploy123 is appended as rel="modulepreload", and the console shows the MIME error above.

npm install && npm run build && npm run verify

verify exits 0 when reproduced; npm run preview shows the console error live.

System Info

System:
  OS: macOS 26.3
  CPU: (14) arm64 Apple M4 Max
  Memory: 3.25 GB / 36.00 GB
  Shell: 5.9 - /bin/zsh
Binaries:
  Node: 24.16.0
  Yarn: 4.12.0
  npm: 11.13.0
Browsers:
  Chrome: 150.0.7871.187
  Safari: 26.3

Used Package Manager

npm

Logs

No response

Validations

Activity

  1. added theissue type on Aug 5, 2026
  2. holm commented on Sep 12, 2026

    @holm

    We have the exact same issue. The linked PR LGTM and would be nice to merge.

  3. added
    p2-edge-caseBug, but has workaround or limited in scope (priority)
    and removed
    p2-edge-caseBug, but has workaround or limited in scope (priority)
    on Sep 30, 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

    Labels

    feat: buildp2-edge-caseBug, but has workaround or limited in scope (priority)

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions