Skip to content
Draft
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
3 changes: 3 additions & 0 deletions docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ flowchart TD
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).
- A `Handler` can serve **multiple URIs** by holding a list of URI matchers (`setUri()` + `addUri()`).
One HTTP method mask applies to all its URIs; `setMethod()` accepts a composite (`HTTP_GET | HTTP_POST`).
See [Routing — Multiple URIs per Handler](routing.md#multiple-uris-per-handler).

### Responses and how do they work

Expand Down
21 changes: 21 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,24 @@ server.setCatchAllHandler(new AsyncCallbackWebHandler()); // server-owned
> 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.

## Multiple URIs per Handler

One handler object can serve **multiple URIs** — the handler holds a list of URI matchers. One HTTP method mask applies to all its URIs. `setMethod()` accepts a composite: `handler.setMethod(HTTP_GET | HTTP_POST)`. Use separate handlers when different methods need different callbacks (e.g. `onBody`/`onUpload`):

```cpp
static AsyncCallbackWebHandler handler;
handler.onRequest([](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
handler.setMethod(HTTP_GET | HTTP_POST); // accepts both GET and POST

// Same handler, three URIs (non-owning, static RAM):
handler.setUri(AsyncURIMatcher::exact("/foo"));
handler.addUri(AsyncURIMatcher::exact("/bar"));
handler.addUri(AsyncURIMatcher::exact("/status"));

server.addHandler(handler); // non-owning (reference)
```

See the [MultiRoute example](https://github.com/ESP32Async/ESPAsyncWebServer/blob/master/examples/arduino/MultiRoute/MultiRoute.ino) and the [Routing](routing.md#multiple-uris-per-handler) documentation for more details.
119 changes: 119 additions & 0 deletions docs/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,125 @@ server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), handler);
#endif
```

## Multiple URIs per Handler

A handler can serve **multiple URIs** by holding a list of matchers instead of a single one:

| Method | Effect |
|---|---|
| `handler.setUri(matcher)` | Replace the list with one matcher (backward-compatible) |
| `handler.addUri(matcher)` | Append an additional URI matcher |
| `handler.clearUris()` | Remove all URI matchers |

One HTTP method mask (`setMethod()`) applies to **all** URIs of a handler. `setMethod()` accepts a composite mask — a handler can accept multiple HTTP methods:

```cpp
handler.setMethod(HTTP_GET | HTTP_POST); // accepts both GET and POST
```

When different methods need different callbacks (e.g. `POST` with `onBody`/`onUpload`), use separate handler objects.

### One Handler, Multiple URIs

```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());
});
greetHandler.setMethod(HTTP_GET);

// Same handler object serving three different URIs:
greetHandler.setUri(AsyncURIMatcher::exact("/hello"));
greetHandler.addUri(AsyncURIMatcher::exact("/hi"));
greetHandler.addUri(AsyncURIMatcher::prefix("/greet/"));

// Non-owning registration (by reference) — handler lives in static RAM:
server.addHandler(greetHandler);

server.begin();
}
```

### Multiple URIs × Multiple Methods

Combine both: one handler, several URIs, accepting several HTTP methods:

```cpp
static AsyncCallbackWebHandler apiHandler;
apiHandler.setMethod(HTTP_GET | HTTP_POST | HTTP_PUT);
apiHandler.setUri(AsyncURIMatcher::exact("/api/data"));
apiHandler.addUri(AsyncURIMatcher::exact("/api/status"));
apiHandler.onRequest([](AsyncWebServerRequest *request) {
request->send(200, "application/json", "{\"ok\":true}");
});
server.addHandler(apiHandler);
// Serves: GET/POST/PUT /api/data and GET/POST/PUT /api/status
```

### Different Methods Need Different Callbacks → Different Handlers

When methods need different callbacks (e.g. `POST` with `onBody`/`onUpload`), use separate handler objects:

```cpp
static AsyncCallbackWebHandler getHandler;
getHandler.setMethod(HTTP_GET);
getHandler.setUri(AsyncURIMatcher::exact("/data"));
getHandler.onRequest([](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "GET response");
});
server.addHandler(getHandler);

