codimd icon indicating copy to clipboard operation
codimd copied to clipboard

Fix: 404 errors for static assets when using CMD_URL_PATH configuration

Open Yukaii opened this issue 3 months ago • 1 comments

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 → 404
  • http://localhost:3000/codimd/build/cover.65bca5da2232a62861ce.css → 404
  • All other static assets → 404

Root Cause

  1. Static assets mounted at root level: Express static middleware was mounted at / instead of respecting the urlPath configuration
  2. Application routes mounted at root level: Main application routes ignored the urlPath prefix
  3. Redirect loop issue: The trailing slash redirect middleware created infinite loops between /codimd and /codimd/
  4. 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:

  1. Don't strip the URL path prefix - CodiMD expects to receive the full path including /codimd
  2. Pass through the complete path to the backend
  3. 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 logic
  • lib/middleware/redirectWithoutTrailingSlashes.js - Fixed redirect loop

Fixes issue with 404 errors when using CMD_URL_PATH configuration for serving CodiMD under subdirectory paths.

Yukaii avatar Sep 18 '25 05:09 Yukaii

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.

Yukaii avatar Sep 30 '25 07:09 Yukaii