Farhan Beg commited on
Commit
1f29b0d
Β·
1 Parent(s): be5c4d4

fix(proxy): buffer request bodies to avoid chunked Transfer-Encoding, add HERMES_WEBUI_TRUST_FORWARDED_HOST=1

Browse files

Two fixes for WebUI behind the Node.js reverse proxy:

1. HTTP 501 Unsupported method error: Python BaseHTTPRequestHandler
cannot decode chunked request bodies (Transfer-Encoding: chunked).
Node.js http.request may use chunked encoding when piping req
streams. Fix: buffer the full request body and send it with an
explicit Content-Length header.

2. Cross-origin CSRF mismatch: hermes-webui no longer trusts
X-Forwarded-Host by default. The proxy sets this header, so
browser Origin vs Host checks fail. Fix: set
HERMES_WEBUI_TRUST_FORWARDED_HOST=1 in Dockerfile ENV.

Files changed (2) hide show
  1. Dockerfile +1 -0
  2. health-server.js +79 -1
Dockerfile CHANGED
@@ -180,6 +180,7 @@ ENV HERMES_HOME=/opt/data \
180
  HUGGINGMES_APP_DIR=/opt/huggingmes \
181
  HERMES_WEBUI_REPO=/opt/hermes-webui \
182
  HERMES_AGENT_VERSION=${HERMES_AGENT_VERSION} \
 
183
  PYTHONUNBUFFERED=1 \
184
  HF_HUB_ENABLE_HF_TRANSFER=1 \
185
  PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
 
180
  HUGGINGMES_APP_DIR=/opt/huggingmes \
181
  HERMES_WEBUI_REPO=/opt/hermes-webui \
182
  HERMES_AGENT_VERSION=${HERMES_AGENT_VERSION} \
183
+ HERMES_WEBUI_TRUST_FORWARDED_HOST=1 \
184
  PYTHONUNBUFFERED=1 \
185
  HF_HUB_ENABLE_HF_TRANSFER=1 \
186
  PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium
health-server.js CHANGED
@@ -303,6 +303,62 @@ function proxyRequest(
303
  "x-forwarded-proto": req.headers["x-forwarded-proto"] || "https",
304
  };
305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  const proxy = http.request(
307
  {
308
  hostname: GATEWAY_HOST,
@@ -438,7 +494,29 @@ function proxyDashboard(req, res) {
438
  res.end(JSON.stringify({ error: "proxy_error", message: error.message }));
439
  });
440
 
441
- req.pipe(upstream);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  }
443
 
444
  /* ── Status JSON + HuggingMes status page ─────────────────────────── */
 
303
  "x-forwarded-proto": req.headers["x-forwarded-proto"] || "https",
304
  };
305
 
306
+ // Python's BaseHTTPRequestHandler (used by hermes-webui and the dashboard)
307
+ // cannot decode chunked request bodies β€” read_body() only reads via
308
+ // Content-Length, and leftover chunk framing corrupts subsequent requests
309
+ // on keep-alive connections (HTTP 501 with junk prepended to the method).
310
+ // Buffer the full body and send it with an explicit Content-Length header
311
+ // so Node.js never uses Transfer-Encoding: chunked.
312
+ const hasBody = req.method === "POST" || req.method === "PUT" || req.method === "PATCH";
313
+ if (hasBody) {
314
+ const chunks = [];
315
+ let size = 0;
316
+ const limit = 20 * 1024 * 1024;
317
+ req.on("data", (chunk) => {
318
+ chunks.push(chunk);
319
+ size += chunk.length;
320
+ if (size > limit) {
321
+ req.destroy();
322
+ if (!res.headersSent) {
323
+ res.writeHead(413, { "content-type": "application/json" });
324
+ res.end(JSON.stringify({ error: "payload_too_large" }));
325
+ }
326
+ }
327
+ });
328
+ req.on("end", () => {
329
+ delete headers["transfer-encoding"];
330
+ headers["content-length"] = String(size);
331
+ const proxy = http.request(
332
+ {
333
+ hostname: GATEWAY_HOST,
334
+ port: targetPort,
335
+ method: req.method,
336
+ path: targetPath,
337
+ headers,
338
+ },
339
+ (upstream) => {
340
+ res.writeHead(upstream.statusCode || 502, upstream.headers);
341
+ upstream.pipe(res);
342
+ },
343
+ );
344
+ proxy.on("error", (error) => {
345
+ if (!res.headersSent) {
346
+ res.writeHead(502, { "content-type": "application/json" });
347
+ res.end(JSON.stringify({ error: "proxy_error", message: error.message }));
348
+ }
349
+ });
350
+ if (size > 0) proxy.write(Buffer.concat(chunks));
351
+ proxy.end();
352
+ });
353
+ req.on("error", (error) => {
354
+ if (!res.headersSent) {
355
+ res.writeHead(502, { "content-type": "application/json" });
356
+ res.end(JSON.stringify({ error: "proxy_error", message: error.message }));
357
+ }
358
+ });
359
+ return;
360
+ }
361
+
362
  const proxy = http.request(
363
  {
364
  hostname: GATEWAY_HOST,
 
494
  res.end(JSON.stringify({ error: "proxy_error", message: error.message }));
495
  });
496
 
497
+ // Buffer body before forwarding β€” same chunked-encoding fix as proxyRequest.
498
+ const hasBody = req.method === "POST" || req.method === "PUT" || req.method === "PATCH";
499
+ if (hasBody) {
500
+ const bodyChunks = [];
501
+ let bodySize = 0;
502
+ req.on("data", (chunk) => {
503
+ bodyChunks.push(chunk);
504
+ bodySize += chunk.length;
505
+ });
506
+ req.on("end", () => {
507
+ delete headers["transfer-encoding"];
508
+ headers["content-length"] = String(bodySize);
509
+ upstream.end(Buffer.concat(bodyChunks));
510
+ });
511
+ req.on("error", (error) => {
512
+ if (!res.headersSent) {
513
+ res.writeHead(502, { "content-type": "application/json" });
514
+ res.end(JSON.stringify({ error: "proxy_error", message: error.message }));
515
+ }
516
+ });
517
+ } else {
518
+ req.pipe(upstream);
519
+ }
520
  }
521
 
522
  /* ── Status JSON + HuggingMes status page ─────────────────────────── */