static AsyncCallbackWebHandler postHandler;
postHandler.setMethod(HTTP_POST);
postHandler.setUri(AsyncURIMatcher::exact("/data"));
postHandler.onRequest([](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "POST received");
});
postHandler.onBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
// handle request body
});
server.addHandler(postHandler);
```

### PSRAM Handler on Multiple URIs (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}");
});
handler->setMethod(HTTP_GET);

// Same PSRAM handler serving two URIs — non-owning (borrow by reference):
handler->setUri(AsyncURIMatcher::prefix("/api/"));
handler->addUri(AsyncURIMatcher::exact("/status"));
server.addHandler(*handler);

server.begin();
}
```

### Removing a Handler

```cpp
// Removes the handler entirely — it no longer matches any URI:
server.removeHandler(&greetHandler);
```

> [!NOTE]
> This is fully backward compatible. Existing code using `on()`, `addHandler()`, `serveStatic()`, and single-URI handlers (`setUri()`) 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.

## Param Rewrite With Matching

It is possible to rewrite the request url with parameter match. Here is an example with one parameter:
Expand Down
156 changes: 156 additions & 0 deletions examples/arduino/MultiRoute/MultiRoute.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles

//
// Shows how one handler object can serve multiple URIs and accept
// multiple HTTP methods.
//
// A handler holds a LIST of URI matchers instead of a single one:
// - setUri() replaces the list with one matcher (backward-compatible)
// - addUri() appends an additional URI matcher
// - clearUris() removes all matchers
//
// setMethod() accepts a composite mask: handler.setMethod(HTTP_GET | HTTP_POST)
// so one handler can serve multiple URIs × multiple methods.
// Use separate handlers when different methods need different callbacks
// (e.g. a GET handler and a POST handler with onBody/onUpload callbacks).
//
// Non-owning registration (addHandler by reference) lets the handler
// live in static RAM or PSRAM without any heap allocation.
//

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

static AsyncWebServer server(80);

// ---------------------------------------------------------------------------
// One handler, many URIs — lives in static RAM (no heap allocation).
// The same object serves /hello, /hi, and /greet/*.
// ---------------------------------------------------------------------------
static AsyncCallbackWebHandler greetHandler;

// ---------------------------------------------------------------------------
// Another handler in PSRAM (ESP32 only), also serving multiple URIs.
// ---------------------------------------------------------------------------
#if defined(ESP32)
static AsyncCallbackWebHandler *apiHandler = nullptr;
static void *apiHandlerMem = nullptr;
#endif

// ---------------------------------------------------------------------------
// A classic server-owned handler (convenience API, fully backward compatible).
// ---------------------------------------------------------------------------
// (see server.on() below)

void setup() {
Serial.begin(115200);
while (!Serial)
;

#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-multi-uri");
#endif

// --- 1. One handler, three URIs, two methods (static RAM) ------------------
// The handler only defines WHAT to do — its URI list defines WHERE.
greetHandler.onRequest([](AsyncWebServerRequest *request) {
String msg = "Hello from " + request->url();
request->send(200, "text/plain", msg);
});
// setMethod accepts a composite: this handler serves GET and POST
greetHandler.setMethod(HTTP_GET | HTTP_POST);

// Same handler object serving multiple URIs:
greetHandler.setUri(AsyncURIMatcher::exact("/hello"));
greetHandler.addUri(AsyncURIMatcher::exact("/hi"));
greetHandler.addUri(AsyncURIMatcher::prefix("/greet/"));

// Non-owning registration (by reference) — greetHandler lives in static RAM (.bss)
server.addHandler(greetHandler);

// --- 2. PSRAM handler serving multiple URIs (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->onRequest([](AsyncWebServerRequest *request) {
String json = "{\"path\":\"" + request->url() + "\",\"region\":\"psram\"}";
request->send(200, "application/json", json);
});
apiHandler->setMethod(HTTP_GET);
// Same PSRAM handler serving two URIs — non-owning (borrow by reference):
apiHandler->setUri(AsyncURIMatcher::prefix("/api/"));
apiHandler->addUri(AsyncURIMatcher::exact("/status"));
server.addHandler(*apiHandler);
Serial.printf("API handler allocated in PSRAM at %p\n", apiHandlerMem);
}
}
#endif

