# Caddy Static File MIME Type Fix (Jul 12, 2026) ## Problem The ops portal at `ops.itpropartner.com` had JS and CSS files served with wrong MIME types (`application/json` instead of `text/javascript` / `text/css`). Browsers enforce correct MIME types for scripts and stylesheets — any JS file served as `application/json` is silently rejected. The entire `Ops` namespace never loaded, login overlays stayed visible, the mobile hamburger menu never worked, and pages appeared broken. Root cause was twofold: ### 1. File permissions (the real fix) Caddy runs as the `caddy` system user. Files in `/opt/ops-portal/static/` were created by root with `chmod 600` — Caddy returned **403 Forbidden** for all static file requests. Even though a Caddy `handle_path` block was configured, the `file_server` handler couldn't read the files and returned 403. **Fix:** `chmod 644 /opt/ops-portal/static/js/*.js /opt/ops-portal/static/css/*.css` ### 2. Caddy handle_path nesting error (compounding factor) The `/js/*` and `/css/*` `handle_path` blocks were initially nested INSIDE the `/data/*` handler block due to a `sed` command error. They only triggered when the URL started with `/data/`, so `/js/app.js` never matched. **Fix:** Extract to the top level of the Caddy site block, before `reverse_proxy`: ```caddy ops.itpropartner.com { handle_path /data/* { root * /var/www/ops/data/ file_server } handle_path /js/* { root * /opt/ops-portal/static/js file_server } handle_path /css/* { root * /opt/ops-portal/static/css file_server } reverse_proxy 127.0.0.1:8090 } ``` ### 3. Backend `media_type` workaround (not needed) The FastAPI backend has an `@app.get("/js/{file_path:path}")` route using `FileResponse`. Even with `media_type="application/javascript"` explicitly set, the response still returned `Content-Type: application/json` for unknown reasons (possibly a FastAPI 0.115.6 quirk, or the `media_type` parameter being ignored in certain response scenarios). The Caddy static file approach bypasses the backend entirely. ## Detection - Curl the JS URL: `curl -sI https://ops.itpropartner.com/js/app.js` — check `content-type` header - Expected: `text/javascript; charset=utf-8` - Broken: `application/json` (backend) or `text/html` (caddy error page) or empty (403/404) - Browser symptom: Pages load but no JS executes — login overlay stays visible, nav not working, "Ops is not defined" errors in console ## Prevention - Always `chmod 644` new static files added to `/opt/ops-portal/static/` - When adding a new Caddy `handle_path` block, verify it's at the TOP LEVEL of the site block, not indented inside another handler - After ANY Caddy config change: `caddy fmt --overwrite /etc/caddy/Caddyfile && caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy`