Skip to content

LoadProhibited in AsyncMiddlewareChain::_runChain when setCloseClientOnQueueFull closes a client during concurrent HTTP request #433

@salekseev

Description

@salekseev

Summary

LoadProhibited panic inside AsyncMiddlewareChain::_runChainstd::_Function_handler::_M_manager when setCloseClientOnQueueFull(true) force-closes an AsyncWebSocketClient while a concurrent HTTP request on the same AsyncClient is mid-flight in the middleware chain.

Crash signature

E async_ws: [<url>][<id>] Too many messages queued: closing connection
Guru Meditation Error: Core 0 panic'ed (LoadProhibited). Exception was unhandled.
EXCVADDR: 0x00000014   EXCCAUSE: 0x1c
PC: std::_Function_handler<void (), AsyncMiddlewareChain::_runChain(...)::{lambda()#1}>::_M_manager
  (inlined) std_function.h:161 _M_create<...>
  (inlined) std_function.h:215 _M_init_functor<...>
  (inlined) std_function.h:198/282 _M_manager

Backtrace frames #0..#2:
  AsyncMiddlewareChain::_runChain lambda _M_manager
  std::_List_iterator<AsyncMiddleware*>::operator++(int)
    (inlined) Middleware.cpp:67 operator()
  AsyncMiddlewareChain::_runChain at Middleware.cpp:56

EXCVADDR = 0x14 ≈ offset of _M_manager in std::function's erased storage → the next std::function the outer chain is about to re-invoke has been torn down (zero-initialized _M_manager).

Mechanism

  1. Same AsyncClient holds both an active HTTP request and a WS upgrade.

  2. Server publishes WS frames faster than the slow client drains, _messageQueue.size() >= WS_MAX_QUEUED_MESSAGES.

  3. AsyncWebSocketClient::_queueMessage with closeWhenFull == true (the default) calls _client->close() synchronously — the comment at AsyncWebSocket.cpp:489–494 itself documents the reentrant chain:

    _client->close() shall call the callback function _onDisconnect()_onDisconnect() → _handleDisconnect() → ~AsyncWebSocketClient().

  4. Meanwhile, an HTTP handler on the same client is inside the nested chain at WebRequest.cpp:851–865:

    _server->_runChain(this, [this]() {
        if (_handler) {
            _handler->_runChain(this, [this]() {
                _handler->handleRequest(this);
            });
        }
    });
  5. _runChain (Middleware.cpp:56–70) captures its next std::function and it iterator by reference into the per-step lambda. The synchronous teardown initiated from (3) frees request/handler state while the outer next() in step (4) is still about to invoke the inner chain. The next evaluation reads a torn std::function (_M_manager = nullptr) and faults at offset 0x14.

Is this already fixed?

No. Adjacent work but not this path:

None of the above protect the HTTP middleware path against synchronous client teardown initiated from _queueMessage.

Reproduction

  1. ws.onEvent(...) sets client->setCloseClientOnQueueFull(true).
  2. Publish small WS frames via ws.textAll(...) at a rate the client cannot drain (e.g. status poll every 250 ms to a slow Wi-Fi client).
  3. Issue concurrent HTTP requests (e.g. /api/status) against the same server.
  4. When the WS queue fills and the lib force-closes the client, any request whose nested _runChain is still unwinding faults.

Observed on: ESPAsyncWebServer v3.10.3, AsyncTCP v3.4.10, arduino-esp32 3.3.x / IDF 5.5.4, ESP32-S3.

Suggested fix

Don't tear the client down synchronously from _queueMessage. Defer to cleanupClients() / the next async-tcp event tick — the same direction #424 proposed for disconnect cleanup — so that no HTTP handler running on the same AsyncClient can be unwound while its stack frames are still live.

A minimal first-order fix inside _queueMessage: mark the client for async close (e.g. set a flag + notify cleanup task) instead of invoking _client->close() directly. cleanupClients() already runs on a safe context.

Workaround for users hitting this today

client->setCloseClientOnQueueFull(false) at WS_EVT_CONNECT — excess frames are silently dropped instead of destroying the client. Requires producer-side rate limiting to avoid unbounded queue growth.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions