Routing¶
URL Matching and Routing¶
The AsyncURIMatcher class provides flexible and powerful URL routing mechanisms.
Important: When using plain strings (not AsyncURIMatcher objects), the library uses auto-detection (URIMatchAuto) which analyzes the URI pattern and applies appropriate matching rules. This is not simple exact matching - it combines exact and folder matching by default!
Auto-Detection Behavior¶
When you pass a plain string or const char* to server.on(), the URIMatchAuto flag is used, which:
- Empty URI: Matches everything
- Ends with
*: Becomes prefix match (URIMatchPrefix) - Contains
/*.ext: Becomes extension match (URIMatchExtension) - Starts with
^and ends with$: Becomes regex match (if regex enabled) - Everything else: Becomes both exact and folder match (
URIMatchPrefixFolder | URIMatchExact)
This means traditional string-based routes like server.on("/path", handler) will match:
/path(exact match)/path/(folder with trailing slash)/path/anything(folder match)
But will NOT match /path-suffix (prefix without folder separator).
Usage Patterns¶
Traditional String-based Routing (Auto-Detection)¶
// Auto-detection with exact + folder matching
server.on("/api", handler); // Matches /api AND /api/anything
server.on("/login", handler); // Matches /login AND /login/sub
// Auto-detection with prefix matching
server.on("/prefix*", handler); // Matches /prefix, /prefix-test, /prefix/sub
// Auto-detection with extension matching
server.on("/images/*.jpg", handler); // Matches /images/pic.jpg, /images/sub/pic.jpg
Factory Functions¶
Using factory functions is clearer and gives you explicit control over matching behavior.
// Exact matching only (matches "/login" but NOT "/login/sub")
server.on(AsyncURIMatcher::exact("/login"), handler);
// Prefix matching (matches "/api", "/api/v1", "/api-test")
server.on(AsyncURIMatcher::prefix("/api"), handler);
// Directory matching (matches "/admin/users" but NOT "/admin")
// Requires trailing slash in URL conceptually
server.on(AsyncURIMatcher::dir("/admin"), handler);
// Extension matching
server.on(AsyncURIMatcher::ext("/images/*.jpg"), handler);
// Case insensitive matching
server.on(AsyncURIMatcher::exact("/case", AsyncURIMatcher::CaseInsensitive), handler);
// matches "/case", "/CASE", "/CaSe"
// Regular Expression (requires -D ASYNCWEBSERVER_REGEX)
#ifdef ASYNCWEBSERVER_REGEX
server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), handler);
#endif
Param Rewrite With Matching¶
It is possible to rewrite the request url with parameter match. Here is an example with one parameter: Rewrite for example "/radio/{frequence}" -> "/radio?f={frequence}"
class OneParamRewrite : public AsyncWebRewrite
{
protected:
String _urlPrefix;
int _paramIndex;
String _paramsBackup;
public:
OneParamRewrite(const char* from, const char* to)
: AsyncWebRewrite(from, to) {
_paramIndex = _from.indexOf('{');
if( _paramIndex >=0 && _from.endsWith("}")) {
_urlPrefix = _from.substring(0, _paramIndex);
int index = _params.indexOf('{');
if(index >= 0) {
_params = _params.substring(0, index);
}
} else {
_urlPrefix = _from;
}
_paramsBackup = _params;
}
bool match(AsyncWebServerRequest *request) override {
if(request->url().startsWith(_urlPrefix)) {
if(_paramIndex >= 0) {
_params = _paramsBackup + request->url().substring(_paramIndex);
} else {
_params = _paramsBackup;
}
return true;
} else {
return false;
}
}
};
Usage:
See the Rewrite example here.
Handler Ownership and Memory Placement¶
By default, handlers created by server.on(), server.serveStatic(), or passed to server.addHandler(new MyHandler()) are owned by the server — it deletes them on removeHandler(), reset(), or destruction.
When you need to control where a handler object lives in memory (e.g. to use PSRAM on ESP32, or to avoid heap allocation entirely), register it as non-owning:
| Registration | Ownership | Use case |
|---|---|---|
server.addHandler(new MyHandler()) |
Server owns | Default, backward compatible |
server.addHandler(myHandler) |
Non-owning (borrow) | Static or global object (see warning below about stack) |
server.addHandler(*psramHandler) |
Non-owning (borrow) | PSRAM via placement new |
With non-owning registration, you are responsible for keeping the handler alive while it is registered, and for destroying it after unregistration.
[!TIP] For objects that live for the entire lifetime of the application (like handlers), static or global allocation is preferable over heap allocation. It results in simpler code, avoids heap fragmentation, and requires no lifetime tracking overhead.
[!WARNING] Borrowing a stack-allocated object is risky unless you have architected your program to keep it in scope for as long as the server uses it. The usual Arduino-style
setup()/loop()pattern does not guarantee this — a handler declared as a local variable insidesetup()would be destroyed whensetup()returns, leaving the server with a dangling pointer. Usestaticor global storage instead.
Static / Global RAM (no heap allocation)¶
static AsyncWebServer server(80);
// Lives in .bss — zero heap usage
static AsyncCallbackWebHandler statusHandler;
void setup() {
statusHandler.setUri(AsyncURIMatcher::exact("/status"));
statusHandler.onRequest([](AsyncWebServerRequest *request) {
request->send(200, "application/json", "{\"ok\":true}");
});
server.addHandler(statusHandler); // non-owning reference overload
server.begin();
}
PSRAM (ESP32)¶
There are two ways to place a handler in PSRAM:
1. Placement new with heap_caps_malloc:
#include <esp_heap_caps.h>
void setup() {
auto *handler = new (heap_caps_malloc(sizeof(AsyncCallbackWebHandler), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT)) AsyncCallbackWebHandler();
handler->setUri(AsyncURIMatcher::exact("/status"));
handler->onRequest([](AsyncWebServerRequest *request) {
request->send(200, "application/json", "{\"ok\":true}");
});
server.addHandler(*handler); // non-owning borrow — manage PSRAM lifetime yourself
server.begin();
}
2. Static object in PSRAM via EXT_RAM_BSS_ATTR (requires CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y, typically only available when using Arduino as an ESP-IDF component):
EXT_RAM_BSS_ATTR static AsyncCallbackWebHandler statusHandler;
void setup() {
statusHandler.setUri(AsyncURIMatcher::exact("/status"));
statusHandler.onRequest([](AsyncWebServerRequest *request) {
request->send(200, "application/json", "{\"ok\":true}");
});
server.addHandler(statusHandler); // non-owning — handler lives in PSRAM .bss
server.begin();
}
[!NOTE] On ESP32,
deleteworks on objects allocated withheap_caps_malloc()becausedeletemaps tofree()which maps toheap_caps_free(). This is specific to Espressif's implementation (GNU libstdc++ + ESP-IDF libc) and is not guaranteed on all platforms.
Works with All Handler Types¶
The same registration pattern applies to any AsyncWebHandler subclass: AsyncCallbackWebHandler, AsyncStaticWebHandler, AsyncWebSocket, AsyncEventSource, AsyncCallbackJsonWebHandler, or your own custom subclass.
static AsyncWebSocket ws("/ws");
ws.onEvent([](AsyncWebSocket *s, AsyncWebSocketClient *c, AwsEventType t, void *a, uint8_t *d, size_t l) {
if (t == WS_EVT_DATA) s->text(c->id(), "echo");
});
server.addHandler(ws); // non-owning — ws lives in static RAM
Custom Catch-All Handler¶
A catch-all (not-found) handler is always installed. It can be customized in two ways:
At construction time — pass your handler as a second argument to the AsyncWebServer constructor:
| Constructor | Ownership |
|---|---|
AsyncWebServer server(80) |
Server creates & owns a default catch-all |
AsyncWebServer server(80, new AsyncCallbackWebHandler()) |
Server owns |
AsyncWebServer server(80, myHandler) |
Non-owning (borrow by reference) |
AsyncWebServer server(80, *psramHandler) |
Non-owning (borrow from PSRAM) |
static AsyncCallbackWebHandler catchAll;
void setup() {
catchAll.onRequest([](AsyncWebServerRequest *request) {
request->send(404, "text/plain", "Not found: " + request->url());
});
// Pass custom catch-all at construction — non-owning, lives in static RAM
AsyncWebServer server(80, catchAll);
server.begin();
}
After construction — replace it with setCatchAllHandler():
| Registration | Ownership |
|---|---|
server.setCatchAllHandler(new AsyncCallbackWebHandler()) |
Server owns |
server.setCatchAllHandler(myHandler) |
Non-owning (borrow by reference) |
server.setCatchAllHandler(*psramHandler) |
Non-owning (borrow from PSRAM) |
[!NOTE] A catch-all handler is always required — passing
nullptrto the constructor orsetCatchAllHandler()is a programming error (asserts in debug builds).catchAllHandler()safely returns a reference to the installed catch-all.
See the CustomAllocation example for a complete sketch.
Remove handlers and rewrites¶
Server goes through handlers in same order as they were added. You can't simple add handler with same path to override them. To remove handler:
// save callback for particular URL path
auto& handler = server.on("/some/path", [](AsyncWebServerRequest *request){
//do something useful
});
// when you don't need handler anymore remove it
server.removeHandler(&handler);
// same with rewrites
server.removeRewrite(&someRewrite);
server.onNotFound([](AsyncWebServerRequest *request){
request->send(404);
});
// remove server.onNotFound handler
server.onNotFound(NULL);
// remove all rewrites, handlers and onNotFound/onFileUpload/onRequestBody callbacks
server.reset();