Skip to content
Closed
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
7 changes: 7 additions & 0 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ 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)` or `addHandler(ptr, false)` is **non-owning** — 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).
- The server maintains a **route table** (`addRoute()`) that decouples URI/method matching from the handler object.
Routes are evaluated before legacy handlers. One handler object can serve multiple routes.
See [Routing — Route Table](routing.md#route-table-one-handler-multiple-routes).

### Responses and how do they work

Expand Down
74 changes: 74 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,77 @@ 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**:

```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>

void *mem = heap_caps_malloc(sizeof(AsyncCallbackWebHandler), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
auto *handler = new (mem) AsyncCallbackWebHandler();
handler->setUri(AsyncURIMatcher::exact("/status"));
handler->onRequest([](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
server.addHandler(handler, false); // non-owning — manage lifetime yourself
```

The same applies to the catch-all (not-found) handler — either at construction time or afterwards:

```cpp
// At construction — non-owning (reference), lives in static RAM
static AsyncCallbackWebHandler myCatchAll;
AsyncWebServer server(80, myCatchAll);

// At construction — non-owning (pointer), e.g. PSRAM
AsyncWebServer server(80, psramCatchAll, false);

// At construction — no catch-all at all (unmatched → bare 501)
AsyncWebServer server(80, nullptr);

// After construction
server.setCatchAllHandler(myCatchAllHandler); // non-owning (reference)
server.setCatchAllHandler(psramCatchAll, false); // non-owning (pointer)
server.setCatchAllHandler(new AsyncCallbackWebHandler()); // server-owned (default)
server.setCatchAllHandler(nullptr); // remove catch-all entirely
```

> [!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.

## Route Table: One Handler, Multiple Routes

The `addRoute()` API decouples URI matching from the handler object — the server's routing table stores the URI matcher and HTTP method alongside a handler pointer. This lets **one handler object serve multiple routes**:

```cpp
static AsyncCallbackWebHandler handler;
handler.onRequest([](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});

// Same handler, three routes (non-owning, static RAM):
server.addRoute(AsyncURIMatcher::exact("/foo"), HTTP_GET, handler);
server.addRoute(AsyncURIMatcher::exact("/bar"), HTTP_GET, handler);
server.addRoute(AsyncURIMatcher::exact("/status"), HTTP_GET, handler, false); // non-owning ptr
```

Routes are evaluated before legacy handlers (`on()`, `addHandler()`, `serveStatic()`). Existing code works identically — no migration required.

See the [MultiRoute example](https://github.com/ESP32Async/ESPAsyncWebServer/blob/master/examples/arduino/MultiRoute/MultiRoute.ino) and the [Routing](routing.md#route-table-one-handler-multiple-routes) documentation for more details.
200 changes: 200 additions & 0 deletions docs/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,206 @@ 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(ptr, false)` | Non-owning (pointer) | Heap or PSRAM via placement new |
| `server.addHandler(myHandler)` | Non-owning (reference) | Stack, static, or global object |

With non-owning registration, **you** are responsible for keeping the handler alive while it is registered, and for destroying it after unregistration.

### 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)

```cpp
#include <esp_heap_caps.h>

void setup() {
void *mem = heap_caps_malloc(sizeof(AsyncCallbackWebHandler), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
auto *handler = new (mem) AsyncCallbackWebHandler();
handler->setUri(AsyncURIMatcher::exact("/status"));
handler->onRequest([](AsyncWebServerRequest *request) {
request->send(200, "application/json", "{\"ok\":true}");
});
server.addHandler(handler, false); // non-owning — manage PSRAM lifetime yourself
server.begin();
}
```

### WebSocket / EventSource / Static File Handlers

The same pattern works for any `AsyncWebHandler` subclass — `AsyncWebSocket`, `AsyncEventSource`, `AsyncStaticWebHandler`, `AsyncCallbackJsonWebHandler`:

```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

static AsyncStaticWebHandler staticHandler("/static", LittleFS, "/www");
server.addHandler(staticHandler); // non-owning
```

### Custom Catch-All Handler

The catch-all (not-found) handler 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, ptr, false)` | Non-owning (pointer) |
| `AsyncWebServer server(80, myHandler)` | Non-owning (reference) |
| `AsyncWebServer server(80, nullptr)` | **No catch-all** — unmatched requests get a bare 501 |

```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 (default) |
| `server.setCatchAllHandler(ptr, false)` | Non-owning (pointer) |
| `server.setCatchAllHandler(myHandler)` | Non-owning (reference) |
| `server.setCatchAllHandler(nullptr)` | **Remove catch-all** — unmatched requests get a bare 501 |

```cpp
server.setCatchAllHandler(catchAll); // non-owning — catchAll lives in static RAM
```

> [!NOTE]
> When no catch-all handler is installed (`nullptr`), `onNotFound()`, `onFileUpload()`, `onRequestBody()`, and `reset()` are no-ops. Use `catchAllHandlerPtr()` to safely check for the catch-all handler — it returns `nullptr` instead of crashing. Unmatched requests receive a bare `501 Not Implemented` response with the message "Handler did not handle the request".
>
> The older `catchAllHandler()` method is deprecated — it crashes if no catch-all is installed. Replace it with:
> ```cpp
> if (auto *catchAll = server.catchAllHandlerPtr()) {
> catchAll->addMiddleware(myMiddleware);
> }
> ```

See the [CustomAllocation example](https://github.com/ESP32Async/ESPAsyncWebServer/blob/master/examples/arduino/CustomAllocation/CustomAllocation.ino) for a complete sketch.

## Route Table: One Handler, Multiple Routes

The `addRoute()` API decouples **URI matching** from the **handler object**. The server's routing table stores `{ URI matcher, HTTP method, handler pointer }`, so a single handler object can serve multiple routes.

Routes are evaluated in insertion order, **before** legacy handlers (registered via `addHandler()` or `on()`).

| Registration | Ownership | Use case |
|---|---|---|
| `server.addRoute(uri, method, new MyHandler())` | Server owns | Default, backward compatible |
| `server.addRoute(uri, method, ptr, false)` | Non-owning (pointer) | Heap or PSRAM via placement new |
| `server.addRoute(uri, method, myHandler)` | Non-owning (reference) | Stack, static, or global object |

### One Handler, Multiple Routes

```cpp
static AsyncWebServer server(80);

// One handler object — lives in static RAM
static AsyncCallbackWebHandler greetHandler;

void setup() {
greetHandler.onRequest([](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "Hello from " + request->url());
});

// Same handler on three different routes:
server.addRoute(AsyncURIMatcher::exact("/hello"), HTTP_GET, greetHandler);
server.addRoute(AsyncURIMatcher::exact("/hi"), HTTP_GET, greetHandler);
server.addRoute(AsyncURIMatcher::prefix("/greet/"), HTTP_GET, greetHandler);

server.begin();
}
```

### Method-Based Routing

The route table also filters by HTTP method — a `GET` route won't match a `POST` request:

```cpp
static AsyncCallbackWebHandler handler;
handler.onRequest([](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "GET response");
});
handler.onBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {});

// Only GET /data matches this route; POST /data falls through to the next handler
server.addRoute(AsyncURIMatcher::exact("/data"), HTTP_GET, handler);
```

### PSRAM Handler on Multiple Routes (ESP32)

```cpp
#include <esp_heap_caps.h>

void setup() {
void *mem = heap_caps_malloc(sizeof(AsyncCallbackWebHandler), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
auto *handler = new (mem) AsyncCallbackWebHandler();
handler->onRequest([](AsyncWebServerRequest *request) {
request->send(200, "application/json", "{\"ok\":true}");
});

// Same PSRAM handler on two routes — non-owning:
server.addRoute(AsyncURIMatcher::prefix("/api/"), HTTP_GET, handler, false);
server.addRoute(AsyncURIMatcher::exact("/status"), HTTP_GET, handler, false);

server.begin();
}
```

### Removing Routes

```cpp
// Remove a specific route by URI + method:
server.removeRoute(AsyncURIMatcher::exact("/hello"), HTTP_GET);

// removeHandler() also works for route-registered handlers:
server.removeHandler(&greetHandler); // removes ALL routes pointing to this handler
```

> [!NOTE]
> Routes and legacy handlers (`on()`, `addHandler()`, `serveStatic()`) coexist. Routes are checked first. Existing code using `on()` / `addHandler()` works identically — no migration required.

See the [MultiRoute example](https://github.com/ESP32Async/ESPAsyncWebServer/blob/master/examples/arduino/MultiRoute/MultiRoute.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
Loading
Loading