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
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.
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; use separate handlers for different methods:

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

// 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.
94 changes: 94 additions & 0 deletions docs/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,100 @@ 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. When different URIs need different methods (e.g. `GET` vs `POST` with body/upload callbacks), 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();
}
```

### Different Methods Need Different Handlers

The method mask applies to all URIs of a handler, so a `POST` endpoint gets its own handler:

```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");
});
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
150 changes: 150 additions & 0 deletions examples/arduino/MultiRoute/MultiRoute.ino
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
// 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.
//
// 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
//
// One HTTP method mask (setMethod) applies to ALL URIs of a handler.
// Use separate handlers when different URIs need different methods
// (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 (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);
});
greetHandler.setMethod(HTTP_GET);

// 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. Different method needs a different handler ------------------------
// The method mask applies to ALL URIs of a handler, so a POST endpoint
// gets its own handler with onBody():
static AsyncCallbackWebHandler postHandler;
postHandler.setMethod(HTTP_POST);
postHandler.setUri(AsyncURIMatcher::exact("/hello"));
postHandler.onRequest([](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "POST received");
});
server.addHandler(postHandler); // 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 (all GET routes served by the SAME greetHandler object):");
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 (different handler for POST):");
Serial.println(" POST http://192.168.4.1/hello → postHandler");
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
25 changes: 19 additions & 6 deletions src/AsyncJson.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -117,21 +117,34 @@ 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())) {
if (!_onRequest || !request->isHTTP()) {
return false;
}

if (!_uri.matches(request)) {
if (!_method.matches(request->method())) {
return false;
}
// 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;
}

#if ASYNC_MSG_PACK_SUPPORT == 1
return request->method() == AsyncWebRequestMethod::HTTP_GET || request->contentType().equalsIgnoreCase(asyncsrv::T_application_json)
|| request->contentType().equalsIgnoreCase(asyncsrv::T_application_msgpack);
Expand Down
19 changes: 18 additions & 1 deletion src/AsyncJson.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,8 @@ class AsyncMessagePackResponse : public AsyncJsonResponse {

class AsyncCallbackJsonWebHandler : public AsyncWebHandler {
protected:
AsyncURIMatcher _uri;
// A handler can serve multiple URIs — one method mask applies to all of them.
std::list<AsyncURIMatcher> _uris;
WebRequestMethodComposite _method;
ArJsonRequestHandlerFunction _onRequest;
#if ARDUINOJSON_VERSION_MAJOR == 6
Expand All @@ -105,6 +106,22 @@ class AsyncCallbackJsonWebHandler : public AsyncWebHandler {
AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest = nullptr);
#endif

/// Set the URI matcher, replacing any previously added matchers (backward-compatible).
void setUri(AsyncURIMatcher uri) {
_uris.clear();
_uris.push_back(std::move(uri));
}

/// Add an additional URI matcher — the same handler serves all added URIs.
void addUri(AsyncURIMatcher uri) {
_uris.push_back(std::move(uri));
}

/// Remove all URI matchers.
void clearUris() {
_uris.clear();
}

void setMethod(WebRequestMethodComposite method) {
_method = std::move(method);
}
Expand Down
Loading
Loading