OverviewFilesCommitsIssuesDocsReleasesPeers

7rnx-proxy commits / 2909ec7

per-route HTTP timeouts, so a hung backend cannot take the whole proxy down
`ProxyTimeout 3600` is set on every vhost. It exists for WEBSOCKETS — a firehose or spacedust subscription idles for long stretches and must not be severed — but it also governed ordinary HTTP, so a backend that accepted a connection and never answered held an Apache worker for up to an HOUR per request. On 2026-08-24 that took the entire proxy down. Both favela-web backends restarted simultaneously (autoupdate on both), every request to them hung, and Apache stopped serving EVERY vhost — including ap.7rnx.net, whose backend was healthy the whole time and which I wasted an hour suspecting. Only `systemctl restart apache2` recovered it. Fix: bound the PLAIN HTTP routes and leave WebSockets alone. A WS upgrade is routed by `RewriteRule [P]`, which a ProxyPass timeout does not affect, so the split costs WS nothing: ProxyPass / http://10.22.0.2:4041/ timeout=300 <- was unbounded ProxyTimeout 3600 <- retained, now WS-only RewriteRule ^/git(.*)$ ws://... [P,L] <- untouched 300s is not a new number: it is what the BALANCER path already used, and its rationale is recorded there — 5s was tried and was too aggressive, since a data stall on a large transfer (the pear-git bootstrap tarball) tripped failover into a 502, issue #101. Liveness is a separate concern handled by the TCP health checks, so the request timeout can be generous without slowing failover. Now one constant with one explanation, overridable per host or service with `timeout`. Verified by `7rnx deploy --dry-run`: every ProxyPass carries timeout=300, every ProxyTimeout 3600 and every ws:// RewriteRule is unchanged. 36 tests pass. NOT deployed — `7rnx deploy` reloads Apache on the live proxy and needs sudo there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2909ec7495286d036d2888f9163f1876f460779d
by plankatron <fleeky@gmail.com> · 2026-08-25T00:41:12.000Z

Changed files (2) +26 -4

lib/config-generator.js +25 -3
……@@ -3,6 +3,26 @@
33// registry's input validation — a malformed value must never reach the config
44// that gets deployed, especially now that deploy() rolls back on configtest
55// failure rather than silently shipping garbage.
6// Per-route request timeout in seconds for PLAIN HTTP proxying.
7//
8// ⛔ NOT the same thing as the vhost-level `ProxyTimeout 3600` below, and the split is the point.
9// 3600 exists for WEBSOCKETS — a firehose or spacedust subscription is idle for long stretches and must
10// not be severed. But it also applied to ordinary HTTP, so a backend that accepted a connection and
11// never answered held an Apache worker for up to an HOUR per request.
12//
13// On 2026-08-24 that took the whole proxy down: both favela-web backends restarted at once (autoupdate
14// on both), every request to them hung, and Apache stopped serving EVERY vhost — including ones whose
15// backends were perfectly healthy. Recovering it required a restart.
16//
17// WebSocket upgrades are routed by a `RewriteRule [P]`, which is NOT affected by a ProxyPass timeout,
18// so bounding the ProxyPass costs WS nothing.
19//
20// 300s, matching what the balancer path already used: 5s was tried and was too aggressive — a data
21// stall on a large transfer (the pear-git bootstrap tarball) tripped failover into a 502, issue #101.
22// Liveness is a separate concern, handled by the TCP health checks. Override per host/service with
23// `timeout`.
24const DEFAULT_PROXY_TIMEOUT = 300
25
626function assertSafe (value, kind) {
727 if (typeof value !== 'string' || value.length === 0 || /[\s"'<>\n\r]/.test(value)) {
828 throw new Error(`unsafe ${kind} for Apache config: ${JSON.stringify(value)}`)
……@@ -90,13 +110,15 @@ export function generateVhosts (registry) {
90110 lines.push(' SSLEngine on')
91111 lines.push(' # mod_md supplies the *.7rnx.net certificate for this host.')
92112 lines.push(' ProxyPreserveHost On')
113 // Governs the WEBSOCKET path only, now that the ProxyPass below carries its own timeout: a WS
114 // upgrade goes through the RewriteRule [P] and must be allowed to idle.
93115 lines.push(' ProxyTimeout 3600')
94116 lines.push(' RequestHeader set X-Forwarded-Proto "https"')
95117 lines.push(' RewriteEngine On')
96118 // WebSocket (e.g. com.atproto.sync.subscribeRepos firehose) — must precede ProxyPass.
97119 lines.push(' RewriteCond %{HTTP:Upgrade} =websocket [NC]')
98120 lines.push(` RewriteRule ^/(.*)$ ws://${node.ip}:${h.port}/$1 [P,L]`)
99 lines.push(` ProxyPass / ${backend}/`)
121 lines.push(` ProxyPass / ${backend}/ timeout=${h.timeout ? Number(h.timeout) : DEFAULT_PROXY_TIMEOUT}`)
100122 lines.push(` ProxyPassReverse / ${backend}/`)
101123 lines.push(` ErrorLog \${APACHE_LOG_DIR}/7rnx-${h.name}-error.log`)
102124 lines.push(` CustomLog \${APACHE_LOG_DIR}/7rnx-${h.name}-access.log combined`)
……@@ -149,7 +171,7 @@ export function generateConfig (registry) {
149171 // separately by the TCP health checks below (a dead member is marked down in
150172 // ~hcinterval*hcfails = ~6s), so the request timeout can be generous without
151173 // slowing failover. Override per service with `svc.timeout`.
152 const memberTimeout = svc.timeout ? Number(svc.timeout) : 300
174 const memberTimeout = svc.timeout ? Number(svc.timeout) : DEFAULT_PROXY_TIMEOUT
153175
154176 lines.push(`# === ${svc.name} (balancer) ===`)
155177 lines.push('')
……@@ -229,7 +251,7 @@ export function generateConfig (registry) {
229251 const backend = `http://${node.ip}:${svc.port}`
230252
231253 lines.push(`<Location ${urlPathClean}>`)
232 lines.push(` ProxyPass ${backend}`)
254 lines.push(` ProxyPass ${backend} timeout=${svc.timeout ? Number(svc.timeout) : DEFAULT_PROXY_TIMEOUT}`)
233255 lines.push(` ProxyPassReverse ${backend}`)
234256 lines.push(' ProxyPreserveHost On')
235257 lines.push(' RequestHeader set X-Forwarded-Proto "https"')
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "7rnx-proxy",
3 "version": "1.3.1",
3 "version": "1.3.2",
44 "description": "Reverse proxy management CLI for 7rnx.net Apache2 server",
55 "type": "module",
66 "bin": {