// --- 3. When methods need different callbacks, use different handlers -----
// greetHandler accepts GET|POST but has the same onRequest for both.
// For a POST with onBody/onUpload, create a separate handler:
static AsyncCallbackWebHandler submitHandler;
submitHandler.setMethod(HTTP_POST);
submitHandler.setUri(AsyncURIMatcher::exact("/submit"));
submitHandler.onRequest([](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "Submitted");
});
submitHandler.onBody([](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
// receive the request body here
});
server.addHandler(submitHandler); // non-owning, static RAM

// --- 4. Server-owned handler (classic convenience API) --------------------
server.on("/ping", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "pong");
});

// --- 5. Custom catch-all --------------------------------------------------
server.onNotFound([](AsyncWebServerRequest *request) {
request->send(404, "text/plain", "Not found: " + request->url());
});

server.begin();
Serial.println("HTTP server started");
#if ASYNCWEBSERVER_WIFI_SUPPORTED
Serial.printf("Connect to AP 'esp-multi-uri' and open http://%s\n",
WiFi.softAPIP().toString().c_str());
#endif
Serial.println("Try (greetHandler serves multiple URIs with GET|POST):");
Serial.println(" http://192.168.4.1/hello → greetHandler");
Serial.println(" http://192.168.4.1/hi → greetHandler (same object!)");
Serial.println(" http://192.168.4.1/greet/world → greetHandler (prefix URI)");
#if defined(ESP32)
Serial.println("Try (PSRAM handler, ESP32 only):");
Serial.println(" http://192.168.4.1/api/test → PSRAM apiHandler");
Serial.println(" http://192.168.4.1/status → PSRAM apiHandler (same object!)");
#endif
Serial.println("Try (separate handler for POST with onBody):");
Serial.println(" POST http://192.168.4.1/submit → submitHandler");
Serial.println("Try (server-owned convenience handler):");
Serial.println(" http://192.168.4.1/ping → server.on() handler");
Serial.println("Try (no handler matches → catch-all):");
Serial.println(" http://192.168.4.1/unknown → 404 catch-all");
}

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 @@ -24,6 +24,7 @@ lib_dir = .
; src_dir = examples/arduino/MessagePack
; src_dir = examples/arduino/Middleware
; src_dir = examples/arduino/MultiPart
; src_dir = examples/arduino/MultiRoute
; src_dir = examples/arduino/Params
; src_dir = examples/arduino/PartitionDownloader
src_dir = examples/arduino/PerfTests
Expand Down
18 changes: 15 additions & 3 deletions src/AsyncJson.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,18 +117,30 @@ constexpr static WebRequestMethodComposite JsonHandlerMethods =

#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest, size_t maxJsonBufferSize)
: _uri(std::move(uri)), _method(JsonHandlerMethods), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {}
: _method(JsonHandlerMethods), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {
_uris.push_back(std::move(uri));
}
#else
AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest)
: _uri(std::move(uri)), _method(JsonHandlerMethods), _onRequest(onRequest), _maxContentLength(16384) {}
: _method(JsonHandlerMethods), _onRequest(onRequest), _maxContentLength(16384) {
_uris.push_back(std::move(uri));
}
#endif

bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) const {
if (!_onRequest || !request->isHTTP() || !_method.matches(request->method())) {
return false;
}

if (!_uri.matches(request)) {
// Match against any of the registered URI matchers
bool uriMatched = false;
for (const auto &uri : _uris) {
if (uri.matches(request)) {
uriMatched = true;
break;
}
}
if (!uriMatched) {
return false;
}

Expand Down
Loading
Loading