Fix: 404 errors for static assets when using CMD_URL_PATH configuration
Problem
When configuring CMD_URL_PATH to serve CodiMD under a subdirectory path (e.g., CMD_URL_PATH=codimd), static assets like CSS, JavaScript files, favicons, and other resources return 404 errors, making the application unusable.
Example Issue
With the following Docker configuration:
environment:
- CMD_URL_PATH=codimd
- CMD_DOMAIN=localhost
- CMD_URL_ADDPORT=true
URLs like these would return 404:
http://localhost:3000/codimd/build/font.68d91a5a4c1afd7f82a9.css→ 404http://localhost:3000/codimd/build/cover.65bca5da2232a62861ce.css→ 404- All other static assets → 404
Root Cause
- Static assets mounted at root level: Express static middleware was mounted at
/instead of respecting theurlPathconfiguration - Application routes mounted at root level: Main application routes ignored the
urlPathprefix - Redirect loop issue: The trailing slash redirect middleware created infinite loops between
/codimdand/codimd/ - Socket.IO path mismatch: WebSocket connections weren't configured for the URL path
Solution
1. Fixed Static Asset and Route Mounting (app.js)
Added conditional mounting based on config.urlPath:
// Handle URL path configuration
if (config.urlPath) {
// Redirect from root to URL path
app.get('/', function (req, res) {
res.redirect(301, `/${config.urlPath}/`)
})
// Mount static files and routes under URL path
const urlPathPrefix = `/${config.urlPath}`
app.use(urlPathPrefix + '/', express.static(...))
app.use(urlPathPrefix + '/docs', express.static(...))
app.use(urlPathPrefix + '/uploads', express.static(...))
app.use(urlPathPrefix, require('./lib/routes').router)
} else {
// Standard mounting when no URL path is configured
// ...
}
2. Fixed Redirect Loop (lib/middleware/redirectWithoutTrailingSlashes.js)
Updated the middleware to avoid redirect loops by allowing the URL path itself to pass through:
// Don't redirect if this is the URL path itself (e.g., /codimd/)
if (config.urlPath && req.path === `/${config.urlPath}/`) {
next()
return
}
3. Socket.IO Configuration
Updated Socket.IO to use the correct path when URL path is configured:
var io = require('socket.io')(server, config.urlPath ? {
path: `/${config.urlPath}/socket.io`
} : {})
Testing
Local Configuration Used
{
"domain": "",
"urlAddPort": false,
"urlPath": "codimd"
}
Results
- ✅ Static assets now load correctly at
/codimd/build/... - ✅ Application accessible at
/codimd/ - ✅ API endpoints work at
/codimd/api/... - ✅ WebSocket connections work correctly
- ✅ No more redirect loops
Reverse Proxy Compatibility
The fix also works correctly with reverse proxies. Here's a working Caddy configuration:
# Caddyfile for CodiMD with URL path configuration
{
# Disable automatic HTTPS for local testing
auto_https off
}
:8080 {
# Handle the subdirectory path - DON'T strip prefix since CodiMD expects full path
handle /codimd* {
reverse_proxy localhost:3000
}
# Redirect root to subdirectory
redir / /codimd/ 301
# Enable logging for debugging
log {
output stdout
level INFO
}
}
Key Points for Reverse Proxy Setup:
- Don't strip the URL path prefix - CodiMD expects to receive the full path including
/codimd - Pass through the complete path to the backend
- Optional root redirect for better UX
Breaking Changes
None. This is a backward-compatible fix that only affects behavior when CMD_URL_PATH is configured.
Files Modified
app.js- Added URL path-aware mounting logiclib/middleware/redirectWithoutTrailingSlashes.js- Fixed redirect loop
Fixes issue with 404 errors when using CMD_URL_PATH configuration for serving CodiMD under subdirectory paths.
Code Simplification Applied ✨
I've simplified the implementation by eliminating code duplication:
Before (duplicated if/else blocks):
- ❌ 33 lines with duplicated static asset mounting
- ❌ Separate code paths for with/without URL path
- ❌ Harder to maintain
After (unified approach):
- ✅ 18 lines total (saved ~15 lines)
- ✅ Single code path using empty string prefix
- ✅ Cleaner and more maintainable
Key Insight
When urlPath is not configured, we use an empty string prefix (''), so:
'' + '/'='/'→ same as mounting at root'' + '/docs'='/docs'→ same as before
This works for both cases without duplication!
Analysis document: I also created URL_PATH_ANALYSIS.md analyzing whether any logic could be offloaded to reverse proxy configuration. Conclusion: Current approach is optimal - reverse proxy path stripping would break templates, OAuth, Socket.IO, and standalone deployments.