From db70675f2058240bd5d8587a6e5363bbc47fac41 Mon Sep 17 00:00:00 2001 From: Mathieu Carbou Date: Thu, 15 Oct 2026 14:26:16 +0200 Subject: [PATCH] feat: multiple URIs per handler (setUri/addUri/clearUris) A handler can now serve multiple URIs and accept multiple HTTP methods by holding a list of 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 (HTTP_GET | HTTP_POST) that applies to all URIs of a handler. Use separate handlers when different methods need different callbacks (onBody/onUpload). Applies to both AsyncCallbackWebHandler and AsyncCallbackJsonWebHandler. - New MultiRoute example: multiple URIs x composite methods, separate handlers for callbacks - Updated docs: routing.md (Multiple URIs per Handler section), configuration.md, concepts.md Fully backward compatible: existing code using on(), addHandler(), serveStatic(), and single-URI handlers works identically. Motivation from #468 discussion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/concepts.md | 3 + docs/configuration.md | 21 +++ docs/routing.md | 119 ++++++++++++++++ examples/arduino/MultiRoute/MultiRoute.ino | 156 +++++++++++++++++++++ platformio.ini | 1 + src/AsyncJson.cpp | 18 ++- src/AsyncJson.h | 23 ++- src/WebHandlerImpl.h | 43 +++++- src/WebHandlers.cpp | 19 ++- 9 files changed, 395 insertions(+), 8 deletions(-) create mode 100644 examples/arduino/MultiRoute/MultiRoute.ino diff --git a/docs/concepts.md b/docs/concepts.md index cc2905163..1bab87feb 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -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 diff --git a/docs/configuration.md b/docs/configuration.md index 728d245a7..13fe27cd5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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. diff --git a/docs/routing.md b/docs/routing.md index 2bde77b81..946324734 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -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 + +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: diff --git a/examples/arduino/MultiRoute/MultiRoute.ino b/examples/arduino/MultiRoute/MultiRoute.ino new file mode 100644 index 000000000..0a562c3cb --- /dev/null +++ b/examples/arduino/MultiRoute/MultiRoute.ino @@ -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 +#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 + +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. +} diff --git a/platformio.ini b/platformio.ini index 4e48443bc..6705bdfc5 100644 --- a/platformio.ini +++ b/platformio.ini @@ -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 diff --git a/src/AsyncJson.cpp b/src/AsyncJson.cpp index 72108addc..d9141d1f4 100644 --- a/src/AsyncJson.cpp +++ b/src/AsyncJson.cpp @@ -117,10 +117,14 @@ 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 { @@ -128,7 +132,15 @@ bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) cons 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; } diff --git a/src/AsyncJson.h b/src/AsyncJson.h index 6f0372cea..befa899d8 100644 --- a/src/AsyncJson.h +++ b/src/AsyncJson.h @@ -6,6 +6,7 @@ #include #include "ChunkPrint.h" +#include #include #if ASYNC_JSON_SUPPORT == 1 @@ -90,7 +91,9 @@ 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. + // setMethod() accepts a composite: handler.setMethod(HTTP_GET | HTTP_POST) + std::list _uris; WebRequestMethodComposite _method; ArJsonRequestHandlerFunction _onRequest; #if ARDUINOJSON_VERSION_MAJOR == 6 @@ -105,6 +108,24 @@ 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(); + } + + /// Set the HTTP method mask for all URIs of this handler. + /// Accepts a composite: `handler.setMethod(HTTP_GET | HTTP_POST)`. void setMethod(WebRequestMethodComposite method) { _method = std::move(method); } diff --git a/src/WebHandlerImpl.h b/src/WebHandlerImpl.h index fbff491fe..920ae4196 100644 --- a/src/WebHandlerImpl.h +++ b/src/WebHandlerImpl.h @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -55,7 +56,11 @@ class AsyncStaticWebHandler : public AsyncWebHandler { class AsyncCallbackWebHandler : public AsyncWebHandler { private: protected: - AsyncURIMatcher _uri; + // A handler can serve multiple URIs. One method mask applies to all of them. + // setMethod() accepts a composite: handler.setMethod(HTTP_GET | HTTP_POST) + // Use separate handlers when different methods need different callbacks + // (e.g. a GET handler and a POST handler with onBody/onUpload callbacks). + std::list _uris; WebRequestMethodComposite _method; ArRequestHandlerFunction _onRequest; ArUploadHandlerFunction _onUpload; @@ -63,8 +68,42 @@ class AsyncCallbackWebHandler : public AsyncWebHandler { bool _isRegex; public: - AsyncCallbackWebHandler() : _uri(), _method(AsyncWebRequestMethod::HTTP_ALL), _onRequest(NULL), _onUpload(NULL), _onBody(NULL), _isRegex(false) {} + AsyncCallbackWebHandler() : _method(AsyncWebRequestMethod::HTTP_ALL), _onRequest(NULL), _onUpload(NULL), _onBody(NULL), _isRegex(false) {} + + /** + * @brief Set the URI matcher, replacing any previously added matchers. + * + * Backward-compatible: same behavior as before when only one URI is used. + * Use addUri() to serve additional URIs with the same handler. + */ void setUri(AsyncURIMatcher uri); + + /** + * @brief Add an additional URI matcher. + * + * The same handler object will serve all added URIs, matched against the + * method mask set via setMethod() (which accepts a composite, e.g. + * `HTTP_GET | HTTP_POST`). + * + * @code + * handler.setMethod(HTTP_GET | HTTP_POST); + * handler.setUri(AsyncURIMatcher::exact("/hello")); + * handler.addUri(AsyncURIMatcher::exact("/hi")); + * handler.addUri(AsyncURIMatcher::prefix("/greet/")); + * @endcode + */ + void addUri(AsyncURIMatcher uri); + + /// Remove all URI matchers (the handler will no longer match any request). + void clearUris(); + + /** + * @brief Set the HTTP method mask for all URIs of this handler. + * + * Accepts a composite: `handler.setMethod(HTTP_GET | HTTP_POST)`. + * Use separate handlers when different methods need different callbacks + * (onBody/onUpload). + */ void setMethod(WebRequestMethodComposite method) { _method = std::move(method); } diff --git a/src/WebHandlers.cpp b/src/WebHandlers.cpp index 5a0977b2e..23a0960fe 100644 --- a/src/WebHandlers.cpp +++ b/src/WebHandlers.cpp @@ -296,14 +296,29 @@ AsyncStaticWebHandler &AsyncStaticWebHandler::setTemplateProcessor(AwsTemplatePr } void AsyncCallbackWebHandler::setUri(AsyncURIMatcher uri) { - _uri = std::move(uri); + _uris.clear(); + _uris.push_back(std::move(uri)); +} + +void AsyncCallbackWebHandler::addUri(AsyncURIMatcher uri) { + _uris.push_back(std::move(uri)); +} + +void AsyncCallbackWebHandler::clearUris() { + _uris.clear(); } bool AsyncCallbackWebHandler::canHandle(AsyncWebServerRequest *request) const { if (!_onRequest || !request->isHTTP() || !_method.matches(request->method())) { return false; } - return _uri.matches(request); + // Match against any of the registered URI matchers + for (const auto &uri : _uris) { + if (uri.matches(request)) { + return true; + } + } + return false; } void AsyncCallbackWebHandler::handleRequest(AsyncWebServerRequest *request) {