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
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions