Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
52 changes: 52 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <esp_heap_caps.h>

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.
131 changes: 131 additions & 0 deletions docs/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Comment thread
mathieucarbou marked this conversation as resolved.
```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)
Comment thread
mathieucarbou marked this conversation as resolved.

There are two ways to place a handler in PSRAM:

**1. Placement new with `heap_caps_malloc`:**

```cpp
#include <esp_heap_caps.h>

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.
Expand Down
142 changes: 142 additions & 0 deletions examples/arduino/CustomAllocation/CustomAllocation.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// 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 <Arduino.h>
#if defined(ESP32) || defined(LIBRETINY)
#include <AsyncTCP.h>
#include <WiFi.h>
#if defined(ESP32)
#include <esp_heap_caps.h>
#endif
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#include <RPAsyncTCP.h>
#include <WiFi.h>
#endif

#include <ESPAsyncWebServer.h>
#include <LittleFS.h>

// ---------------------------------------------------------------------------
// 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.
}
1 change: 1 addition & 0 deletions platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading