Nginx 静态资源缺失 mjs 映射导致 ESM 严格 MIME 校验拦截 (Nginx Missing mjs MIME Type Breaking ESM Module Scripts)
1. 现象与触发场景 (Symptom & Scenario)
使用现代前端构建工具(如 Vite、Rollup、Webpack 5)打包时,针对 Web Worker、ES Module 动态导入或第三方库(如 pdfjs-dist 的 pdf.worker.mjs)通常会产出扩展名为 .mjs 的静态资源。
触发条件
- 前端应用通过动态
import()或<script type="module">引入.mjs静态资源。 - 静态资源托管在基于 Debian / Ubuntu 的 Nginx 服务器上,且未显式指定
.mjs的 MIME 映射。 - 线上经由 CDN(如腾讯云 EdgeOne、Cloudflare 等)缓存加速。
异常症状
本地开发环境(如 Vite 开发服务器)正常工作,但部署上线后浏览器报错:
Failed to load module script: Expected a JavaScript-or-Wasm module script but the server responded with a MIME type of "application/octet-stream". Strict MIME type checking is enforced for module scripts per HTML spec.
Error: Setting up fake worker failed: "Failed to fetch dynamically imported module: https://.../assets/pdf.worker-xxxx.mjs".功能(如 PDF 预览器)无法初始化,永远挂起在加载状态。
2. 根本原因剖析 (Root Cause)
2.1 HTML 规范对 ESM 模块的严格 MIME 校验
根据 HTML 与 Fetch 标准,浏览器在加载 ES Module(type="module"、import()、Worker ESM)时,强制要求服务端的 Content-Type 响应头必须为合法的 JavaScript MIME 类型(如 text/javascript 或 application/javascript)。如果服务器返回兜底类型 application/octet-stream,浏览器将立即拒绝执行该模块。
2.2 Ubuntu 官方 Nginx 基础配置缺陷
Ubuntu / Debian 发行版的标准 /etc/nginx/mime.types 中,JavaScript 仅绑定了 js 扩展名:
application/javascript js;由于没有包含 mjs,Nginx 会自动回退到 default_type application/octet-stream;。
2.3 CDN 强缓存的陷阱 (CDN Cache Trap)
如果静态资源配置了强缓存策略(如 Cache-Control: public, immutable, max-age=2592000),CDN 边缘节点会把错误的 Content-Type: application/octet-stream 缓存长达 30 天。即使随后修复了源站 Nginx,客户端请求如果命中 CDN 缓存,仍然会因为错误的历史响应头报错。
3. 彻底解决方案 (Resolution Steps)
步骤 1:修复源站 Nginx 全局 MIME 映射
修改 /etc/nginx/mime.types,将 mjs 加入到 application/javascript 中:
sudo sed -i 's/application\/javascript js;/application\/javascript js mjs;/' /etc/nginx/mime.types
sudo nginx -t
sudo systemctl reload nginx验证源站响应头:
curl -k -I https://127.0.0.1/assets/xxx.mjs -H "Host: your-domain.com"
# 确保输出:Content-Type: application/javascript步骤 2:代码层追加版本标识击穿 CDN 历史缓存
由于 CDN 对带参数的 URL 默认视为独立缓存,为了立竿见影且无缝让全球所有用户即刻生效,在引用 worker 或动态 module 时追加构建版本查询参数:
// 针对 Vite / Webpack 生成的 worker url 追加版本参数
const workerUrl = new URL('pdfjs-dist/build/pdf.worker.mjs', import.meta.url).href + '?v=2';
pdfjsLib.GlobalWorkerOptions.workerSrc = workerUrl;这样重新编译打包后,动态导入的 URL 为 /assets/pdf.worker-xxxx.mjs?v=2,CDN 收到新请求时会穿透回源站获取正确的 application/javascript 响应头并缓存。
4. 下次如何快速识别 (Diagnostic Heuristics)
- 特征 1:控制台红字明确提示
Strict MIME type checking is enforced for module scripts per HTML spec。 - 特征 2:网络面板中该
.mjs资源 HTTP 状态码为 200,但响应头中的Content-Type为application/octet-stream。 - 特征 3:本地运行正常,上线后报错,或者部分 CDN 节点正常部分节点报错。
- 结论:100% 为 Nginx 缺失
.mjsMIME 声明,配合 CDN 历史强缓存未刷新。
5. 相关参考 (Related Concepts)
- 交叉观察器在已有相交元素内容失效时不触发回调 (IntersectionObserver Fails to Fire on In-Viewport Content Invalidation)
- PDF 阅读器双轨手势解耦与阅读进度保全架构 (PDF Viewer Dual-Track Gesture Decoupling and Reading Progress Preservation Architecture)
本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Web