diff --git a/docs/concepts.md b/docs/concepts.md index cc290516..b2af3e41 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. + 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 728d245a..2db1e205 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; 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. diff --git a/docs/routing.md b/docs/routing.md index 2bde77b8..d2f14309 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -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 + +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 00000000..fea3a138 --- /dev/null +++ b/examples/arduino/MultiRoute/MultiRoute.ino @@ -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 +#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 (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. +} diff --git a/platformio.ini b/platformio.ini index 4e48443b..6705bdfc 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 72108add..acf5d2ce 100644 --- a/src/AsyncJson.cpp +++ b/src/AsyncJson.cpp @@ -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); diff --git a/src/AsyncJson.h b/src/AsyncJson.h index 6f0372ce..c4a84199 100644 --- a/src/AsyncJson.h +++ b/src/AsyncJson.h @@ -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 _uris; WebRequestMethodComposite _method; ArJsonRequestHandlerFunction _onRequest; #if ARDUINOJSON_VERSION_MAJOR == 6 @@ -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); } diff --git a/src/WebHandlerImpl.h b/src/WebHandlerImpl.h index fbff491f..7c2201bd 100644 --- a/src/WebHandlerImpl.h +++ b/src/WebHandlerImpl.h @@ -55,7 +55,10 @@ 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. + // Use separate handlers when different URIs need different HTTP methods + // (e.g. a GET handler and a POST handler with onBody/onUpload callbacks). + std::list _uris; WebRequestMethodComposite _method; ArRequestHandlerFunction _onRequest; ArUploadHandlerFunction _onUpload; @@ -63,8 +66,33 @@ 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 + * single method mask set via setMethod(). + * + * @code + * 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(); + void setMethod(WebRequestMethodComposite method) { _method = std::move(method); } diff --git a/src/WebHandlers.cpp b/src/WebHandlers.cpp index 5a0977b2..5a81915d 100644 --- a/src/WebHandlers.cpp +++ b/src/WebHandlers.cpp @@ -296,14 +296,32 @@ 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())) { + if (!_onRequest || !request->isHTTP()) { return false; } - return _uri.matches(request); + if (!_method.matches(request->method())) { + return false; + } + // 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) {