|
| 1 | +# URL Path Configuration: Approach Comparison |
| 2 | + |
| 3 | +This document compares two approaches for handling `CMD_URL_PATH` configuration in CodiMD. |
| 4 | + |
| 5 | +## Approach 1: Application Handles URL Path (Main PR #1943) |
| 6 | + |
| 7 | +**Branch:** `bugfix/1936-404-errors-when-using-cmd_url_path-in-new-260` |
| 8 | + |
| 9 | +### How It Works |
| 10 | + |
| 11 | +1. App mounts routes and static assets at URL path prefix (e.g., `/codimd`) |
| 12 | +2. Express handles all path routing internally |
| 13 | +3. Works standalone or behind reverse proxy |
| 14 | + |
| 15 | +### Configuration |
| 16 | + |
| 17 | +**App (app.js):** |
| 18 | +```javascript |
| 19 | +const urlPathPrefix = config.urlPath ? `/${config.urlPath}` : '' |
| 20 | +app.use(urlPathPrefix + '/', express.static(...)) |
| 21 | +app.use(urlPathPrefix, require('./lib/routes').router) |
| 22 | +``` |
| 23 | + |
| 24 | +**Reverse Proxy (Caddy):** |
| 25 | +```caddyfile |
| 26 | +:8080 { |
| 27 | + # Just pass everything through - app handles the path |
| 28 | + handle /codimd* { |
| 29 | + reverse_proxy localhost:3000 |
| 30 | + } |
| 31 | + redir / /codimd/ 301 |
| 32 | +} |
| 33 | +``` |
| 34 | + |
| 35 | +### Pros ✅ |
| 36 | + |
| 37 | +- ✅ **Works standalone** - No reverse proxy required |
| 38 | +- ✅ **Simpler proxy config** - Just pass through requests |
| 39 | +- ✅ **Flexible deployment** - Docker, Kubernetes, bare metal, etc. |
| 40 | +- ✅ **Backward compatible** - No breaking changes |
| 41 | +- ✅ **Single source of truth** - App controls its URL structure |
| 42 | + |
| 43 | +### Cons ❌ |
| 44 | + |
| 45 | +- ❌ **More app code** - Logic in application layer |
| 46 | +- ❌ **Code duplication** - Some if/else blocks (though minimized) |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +## Approach 2: Reverse Proxy Handles URL Path (Experiment) |
| 51 | + |
| 52 | +**Branch:** `experiment/reverse-proxy-path-rewriting` |
| 53 | + |
| 54 | +### How It Works |
| 55 | + |
| 56 | +1. App runs at root path (no URL path mounting) |
| 57 | +2. Reverse proxy strips path prefix before forwarding |
| 58 | +3. App still generates URLs with prefix (via serverURL config) |
| 59 | + |
| 60 | +### Configuration |
| 61 | + |
| 62 | +**App (app.js):** |
| 63 | +```javascript |
| 64 | +// No URL path mounting - runs at root |
| 65 | +app.use('/', express.static(...)) |
| 66 | +app.use(require('./lib/routes').router) |
| 67 | + |
| 68 | +// But serverURL still includes path for URL generation |
| 69 | +config.serverURL = 'http://localhost:3000/codimd' |
| 70 | +``` |
| 71 | + |
| 72 | +**Reverse Proxy (Caddy):** |
| 73 | +```caddyfile |
| 74 | +:8080 { |
| 75 | + # Redirect root |
| 76 | + route { |
| 77 | + @root path_regexp ^/$ |
| 78 | + redir @root /codimd/ 301 |
| 79 | + } |
| 80 | + |
| 81 | + # Strip path and forward |
| 82 | + route /codimd/* { |
| 83 | + uri strip_prefix /codimd |
| 84 | + reverse_proxy localhost:3000 { |
| 85 | + header_up X-Forwarded-Prefix /codimd |
| 86 | + } |
| 87 | + } |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +### Pros ✅ |
| 92 | + |
| 93 | +- ✅ **Cleaner app code** - No URL path mounting logic |
| 94 | +- ✅ **Separation of concerns** - Path handling in proxy layer |
| 95 | +- ✅ **Proxy controls routing** - Easier to change paths without app changes |
| 96 | + |
| 97 | +### Cons ❌ |
| 98 | + |
| 99 | +- ❌ **REQUIRES reverse proxy** - Won't work standalone |
| 100 | +- ❌ **Complex proxy config** - Must handle path stripping correctly |
| 101 | +- ❌ **Harder to debug** - Path transformation happens outside app |
| 102 | +- ❌ **Socket.IO complexity** - WebSocket path rewriting needed |
| 103 | +- ❌ **OAuth callback issues** - May need special handling |
| 104 | +- ❌ **Cookie path issues** - Session cookies may not work correctly |
| 105 | + |
| 106 | +--- |
| 107 | + |
| 108 | +## Test Results |
| 109 | + |
| 110 | +### Approach 1 (Main PR) - FULLY TESTED ✅ |
| 111 | + |
| 112 | +| Feature | Status | Notes | |
| 113 | +|---------|--------|-------| |
| 114 | +| Static assets | ✅ Pass | All assets load correctly | |
| 115 | +| Build bundles | ✅ Pass | Webpack assets working | |
| 116 | +| Routes | ✅ Pass | All routes accessible | |
| 117 | +| Redirects | ✅ Pass | No redirect loops | |
| 118 | +| Socket.IO | ✅ Pass | WebSocket connections work | |
| 119 | +| Standalone | ✅ Pass | Works without proxy | |
| 120 | +| With proxy | ✅ Pass | Works with Caddy/Nginx | |
| 121 | + |
| 122 | +### Approach 2 (Experiment) - PARTIALLY TESTED ⚠️ |
| 123 | + |
| 124 | +| Feature | Status | Notes | |
| 125 | +|---------|--------|-------| |
| 126 | +| Static assets | ✅ Pass | Assets load via path stripping | |
| 127 | +| Build bundles | ✅ Pass | Webpack assets working | |
| 128 | +| Routes | ✅ Pass | Main routes accessible | |
| 129 | +| Redirects | ✅ Pass | Root and path redirects work | |
| 130 | +| Socket.IO | ⚠️ Unknown | Config present, not tested live | |
| 131 | +| Standalone | ❌ Fail | REQUIRES reverse proxy | |
| 132 | +| OAuth | ⚠️ Unknown | Not tested | |
| 133 | +| Cookies | ⚠️ Unknown | Session paths not verified | |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +## Recommendation |
| 138 | + |
| 139 | +### For Production: Use Approach 1 (Main PR) ✅ |
| 140 | + |
| 141 | +**Why:** |
| 142 | +1. **Flexibility** - Works in any deployment scenario |
| 143 | +2. **Tested** - All features verified working |
| 144 | +3. **Simple** - Proxy config is straightforward |
| 145 | +4. **Reliable** - App has full control over routing |
| 146 | + |
| 147 | +### For Experiment: Approach 2 is Viable ⚠️ |
| 148 | + |
| 149 | +**When to use:** |
| 150 | +- You always run behind a reverse proxy (Kubernetes ingress, etc.) |
| 151 | +- You want path routing completely separated from app |
| 152 | +- You're willing to handle edge cases (OAuth, WebSockets, cookies) |
| 153 | + |
| 154 | +**When NOT to use:** |
| 155 | +- Need standalone deployment |
| 156 | +- Running locally for development |
| 157 | +- Can't guarantee reverse proxy presence |
| 158 | + |
| 159 | +--- |
| 160 | + |
| 161 | +## Working Configurations |
| 162 | + |
| 163 | +### Approach 1: Caddy with App Handling Path |
| 164 | + |
| 165 | +```caddyfile |
| 166 | +{ |
| 167 | + auto_https off |
| 168 | +} |
| 169 | +
|
| 170 | +:8080 { |
| 171 | + # App handles everything - just pass through |
| 172 | + handle /codimd* { |
| 173 | + reverse_proxy localhost:3000 |
| 174 | + } |
| 175 | + |
| 176 | + redir / /codimd/ 301 |
| 177 | + |
| 178 | + log { |
| 179 | + output stdout |
| 180 | + level INFO |
| 181 | + } |
| 182 | +} |
| 183 | +``` |
| 184 | + |
| 185 | +### Approach 2: Caddy with Path Stripping |
| 186 | + |
| 187 | +```caddyfile |
| 188 | +{ |
| 189 | + auto_https off |
| 190 | +} |
| 191 | +
|
| 192 | +:8080 { |
| 193 | + log { |
| 194 | + output stdout |
| 195 | + level INFO |
| 196 | + } |
| 197 | + |
| 198 | + # Redirect root |
| 199 | + route { |
| 200 | + @root path_regexp ^/$ |
| 201 | + redir @root /codimd/ 301 |
| 202 | + } |
| 203 | + |
| 204 | + # Redirect /codimd to /codimd/ |
| 205 | + route { |
| 206 | + @codimd_exact path_regexp ^/codimd$ |
| 207 | + redir @codimd_exact /codimd/ 301 |
| 208 | + } |
| 209 | + |
| 210 | + # Handle Socket.IO |
| 211 | + route /codimd/socket.io/* { |
| 212 | + uri strip_prefix /codimd |
| 213 | + reverse_proxy localhost:3000 { |
| 214 | + header_up X-Forwarded-Prefix /codimd |
| 215 | + } |
| 216 | + } |
| 217 | + |
| 218 | + # Handle all other /codimd/* requests |
| 219 | + route /codimd/* { |
| 220 | + uri strip_prefix /codimd |
| 221 | + reverse_proxy localhost:3000 { |
| 222 | + header_up X-Forwarded-Prefix /codimd |
| 223 | + } |
| 224 | + } |
| 225 | +} |
| 226 | +``` |
| 227 | + |
| 228 | +--- |
| 229 | + |
| 230 | +## Nginx Equivalent Configs |
| 231 | + |
| 232 | +### Approach 1: Nginx with App Handling Path |
| 233 | + |
| 234 | +```nginx |
| 235 | +server { |
| 236 | + listen 8080; |
| 237 | + server_name localhost; |
| 238 | + |
| 239 | + # App handles everything - just pass through |
| 240 | + location /codimd { |
| 241 | + proxy_pass http://localhost:3000; |
| 242 | + proxy_set_header Host $host; |
| 243 | + proxy_set_header X-Real-IP $remote_addr; |
| 244 | + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 245 | + proxy_set_header X-Forwarded-Proto $scheme; |
| 246 | + } |
| 247 | + |
| 248 | + # Redirect root |
| 249 | + location = / { |
| 250 | + return 301 /codimd/; |
| 251 | + } |
| 252 | +} |
| 253 | +``` |
| 254 | + |
| 255 | +### Approach 2: Nginx with Path Stripping |
| 256 | + |
| 257 | +```nginx |
| 258 | +server { |
| 259 | + listen 8080; |
| 260 | + server_name localhost; |
| 261 | + |
| 262 | + # Redirect root |
| 263 | + location = / { |
| 264 | + return 301 /codimd/; |
| 265 | + } |
| 266 | + |
| 267 | + # Redirect /codimd to /codimd/ |
| 268 | + location = /codimd { |
| 269 | + return 301 /codimd/; |
| 270 | + } |
| 271 | + |
| 272 | + # Strip /codimd and forward |
| 273 | + location /codimd/ { |
| 274 | + rewrite ^/codimd/(.*)$ /$1 break; |
| 275 | + proxy_pass http://localhost:3000; |
| 276 | + proxy_set_header Host $host; |
| 277 | + proxy_set_header X-Real-IP $remote_addr; |
| 278 | + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 279 | + proxy_set_header X-Forwarded-Proto $scheme; |
| 280 | + proxy_set_header X-Forwarded-Prefix /codimd; |
| 281 | + } |
| 282 | +} |
| 283 | +``` |
| 284 | + |
| 285 | +--- |
| 286 | + |
| 287 | +## Conclusion |
| 288 | + |
| 289 | +**✅ Recommend Approach 1 (Main PR #1943)** for production use: |
| 290 | +- Proven to work in all scenarios |
| 291 | +- Simpler to deploy and maintain |
| 292 | +- No reverse proxy dependency |
| 293 | + |
| 294 | +**⚠️ Approach 2 (Experiment)** is interesting but has limitations: |
| 295 | +- Successfully demonstrates path stripping |
| 296 | +- Requires reverse proxy (not standalone) |
| 297 | +- Needs more testing for edge cases |
| 298 | + |
| 299 | +Both approaches are technically valid, but Approach 1 offers better flexibility and has been thoroughly tested. |
0 commit comments