⚡♦️ Fixing module preload paths with a Rails/Vite Setup
Hello and happy new year! Today I'm writing about an asset related issue in a Rails application. Enjoy!
Several months ago, I migrated my Rails app from Webpack(er) to Vite, and I’ve been quite happy with the switch. Vite is blazingly fast, well-documented, and works seamlessly once properly configured.
This morning, while resolving CSP violations (all fixed now, by the way 🎉), I encountered the following error in the browser’s developer console:
Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of “text/html”. Strict MIME type checking is enforced for module scripts per HTML spec.
(This message comes from Chromium; it may appear slightly differently in other browsers.)
⏬ TL;DR
Stick around for the full story:
Nothing on my site appeared broken, but I wanted to resolve the issue anyway so I put on my debugging glasses.
My first findings in order:
- The problem doesn’t occur on every page load
- In the network tab, I noticed that certain requests were failing (highlighted in red)
- The trigger was the import of chart library modules
- The response content type of the failing requests was
text/html, even though a JS file was requested - The issue occurred only on specific servers (visible as i have a header showing the server name)
Given these observations, it became clear that the issue was tied to my asset host configuration. In short, I have one server that serves assets via a subdomain and also functions as a regular app server.
But why did the site still work despite failing imports?
A bit of research revealed that Vite preloads modules by default (see MDN on modulepreload). In a nutshell, JavaScript modules are preloaded for performance gains (they are not actually used until explicitly imported in your JS).
Since this behavior is controlled via an HTML attribute, I checked my page source and found several modulepreload links. These used relative paths, meaning they pointed to myapp.com/assets/foo.js instead of assets.myapp.com/assets/foo.js. As a result, requests hit the application servers, which responded with HTML instead of the JS file.
So why was Vite generating links with relative paths?
After some digging, I found a related issue. The fix is to set Rails.application.config.action_controller.asset_host (see the Vite Ruby docs).
Once configured, Vite generates absolute paths in the preload tags, resolving the issue.
However, I noticed URLs with double leading slashes (e.g., //assets.myapp.com/...). To fix this, I adjusted the Vite config:
import { defineConfig } from "vite"
import RubyPlugin from "vite-plugin-ruby"
export default defineConfig({
base: "./",
// other build configuration omitted
})
TL;DR
Adapt the following two things (first one is mandatory) to fix the issue:
# config/application.rb
# Can be set via environment variable
config.action_controller.asset_host = "https://yourassethost.yourapp.com"
// vite.config.mts
export default defineConfig({
base: "./",
// other build configuration omitted
})
I needed to fix some specs and could deploy to staging for acceptance 😌
I hope this helps you if you run into a similar issue. Enjoy your day, and stay curious!