Web server sends "Connection: Keep-Alive" but closes every connection — breaks reverse proxies that reuse connections

UrBackup Server: 2.5.38 (Docker, binhex/arch-urbackup, urbackup2-server 2.5.38-1)
Front end: Zoraxy 3.3.4 reverse proxy → built-in HTTP server on port 55414

Symptom

Behind a reverse proxy, the web UI loads only partly or hangs at login: some JS/CSS files (often sjcl and md5, which login needs) fail with 502. Going straight to http://<server>:55414 works. Backups are not affected.

Cause

Every response from the built-in HTTP server says the connection stays open:

HTTP/1.1 200 ok
Server: CS
Connection: Keep-Alive
Keep-Alive: timeout=15, max=95
Content-Length: ...

The server then closes the connection after that one response. Both are visible in the source:

  • httpserver/HTTPFile.cpp hard-codes the Connection: Keep-Alive / Keep-Alive: timeout=15, max=95 header.
  • In httpserver/HTTPClient.cpp (CHTTPClient::Run), the code that moves to HTTP_STATE_KEEPALIVE is commented out. After each request the state becomes HTTP_STATE_DONE, so the socket is closed.

A client that believes the header sends its next request on the same connection. If that request goes out before the client sees the FIN, the server reads it and closes without answering. Browsers connecting directly mostly recover because they retry the request silently. A proxy usually can’t retry a request it has already sent, so it returns 502 for that file.

Evidence (packet capture between proxy and port 55414, one page load)

  • Proxy sent 42 GET requests (the UI’s JS/CSS) and got 30 responses.
  • All 12 failed connections look the same: first request answered with Connection: Keep-Alive, the proxy reuses the connection about 0.04 ms later, and the server sends FIN after about 0.75 ms without responding.
  • The server sent no RSTs, and its listen backlog never overflowed. The connections are closed on purpose, not refused.
  • Another client sending Connection: close still gets Connection: Keep-Alive back (also seen with --http1.0).

Failure rate through the proxy, 8 runs of the full asset set:

Browser to proxy Proxy reuses connections to UrBackup Failed
HTTP/2 yes 94 / 336
HTTP/1.1 yes 0 / 336
HTTP/2 no (new connection per request) 0 / 336

Suggested fix (either one)

  1. Send Connection: close (and drop Keep-Alive:) while keep-alive is disabled, so the headers match what the server does. Clients and proxies then open a new connection per request and nothing is lost.
  2. Or re-enable the keep-alive path in CHTTPClient::Run.

Option 1 is a one-line header change in HTTPFile.cpp. Other response paths should be checked for the same header.

Workaround for proxy users

Stop the proxy from reusing connections to port 55414:

  • nginx: before 1.29.7 nginx used HTTP/1.0 to upstreams, which doesn’t reuse connections. 1.29.7 switched to keep-alive by default ( Keep-alive to upstreams is now default in NGINX 1.29.7 – NGINX Community Blog ), so newer versions may be affected. I haven’t tested that.
  • Zoraxy: ticking “Force HTTP/1.1” on the route fixed it. It only works because that setting happens to stop connection reuse.

Alternatively, use the documented setup from the admin manual: the proxy serves the static files and forwards only /x to FastCGI on port 55413.

Related: the podman/pasta truncation issue ( pasta: HTTP downloads get truncated (UrBackup web UI). slirp4netns works fine. · Issue #27179 · podman-container-tools/podman · GitHub ) showed the same send-then-close behaviour from this server. The bug there was in pasta, but this server’s behaviour is what made it show up.

This would be the proper way to use it, yes.