fix(http): keep reaping open TLS connections under low heap so the heap can recover (#11539)

* fix(http): keep reaping open TLS connections under low heap so the heap can recover

Once free heap dropped below MIN_HEAP_FOR_SSL (40 KB) with HTTPS connections
open, the node's heap never came back and every later HTTPS or TCP-API
connection failed until a reset - node alive, on WiFi, unusable.

handleWebResponse() skipped secureServer->loop() entirely under low heap so no
new TLS handshake would be attempted on a heap that can't hold its context.
But HTTPServer::loop() is the only place already-accepted connections are
serviced and reaped: its first pass calls ->loop() on each open one (where the
20 s idle timeout and the SSL close-notify state machine run) and deletes the
closed ones. Skipping the whole loop froze the up-to-MAX_HTTPS_CONNECTIONS TLS
sessions already open. Never looped, they never timed out, their mbedTLS
contexts and pbufs were never freed, so free heap never climbed back over
40 KB, so the loop was skipped forever. The guard's own precondition was what
kept it from clearing.

Split the two halves. Under low heap keep driving and reaping the connections
we already hold, and only skip the accept. HTTPServer keeps its connection
table protected, so a thin MeshHTTPSServer subclass exposes
serviceExistingConnections(), the first half of HTTPServer::loop() verbatim.
Log line reworded to say what now happens: not accepting, not skipping.

Verified on a Heltec V3 (Endor AP) against a control build with #11537 (so the
node survives the squeeze instead of aborting first):

- Recipe: held sockets on 80/4403 + pending TLS, 100 s of HTTPS pokes, repeat.
  Control: Low heap pins at 6-17 KB, HTTPS dead, and 3 min after all pressure
  is released heap is still ~12 KB with Low heap firing every 30 s - permanent
  until reset. Fix: never dips under 40 KB, both pressure rounds 3/3, 65 KB
  after.
- Branch driven deliberately (verify-only heap hog pinning free heap at ~28 KB
  with a real idle TLS session held open): under the guard the fix logs
  open=1 -> reaped=1 at the 20 s idle timeout, and heap goes 26 -> 65 KB
  before the hog is even released. On the control logic that session stays
  frozen for the whole window.

Fixes #11538.

* fix(http): trim the low-heap comments to the two-line guideline

The mechanism is in the commit message and PR; the source keeps the one-line
why. No code change. (CodeRabbit)
This commit is contained in:
Ben Meadors
2026-08-18 20:38:20 +00:00
committed by GitHub
parent 692adc8131
commit 48699a7a48
+31 -4
View File
@@ -62,8 +62,33 @@ static const uint8_t MAX_HTTPS_CONNECTIONS = 2;
// Minimum free heap required for SSL handshake (~40KB for mbedTLS contexts)
static const uint32_t MIN_HEAP_FOR_SSL = 40000;
// HTTPSServer that can service and reap the connections it already holds without accepting new ones,
// so a low-heap pause doesn't freeze open TLS sessions (and their heap) in place. Needs the protected table.
class MeshHTTPSServer : public HTTPSServer
{
public:
using HTTPSServer::HTTPSServer;
/// The first half of HTTPServer::loop(): drive and reap existing connections, accept nothing.
void serviceExistingConnections()
{
if (!_running)
return;
for (uint8_t i = 0; i < _maxConnections; i++) {
if (!_connections[i])
continue;
if (_connections[i]->isClosed()) {
delete _connections[i];
_connections[i] = nullptr;
} else {
_connections[i]->loop();
}
}
}
};
static SSLCert *cert;
static HTTPSServer *secureServer;
static MeshHTTPSServer *secureServer;
static HTTPServer *insecureServer;
volatile bool isWebServerReady;
@@ -80,10 +105,12 @@ static void handleWebResponse()
if (freeHeap >= MIN_HEAP_FOR_SSL) {
secureServer->loop();
} else {
// Skip HTTPS when memory is low to prevent SSL setup failures
// Low heap: accept nothing new, but keep servicing open connections so they can time out
// and free their contexts - skipping them pins the heap below the threshold for good.
secureServer->serviceExistingConnections();
static uint32_t lastHeapWarning = 0;
if (lastHeapWarning == 0 || !Throttle::isWithinTimespanMs(lastHeapWarning, 30000)) {
LOG_WARN("Low heap (%u bytes), skipping HTTPS processing", freeHeap);
LOG_WARN("Low heap (%u bytes), not accepting HTTPS connections", freeHeap);
lastHeapWarning = millis();
}
}
@@ -231,7 +258,7 @@ void initWebServer()
LOG_DEBUG("Init Web Server");
// We can now use the new certificate to setup our server as usual.
secureServer = new HTTPSServer(cert, 443, MAX_HTTPS_CONNECTIONS);
secureServer = new MeshHTTPSServer(cert, 443, MAX_HTTPS_CONNECTIONS);
insecureServer = new HTTPServer();
registerHandlers(insecureServer, secureServer);