* 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
@@ -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