Repository navigation
feat: multiple URIs per handler (setUri/addUri/clearUris) - #496
mathieucarbou wants to merge 1 commit into
Conversation
e6ec25c to
2795ff7
Compare
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>
2795ff7 to
db70675
Compare
|
Note: most users having only 1 url matcher per handler would pay the cost of a std::list instead of just 1 uri field. |
Yup - not just a memory space cost (should be ~4 dwords total), it's also an indirection cost (ie. the handler object is now storing a pointer to the matcher, instead of the matcher being a fixed offset from the handler pointer). Given that template<typename handler_t>
class AsyncMultiURL : public handler_t {
private:
std::list<AsyncURIMatcher> _uris;
public:
AsyncMultiURL() : handler_t() {
// We manage the URIs at this layer, so the wrapped handler should match everything
handler_t::setURI(AsyncURIMatcher::all());
}
void setUri(AsyncURIMatcher uri) {
_uris.clear();
_uris.push_back(std::move(uri));
}
void addUri(AsyncURIMatcher uri) {
_uris.push_back(std::move(uri));
}
void clearUris() {
_uris.clear();
}
bool canHandle(AsyncWebServerRequest *request) const {
if (handler_t::canHandle(request)) { // Check other properties
// Match against any of the registered URI matchers
for (const auto &uri : _uris) {
if (uri.matches(request)) {
return true;
}
}
}
return false;
}
};
// Usage
void setup() {
/// ...
static AsyncMultiURL<AsyncCallbackWebHandler> greetHandler;
// --- 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);
} (code above sketched in reply box and totally untested) |
I like that! another first implementation I got was a routing table in the main class containing a list of struct composed of a pointer to a handler in the handler list and a uri matcher. this list would need to be kept in sync with the handler list though but it wouldn’t add too much memory overhead. |
Yeah, I saw it go by when it was accidentally posted earlier. I really liked the idea, especially because separating the URL and method matching from the handler objects is a cleaner design, and would move towards const or constexpr allocation of both matcher and handler. Unfortunately I don't know if it would be possible to be backwards compatible with the current API without essentially having to run both systems in parallel, especially given the extremely general |
Summary
A handler can now serve multiple URIs and accept multiple HTTP methods by holding a list of matchers instead of a single one.
handler.setUri(matcher)handler.addUri(matcher)handler.clearUris()setMethod()accepts a composite mask — one method mask applies to all URIs of a handler:When different methods need different callbacks (e.g.
POSTwithonBody/onUpload), use separate handler objects.Applies to both
AsyncCallbackWebHandlerandAsyncCallbackJsonWebHandler.Motivation
In the #468 discussion, @inF1704 pointed out that URI matching lives inside handler objects (
AsyncCallbackWebHandlercarries_uriand_method), which prevents one handler from serving multiple routes.Instead of introducing a server-side routing table (
addRoute()), this PR moves URI matching to a list inside the handler. The handler declares what URIs it serves — no parallel routing data structure in the server, no split-brain matching logic.Usage
One Handler, Multiple URIs
Multiple URIs × Multiple Methods
Different Methods Need Different Callbacks → Different Handlers
PSRAM Handler on Multiple URIs (ESP32)
Backward Compatibility
Fully backward compatible. Existing code using
server.on(),addHandler(),serveStatic(), and single-URI handlers (setUri()) works identically — no migration required.Changes
src/WebHandlerImpl.hAsyncCallbackWebHandler::_uri→std::list<AsyncURIMatcher> _uris; newaddUri()/clearUris()src/WebHandlers.cppsetUri()clears+pushes;canHandle()iterates_urissrc/AsyncJson.hAsyncCallbackJsonWebHandler::_uri→_uris; inlinesetUri()/addUri()/clearUris()src/AsyncJson.cpp_uris;canHandle()iterates_urisexamples/arduino/MultiRoute/MultiRoute.inodocs/routing.mddocs/configuration.mddocs/concepts.mdplatformio.iniDocumentation