* feat: add response compression middleware for HTTP responses Add compression middleware to Express server with SSE route exclusion and 1KB threshold. Reduces bandwidth for non-streaming API responses (history, sessions, files, static assets) by 60-80%. Closes #928 * fix: harden proxy compression and proxy docs --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
71eac0d2f6
commit
304b14b4b1
@@ -244,6 +244,11 @@ environment:
|
||||
|
||||
Managed-local path note: `OPENCHAMBER_TUNNEL_CONFIG` must point to a path inside the container user home (`/home/openchamber/...`). If your Cloudflare config references a credentials JSON file, that file path must also be accessible inside the container (mount with `volumes`).
|
||||
|
||||
### Reverse proxy notes
|
||||
|
||||
- For a complete reverse proxy setup guide, see [`docs/REVERSE_PROXY.md`](./docs/REVERSE_PROXY.md).
|
||||
- Website docs source lives at `packages/docs/content/docs/reverse-proxy.mdx`.
|
||||
|
||||
### Tunnel behavior notes
|
||||
|
||||
- OpenChamber supports one active tunnel per running instance (port).
|
||||
|
||||
@@ -265,6 +265,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"compression": "^1.8.1",
|
||||
"cron-parser": "^4.9.0",
|
||||
"express": "^5.1.0",
|
||||
"ghostty-web": "0.3.0",
|
||||
@@ -1420,6 +1421,10 @@
|
||||
|
||||
"common-tags": ["common-tags@1.8.2", "", {}, "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA=="],
|
||||
|
||||
"compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="],
|
||||
|
||||
"compression": ["compression@1.8.1", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.1.0", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="],
|
||||
@@ -2152,7 +2157,7 @@
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
"negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
|
||||
|
||||
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||
|
||||
@@ -2188,6 +2193,8 @@
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"on-headers": ["on-headers@1.1.0", "", {}, "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
|
||||
@@ -2858,6 +2865,8 @@
|
||||
|
||||
"@vscode/vsce/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
||||
|
||||
"accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
|
||||
|
||||
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
@@ -2868,6 +2877,8 @@
|
||||
|
||||
"cmdk/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
|
||||
|
||||
"compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
|
||||
|
||||
"decode-named-character-reference/character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
|
||||
|
||||
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
@@ -2982,6 +2993,8 @@
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
|
||||
|
||||
"filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
# Reverse Proxy Setup
|
||||
|
||||
Use this guide when running OpenChamber behind Nginx, Nginx Proxy Manager, Caddy, Cloudflare, or another reverse proxy.
|
||||
|
||||
## Before you proxy it
|
||||
|
||||
1. Confirm OpenChamber works directly first.
|
||||
2. Open `http://<server-ip>:3000` or your custom port from the same network.
|
||||
3. Only add the reverse proxy after the direct connection works.
|
||||
|
||||
## What the proxy must support
|
||||
|
||||
- WebSockets for live message transport:
|
||||
- `/api/event/ws`
|
||||
- `/api/global/event/ws`
|
||||
- `/api/terminal/ws`
|
||||
- SSE without buffering:
|
||||
- `/api/event`
|
||||
- `/api/global/event`
|
||||
- `/api/notifications/stream`
|
||||
- `/api/openchamber/events`
|
||||
- `/api/terminal/:sessionId/stream`
|
||||
- Large request bodies for attachments and file operations
|
||||
- Long-lived read timeouts for live streams and terminal sessions
|
||||
|
||||
## Rules that matter
|
||||
|
||||
- Enable WebSocket proxying.
|
||||
- Disable buffering on SSE routes.
|
||||
- Disable gzip on the proxy if OpenChamber is already compressing responses.
|
||||
- Keep compression enabled in only one layer.
|
||||
- Forward normal proxy headers such as `Host`, `X-Forwarded-For`, and `X-Forwarded-Proto`.
|
||||
- Increase body size limits if users upload files.
|
||||
|
||||
## Quick checklist
|
||||
|
||||
- OpenChamber reachable directly on LAN
|
||||
- WebSockets enabled in the proxy
|
||||
- SSE routes have buffering off
|
||||
- `gzip off` on the proxy host, or proxy compression disabled another way
|
||||
- `client_max_body_size` large enough for attachments
|
||||
- `proxy_read_timeout` long enough for streams
|
||||
|
||||
## Example: Nginx
|
||||
|
||||
<details>
|
||||
<summary>Show example config</summary>
|
||||
|
||||
```nginx
|
||||
client_max_body_size 50M;
|
||||
client_body_buffer_size 50M;
|
||||
proxy_request_buffering off;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
gzip off;
|
||||
|
||||
location = /api/terminal/ws {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location = /api/global/event/ws {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location = /api/event/ws {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location ~ ^/api/(event|global/event|notifications/stream|openchamber/events)$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location ~ ^/api/terminal/.+/stream$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Example: Nginx Proxy Manager
|
||||
|
||||
<details>
|
||||
<summary>Show Advanced tab example</summary>
|
||||
|
||||
```nginx
|
||||
client_max_body_size 50M;
|
||||
client_body_buffer_size 50M;
|
||||
proxy_request_buffering off;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
gzip off;
|
||||
|
||||
location = /api/terminal/ws {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location = /api/global/event/ws {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location = /api/event/ws {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location = /api/event {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location = /api/global/event {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location = /api/notifications/stream {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location = /api/openchamber/events {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location ~ ^/api/terminal/.+/stream$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Also enable `Websockets Support` in Nginx Proxy Manager for this host.
|
||||
|
||||
## Common failure signs
|
||||
|
||||
### Page loads, but sending messages fails
|
||||
|
||||
- WebSockets are not enabled in the proxy
|
||||
- `/api/event/ws` or `/api/global/event/ws` is not passing through correctly
|
||||
|
||||
### Notifications or live status do not update
|
||||
|
||||
- one of the SSE routes is buffered or cached
|
||||
- `X-Accel-Buffering "no"` is missing
|
||||
|
||||
### File uploads fail
|
||||
|
||||
- `client_max_body_size` is too small
|
||||
|
||||
### Everything works locally, but breaks only behind the proxy
|
||||
|
||||
- the proxy is compressing and buffering live traffic
|
||||
- the proxy is missing WebSocket support
|
||||
|
||||
## Website docs
|
||||
|
||||
- Website version: `packages/docs/content/docs/reverse-proxy.mdx`
|
||||
@@ -0,0 +1,301 @@
|
||||
---
|
||||
title: Reverse Proxy
|
||||
description: Configure OpenChamber correctly behind Nginx, Nginx Proxy Manager, or another reverse proxy.
|
||||
---
|
||||
|
||||
# Reverse Proxy
|
||||
|
||||
Use this page if you run OpenChamber behind Nginx, Nginx Proxy Manager, Caddy, Cloudflare, or another reverse proxy.
|
||||
|
||||
## Before you proxy it
|
||||
|
||||
1. Confirm OpenChamber works directly first.
|
||||
2. Open `http://<server-ip>:3000` or your custom port from the same network.
|
||||
3. Only add the reverse proxy after the direct connection works.
|
||||
|
||||
## What the proxy must support
|
||||
|
||||
- WebSockets for live message transport:
|
||||
- `/api/event/ws`
|
||||
- `/api/global/event/ws`
|
||||
- `/api/terminal/ws`
|
||||
- SSE without buffering:
|
||||
- `/api/event`
|
||||
- `/api/global/event`
|
||||
- `/api/notifications/stream`
|
||||
- `/api/openchamber/events`
|
||||
- `/api/terminal/:sessionId/stream`
|
||||
- Large request bodies for attachments and file operations
|
||||
- Long-lived read timeouts for live streams and terminal sessions
|
||||
|
||||
## Rules that matter
|
||||
|
||||
- Enable WebSocket proxying.
|
||||
- Disable buffering on SSE routes.
|
||||
- Disable gzip on the proxy if OpenChamber is already compressing responses.
|
||||
- Keep compression enabled in only one layer.
|
||||
- Forward normal proxy headers such as `Host`, `X-Forwarded-For`, and `X-Forwarded-Proto`.
|
||||
- Increase body size limits if users upload files.
|
||||
|
||||
## Quick checklist
|
||||
|
||||
- OpenChamber reachable directly on LAN
|
||||
- WebSockets enabled in the proxy
|
||||
- SSE routes have buffering off
|
||||
- `gzip off` on the proxy host, or proxy compression disabled another way
|
||||
- `client_max_body_size` large enough for attachments
|
||||
- `proxy_read_timeout` long enough for streams
|
||||
|
||||
## Example: Nginx
|
||||
|
||||
<details>
|
||||
<summary>Show example config</summary>
|
||||
|
||||
```nginx
|
||||
client_max_body_size 50M;
|
||||
client_body_buffer_size 50M;
|
||||
proxy_request_buffering off;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
gzip off;
|
||||
|
||||
location = /api/terminal/ws {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location = /api/global/event/ws {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location = /api/event/ws {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location ~ ^/api/(event|global/event|notifications/stream|openchamber/events)$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location ~ ^/api/terminal/.+/stream$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Example: Nginx Proxy Manager
|
||||
|
||||
<details>
|
||||
<summary>Show Advanced tab example</summary>
|
||||
|
||||
```nginx
|
||||
client_max_body_size 50M;
|
||||
client_body_buffer_size 50M;
|
||||
proxy_request_buffering off;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
|
||||
gzip off;
|
||||
|
||||
location = /api/terminal/ws {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location = /api/global/event/ws {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location = /api/event/ws {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location = /api/event {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location = /api/global/event {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location = /api/notifications/stream {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location = /api/openchamber/events {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location ~ ^/api/terminal/.+/stream$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_set_header Accept "text/event-stream";
|
||||
proxy_set_header Cache-Control "no-cache";
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
gzip off;
|
||||
add_header X-Accel-Buffering "no" always;
|
||||
add_header Cache-Control "no-cache, no-transform" always;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location /api {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_read_timeout 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
proxy_connect_timeout 30s;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Also enable `Websockets Support` in Nginx Proxy Manager for this host.
|
||||
|
||||
## Common failure signs
|
||||
|
||||
### Page loads, but sending messages fails
|
||||
|
||||
- WebSockets are not enabled in the proxy
|
||||
- `/api/event/ws` or `/api/global/event/ws` is not passing through correctly
|
||||
|
||||
### Notifications or live status do not update
|
||||
|
||||
- one of the SSE routes is buffered or cached
|
||||
- `X-Accel-Buffering "no"` is missing
|
||||
|
||||
### File uploads fail
|
||||
|
||||
- `client_max_body_size` is too small
|
||||
|
||||
### Everything works locally, but breaks only behind the proxy
|
||||
|
||||
- the proxy is compressing and buffering live traffic
|
||||
- the proxy is missing WebSocket support
|
||||
|
||||
## Related
|
||||
|
||||
- [Tunnels](/tunnels/)
|
||||
- [Troubleshooting](/troubleshooting/)
|
||||
@@ -18,6 +18,7 @@
|
||||
{
|
||||
"label": "Help",
|
||||
"items": [
|
||||
{ "label": "Reverse Proxy", "link": "/reverse-proxy/" },
|
||||
{ "label": "Troubleshooting", "link": "/troubleshooting/" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"compression": "^1.8.1",
|
||||
"cron-parser": "^4.9.0",
|
||||
"express": "^5.1.0",
|
||||
"ghostty-web": "0.3.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'reflect-metadata';
|
||||
import express from 'express';
|
||||
import compression from 'compression';
|
||||
import path from 'path';
|
||||
import { spawn, spawnSync } from 'child_process';
|
||||
import fs from 'fs';
|
||||
@@ -95,6 +96,27 @@ const TUNNEL_BOOTSTRAP_TTL_MAX_MS = 24 * 60 * 60 * 1000;
|
||||
const TUNNEL_SESSION_TTL_DEFAULT_MS = 8 * 60 * 60 * 1000;
|
||||
const TUNNEL_SESSION_TTL_MIN_MS = 5 * 60 * 1000;
|
||||
const TUNNEL_SESSION_TTL_MAX_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
function headerIncludesEventStream(value) {
|
||||
if (typeof value === 'string') {
|
||||
return value.toLowerCase().includes('text/event-stream');
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.some((entry) => typeof entry === 'string' && entry.toLowerCase().includes('text/event-stream'));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function shouldSkipCompression(req, res) {
|
||||
if (headerIncludesEventStream(req.headers.accept)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return headerIncludesEventStream(res.getHeader('Content-Type'));
|
||||
}
|
||||
|
||||
const OPENCHAMBER_VERSION = (() => {
|
||||
try {
|
||||
const packagePath = path.resolve(__dirname, '..', 'package.json');
|
||||
@@ -970,6 +992,13 @@ async function main(options = {}) {
|
||||
const app = express();
|
||||
const serverStartedAt = new Date().toISOString();
|
||||
app.set('trust proxy', true);
|
||||
app.use(compression({
|
||||
filter: (req, res) => {
|
||||
if (shouldSkipCompression(req, res)) return false;
|
||||
return compression.filter(req, res);
|
||||
},
|
||||
threshold: 1024,
|
||||
}));
|
||||
expressApp = app;
|
||||
server = http.createServer(app);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user