From 9b832bcaa1fe2a71f41467ad3c4ae64b3aefed83 Mon Sep 17 00:00:00 2001 From: Mathieu Carbou Date: Mon, 12 Oct 2026 21:35:03 +0200 Subject: [PATCH 1/2] feat: user-controlled handler allocation (non-owning registration) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Give users full control over where handler objects live in memory (internal SRAM, PSRAM, static RAM) without allocator hooks or operator new overrides. Changes: - asyncsrv::MaybeOwned: tagged pointer tracking ownership in a single machine word (1 pointer instead of 2 — halves per-entry SRAM usage). Low bit = ownership flag. adopt() for owning, borrow() for non-owning. - std::list> _handlers — unified list replacing the old unique_ptr list. - asyncsrv::MaybeOwned _catchAllHandler — catch-all uses the same ownership mechanism (no separate bool flag). - addHandler(ptr) — server takes ownership (backward compatible) - addHandler(ref) — non-owning borrow (reference = borrow in C++) - AsyncWebServer(port, catchAllPtr) — custom server-owned catch-all - AsyncWebServer(port, catchAllRef) — custom non-owning catch-all - setCatchAllHandler(ptr) — replace with server-owned catch-all - setCatchAllHandler(ref) — replace with non-owning catch-all - Catch-all invariant preserved: always installed, never nullptr (assert). catchAllHandler() restored without deprecation — always safe to call. - removeHandler(ptr) — removes from unified list, deletes if owned - New CustomAllocation example: static RAM, PSRAM, server-owned patterns - Updated docs: routing.md, configuration.md, concepts.md Fully backward compatible: all existing APIs work unchanged. - server.addHandler(new T()) — unchanged behavior - server.addHandler(&staticObj) — unchanged behavior - server.on() / serveStatic() / catchAllHandler() / onNotFound() — unchanged - No existing examples need modification (SkipServerMiddleware untouched) Motivation: ESP32 internal SRAM is scarce (lwIP, WiFi buffers). Long-lived handler objects can now be moved to PSRAM or static RAM, freeing internal SRAM for networking. See discussion in #468. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/concepts.md | 4 + docs/configuration.md | 52 ++++ docs/routing.md | 131 +++++++++ .../CustomAllocation/CustomAllocation.ino | 145 +++++++++ platformio.ini | 1 + src/ESPAsyncWebServer.h | 275 +++++++++++++++++- src/WebServer.cpp | 73 +++-- 7 files changed, 654 insertions(+), 27 deletions(-) create mode 100644 examples/arduino/CustomAllocation/CustomAllocation.ino diff --git a/docs/concepts.md b/docs/concepts.md index e92f7aae4..cc2905163 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -75,6 +75,10 @@ flowchart TD - `Handlers` are evaluated in the order they are attached to the server. The `canHandle` is called only if the `Filter` that was set to the `Handler` return true. - The first `Handler` that can handle the request is selected, not further `Filter` and `canHandle` are called. +- A `Handler` registered with `addHandler(ptr)` is **owned by the server** (deleted on removal or server destruction). + A `Handler` registered with `addHandler(ref)` is **non-owning** (borrow) — the caller manages + its lifetime. This lets you place handlers in static RAM, PSRAM, or any custom memory region. + See [Routing — Handler Ownership and Memory Placement](routing.md#handler-ownership-and-memory-placement). ### Responses and how do they work diff --git a/docs/configuration.md b/docs/configuration.md index 3eab09482..728d245a7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -38,3 +38,55 @@ Recommended configurations: - You can not use yield or delay or any function that uses them inside the callbacks - The server is smart enough to know when to close the connection and free resources - You can not send more than one response to a single request + +## Memory placement: PSRAM and static allocation + +On ESP32 targets with PSRAM, internal SRAM is often the scarcest resource (lwIP segments, WiFi buffers, and other networking allocations all live there). Handler objects — `AsyncCallbackWebHandler`, `AsyncStaticWebHandler`, `AsyncWebSocket`, `AsyncEventSource`, `AsyncCallbackJsonWebHandler` — are long-lived: they are created once at setup and only read during request dispatch. Moving them out of internal SRAM can free significant memory. + +By default, handlers created by `server.on()` or `server.serveStatic()` are allocated on the internal heap and owned by the server. To control where handler objects live, register them as **non-owning**: + +> [!WARNING] +> When borrowing a handler by reference, do not use a stack-allocated (automatic) object unless it outlives the server. In typical Arduino sketches, a local variable inside `setup()` is destroyed when `setup()` returns — always use `static` or global storage for borrowed handlers. + +```cpp +// Static RAM — no heap allocation at all +static AsyncCallbackWebHandler handler; +handler.setUri(AsyncURIMatcher::exact("/status")); +handler.onRequest([](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); +}); +server.addHandler(handler); // non-owning reference overload +``` + +```cpp +// PSRAM — placement new with heap_caps_malloc (ESP32) +#include + +auto *handler = new (heap_caps_malloc(sizeof(AsyncCallbackWebHandler), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)) AsyncCallbackWebHandler(); +handler->setUri(AsyncURIMatcher::exact("/status")); +handler->onRequest([](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); +}); +server.addHandler(*handler); // non-owning borrow — manage lifetime yourself +``` + +The same applies to the catch-all (not-found) handler — either at construction time or afterwards: + +```cpp +// At construction — non-owning borrow, lives in static RAM +static AsyncCallbackWebHandler myCatchAll; +AsyncWebServer server(80, myCatchAll); + +// At construction — non-owning borrow from PSRAM +AsyncWebServer server(80, *psramCatchAll); + +// After construction +server.setCatchAllHandler(myCatchAllHandler); // non-owning borrow +server.setCatchAllHandler(*psramCatchAll); // non-owning borrow from PSRAM +server.setCatchAllHandler(new AsyncCallbackWebHandler()); // server-owned +``` + +> [!NOTE] +> When using non-owning registration, you are responsible for keeping the handler alive while it is registered with the server, and for destroying it after unregistration. For PSRAM handlers, remember to call the destructor explicitly and then `heap_caps_free()` the memory block. + +See the [CustomAllocation example](https://github.com/ESP32Async/ESPAsyncWebServer/blob/master/examples/arduino/CustomAllocation/CustomAllocation.ino) and the [Routing](routing.md#handler-ownership-and-memory-placement) documentation for more details. diff --git a/docs/routing.md b/docs/routing.md index e254916f7..2bde77b81 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -123,6 +123,137 @@ Usage: See the [Rewrite example here](https://github.com/ESP32Async/ESPAsyncWebServer/blob/master/examples/arduino/Rewrite/Rewrite.ino). +## Handler Ownership and Memory Placement + +By default, handlers created by `server.on()`, `server.serveStatic()`, or passed to `server.addHandler(new MyHandler())` are **owned by the server** — it deletes them on `removeHandler()`, `reset()`, or destruction. + +When you need to control **where** a handler object lives in memory (e.g. to use PSRAM on ESP32, or to avoid heap allocation entirely), register it as **non-owning**: + +| Registration | Ownership | Use case | +|---|---|---| +| `server.addHandler(new MyHandler())` | Server owns | Default, backward compatible | +| `server.addHandler(myHandler)` | Non-owning (borrow) | Static or global object (see warning below about stack) | +| `server.addHandler(*psramHandler)` | Non-owning (borrow) | PSRAM via placement new | + +With non-owning registration, **you** are responsible for keeping the handler alive while it is registered, and for destroying it after unregistration. + +> [!TIP] +> For objects that live for the entire lifetime of the application (like handlers), static or global allocation is preferable over heap allocation. It results in simpler code, avoids heap fragmentation, and requires no lifetime tracking overhead. + +> [!WARNING] +> Borrowing a **stack-allocated** object is risky unless you have architected your program to keep it in scope for as long as the server uses it. The usual Arduino-style `setup()` / `loop()` pattern does **not** guarantee this — a handler declared as a local variable inside `setup()` would be destroyed when `setup()` returns, leaving the server with a dangling pointer. Use `static` or global storage instead. + +### Static / Global RAM (no heap allocation) + +```cpp +static AsyncWebServer server(80); + +// Lives in .bss — zero heap usage +static AsyncCallbackWebHandler statusHandler; + +void setup() { + statusHandler.setUri(AsyncURIMatcher::exact("/status")); + statusHandler.onRequest([](AsyncWebServerRequest *request) { + request->send(200, "application/json", "{\"ok\":true}"); + }); + server.addHandler(statusHandler); // non-owning reference overload + server.begin(); +} +``` + +### PSRAM (ESP32) + +There are two ways to place a handler in PSRAM: + +**1. Placement new with `heap_caps_malloc`:** + +```cpp +#include + +void setup() { + auto *handler = new (heap_caps_malloc(sizeof(AsyncCallbackWebHandler), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)) AsyncCallbackWebHandler(); + handler->setUri(AsyncURIMatcher::exact("/status")); + handler->onRequest([](AsyncWebServerRequest *request) { + request->send(200, "application/json", "{\"ok\":true}"); + }); + server.addHandler(*handler); // non-owning borrow — manage PSRAM lifetime yourself + server.begin(); +} +``` + +**2. Static object in PSRAM via `EXT_RAM_BSS_ATTR`** (requires `CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y`, typically only available when using Arduino as an ESP-IDF component): + +```cpp +EXT_RAM_BSS_ATTR static AsyncCallbackWebHandler statusHandler; + +void setup() { + statusHandler.setUri(AsyncURIMatcher::exact("/status")); + statusHandler.onRequest([](AsyncWebServerRequest *request) { + request->send(200, "application/json", "{\"ok\":true}"); + }); + server.addHandler(statusHandler); // non-owning — handler lives in PSRAM .bss + server.begin(); +} +``` + +> [!NOTE] +> On ESP32, `delete` works on objects allocated with `heap_caps_malloc()` because `delete` maps to `free()` which maps to `heap_caps_free()`. This is specific to Espressif's implementation (GNU libstdc++ + ESP-IDF libc) and is not guaranteed on all platforms. + +### Works with All Handler Types + +The same registration pattern applies to any `AsyncWebHandler` subclass: `AsyncCallbackWebHandler`, `AsyncStaticWebHandler`, `AsyncWebSocket`, `AsyncEventSource`, `AsyncCallbackJsonWebHandler`, or your own custom subclass. + +```cpp +static AsyncWebSocket ws("/ws"); +ws.onEvent([](AsyncWebSocket *s, AsyncWebSocketClient *c, AwsEventType t, void *a, uint8_t *d, size_t l) { + if (t == WS_EVT_DATA) s->text(c->id(), "echo"); +}); +server.addHandler(ws); // non-owning — ws lives in static RAM +``` + +### Custom Catch-All Handler + +A catch-all (not-found) handler is **always installed**. It can be customized in two ways: + +**At construction time** — pass your handler as a second argument to the `AsyncWebServer` constructor: + +| Constructor | Ownership | +|---|---| +| `AsyncWebServer server(80)` | Server creates & owns a default catch-all | +| `AsyncWebServer server(80, new AsyncCallbackWebHandler())` | Server owns | +| `AsyncWebServer server(80, myHandler)` | Non-owning (borrow by reference) | +| `AsyncWebServer server(80, *psramHandler)` | Non-owning (borrow from PSRAM) | + +```cpp +static AsyncCallbackWebHandler catchAll; + +void setup() { + catchAll.onRequest([](AsyncWebServerRequest *request) { + request->send(404, "text/plain", "Not found: " + request->url()); + }); + // Pass custom catch-all at construction — non-owning, lives in static RAM + AsyncWebServer server(80, catchAll); + server.begin(); +} +``` + +**After construction** — replace it with `setCatchAllHandler()`: + +| Registration | Ownership | +|---|---| +| `server.setCatchAllHandler(new AsyncCallbackWebHandler())` | Server owns | +| `server.setCatchAllHandler(myHandler)` | Non-owning (borrow by reference) | +| `server.setCatchAllHandler(*psramHandler)` | Non-owning (borrow from PSRAM) | + +```cpp +server.setCatchAllHandler(catchAll); // non-owning — catchAll lives in static RAM +``` + +> [!NOTE] +> A catch-all handler is always required — passing `nullptr` to the constructor or `setCatchAllHandler()` is a programming error (asserts in debug builds). `catchAllHandler()` safely returns a reference to the installed catch-all. + +See the [CustomAllocation example](https://github.com/ESP32Async/ESPAsyncWebServer/blob/master/examples/arduino/CustomAllocation/CustomAllocation.ino) for a complete sketch. + ## Remove handlers and rewrites Server goes through handlers in same order as they were added. You can't simple add handler with same path to override them. diff --git a/examples/arduino/CustomAllocation/CustomAllocation.ino b/examples/arduino/CustomAllocation/CustomAllocation.ino new file mode 100644 index 000000000..ae0ea3b90 --- /dev/null +++ b/examples/arduino/CustomAllocation/CustomAllocation.ino @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles + +// +// Shows how to control where handler objects are allocated. +// +// The server offers two registration styles: +// - addHandler(ptr) — server owns the handler (backward compatible) +// - addHandler(ref) — non-owning borrow, the user manages the handler's lifetime +// +// With non-owning registration you can place handlers in: +// - Static / global RAM (no heap allocation at all) +// - PSRAM (ESP32: placement new with heap_caps_malloc) +// +// The same applies to the catch-all handler — via the constructor or setCatchAllHandler(). +// + +#include +#if defined(ESP32) || defined(LIBRETINY) +#include +#include +#if defined(ESP32) +#include +#endif +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include +#include +#endif + +#include +#include + +// --------------------------------------------------------------------------- +// Custom catch-all handler — static RAM, non-owning. +// Passed to the server constructor so it's used from the very first request. +// --------------------------------------------------------------------------- +static AsyncCallbackWebHandler catchAllHandler; + +static AsyncWebServer server(80, catchAllHandler); // non-owning reference + +// --------------------------------------------------------------------------- +// 1. Static RAM handler — allocated in .bss, zero heap usage +// --------------------------------------------------------------------------- +static AsyncCallbackWebHandler statusHandler; + +// --------------------------------------------------------------------------- +// 2. PSRAM handler (ESP32 only) — allocated via placement new in PSRAM +// --------------------------------------------------------------------------- +#if defined(ESP32) +static AsyncCallbackWebHandler *apiHandler = nullptr; +static void *apiHandlerMem = nullptr; +#endif + +// --------------------------------------------------------------------------- +// 3. Server-owned handler — classic style, server manages lifetime +// --------------------------------------------------------------------------- +// (see server.on() below) + +void setup() { + Serial.begin(115200); + while (!Serial) + ; + +#if ASYNCWEBSERVER_WIFI_SUPPORTED + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-custom-alloc"); +#endif + + if (!LittleFS.begin()) { + Serial.println("LittleFS mount failed"); + } + + // --- 1. Static RAM handler ------------------------------------------------ + // No heap allocation — the object lives in .bss for the entire program. + statusHandler.setUri(AsyncURIMatcher::exact("/status")); + statusHandler.setMethod(HTTP_GET); + statusHandler.onRequest([](AsyncWebServerRequest *request) { +#if defined(ESP32) + String json = "{\"uptime\":" + String(millis()) + ",\"free_heap\":" + String(ESP.getFreeHeap()) + "}"; +#else + String json = "{\"uptime\":" + String(millis()) + "}"; +#endif + request->send(200, "application/json", json); + }); + server.addHandler(statusHandler); // non-owning reference overload + + // --- 2. PSRAM handler (ESP32 only) ---------------------------------------- +#if defined(ESP32) + if (psramFound()) { + apiHandlerMem = heap_caps_malloc(sizeof(AsyncCallbackWebHandler), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + if (apiHandlerMem) { + apiHandler = new (apiHandlerMem) AsyncCallbackWebHandler(); + apiHandler->setUri(AsyncURIMatcher::prefix("/api")); + apiHandler->setMethod(HTTP_GET); + apiHandler->onRequest([](AsyncWebServerRequest *request) { + request->send(200, "application/json", "{\"ok\":true,\"region\":\"psram\"}"); + }); + // Non-owning borrow: the server stores a reference; we manage PSRAM lifetime. + server.addHandler(*apiHandler); + Serial.printf("API handler allocated in PSRAM at %p (%u bytes free in PSRAM)\n", + apiHandlerMem, ESP.getFreePsram()); + } else { + Serial.println("PSRAM allocation failed for API handler"); + } + } else { + Serial.println("No PSRAM found — skipping PSRAM handler demo"); + } +#endif + + // --- 3. Server-owned handler (classic convenience API) -------------------- + server.on("/hello", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "Hello from a server-owned handler!"); + }); + + // --- Static file serving (server-owned) ----------------------------------- + server.serveStatic("/static", LittleFS, "/www"); + + // --- Configure the custom catch-all (passed to constructor above) ---------- + catchAllHandler.onRequest([](AsyncWebServerRequest *request) { + String msg = "404 - Not found: " + request->url(); + request->send(404, "text/plain", msg); + }); + + server.begin(); + Serial.println("HTTP server started"); +#if ASYNCWEBSERVER_WIFI_SUPPORTED + Serial.printf("Connect to AP 'esp-custom-alloc' and open http://%s\n", + WiFi.softAPIP().toString().c_str()); +#endif + Serial.println("Try:"); + Serial.println(" http://192.168.4.1/status (static RAM handler)"); +#if defined(ESP32) + Serial.println(" http://192.168.4.1/api/test (PSRAM handler, ESP32 only)"); +#endif + Serial.println(" http://192.168.4.1/hello (server-owned handler)"); + Serial.println(" http://192.168.4.1/anything (custom catch-all)"); + Serial.println(" http://192.168.4.1/static/ (static file serving)"); +} + +void loop() { + // Nothing to do — the async server handles everything. +} diff --git a/platformio.ini b/platformio.ini index 37e0a3e29..4e48443bc 100644 --- a/platformio.ini +++ b/platformio.ini @@ -10,6 +10,7 @@ lib_dir = . ; src_dir = examples/arduino/ChunkResponse ; src_dir = examples/arduino/ChunkRetryResponse ; src_dir = examples/arduino/CORS +; src_dir = examples/arduino/CustomAllocation ; src_dir = examples/arduino/EndBegin ; src_dir = examples/arduino/Filters ; src_dir = examples/arduino/FlashResponse diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index 7fcadeb5a..0d56313c3 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -85,6 +85,107 @@ #define ASYNCWEBSERVER_NO_GLOBAL_HTTP_METHODS #endif +namespace asyncsrv { + +/** + * @brief A tagged pointer that tracks ownership in a single machine word. + * + * Stores a pointer to T with the low bit used as an ownership flag. + * If the low bit is set, the MaybeOwned owns the object and will delete it + * on destruction/reset. If clear, it is a non-owning (borrowed) reference. + * + * This halves the per-entry storage compared to a struct with a raw pointer + * plus a unique_ptr (one pointer instead of two on 32-bit platforms). + * + * Requires alignof(T) >= 2, which is always true for polymorphic types + * (they have a vtable pointer, guaranteeing alignment). + */ +template +class MaybeOwned { + static constexpr uintptr_t OWNED = 1; + uintptr_t _p; // low bit set = owned + + explicit MaybeOwned(uintptr_t p) noexcept : _p(p) {} + +public: + /// Adopt (take ownership of) a heap-allocated pointer. + static MaybeOwned adopt(T *p) noexcept { + static_assert(alignof(T) >= 2, "MaybeOwned: need a free low bit (alignof(T) >= 2)"); + return MaybeOwned(reinterpret_cast(p) | OWNED); + } + + /// Borrow (non-owning reference to) an existing object. + static MaybeOwned borrow(T &r) noexcept { + static_assert(alignof(T) >= 2, "MaybeOwned: need a free low bit (alignof(T) >= 2)"); + return MaybeOwned(reinterpret_cast(&r)); + } + + /// Borrow (non-owning) from a pointer. + static MaybeOwned borrow(T *p) noexcept { + static_assert(alignof(T) >= 2, "MaybeOwned: need a free low bit (alignof(T) >= 2)"); + return MaybeOwned(reinterpret_cast(p)); + } + + constexpr MaybeOwned() noexcept : _p(0) {} + constexpr MaybeOwned(std::nullptr_t) noexcept : _p(0) {} + ~MaybeOwned() { + reset(); + } + + MaybeOwned(const MaybeOwned &) = delete; + MaybeOwned &operator=(const MaybeOwned &) = delete; + + MaybeOwned(MaybeOwned &&o) noexcept : _p(o._p) { + o._p = 0; + } + MaybeOwned &operator=(MaybeOwned &&o) noexcept { + if (this != &o) { + reset(); + _p = o._p; + o._p = 0; + } + return *this; + } + MaybeOwned &operator=(std::nullptr_t) noexcept { + reset(); + return *this; + } + + /// Delete the owned object (if any) and become empty. + void reset() noexcept { + if (_p & OWNED) { + delete get(); + } + _p = 0; + } + + /// Get the raw pointer (ownership flag stripped). + T *get() const noexcept { + return reinterpret_cast(_p & ~OWNED); + } + + /// True if this MaybeOwned owns the object. + bool isOwned() const noexcept { + return (_p & OWNED) != 0; + } + + T &operator*() const noexcept { + return *get(); + } + T *operator->() const noexcept { + return get(); + } + explicit operator bool() const noexcept { + return get() != nullptr; + } + + void swap(MaybeOwned &o) noexcept { + std::swap(_p, o._p); + } +}; + +} // namespace asyncsrv + class AsyncWebServer; class AsyncWebServerRequest; class AsyncWebServerResponse; @@ -1683,11 +1784,74 @@ class AsyncWebServer : public AsyncMiddlewareChain { protected: AsyncServer _server; std::list> _rewrites; - std::list> _handlers; - AsyncCallbackWebHandler *_catchAllHandler; + // Unified handler list: each entry is an asyncsrv::MaybeOwned. + // A single tagged pointer tracks both the handler address and ownership, + // halving the per-entry memory compared to a raw pointer + unique_ptr pair. + std::list> _handlers; + // Catch-all handler, also tracked with asyncsrv::MaybeOwned for consistent ownership semantics. + // A catch-all handler is always installed (never nullptr). + asyncsrv::MaybeOwned _catchAllHandler; public: + /** + * @brief Construct a server with a default internal catch-all handler. + * + * The server creates and owns an `AsyncCallbackWebHandler` used when no + * registered handler matches a request. + */ AsyncWebServer(uint16_t port); + + /** + * @brief Construct a server with a user-provided, server-owned catch-all handler. + * + * The server takes ownership and will delete the handler on destruction. + * The pointer must not be nullptr — a catch-all handler is always required. + * + * Example (heap-allocated, server-owned): + * @code + * AsyncWebServer server(80, new AsyncCallbackWebHandler()); + * @endcode + * + * Example (PSRAM on ESP32 — server-owned, non-owning borrow also possible): + * @code + * auto *catchAll = new (heap_caps_malloc(sizeof(AsyncCallbackWebHandler), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)) AsyncCallbackWebHandler(); + * AsyncWebServer server(80, catchAll); // server owns and will delete + * @endcode + * + * @param port TCP port to listen on. + * @param catchAllHandler Pointer to the catch-all handler. Must not be nullptr. + * The server takes ownership and will delete it. + */ + AsyncWebServer(uint16_t port, AsyncCallbackWebHandler *catchAllHandler); + + /** + * @brief Construct a server with a user-provided catch-all handler (non-owning). + * + * The server stores a non-owning reference (borrow). The caller is responsible + * for keeping the handler alive and for destroying it after the server is gone. + * This is the natural choice for static, global, or PSRAM-allocated catch-all + * handlers — it makes the non-owning intent explicit at the call site. + * + * @warning Do not pass a stack-allocated (automatic) object unless it + * outlives the server. Use static or global storage instead. + * + * Example (static RAM): + * @code + * static AsyncCallbackWebHandler catchAll; + * AsyncWebServer server(80, catchAll); // non-owning + * @endcode + * + * Example (PSRAM on ESP32 — non-owning reference): + * @code + * auto* catchAll = new (heap_caps_malloc(sizeof(sizeof(AsyncCallbackWebHandler), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)) AsyncCallbackWebHandler(); + * AsyncWebServer server(80, *catchAll); // non-owning borrow + * @endcode + * + * @param port TCP port to listen on. + * @param catchAllHandler Reference to the catch-all handler. Must remain valid. + */ + AsyncWebServer(uint16_t port, AsyncCallbackWebHandler &catchAllHandler); + ~AsyncWebServer(); void begin(); @@ -1748,9 +1912,116 @@ class AsyncWebServer : public AsyncMiddlewareChain { */ bool removeRewrite(const char *from, const char *to); + /** + * @brief Register a server-owned handler. + * + * The server takes ownership and will delete the handler on + * removeHandler(), reset(), or destruction. + * + * Example (heap-allocated, server-owned — backward compatible): + * @code + * server.addHandler(new MyHandler()); + * @endcode + * + * @param handler Pointer to a heap-allocated handler. The server takes ownership. + * @return Reference to the registered handler. + */ AsyncWebHandler &addHandler(AsyncWebHandler *handler); + + /** + * @brief Register a user-managed handler by reference (non-owning borrow). + * + * The server stores a non-owning reference. The caller is responsible for + * keeping the handler alive while it is registered, and for destroying it + * after unregistration (or at program end). + * + * This is the C++ idiom for borrowing: passing by reference makes the + * non-owning intent explicit at the call site. It is the natural choice + * for static, global, or PSRAM-allocated handlers. + * + * @warning Do not pass a stack-allocated (automatic) object unless you + * guarantee it outlives the server. In typical Arduino sketches, + * a local variable inside setup() is destroyed when setup() + * returns — use static or global storage instead. + * + * Example (static RAM — no heap allocation): + * @code + * static AsyncCallbackWebHandler handler; + * handler.setUri(AsyncURIMatcher::exact("/status")); + * handler.onRequest([](AsyncWebServerRequest *req) { req->send(200, "text/plain", "OK"); }); + * server.addHandler(handler); + * @endcode + * + * Example (PSRAM on ESP32 — non-owning reference): + * @code + * void *mem = heap_caps_malloc(sizeof(AsyncCallbackWebHandler), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + * auto *h = new (mem) AsyncCallbackWebHandler(); + * h->setUri(AsyncURIMatcher::exact("/status")); + * h->onRequest([](AsyncWebServerRequest *req) { req->send(200, "text/plain", "OK"); }); + * server.addHandler(*h); // borrow — user manages lifetime + * @endcode + * + * @param handler Reference to the handler to register. + * @return Reference to the registered handler (same object). + */ + AsyncWebHandler &addHandler(AsyncWebHandler &handler); + + /** + * @brief Remove a handler from the server. + * + * If the handler was server-owned, it is deleted. If it was non-owning + * (registered by reference), only the registration is removed — the caller + * still owns the object. + * + * @param handler Pointer to the handler to remove. + * @return true if the handler was found and removed, false otherwise. + */ bool removeHandler(AsyncWebHandler *handler); + /** + * @brief Replace the catch-all (not-found) handler with a server-owned one. + * + * The server takes ownership and will delete the handler on reset() or + * destruction. The pointer must not be nullptr — a catch-all handler is + * always required. + * + * If the previously installed catch-all was server-owned, it is deleted + * when replaced. + * + * Example (heap-allocated, server-owned): + * @code + * server.setCatchAllHandler(new AsyncCallbackWebHandler()); + * @endcode + * + * @param handler Pointer to the replacement catch-all handler. Must not be nullptr. + * The server takes ownership and will delete it. + */ + void setCatchAllHandler(AsyncCallbackWebHandler *handler); + + /** + * @brief Replace the catch-all (not-found) handler with a user-provided one (non-owning). + * + * The server stores a non-owning reference. The caller is responsible for + * keeping the handler alive and for destroying it after replacement or at + * program end. + * + * This overload is the natural choice for static, global, or PSRAM-allocated + * catch-all handlers — it makes the non-owning intent explicit at the call site. + * + * @warning Do not pass a stack-allocated (automatic) object unless it + * outlives the server. Use static or global storage instead. + * + * Example (static RAM): + * @code + * static AsyncCallbackWebHandler catchAll; + * catchAll.onRequest([](AsyncWebServerRequest *req) { req->send(404, "text/plain", "Not found"); }); + * server.setCatchAllHandler(catchAll); + * @endcode + * + * @param handler Reference to the replacement catch-all handler. Must remain valid. + */ + void setCatchAllHandler(AsyncCallbackWebHandler &handler); + AsyncCallbackWebHandler &on(AsyncURIMatcher uri, ArRequestHandlerFunction onRequest) { return on(std::move(uri), AsyncWebRequestMethod::HTTP_ALL, onRequest); } diff --git a/src/WebServer.cpp b/src/WebServer.cpp index a8ef376b4..d759083cc 100644 --- a/src/WebServer.cpp +++ b/src/WebServer.cpp @@ -40,29 +40,35 @@ const char *fs::FileOpenMode::write = "w"; const char *fs::FileOpenMode::append = "a"; #endif -AsyncWebServer::AsyncWebServer(uint16_t port) : _server(port) { - _catchAllHandler = new AsyncCallbackWebHandler(); - _server.onClient( - [](void *s, AsyncClient *c) { - if (c == NULL) { - return; - } - c->setRxTimeout(3); - std::shared_ptr r = AsyncWebServerRequest::create(static_cast(s), c); - if (!r) { - c->abort(); - delete c; - } - }, - this - ); +static void _onClientConnect(void *s, AsyncClient *c) { + if (c == NULL) { + return; + } + c->setRxTimeout(3); + std::shared_ptr r = AsyncWebServerRequest::create(static_cast(s), c); + if (!r) { + c->abort(); + delete c; + } +} + +AsyncWebServer::AsyncWebServer(uint16_t port) : AsyncWebServer(port, new AsyncCallbackWebHandler()) {} + +AsyncWebServer::AsyncWebServer(uint16_t port, AsyncCallbackWebHandler *catchAllHandler) : _server(port) { + assert(catchAllHandler != nullptr); + _catchAllHandler = MaybeOwned::adopt(catchAllHandler); + _server.onClient(_onClientConnect, this); +} + +AsyncWebServer::AsyncWebServer(uint16_t port, AsyncCallbackWebHandler &catchAllHandler) : _server(port) { + _catchAllHandler = MaybeOwned::borrow(catchAllHandler); + _server.onClient(_onClientConnect, this); } AsyncWebServer::~AsyncWebServer() { reset(); end(); - delete _catchAllHandler; - _catchAllHandler = nullptr; // Prevent potential use-after-free + _catchAllHandler = nullptr; } AsyncWebRewrite &AsyncWebServer::addRewrite(std::shared_ptr rewrite) { @@ -95,13 +101,19 @@ AsyncWebRewrite &AsyncWebServer::rewrite(const char *from, const char *to) { } AsyncWebHandler &AsyncWebServer::addHandler(AsyncWebHandler *handler) { - _handlers.emplace_back(handler); - return *(_handlers.back().get()); + _handlers.push_back(MaybeOwned::adopt(handler)); + return *handler; +} + +AsyncWebHandler &AsyncWebServer::addHandler(AsyncWebHandler &handler) { + _handlers.push_back(MaybeOwned::borrow(handler)); + return handler; } bool AsyncWebServer::removeHandler(AsyncWebHandler *handler) { for (auto i = _handlers.begin(); i != _handlers.end(); ++i) { if (i->get() == handler) { + // MaybeOwned destructor will delete if owned; non-owned entries are just removed _handlers.erase(i); return true; } @@ -140,14 +152,14 @@ void AsyncWebServer::_rewriteRequest(AsyncWebServerRequest *request) { } void AsyncWebServer::_attachHandler(AsyncWebServerRequest *request) { - for (auto &h : _handlers) { - if (h->filter(request) && h->canHandle(request)) { - request->setHandler(h.get()); + for (auto &entry : _handlers) { + if (entry->filter(request) && entry->canHandle(request)) { + request->setHandler(entry.get()); return; } } - // ESP_LOGD("AsyncWebServer", "No handler found for %s, using _catchAllHandler pointer: %p", request->url().c_str(), _catchAllHandler); - request->setHandler(_catchAllHandler); + // A catch-all handler is always installed — fallback to it. + request->setHandler(_catchAllHandler.get()); } AsyncCallbackWebHandler &AsyncWebServer::on( @@ -194,9 +206,20 @@ AsyncWebHandler &AsyncWebServer::catchAllHandler() const { return *_catchAllHandler; } +void AsyncWebServer::setCatchAllHandler(AsyncCallbackWebHandler *handler) { + assert(handler != nullptr); + _catchAllHandler = MaybeOwned::adopt(handler); +} + +void AsyncWebServer::setCatchAllHandler(AsyncCallbackWebHandler &handler) { + _catchAllHandler = MaybeOwned::borrow(handler); +} + void AsyncWebServer::reset() { _rewrites.clear(); _handlers.clear(); + // Note: _catchAllHandler is intentionally NOT cleared here — it persists + // across reset() calls (only its callbacks are cleared). _catchAllHandler->onRequest(NULL); _catchAllHandler->onUpload(NULL); From 508b24f1159e6ecaa061db8e04a3fe51b108a047 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:57:34 +0000 Subject: [PATCH 2/2] ci(pre-commit): Apply automatic fixes --- examples/arduino/CustomAllocation/CustomAllocation.ino | 9 +++------ src/ESPAsyncWebServer.h | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/examples/arduino/CustomAllocation/CustomAllocation.ino b/examples/arduino/CustomAllocation/CustomAllocation.ino index ae0ea3b90..c0de0f620 100644 --- a/examples/arduino/CustomAllocation/CustomAllocation.ino +++ b/examples/arduino/CustomAllocation/CustomAllocation.ino @@ -61,8 +61,7 @@ static void *apiHandlerMem = nullptr; void setup() { Serial.begin(115200); - while (!Serial) - ; + while (!Serial); #if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); @@ -100,8 +99,7 @@ void setup() { }); // Non-owning borrow: the server stores a reference; we manage PSRAM lifetime. server.addHandler(*apiHandler); - Serial.printf("API handler allocated in PSRAM at %p (%u bytes free in PSRAM)\n", - apiHandlerMem, ESP.getFreePsram()); + Serial.printf("API handler allocated in PSRAM at %p (%u bytes free in PSRAM)\n", apiHandlerMem, ESP.getFreePsram()); } else { Serial.println("PSRAM allocation failed for API handler"); } @@ -127,8 +125,7 @@ void setup() { server.begin(); Serial.println("HTTP server started"); #if ASYNCWEBSERVER_WIFI_SUPPORTED - Serial.printf("Connect to AP 'esp-custom-alloc' and open http://%s\n", - WiFi.softAPIP().toString().c_str()); + Serial.printf("Connect to AP 'esp-custom-alloc' and open http://%s\n", WiFi.softAPIP().toString().c_str()); #endif Serial.println("Try:"); Serial.println(" http://192.168.4.1/status (static RAM handler)"); diff --git a/src/ESPAsyncWebServer.h b/src/ESPAsyncWebServer.h index 0d56313c3..9dae0c5e1 100644 --- a/src/ESPAsyncWebServer.h +++ b/src/ESPAsyncWebServer.h @@ -100,8 +100,7 @@ namespace asyncsrv { * Requires alignof(T) >= 2, which is always true for polymorphic types * (they have a vtable pointer, guaranteeing alignment). */ -template -class MaybeOwned { +template class MaybeOwned { static constexpr uintptr_t OWNED = 1; uintptr_t _p; // low bit set = owned