Update to version 3.9.6
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include <DNSServer.h>
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
@@ -20,7 +20,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to trigger an async client request from a browser request and send the client response back to the browser through websocket
|
||||
@@ -70,7 +70,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
delay(500);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Authentication and authorization middlewares
|
||||
@@ -29,30 +29,36 @@ static AsyncAuthenticationMiddleware basicAuthHash;
|
||||
static AsyncAuthenticationMiddleware digestAuth;
|
||||
static AsyncAuthenticationMiddleware digestAuthHash;
|
||||
|
||||
static AsyncAuthenticationMiddleware bearerAuthSharedKey;
|
||||
static AsyncAuthenticationMiddleware bearerAuthJWT;
|
||||
|
||||
// complex authentication which adds request attributes for the next middlewares and handler
|
||||
static AsyncMiddlewareFunction complexAuth([](AsyncWebServerRequest *request, ArMiddlewareNext next) {
|
||||
if (!request->authenticate("user", "password")) {
|
||||
if (request->authenticate("Mathieu", "password")) {
|
||||
request->setAttribute("user", "Mathieu");
|
||||
} else if (request->authenticate("Bob", "password")) {
|
||||
request->setAttribute("user", "Bob");
|
||||
} else {
|
||||
return request->requestAuthentication();
|
||||
}
|
||||
|
||||
// add attributes to the request for the next middlewares and handler
|
||||
request->setAttribute("user", "Mathieu");
|
||||
request->setAttribute("role", "staff");
|
||||
if (request->hasParam("token")) {
|
||||
request->setAttribute("token", request->getParam("token")->value().c_str());
|
||||
if (request->getAttribute("user") == "Mathieu") {
|
||||
request->setAttribute("role", "staff");
|
||||
} else {
|
||||
request->setAttribute("role", "user");
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
static AsyncAuthorizationMiddleware authz([](AsyncWebServerRequest *request) {
|
||||
return request->getAttribute("token") == "123";
|
||||
return request->getAttribute("role") == "staff";
|
||||
});
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -87,6 +93,36 @@ void setup() {
|
||||
digestAuthHash.setAuthFailureMessage("Authentication failed");
|
||||
digestAuthHash.setAuthType(AsyncAuthType::AUTH_DIGEST);
|
||||
|
||||
// bearer authentication with shared key
|
||||
bearerAuthSharedKey.setAuthType(AsyncAuthType::AUTH_BEARER);
|
||||
bearerAuthSharedKey.setToken("shared-secret-key");
|
||||
|
||||
// bearer authentication with a JWT token
|
||||
bearerAuthJWT.setAuthType(AsyncAuthType::AUTH_BEARER);
|
||||
bearerAuthJWT.setAuthentificationFunction([](AsyncWebServerRequest *request) {
|
||||
const String &token = request->authChallenge();
|
||||
// 1. decode base64 token
|
||||
// 2. decrypt token
|
||||
const String &decrypted = "..."; // TODO
|
||||
// 3. validate token (check signature, expiration, etc)
|
||||
bool valid = token == "<token>" || token == "<another token>";
|
||||
if (!valid) {
|
||||
return false;
|
||||
}
|
||||
// 4. extract user info from token and set request attributes
|
||||
if (token == "<token>") {
|
||||
request->setAttribute("user", "Mathieu");
|
||||
request->setAttribute("role", "staff");
|
||||
return true; // return true if token is valid, false otherwise
|
||||
}
|
||||
if (token == "<another token>") {
|
||||
request->setAttribute("user", "Bob");
|
||||
request->setAttribute("role", "user");
|
||||
return true; // return true if token is valid, false otherwise
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// basic authentication method
|
||||
// curl -v -u admin:admin http://192.168.4.1/auth-basic
|
||||
server
|
||||
@@ -132,9 +168,9 @@ void setup() {
|
||||
.addMiddleware(&digestAuthHash);
|
||||
|
||||
// test digest auth custom authorization middleware
|
||||
// curl -v --digest -u user:password http://192.168.4.1/auth-custom?token=123 => OK
|
||||
// curl -v --digest -u user:password http://192.168.4.1/auth-custom?token=456 => 403
|
||||
// curl -v --digest -u user:FAILED http://192.168.4.1/auth-custom?token=456 => 401
|
||||
// curl -v --digest -u Mathieu:password http://192.168.4.1/auth-custom => OK
|
||||
// curl -v --digest -u Bob:password http://192.168.4.1/auth-custom => 403
|
||||
// curl -v --digest -u any:password http://192.168.4.1/auth-custom => 401
|
||||
server
|
||||
.on(
|
||||
"/auth-custom", HTTP_GET,
|
||||
@@ -148,6 +184,32 @@ void setup() {
|
||||
)
|
||||
.addMiddlewares({&complexAuth, &authz});
|
||||
|
||||
// Bearer authentication with a shared key
|
||||
// curl -v -H "Authorization: Bearer shared-secret-key" http://192.168.4.1/auth-bearer-shared-key => OK
|
||||
server
|
||||
.on(
|
||||
"/auth-bearer-shared-key", HTTP_GET,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "Hello, world!");
|
||||
}
|
||||
)
|
||||
.addMiddleware(&bearerAuthSharedKey);
|
||||
|
||||
// Bearer authentication with a JWT token
|
||||
// curl -v -H "Authorization: Bearer <token>" http://192.168.4.1/auth-bearer-jwt => OK
|
||||
// curl -v -H "Authorization: Bearer <another token>" http://192.168.4.1/auth-bearer-jwt => 403 Forbidden
|
||||
// curl -v -H "Authorization: Bearer invalid-token" http://192.168.4.1/auth-bearer-jwt => 401 Unauthorized
|
||||
server
|
||||
.on(
|
||||
"/auth-bearer-jwt", HTTP_GET,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
Serial.println("User: " + request->getAttribute("user"));
|
||||
Serial.println("Role: " + request->getAttribute("role"));
|
||||
request->send(200, "text/plain", "Hello, world!");
|
||||
}
|
||||
)
|
||||
.addMiddlewares({&bearerAuthJWT, &authz});
|
||||
|
||||
server.begin();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// How to use CORS middleware
|
||||
@@ -25,7 +25,7 @@ static AsyncCorsMiddleware cors;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
#include <DNSServer.h>
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
@@ -28,7 +28,7 @@ public:
|
||||
response->print("<!DOCTYPE html><html><head><title>Captive Portal</title></head><body>");
|
||||
response->print("<p>This is our captive portal front page.</p>");
|
||||
response->printf("<p>You were trying to reach: http://%s%s</p>", request->host().c_str(), request->url().c_str());
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
response->printf("<p>Try opening <a href='http://%s'>this link</a> instead</p>", WiFi.softAPIP().toString().c_str());
|
||||
#endif
|
||||
response->print("</body></html>");
|
||||
@@ -41,7 +41,7 @@ void setup() {
|
||||
Serial.println();
|
||||
Serial.println("Configuring access point...");
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
if (!WiFi.softAP("esp-captive")) {
|
||||
Serial.println("Soft AP creation failed.");
|
||||
while (1);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to catch all requests and send a 404 Not Found response
|
||||
@@ -86,7 +86,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Chunk response with caching example
|
||||
@@ -86,7 +86,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -94,11 +94,11 @@ void setup() {
|
||||
// first time: serves the file and cache headers
|
||||
// curl -N -v http://192.168.4.1/ --output -
|
||||
//
|
||||
// secodn time: serves 304
|
||||
// second time: serves 304
|
||||
// curl -N -v -H "if-none-match: 4272" http://192.168.4.1/ --output -
|
||||
//
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
String etag = String(htmlContentLength);
|
||||
String etag = "\"" + String(htmlContentLength) + "\""; // RFC9110: ETag must be enclosed in double quotes
|
||||
|
||||
if (request->header(asyncsrv::T_INM) == etag) {
|
||||
request->send(304);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to wait in a chunk response for incoming data
|
||||
@@ -19,12 +19,6 @@
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#include <ArduinoJson.h>
|
||||
#include <AsyncJson.h>
|
||||
#include <AsyncMessagePack.h>
|
||||
#endif
|
||||
|
||||
static const char *htmlContent PROGMEM = R"(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
@@ -96,7 +90,7 @@ static int key = -1;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -107,7 +101,7 @@ void setup() {
|
||||
|
||||
server.addMiddleware(&requestLogger);
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
|
||||
//
|
||||
// HOW TO RUN THIS EXAMPLE:
|
||||
@@ -174,7 +168,7 @@ void setup() {
|
||||
return 0; // 0 means we are done
|
||||
}
|
||||
|
||||
// log_d("UART answered!");
|
||||
// async_ws_log_d("UART answered!");
|
||||
|
||||
String answer = "You typed: ";
|
||||
answer.concat((char)key);
|
||||
@@ -193,10 +187,10 @@ void setup() {
|
||||
},
|
||||
NULL, // upload handler is not used so it should be NULL
|
||||
[](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
|
||||
// log_d("Body: index: %u, len: %u, total: %u", index, len, total);
|
||||
// async_ws_log_d("Body: index: %u, len: %u, total: %u", index, len, total);
|
||||
|
||||
if (!index) {
|
||||
// log_d("Start body parsing");
|
||||
// async_ws_log_d("Start body parsing");
|
||||
request->_tempObject = new String();
|
||||
// cast request->_tempObject pointer to String and reserve total size
|
||||
((String *)request->_tempObject)->reserve(total);
|
||||
@@ -204,7 +198,7 @@ void setup() {
|
||||
request->client()->setRxTimeout(30);
|
||||
}
|
||||
|
||||
// log_d("Append body data");
|
||||
// async_ws_log_d("Append body data");
|
||||
((String *)request->_tempObject)->concat((const char *)data, len);
|
||||
}
|
||||
);
|
||||
@@ -217,13 +211,13 @@ void setup() {
|
||||
void loop() {
|
||||
if (triggerUART.length() && key == -1) {
|
||||
Serial.println(triggerUART);
|
||||
// log_d("Waiting for UART input...");
|
||||
// async_ws_log_d("Waiting for UART input...");
|
||||
while (!Serial.available()) {
|
||||
delay(100);
|
||||
}
|
||||
key = Serial.read();
|
||||
Serial.flush();
|
||||
// log_d("UART input: %c", key);
|
||||
// async_ws_log_d("UART input: %c", key);
|
||||
triggerUART = emptyString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// https://github.com/ESP32Async/ESPAsyncWebServer/discussions/23
|
||||
@@ -24,7 +24,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -39,6 +39,10 @@ void setup() {
|
||||
|
||||
Serial.println("end()");
|
||||
server.end();
|
||||
|
||||
Serial.println("waiting before restarting server...");
|
||||
delay(100);
|
||||
|
||||
server.begin();
|
||||
Serial.println("begin() - run: curl -v http://192.168.4.1/ => should succeed");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to use setFilter to route requests to different handlers based on WiFi mode
|
||||
@@ -32,7 +32,7 @@ public:
|
||||
response->print("<!DOCTYPE html><html><head><title>Captive Portal</title></head><body>");
|
||||
response->print("<p>This is out captive portal front page.</p>");
|
||||
response->printf("<p>You were trying to reach: http://%s%s</p>", request->host().c_str(), request->url().c_str());
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
response->printf("<p>Try opening <a href='http://%s'>this link</a> instead</p>", WiFi.softAPIP().toString().c_str());
|
||||
#endif
|
||||
response->print("</body></html>");
|
||||
@@ -51,17 +51,17 @@ void setup() {
|
||||
"/", HTTP_GET,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
Serial.println("Captive portal request...");
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
Serial.println("WiFi.localIP(): " + WiFi.localIP().toString());
|
||||
#endif
|
||||
Serial.println("request->client()->localIP(): " + request->client()->localIP().toString());
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
Serial.println("WiFi.type(): " + String((int)WiFi.localIP().type()));
|
||||
#endif
|
||||
Serial.println("request->client()->type(): " + String((int)request->client()->localIP().type()));
|
||||
#endif
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
Serial.println(WiFi.localIP() == request->client()->localIP() ? "should be: ON_STA_FILTER" : "should be: ON_AP_FILTER");
|
||||
Serial.println(WiFi.localIP() == request->client()->localIP());
|
||||
Serial.println(WiFi.localIP().toString() == request->client()->localIP().toString());
|
||||
@@ -77,17 +77,17 @@ void setup() {
|
||||
"/", HTTP_GET,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
Serial.println("Website request...");
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
Serial.println("WiFi.localIP(): " + WiFi.localIP().toString());
|
||||
#endif
|
||||
Serial.println("request->client()->localIP(): " + request->client()->localIP().toString());
|
||||
#if ESP_IDF_VERSION_MAJOR >= 5
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
Serial.println("WiFi.type(): " + String((int)WiFi.localIP().type()));
|
||||
#endif
|
||||
Serial.println("request->client()->type(): " + String((int)request->client()->localIP().type()));
|
||||
#endif
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
Serial.println(WiFi.localIP() == request->client()->localIP() ? "should be: ON_STA_FILTER" : "should be: ON_AP_FILTER");
|
||||
Serial.println(WiFi.localIP() == request->client()->localIP());
|
||||
Serial.println(WiFi.localIP().toString() == request->client()->localIP().toString());
|
||||
@@ -113,7 +113,7 @@ void setup() {
|
||||
// dnsServer.stop();
|
||||
// WiFi.softAPdisconnect();
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.persistent(false);
|
||||
WiFi.begin("IoT");
|
||||
while (WiFi.status() != WL_CONNECTED) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to serve a large HTML page from flash memory without copying it to heap in a temporary buffer
|
||||
@@ -86,7 +86,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Show how to manipulate headers in the request / response
|
||||
@@ -33,7 +33,7 @@ AsyncHeaderFreeMiddleware headerFree;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Query and send headers
|
||||
@@ -24,7 +24,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to send and receive Json data
|
||||
@@ -19,27 +19,21 @@
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#include <ArduinoJson.h>
|
||||
#include <AsyncJson.h>
|
||||
#include <AsyncMessagePack.h>
|
||||
#endif
|
||||
|
||||
static AsyncWebServer server(80);
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
static AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler("/json2");
|
||||
#endif
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
//
|
||||
// sends JSON using AsyncJsonResponse
|
||||
//
|
||||
@@ -62,8 +56,8 @@ void setup() {
|
||||
JsonDocument doc;
|
||||
JsonObject root = doc.to<JsonObject>();
|
||||
root["foo"] = "bar";
|
||||
serializeJson(root, *response);
|
||||
Serial.println();
|
||||
// serializeJson(root, Serial);
|
||||
// Serial.println();
|
||||
request->send(response);
|
||||
});
|
||||
|
||||
@@ -92,12 +86,38 @@ void setup() {
|
||||
});
|
||||
|
||||
server.addHandler(handler);
|
||||
|
||||
// New Json API since 3.8.2, which works for both Json and MessagePack bodies
|
||||
// curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"You"}' http://192.168.4.1/json3
|
||||
|
||||
server.on("/json3", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
Serial.printf("Body request : ");
|
||||
serializeJson(json, Serial);
|
||||
Serial.println();
|
||||
AsyncJsonResponse *response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot().to<JsonObject>();
|
||||
root["hello"] = json.as<JsonObject>()["name"];
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
});
|
||||
#endif
|
||||
|
||||
server.begin();
|
||||
}
|
||||
|
||||
// not needed
|
||||
static uint32_t lastHeapTime = 0;
|
||||
static uint32_t lastHeap = 0;
|
||||
|
||||
void loop() {
|
||||
delay(100);
|
||||
#ifdef ESP32
|
||||
uint32_t now = millis();
|
||||
if (now - lastHeapTime >= 500) {
|
||||
uint32_t heap = ESP.getFreeHeap();
|
||||
if (heap != lastHeap) {
|
||||
lastHeap = heap;
|
||||
async_ws_log_w("Free heap: %" PRIu32, heap);
|
||||
}
|
||||
lastHeapTime = now;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
178
examples/LargeResponse/LargeResponse.ino
Normal file
178
examples/LargeResponse/LargeResponse.ino
Normal file
@@ -0,0 +1,178 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Example to send a large response and control the filling of the buffer.
|
||||
//
|
||||
// This is also a MRE for:
|
||||
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/242
|
||||
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/315
|
||||
//
|
||||
|
||||
#include <Arduino.h>
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
#include <AsyncTCP.h>
|
||||
#include <WiFi.h>
|
||||
#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);
|
||||
|
||||
static const size_t totalResponseSize = 16 * 1000; // 16 KB
|
||||
static char fillChar = 'A';
|
||||
|
||||
class CustomResponse : public AsyncAbstractResponse {
|
||||
public:
|
||||
explicit CustomResponse() {
|
||||
_code = 200;
|
||||
_contentType = "text/plain";
|
||||
_sendContentLength = false;
|
||||
}
|
||||
|
||||
bool _sourceValid() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t _fillBuffer(uint8_t *buf, size_t buflen) override {
|
||||
if (_sent == RESPONSE_TRY_AGAIN) {
|
||||
Serial.println("Simulating temporary unavailability of data...");
|
||||
_sent = 0;
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
size_t remaining = totalResponseSize - _sent;
|
||||
if (remaining == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (buflen > remaining) {
|
||||
buflen = remaining;
|
||||
}
|
||||
Serial.printf("Filling '%c' @ sent: %u, buflen: %u\n", fillChar, _sent, buflen);
|
||||
std::fill_n(buf, buflen, static_cast<uint8_t>(fillChar));
|
||||
_sent += buflen;
|
||||
fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1;
|
||||
return buflen;
|
||||
}
|
||||
|
||||
private:
|
||||
char fillChar = 'A';
|
||||
size_t _sent = 0;
|
||||
};
|
||||
|
||||
// Code to reproduce issues:
|
||||
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/242
|
||||
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/315
|
||||
//
|
||||
// https://github.com/ESP32Async/ESPAsyncWebServer/pull/317#issuecomment-3421141039
|
||||
//
|
||||
// I cracked it.
|
||||
// So this is how it works:
|
||||
// That space that _tcp is writing to identified by CONFIG_TCP_SND_BUF_DEFAULT (and is value-matching with default TCP windows size which is very confusing itself).
|
||||
// The space returned by client()->write() and client->space() somehow might not be atomically/thread synced (had not dived that deep yet). So if first call to _fillBuffer is done via user-code thread and ended up with some small amount of data consumed and second one is done by _poll or _ack? returns full size again! This is where old code fails.
|
||||
// If you change your class this way it will fail 100%.
|
||||
class CustomResponseMRE : public AsyncAbstractResponse {
|
||||
public:
|
||||
explicit CustomResponseMRE() {
|
||||
_code = 200;
|
||||
_contentType = "text/plain";
|
||||
_sendContentLength = false;
|
||||
// add some useless headers
|
||||
addHeader("Clear-Site-Data", "Clears browsing data (e.g., cookies, storage, cache) associated with the requesting website.");
|
||||
addHeader(
|
||||
"No-Vary-Search", "Specifies a set of rules that define how a URL's query parameters will affect cache matching. These rules dictate whether the same "
|
||||
"URL with different URL parameters should be saved as separate browser cache entries"
|
||||
);
|
||||
}
|
||||
|
||||
bool _sourceValid() const override {
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t _fillBuffer(uint8_t *buf, size_t buflen) override {
|
||||
if (fillChar == NULL) {
|
||||
fillChar = 'A';
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
if (_sent == RESPONSE_TRY_AGAIN) {
|
||||
Serial.println("Simulating temporary unavailability of data...");
|
||||
_sent = 0;
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
size_t remaining = totalResponseSize - _sent;
|
||||
if (remaining == 0) {
|
||||
return 0;
|
||||
}
|
||||
if (buflen > remaining) {
|
||||
buflen = remaining;
|
||||
}
|
||||
Serial.printf("Filling '%c' @ sent: %u, buflen: %u\n", fillChar, _sent, buflen);
|
||||
std::fill_n(buf, buflen, static_cast<uint8_t>(fillChar));
|
||||
_sent += buflen;
|
||||
fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1;
|
||||
return buflen;
|
||||
}
|
||||
|
||||
private:
|
||||
char fillChar = NULL;
|
||||
size_t _sent = 0;
|
||||
};
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
// Example to use a AwsResponseFiller
|
||||
//
|
||||
// curl -v http://192.168.4.1/1 | grep -o '.' | sort | uniq -c
|
||||
//
|
||||
// Should output 16000 and a distribution of letters which is the same in ESP32 logs and console
|
||||
//
|
||||
server.on("/1", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
fillChar = 'A';
|
||||
AsyncWebServerResponse *response = request->beginResponse("text/plain", totalResponseSize, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
|
||||
size_t remaining = totalResponseSize - index;
|
||||
size_t toSend = (remaining < maxLen) ? remaining : maxLen;
|
||||
Serial.printf("Filling '%c' @ index: %u, maxLen: %u, toSend: %u\n", fillChar, index, maxLen, toSend);
|
||||
std::fill_n(buffer, toSend, static_cast<uint8_t>(fillChar));
|
||||
fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1;
|
||||
return toSend;
|
||||
});
|
||||
request->send(response);
|
||||
});
|
||||
|
||||
// Example to use a AsyncAbstractResponse
|
||||
//
|
||||
// curl -v http://192.168.4.1/2 | grep -o '.' | sort | uniq -c
|
||||
//
|
||||
// Should output 16000 and a distribution of letters which is the same in ESP32 logs and console
|
||||
//
|
||||
server.on("/2", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(new CustomResponse());
|
||||
});
|
||||
|
||||
// Example to use a AsyncAbstractResponse
|
||||
//
|
||||
// curl -v http://192.168.4.1/3 | grep -o '.' | sort | uniq -c
|
||||
//
|
||||
// Should output 16000 and a distribution of letters which is the same in ESP32 logs and console
|
||||
//
|
||||
server.on("/3", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(new CustomResponseMRE());
|
||||
});
|
||||
|
||||
server.begin();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
delay(100);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Show how to log the incoming request and response as a curl-like syntax
|
||||
@@ -25,7 +25,7 @@ static AsyncLoggingMiddleware requestLogger;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to send and receive Message Pack data
|
||||
@@ -19,27 +19,21 @@
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#include <ArduinoJson.h>
|
||||
#include <AsyncJson.h>
|
||||
#include <AsyncMessagePack.h>
|
||||
#endif
|
||||
|
||||
static AsyncWebServer server(80);
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
static AsyncCallbackMessagePackWebHandler *handler = new AsyncCallbackMessagePackWebHandler("/msgpack2");
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
static AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler("/msgpack2");
|
||||
#endif
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
#if __has_include("ArduinoJson.h")
|
||||
#if ASYNC_JSON_SUPPORT == 1
|
||||
//
|
||||
// sends MessagePack using AsyncMessagePackResponse
|
||||
//
|
||||
@@ -57,18 +51,26 @@ void setup() {
|
||||
//
|
||||
// curl -v http://192.168.4.1/msgpack2
|
||||
//
|
||||
// Save file: curl -v http://192.168.4.1/msgpack2 -o msgpack.bin
|
||||
//
|
||||
server.on("/msgpack2", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
AsyncResponseStream *response = request->beginResponseStream("application/msgpack");
|
||||
JsonDocument doc;
|
||||
JsonObject root = doc.to<JsonObject>();
|
||||
root["foo"] = "bar";
|
||||
root["name"] = "Bob";
|
||||
serializeMsgPack(root, *response);
|
||||
request->send(response);
|
||||
});
|
||||
|
||||
// POST file:
|
||||
//
|
||||
// curl -v -X POST -H 'Content-Type: application/msgpack' --data-binary @msgpack.bin http://192.168.4.1/msgpack2
|
||||
//
|
||||
handler->setMethod(HTTP_POST | HTTP_PUT);
|
||||
handler->onRequest([](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
Serial.printf("Body request /msgpack2 : "); // should print: Body request /msgpack2 : {"name":"Bob"}
|
||||
serializeJson(json, Serial);
|
||||
Serial.println();
|
||||
AsyncMessagePackResponse *response = new AsyncMessagePackResponse();
|
||||
JsonObject root = response->getRoot().to<JsonObject>();
|
||||
root["hello"] = json.as<JsonObject>()["name"];
|
||||
@@ -77,6 +79,22 @@ void setup() {
|
||||
});
|
||||
|
||||
server.addHandler(handler);
|
||||
|
||||
// New Json API since 3.8.2, which works for both Json and MessagePack bodies
|
||||
//
|
||||
// curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"You"}' http://192.168.4.1/msgpack3
|
||||
// curl -v -X POST -H 'Content-Type: application/msgpack' --data-binary @msgpack.bin http://192.168.4.1/msgpack3
|
||||
//
|
||||
server.on("/msgpack3", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &json) {
|
||||
Serial.printf("Body request /msgpack3 : "); // should print: Body request /msgpack3 : {"name":"Bob"}
|
||||
serializeJson(json, Serial);
|
||||
Serial.println();
|
||||
AsyncJsonResponse *response = new AsyncJsonResponse();
|
||||
JsonObject root = response->getRoot().to<JsonObject>();
|
||||
root["hello"] = json.as<JsonObject>()["name"];
|
||||
response->setLength();
|
||||
request->send(response);
|
||||
});
|
||||
#endif
|
||||
|
||||
server.begin();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Show how to sue Middleware
|
||||
@@ -34,7 +34,7 @@ public:
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Query parameters and body parameters
|
||||
@@ -74,7 +74,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// - Download ESP32 partition by name and/or type and/or subtype
|
||||
@@ -34,7 +34,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Perf tests
|
||||
@@ -91,7 +91,7 @@ static volatile size_t requests = 0;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -118,9 +118,8 @@ void setup() {
|
||||
|
||||
// HTTP endpoint
|
||||
//
|
||||
// > brew install autocannon
|
||||
// > autocannon -c 10 -w 10 -d 20 http://192.168.4.1
|
||||
// > autocannon -c 16 -w 16 -d 20 http://192.168.4.1
|
||||
// > autocannon -c 16 -w 16 -d 20 --renderStatusCodes http://192.168.4.1/
|
||||
// > ab -c 16 -t 20 http://192.168.4.1/
|
||||
//
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
// need to cast to uint8_t*
|
||||
@@ -142,6 +141,11 @@ void setup() {
|
||||
//
|
||||
// time curl -N -v -G -d 'd=2000' -d 'l=10000' http://192.168.4.1/slow.html --output -
|
||||
//
|
||||
// THIS CODE WILL CRASH BECAUSE OF THE WATCHDOG.
|
||||
// IF YOU REALLY NEED TO DO THIS, YOU MUST DISABLE THE TWDT
|
||||
//
|
||||
// CORRECT WAY IS TO USE SSE OR WEBSOCKETS TO DO THE COSTLY PROCESSING ASYNC.
|
||||
//
|
||||
server.on("/slow.html", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
requests = requests + 1;
|
||||
uint32_t d = request->getParam("d")->value().toInt();
|
||||
@@ -168,7 +172,6 @@ void setup() {
|
||||
// SSS endpoint
|
||||
//
|
||||
// launch 16 concurrent workers for 30 seconds
|
||||
// > for i in {1..10}; do ( count=$(gtimeout 30 curl -s -N -H "Accept: text/event-stream" http://192.168.4.1/events 2>&1 | grep -c "^data:"); echo "Total: $count events, $(echo "$count / 4" | bc -l) events / second" ) & done;
|
||||
// > for i in {1..16}; do ( count=$(gtimeout 30 curl -s -N -H "Accept: text/event-stream" http://192.168.4.1/events 2>&1 | grep -c "^data:"); echo "Total: $count events, $(echo "$count / 4" | bc -l) events / second" ) & done;
|
||||
//
|
||||
// With AsyncTCP, with 16 workers: a lot of "Event message queue overflow: discard message", no crash
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Show how to rate limit the server or some endpoints
|
||||
@@ -25,7 +25,7 @@ static AsyncRateLimitMiddleware rateLimit;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to redirect
|
||||
@@ -24,7 +24,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to use request continuation to pause a request for a long processing task, and be able to resume it later.
|
||||
@@ -34,7 +34,7 @@ static AsyncWebServerRequestPtr gpioRequest;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to use request continuation to pause a request for a long processing task, and be able to resume it later.
|
||||
@@ -94,7 +94,7 @@ static bool processLongRunningOperation(LongRunningOperation *op) {
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Make sure resumable downloads can be implemented (HEAD request / response and Range header)
|
||||
@@ -24,7 +24,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to rewrite URLs
|
||||
@@ -24,7 +24,7 @@ static AsyncWebServer server(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// SSE example
|
||||
@@ -58,7 +58,7 @@ static AsyncEventSource events("/events");
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -71,12 +71,12 @@ void setup() {
|
||||
});
|
||||
|
||||
events.onConnect([](AsyncEventSourceClient *client) {
|
||||
Serial.printf("SSE Client connected! ID: %" PRIu32 "\n", client->lastId());
|
||||
Serial.printf("SSE Client connected!");
|
||||
client->send("hello!", NULL, millis(), 1000);
|
||||
});
|
||||
|
||||
events.onDisconnect([](AsyncEventSourceClient *client) {
|
||||
Serial.printf("SSE Client disconnected! ID: %" PRIu32 "\n", client->lastId());
|
||||
Serial.printf("SSE Client disconnected!");
|
||||
});
|
||||
|
||||
server.addHandler(&events);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// SSE example
|
||||
@@ -64,7 +64,7 @@ static constexpr uint32_t timeoutClose = 15000;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Server state example
|
||||
@@ -25,7 +25,7 @@ static AsyncWebServer server2(80);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Authentication and authorization middlewares
|
||||
@@ -27,7 +27,7 @@ static AsyncLoggingMiddleware logging;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Simulate a slow response in a chunk response (like file download from SD Card)
|
||||
@@ -89,7 +89,7 @@ static size_t charactersIndex = 0;
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -114,6 +114,11 @@ void setup() {
|
||||
//
|
||||
// time curl -N -v -G -d 'd=2000' -d 'l=10000' http://192.168.4.1/slow.html --output -
|
||||
//
|
||||
// THIS CODE WILL CRASH BECAUSE OF THE WATCHDOG.
|
||||
// IF YOU REALLY NEED TO DO THIS, YOU MUST DISABLE THE TWDT
|
||||
//
|
||||
// CORRECT WAY IS TO USE SSE OR WEBSOCKETS TO DO THE COSTLY PROCESSING ASYNC.
|
||||
//
|
||||
server.on("/slow.html", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
uint32_t d = request->getParam("d")->value().toInt();
|
||||
uint32_t l = request->getParam("l")->value().toInt();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to serve a static file
|
||||
@@ -111,7 +111,7 @@ static const size_t index2_html_gz_len = sizeof(index2_html_gz);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Shows how to serve a static and dynamic template
|
||||
@@ -33,10 +33,23 @@ static const char *htmlContent PROGMEM = R"(
|
||||
|
||||
static const size_t htmlContentLength = strlen_P(htmlContent);
|
||||
|
||||
// Variables used for dynamic cacheable template
|
||||
static unsigned uptimeInMinutes = 0;
|
||||
static AsyncStaticWebHandler *uptimeHandler = nullptr;
|
||||
|
||||
// Utility function for performing that update
|
||||
static void setUptimeInMinutes(unsigned t) {
|
||||
uptimeInMinutes = t;
|
||||
// Update caching header with a new value as well
|
||||
if (uptimeHandler) {
|
||||
uptimeHandler->setLastModified();
|
||||
}
|
||||
}
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -56,35 +69,68 @@ void setup() {
|
||||
|
||||
// Serve the static template file
|
||||
//
|
||||
// This call will have caching headers automatically added as it is a static file.
|
||||
//
|
||||
// curl -v http://192.168.4.1/template.html
|
||||
server.serveStatic("/template.html", LittleFS, "/template.html");
|
||||
|
||||
// Serve the static template with a template processor
|
||||
// Serve a template with dynamic content
|
||||
//
|
||||
// ServeStatic static is used to serve static output which never changes over time.
|
||||
// This special endpoints automatically adds caching headers.
|
||||
// If a template processor is used, it must ensure that the outputted content will always be the same over time and never changes.
|
||||
// Otherwise, do not use serveStatic.
|
||||
// Example below: IP never changes.
|
||||
// serveStatic recognizes that template processing is in use, and will not automatically
|
||||
// add caching headers.
|
||||
//
|
||||
// curl -v http://192.168.4.1/index.html
|
||||
server.serveStatic("/index.html", LittleFS, "/template.html").setTemplateProcessor([](const String &var) -> String {
|
||||
// curl -v http://192.168.4.1/dynamic.html
|
||||
server.serveStatic("/dynamic.html", LittleFS, "/template.html").setTemplateProcessor([](const String &var) -> String {
|
||||
if (var == "USER") {
|
||||
return "Bob";
|
||||
return String("Bob ") + millis();
|
||||
}
|
||||
return emptyString;
|
||||
});
|
||||
|
||||
// Serve a template with dynamic content
|
||||
// Serve a static template with a template processor
|
||||
//
|
||||
// to serve a template with dynamic content (output changes over time), use normal
|
||||
// Example below: content changes over tinme do not use serveStatic.
|
||||
// By explicitly calling setLastModified() on the handler object, we enable
|
||||
// sending the caching headers, even when a template is in use.
|
||||
// This pattern should never be used with template data that can change.
|
||||
// Example below: USER never changes.
|
||||
//
|
||||
// curl -v http://192.168.4.1/dynamic.html
|
||||
server.on("/dynamic.html", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(LittleFS, "/template.html", "text/html", false, [](const String &var) -> String {
|
||||
// curl -v http://192.168.4.1/index.html
|
||||
server.serveStatic("/index.html", LittleFS, "/template.html")
|
||||
.setTemplateProcessor([](const String &var) -> String {
|
||||
if (var == "USER") {
|
||||
return String("Bob ") + millis();
|
||||
return "Bob";
|
||||
}
|
||||
return emptyString;
|
||||
})
|
||||
.setLastModified("Sun, 28 Sep 2025 01:02:03 GMT");
|
||||
|
||||
// Serve a template with dynamic content *and* caching
|
||||
//
|
||||
// The data used in this template is updated in loop(). loop() is then responsible
|
||||
// for calling setLastModified() on the handler object to notify any caches that
|
||||
// the data has changed.
|
||||
//
|
||||
// curl -v http://192.168.4.1/uptime.html
|
||||
uptimeHandler = &server.serveStatic("/uptime.html", LittleFS, "/template.html").setTemplateProcessor([](const String &var) -> String {
|
||||
if (var == "USER") {
|
||||
return String("Bob ") + uptimeInMinutes + " minutes";
|
||||
}
|
||||
return emptyString;
|
||||
});
|
||||
|
||||
// Serve a template with dynamic content based on user request
|
||||
//
|
||||
// In this case, the template is served via a callback request. Data from the request
|
||||
// is used to generate the template callback.
|
||||
//
|
||||
// curl -v -G -d "USER=Bob" http://192.168.4.1/user_request.html
|
||||
server.on("/user_request.html", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(LittleFS, "/template.html", "text/html", false, [=](const String &var) -> String {
|
||||
if (var == "USER") {
|
||||
const AsyncWebParameter *param = request->getParam("USER");
|
||||
if (param) {
|
||||
return param->value();
|
||||
}
|
||||
}
|
||||
return emptyString;
|
||||
});
|
||||
@@ -96,4 +142,11 @@ void setup() {
|
||||
// not needed
|
||||
void loop() {
|
||||
delay(100);
|
||||
|
||||
// Compute uptime
|
||||
unsigned currentUptimeInMinutes = millis() / (60 * 1000);
|
||||
|
||||
if (currentUptimeInMinutes != uptimeInMinutes) {
|
||||
setUptimeInMinutes(currentUptimeInMinutes);
|
||||
}
|
||||
}
|
||||
|
||||
349
examples/URIMatcher/README.md
Normal file
349
examples/URIMatcher/README.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# AsyncURIMatcher Example
|
||||
|
||||
This example demonstrates the comprehensive URI matching capabilities of the ESPAsyncWebServer library using the `AsyncURIMatcher` class.
|
||||
|
||||
## Overview
|
||||
|
||||
The `AsyncURIMatcher` class provides flexible and powerful URL routing mechanisms that go beyond simple string matching. It supports various matching strategies that can be combined to create sophisticated routing rules.
|
||||
|
||||
**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!
|
||||
|
||||
## What's Demonstrated
|
||||
|
||||
This example includes two Arduino sketches:
|
||||
|
||||
1. **URIMatcher.ino** - Interactive web-based demonstration with a user-friendly homepage
|
||||
2. **URIMatcherTest.ino** - Comprehensive test suite with automated shell script testing
|
||||
|
||||
Both sketches create a WiFi Access Point (`esp-captive`) for easy testing without network configuration.
|
||||
|
||||
## Auto-Detection Behavior
|
||||
|
||||
When you pass a plain string or `const char*` to `server.on()`, the `URIMatchAuto` flag is used, which:
|
||||
|
||||
1. **Empty URI**: Matches everything
|
||||
2. **Ends with `*`**: Becomes prefix match (`URIMatchPrefix`)
|
||||
3. **Contains `/*.ext`**: Becomes extension match (`URIMatchExtension`)
|
||||
4. **Starts with `^` and ends with `$`**: Becomes regex match (if enabled)
|
||||
5. **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).
|
||||
|
||||
## Features Demonstrated
|
||||
|
||||
### 1. **Auto-Detection (Traditional Behavior)**
|
||||
|
||||
Demonstrates how traditional string-based routing automatically combines exact and folder matching.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `/auto` - Matches both `/auto` exactly AND `/auto/sub` as folder
|
||||
- `/wildcard*` - Auto-detects as prefix match (due to trailing `*`)
|
||||
- `/auto-images/*.png` - Auto-detects as extension match (due to `/*.ext` pattern)
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- `/exact` - Matches `/exact`, `/exact/`, and `/exact/sub`
|
||||
- `/api/users` - Matches exact path and subpaths under `/api/users/`
|
||||
- `/*.json` - Matches any `.json` file anywhere
|
||||
- `/*.css` - Matches any `.css` file anywhere
|
||||
|
||||
### 2. **Exact Matching (Factory Method)**
|
||||
|
||||
Using `AsyncURIMatcher::exact()` matches only the exact URL, **NOT** subpaths.
|
||||
|
||||
**Key difference from auto-detection:** `AsyncURIMatcher::exact("/path")` matches **only** `/path`, while `server.on("/path", ...)` matches both `/path` and `/path/sub`.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `AsyncURIMatcher::exact("/exact")` - Matches only `/exact`
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- `AsyncURIMatcher::exact("/factory/exact")` - Matches only `/factory/exact`
|
||||
- Does NOT match `/factory/exact/sub` (404 response)
|
||||
|
||||
### 3. **Prefix Matching**
|
||||
|
||||
Using `AsyncURIMatcher::prefix()` matches URLs that start with the specified pattern.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `AsyncURIMatcher::prefix("/service")` - Matches `/service`, `/service-test`, `/service/status`
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- `AsyncURIMatcher::prefix("/factory/prefix")` - Matches `/factory/prefix`, `/factory/prefix-test`, `/factory/prefix/sub`
|
||||
- Traditional: `/api/*` - Matches `/api/data`, `/api/v1/posts`
|
||||
- Traditional: `/files/*` - Matches `/files/document.pdf`, `/files/images/photo.jpg`
|
||||
|
||||
### 4. **Folder/Directory Matching**
|
||||
|
||||
Using `AsyncURIMatcher::dir()` matches URLs under a directory (automatically adds trailing slash).
|
||||
|
||||
**Important:** Directory matching requires a trailing slash in the URL - it does NOT match the directory itself.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `AsyncURIMatcher::dir("/admin")` - Matches `/admin/users`, `/admin/settings`
|
||||
- Does NOT match `/admin` without trailing slash
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- `AsyncURIMatcher::dir("/factory/dir")` - Matches `/factory/dir/users`, `/factory/dir/sub/path`
|
||||
- Does NOT match `/factory/dir` itself (404 response)
|
||||
|
||||
### 5. **Extension Matching**
|
||||
|
||||
Using `AsyncURIMatcher::ext()` matches files with specific extensions.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `AsyncURIMatcher::ext("/images/*.jpg")` - Matches `/images/photo.jpg`, `/images/sub/pic.jpg`
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- `AsyncURIMatcher::ext("/factory/files/*.txt")` - Matches `/factory/files/doc.txt`, `/factory/files/sub/readme.txt`
|
||||
- Does NOT match `/factory/files/doc.pdf` (wrong extension)
|
||||
|
||||
### 6. **Case Insensitive Matching**
|
||||
|
||||
Using `AsyncURIMatcher::CaseInsensitive` flag matches URLs regardless of character case.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `AsyncURIMatcher::exact("/case", AsyncURIMatcher::CaseInsensitive)` - Matches `/case`, `/CASE`, `/CaSe`
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- Case insensitive exact: `/case/exact`, `/CASE/EXACT`, `/Case/Exact` all work
|
||||
- Case insensitive prefix: `/case/prefix`, `/CASE/PREFIX-test`, `/Case/Prefix/sub` all work
|
||||
- Case insensitive directory: `/case/dir/users`, `/CASE/DIR/admin`, `/Case/Dir/settings` all work
|
||||
- Case insensitive extension: `/case/files/doc.pdf`, `/CASE/FILES/DOC.PDF`, `/Case/Files/Doc.Pdf` all work
|
||||
|
||||
### 7. **Regular Expression Matching**
|
||||
|
||||
Using `AsyncURIMatcher::regex()` for advanced pattern matching (requires `ASYNCWEBSERVER_REGEX`).
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
```cpp
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
AsyncURIMatcher::regex("^/user/([0-9]+)$") // Matches /user/123, captures ID
|
||||
#endif
|
||||
```
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- Traditional regex: `^/user/([0-9]+)$` - Matches `/user/123`, `/user/456`
|
||||
- Traditional regex: `^/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$` - Matches `/blog/2023/10/15`
|
||||
- Factory regex: `AsyncURIMatcher::regex("^/factory/user/([0-9]+)$")` - Matches `/factory/user/123`
|
||||
- Factory regex with multiple captures: `^/factory/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$`
|
||||
- Case insensitive regex: `AsyncURIMatcher::regex("^/factory/search/(.+)$", AsyncURIMatcher::CaseInsensitive)`
|
||||
|
||||
### 8. **Combined Flags**
|
||||
|
||||
Multiple matching strategies can be combined using the `|` operator.
|
||||
|
||||
**Examples in URIMatcher.ino:**
|
||||
|
||||
- `AsyncURIMatcher::prefix("/MixedCase", AsyncURIMatcher::CaseInsensitive)` - Prefix match that's case insensitive
|
||||
|
||||
### 9. **Special Matchers**
|
||||
|
||||
**Examples in URIMatcherTest.ino:**
|
||||
|
||||
- `AsyncURIMatcher::all()` - Matches all requests (used with POST method as catch-all)
|
||||
|
||||
## Usage Patterns
|
||||
|
||||
### Traditional String-based Routing (Auto-Detection)
|
||||
|
||||
```cpp
|
||||
// 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
|
||||
```
|
||||
|
||||
### Explicit AsyncURIMatcher Syntax
|
||||
|
||||
### Explicit AsyncURIMatcher Syntax
|
||||
|
||||
```cpp
|
||||
// Exact matching only
|
||||
server.on(AsyncURIMatcher("/path", URIMatchExact), handler);
|
||||
|
||||
// Prefix matching only
|
||||
server.on(AsyncURIMatcher("/api", URIMatchPrefix), handler);
|
||||
|
||||
// Combined flags
|
||||
server.on(AsyncURIMatcher("/api", URIMatchPrefix | URIMatchCaseInsensitive), handler);
|
||||
```
|
||||
|
||||
### Factory Functions
|
||||
|
||||
```cpp
|
||||
// More readable and expressive
|
||||
server.on(AsyncURIMatcher::exact("/login"), handler);
|
||||
server.on(AsyncURIMatcher::prefix("/api"), handler);
|
||||
server.on(AsyncURIMatcher::dir("/admin"), handler);
|
||||
server.on(AsyncURIMatcher::ext("/images/*.jpg"), handler);
|
||||
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), handler);
|
||||
#endif
|
||||
```
|
||||
|
||||
## Available Flags
|
||||
|
||||
| Flag | Description |
|
||||
| ------------------------- | ----------------------------------------------------------- |
|
||||
| `URIMatchAuto` | Auto-detect match type from pattern (default) |
|
||||
| `URIMatchExact` | Exact URL match |
|
||||
| `URIMatchPrefix` | Prefix match |
|
||||
| `URIMatchPrefixFolder` | Folder prefix match (requires trailing /) |
|
||||
| `URIMatchExtension` | File extension match pattern |
|
||||
| `URIMatchCaseInsensitive` | Case insensitive matching |
|
||||
| `URIMatchRegex` | Regular expression matching (requires ASYNCWEBSERVER_REGEX) |
|
||||
|
||||
## Testing the Example
|
||||
|
||||
1. **Upload the sketch** to your ESP32/ESP8266
|
||||
2. **Connect to WiFi AP**: `esp-captive` (no password required)
|
||||
3. **Navigate to**: `http://192.168.4.1/`
|
||||
4. **Explore the examples** by clicking the organized test links
|
||||
5. **Monitor Serial output**: Open Serial Monitor to see detailed debugging information for each matched route
|
||||
|
||||
### Test URLs Available (All Clickable from Homepage)
|
||||
|
||||
**Auto-Detection Examples:**
|
||||
|
||||
- `http://192.168.4.1/auto` (exact + folder match)
|
||||
- `http://192.168.4.1/auto/sub` (folder match - same handler!)
|
||||
- `http://192.168.4.1/wildcard-test` (auto-detected prefix)
|
||||
- `http://192.168.4.1/auto-images/photo.png` (auto-detected extension)
|
||||
|
||||
**Factory Method Examples:**
|
||||
|
||||
- `http://192.168.4.1/exact` (AsyncURIMatcher::exact)
|
||||
- `http://192.168.4.1/service/status` (AsyncURIMatcher::prefix)
|
||||
- `http://192.168.4.1/admin/users` (AsyncURIMatcher::dir)
|
||||
- `http://192.168.4.1/images/photo.jpg` (AsyncURIMatcher::ext)
|
||||
|
||||
**Case Insensitive Examples:**
|
||||
|
||||
- `http://192.168.4.1/case` (lowercase)
|
||||
- `http://192.168.4.1/CASE` (uppercase)
|
||||
- `http://192.168.4.1/CaSe` (mixed case)
|
||||
|
||||
**Regex Examples (if ASYNCWEBSERVER_REGEX enabled):**
|
||||
|
||||
- `http://192.168.4.1/user/123` (captures numeric ID)
|
||||
- `http://192.168.4.1/user/456` (captures numeric ID)
|
||||
|
||||
**Combined Flags Examples:**
|
||||
|
||||
- `http://192.168.4.1/mixedcase-test` (prefix + case insensitive)
|
||||
- `http://192.168.4.1/MIXEDCASE/sub` (prefix + case insensitive)
|
||||
|
||||
### Console Output
|
||||
|
||||
Each handler provides detailed debugging information via Serial output:
|
||||
|
||||
```
|
||||
Auto-Detection Match (Traditional)
|
||||
Matched URL: /auto
|
||||
Uses auto-detection: exact + folder matching
|
||||
```
|
||||
|
||||
```
|
||||
Factory Exact Match
|
||||
Matched URL: /exact
|
||||
Uses AsyncURIMatcher::exact() factory function
|
||||
```
|
||||
|
||||
```
|
||||
Regex Match - User ID
|
||||
Matched URL: /user/123
|
||||
Captured User ID: 123
|
||||
This regex matches /user/{number} pattern
|
||||
```
|
||||
|
||||
## Compilation Options
|
||||
|
||||
### Enable Regex Support
|
||||
|
||||
To enable regular expression matching, compile with:
|
||||
|
||||
```
|
||||
-D ASYNCWEBSERVER_REGEX
|
||||
```
|
||||
|
||||
In PlatformIO, add to `platformio.ini`:
|
||||
|
||||
```ini
|
||||
build_flags = -D ASYNCWEBSERVER_REGEX
|
||||
```
|
||||
|
||||
In Arduino IDE, add to your sketch:
|
||||
|
||||
```cpp
|
||||
#define ASYNCWEBSERVER_REGEX
|
||||
```
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
1. **Exact matches** are fastest
|
||||
2. **Prefix matches** are very efficient
|
||||
3. **Regex matches** are slower but most flexible
|
||||
4. **Case insensitive** matching adds minimal overhead
|
||||
5. **Auto-detection** adds slight parsing overhead at construction time
|
||||
|
||||
## Real-World Applications
|
||||
|
||||
### REST API Design
|
||||
|
||||
```cpp
|
||||
// API versioning
|
||||
server.on(AsyncURIMatcher::prefix("/api/v1"), handleAPIv1);
|
||||
server.on(AsyncURIMatcher::prefix("/api/v2"), handleAPIv2);
|
||||
|
||||
// Resource endpoints with IDs
|
||||
server.on(AsyncURIMatcher::regex("^/api/users/([0-9]+)$"), handleUserById);
|
||||
server.on(AsyncURIMatcher::regex("^/api/posts/([0-9]+)/comments$"), handlePostComments);
|
||||
```
|
||||
|
||||
### File Serving
|
||||
|
||||
```cpp
|
||||
// Serve different file types
|
||||
server.on(AsyncURIMatcher::ext("/assets/*.css"), serveCSSFiles);
|
||||
server.on(AsyncURIMatcher::ext("/assets/*.js"), serveJSFiles);
|
||||
server.on(AsyncURIMatcher::ext("/images/*.jpg"), serveImageFiles);
|
||||
```
|
||||
|
||||
### Admin Interface
|
||||
|
||||
```cpp
|
||||
// Admin section with authentication
|
||||
server.on(AsyncURIMatcher::dir("/admin"), handleAdminPages);
|
||||
server.on(AsyncURIMatcher::exact("/admin"), redirectToAdminDashboard);
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [ESPAsyncWebServer Documentation](https://github.com/ESP32Async/ESPAsyncWebServer)
|
||||
- [Regular Expression Reference](https://en.cppreference.com/w/cpp/regex)
|
||||
- Other examples in the `examples/` directory
|
||||
276
examples/URIMatcher/URIMatcher.ino
Normal file
276
examples/URIMatcher/URIMatcher.ino
Normal file
@@ -0,0 +1,276 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// AsyncURIMatcher Examples - Advanced URI Matching and Routing
|
||||
//
|
||||
// This example demonstrates the various ways to use AsyncURIMatcher class
|
||||
// for flexible URL routing with different matching strategies:
|
||||
//
|
||||
// 1. Exact matching
|
||||
// 2. Prefix matching
|
||||
// 3. Folder/directory matching
|
||||
// 4. Extension matching
|
||||
// 5. Case insensitive matching
|
||||
// 6. Regex matching (if ASYNCWEBSERVER_REGEX is enabled)
|
||||
// 7. Factory functions for common patterns
|
||||
//
|
||||
// Test URLs:
|
||||
// - Exact: http://192.168.4.1/exact
|
||||
// - Prefix: http://192.168.4.1/prefix-anything
|
||||
// - Folder: http://192.168.4.1/api/users, http://192.168.4.1/api/posts
|
||||
// - Extension: http://192.168.4.1/images/photo.jpg, http://192.168.4.1/docs/readme.pdf
|
||||
// - Case insensitive: http://192.168.4.1/CaSe or http://192.168.4.1/case
|
||||
// - Wildcard: http://192.168.4.1/wildcard-test
|
||||
|
||||
#include <Arduino.h>
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
#include <AsyncTCP.h>
|
||||
#include <WiFi.h>
|
||||
#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);
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
Serial.println();
|
||||
Serial.println("=== AsyncURIMatcher Example ===");
|
||||
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
Serial.print("AP IP address: ");
|
||||
Serial.println(WiFi.softAPIP());
|
||||
#endif
|
||||
|
||||
// =============================================================================
|
||||
// 1. AUTO-DETECTION BEHAVIOR - traditional string-based routing
|
||||
// =============================================================================
|
||||
|
||||
// Traditional string-based routing with auto-detection
|
||||
// This uses URIMatchAuto which combines URIMatchPrefixFolder | URIMatchExact
|
||||
// It will match BOTH "/auto" exactly AND "/auto/" + anything
|
||||
server.on("/auto", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Auto-Detection Match (Traditional)");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("Uses auto-detection: exact + folder matching");
|
||||
request->send(200, "text/plain", "OK - Auto-detection match");
|
||||
});
|
||||
|
||||
// Auto-detection for wildcard patterns (ends with *)
|
||||
// This auto-detects as URIMatchPrefix
|
||||
server.on("/wildcard*", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Auto-Detected Wildcard (Prefix)");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("Auto-detected as prefix match due to trailing *");
|
||||
request->send(200, "text/plain", "OK - Wildcard prefix match");
|
||||
});
|
||||
|
||||
// Auto-detection for extension patterns (contains /*.ext)
|
||||
// This auto-detects as URIMatchExtension
|
||||
server.on("/auto-images/*.png", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Auto-Detected Extension Pattern");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("Auto-detected as extension match due to /*.png pattern");
|
||||
request->send(200, "text/plain", "OK - Extension match");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 2. EXACT MATCHING - matches only the exact URL (explicit)
|
||||
// =============================================================================
|
||||
|
||||
// Using factory function for exact match
|
||||
server.on(AsyncURIMatcher::exact("/exact"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Factory Exact Match");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("Uses AsyncURIMatcher::exact() factory function");
|
||||
request->send(200, "text/plain", "OK - Factory exact match");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 3. PREFIX MATCHING - matches URLs that start with the pattern
|
||||
// =============================================================================
|
||||
|
||||
// Using factory function for prefix match
|
||||
server.on(AsyncURIMatcher::prefix("/service"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Service Prefix Match (Factory)");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("Uses AsyncURIMatcher::prefix() factory function");
|
||||
request->send(200, "text/plain", "OK - Factory prefix match");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 4. FOLDER/DIRECTORY MATCHING - matches URLs in a folder structure
|
||||
// =============================================================================
|
||||
|
||||
// Folder match using factory function (automatically adds trailing slash)
|
||||
server.on(AsyncURIMatcher::dir("/admin"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Admin Directory Match");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("This matches URLs under /admin/ directory");
|
||||
Serial.println("Note: /admin (without slash) will NOT match");
|
||||
request->send(200, "text/plain", "OK - Directory match");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 5. EXTENSION MATCHING - matches files with specific extensions
|
||||
// =============================================================================
|
||||
|
||||
// Image extension matching
|
||||
server.on(AsyncURIMatcher::ext("/images/*.jpg"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("JPG Image Handler");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("This matches any .jpg file under /images/");
|
||||
request->send(200, "text/plain", "OK - Extension match");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 6. CASE INSENSITIVE MATCHING
|
||||
// =============================================================================
|
||||
|
||||
// Case insensitive exact match
|
||||
server.on(AsyncURIMatcher::exact("/case", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Case Insensitive Match");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("This matches /case in any case combination");
|
||||
request->send(200, "text/plain", "OK - Case insensitive match");
|
||||
});
|
||||
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
// =============================================================================
|
||||
// 7. REGEX MATCHING (only available if ASYNCWEBSERVER_REGEX is enabled)
|
||||
// =============================================================================
|
||||
|
||||
// Regex match for numeric IDs
|
||||
server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Regex Match - User ID");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
if (request->pathArg(0).length() > 0) {
|
||||
Serial.println("Captured User ID: " + request->pathArg(0));
|
||||
}
|
||||
Serial.println("This regex matches /user/{number} pattern");
|
||||
request->send(200, "text/plain", "OK - Regex match");
|
||||
});
|
||||
#endif
|
||||
|
||||
// =============================================================================
|
||||
// 8. COMBINED FLAGS EXAMPLE
|
||||
// =============================================================================
|
||||
|
||||
// Combine multiple flags
|
||||
server.on(AsyncURIMatcher::prefix("/MixedCase", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Combined Flags Example");
|
||||
Serial.println("Matched URL: " + request->url());
|
||||
Serial.println("Uses both AsyncURIMatcher::Prefix and AsyncURIMatcher::CaseInsensitive");
|
||||
request->send(200, "text/plain", "OK - Combined flags match");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 9. HOMEPAGE WITH NAVIGATION
|
||||
// =============================================================================
|
||||
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
Serial.println("Homepage accessed");
|
||||
String response = R"(<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>AsyncURIMatcher Examples</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 20px; }
|
||||
h1 { color: #2E86AB; }
|
||||
.test-link { display: block; margin: 5px 0; padding: 8px; background: #f0f0f0; text-decoration: none; color: #333; border-radius: 4px; }
|
||||
.test-link:hover { background: #e0e0e0; }
|
||||
.section { margin: 20px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>AsyncURIMatcher Examples</h1>
|
||||
|
||||
<div class="section">
|
||||
<h3>Auto-Detection (Traditional String Matching)</h3>
|
||||
<a href="/auto" class="test-link">/auto (auto-detection: exact + folder)</a>
|
||||
<a href="/auto/sub" class="test-link">/auto/sub (folder match)</a>
|
||||
<a href="/wildcard-test" class="test-link">/wildcard-test (auto prefix)</a>
|
||||
<a href="/auto-images/photo.png" class="test-link">/auto-images/photo.png (auto extension)</a>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Factory Method Examples</h3>
|
||||
<a href="/exact" class="test-link">/exact (factory exact)</a>
|
||||
<a href="/service/status" class="test-link">/service/status (factory prefix)</a>
|
||||
<a href="/admin/users" class="test-link">/admin/users (factory directory)</a>
|
||||
<a href="/images/photo.jpg" class="test-link">/images/photo.jpg (factory extension)</a>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Case Insensitive Matching</h3>
|
||||
<a href="/case" class="test-link">/case (lowercase)</a>
|
||||
<a href="/CASE" class="test-link">/CASE (uppercase)</a>
|
||||
<a href="/CaSe" class="test-link">/CaSe (mixed case)</a>
|
||||
</div>
|
||||
|
||||
)";
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
response += R"( <div class="section">
|
||||
<h3>Regex Matching</h3>
|
||||
<a href="/user/123" class="test-link">/user/123 (regex numeric ID)</a>
|
||||
<a href="/user/456" class="test-link">/user/456 (regex numeric ID)</a>
|
||||
</div>
|
||||
|
||||
)";
|
||||
#endif
|
||||
response += R"( <div class="section">
|
||||
<h3>Combined Flags</h3>
|
||||
<a href="/mixedcase-test" class="test-link">/mixedcase-test (prefix + case insensitive)</a>
|
||||
<a href="/MIXEDCASE/sub" class="test-link">/MIXEDCASE/sub (prefix + case insensitive)</a>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>)";
|
||||
request->send(200, "text/html", response);
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// 10. NOT FOUND HANDLER
|
||||
// =============================================================================
|
||||
|
||||
server.onNotFound([](AsyncWebServerRequest *request) {
|
||||
String html = "<h1>404 - Not Found</h1>";
|
||||
html += "<p>The requested URL <strong>" + request->url() + "</strong> was not found.</p>";
|
||||
html += "<p><a href='/'>← Back to Examples</a></p>";
|
||||
request->send(404, "text/html", html);
|
||||
});
|
||||
|
||||
server.begin();
|
||||
|
||||
Serial.println();
|
||||
Serial.println("=== Server Started ===");
|
||||
Serial.println("Open your browser and navigate to:");
|
||||
Serial.println("http://192.168.4.1/ - Main examples page");
|
||||
Serial.println();
|
||||
Serial.println("Available test endpoints:");
|
||||
Serial.println("• Auto-detection: /auto (exact+folder), /wildcard*, /auto-images/*.png");
|
||||
Serial.println("• Exact matches: /exact");
|
||||
Serial.println("• Prefix matches: /service*");
|
||||
Serial.println("• Folder matches: /admin/*");
|
||||
Serial.println("• Extension matches: /images/*.jpg");
|
||||
Serial.println("• Case insensitive: /case (try /CASE, /Case)");
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
Serial.println("• Regex matches: /user/123");
|
||||
#endif
|
||||
Serial.println("• Combined flags: /mixedcase*");
|
||||
Serial.println();
|
||||
}
|
||||
|
||||
void loop() {
|
||||
// Nothing to do here - the server handles everything asynchronously
|
||||
delay(1000);
|
||||
}
|
||||
165
examples/URIMatcherTest/URIMatcherTest.ino
Normal file
165
examples/URIMatcherTest/URIMatcherTest.ino
Normal file
@@ -0,0 +1,165 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Test for ESPAsyncWebServer URI matching
|
||||
//
|
||||
// Usage: upload, connect to the AP and run test_routes.sh
|
||||
//
|
||||
|
||||
#include <Arduino.h>
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
#include <AsyncTCP.h>
|
||||
#include <WiFi.h>
|
||||
#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>
|
||||
|
||||
AsyncWebServer server(80);
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
// Status endpoint
|
||||
server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Exact paths, plus the subpath (/exact matches /exact/sub but not /exact-no-match)
|
||||
server.on("/exact", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
server.on("/api/users", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Prefix matching
|
||||
server.on("/api/*", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
server.on("/files/*", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Extensions
|
||||
server.on("/*.json", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "application/json", "{\"status\":\"OK\"}");
|
||||
});
|
||||
|
||||
server.on("/*.css", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/css", "/* OK */");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// NEW ASYNCURIMATCHER FACTORY METHODS TESTS
|
||||
// =============================================================================
|
||||
|
||||
// Exact match using factory method (does NOT match subpaths like traditional)
|
||||
server.on(AsyncURIMatcher::exact("/factory/exact"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Prefix match using factory method
|
||||
server.on(AsyncURIMatcher::prefix("/factory/prefix"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Directory match using factory method (matches /dir/anything but not /dir itself)
|
||||
server.on(AsyncURIMatcher::dir("/factory/dir"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Extension match using factory method
|
||||
server.on(AsyncURIMatcher::ext("/factory/files/*.txt"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// CASE INSENSITIVE MATCHING TESTS
|
||||
// =============================================================================
|
||||
|
||||
// Case insensitive exact match
|
||||
server.on(AsyncURIMatcher::exact("/case/exact", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Case insensitive prefix match
|
||||
server.on(AsyncURIMatcher::prefix("/case/prefix", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Case insensitive directory match
|
||||
server.on(AsyncURIMatcher::dir("/case/dir", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Case insensitive extension match
|
||||
server.on(AsyncURIMatcher::ext("/case/files/*.PDF", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
#ifdef ASYNCWEBSERVER_REGEX
|
||||
// Traditional regex patterns (backward compatibility)
|
||||
server.on("^/user/([0-9]+)$", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
server.on("^/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// =============================================================================
|
||||
// NEW ASYNCURIMATCHER REGEX FACTORY METHODS
|
||||
// =============================================================================
|
||||
|
||||
// Regex match using factory method
|
||||
server.on(AsyncURIMatcher::regex("^/factory/user/([0-9]+)$"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Case insensitive regex match using factory method
|
||||
server.on(AsyncURIMatcher::regex("^/factory/search/(.+)$", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// Complex regex with multiple capture groups
|
||||
server.on(AsyncURIMatcher::regex("^/factory/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$"), HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
#endif
|
||||
|
||||
// =============================================================================
|
||||
// SPECIAL MATCHERS
|
||||
// =============================================================================
|
||||
|
||||
// Match all POST requests (catch-all before 404)
|
||||
server.on(AsyncURIMatcher::all(), HTTP_POST, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/plain", "OK");
|
||||
});
|
||||
|
||||
// 404 handler
|
||||
server.onNotFound([](AsyncWebServerRequest *request) {
|
||||
request->send(404, "text/plain", "Not Found");
|
||||
});
|
||||
|
||||
server.begin();
|
||||
Serial.println("Server ready");
|
||||
}
|
||||
|
||||
// not needed
|
||||
void loop() {
|
||||
delay(100);
|
||||
}
|
||||
174
examples/URIMatcherTest/test_routes.sh
Executable file
174
examples/URIMatcherTest/test_routes.sh
Executable file
@@ -0,0 +1,174 @@
|
||||
#!/bin/bash
|
||||
|
||||
# URI Matcher Test Script
|
||||
# Tests all routes defined in URIMatcherTest.ino
|
||||
|
||||
SERVER_IP="${1:-192.168.4.1}"
|
||||
SERVER_PORT="80"
|
||||
BASE_URL="http://${SERVER_IP}:${SERVER_PORT}"
|
||||
|
||||
echo "Testing URI Matcher at $BASE_URL"
|
||||
echo "=================================="
|
||||
|
||||
# Function to test a route
|
||||
test_route() {
|
||||
local path="$1"
|
||||
local expected_status="$2"
|
||||
local description="$3"
|
||||
|
||||
echo -n "Testing $path ... "
|
||||
|
||||
response=$(curl -s -w "HTTPSTATUS:%{http_code}" "$BASE_URL$path" 2>/dev/null)
|
||||
status_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2)
|
||||
|
||||
if [ "$status_code" = "$expected_status" ]; then
|
||||
echo "✅ PASS ($status_code)"
|
||||
else
|
||||
echo "❌ FAIL (expected $expected_status, got $status_code)"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Test counter
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# Test all routes that should return 200 OK
|
||||
echo "Testing routes that should work (200 OK):"
|
||||
|
||||
if test_route "/status" "200" "Status endpoint"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/exact" "200" "Exact path"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/exact/" "200" "Exact path ending with /"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/exact/sub" "200" "Exact path with subpath /"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/api/users" "200" "Exact API path"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/api/data" "200" "API prefix match"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/api/v1/posts" "200" "API prefix deep"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/files/document.pdf" "200" "Files prefix"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/files/images/photo.jpg" "200" "Files prefix deep"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/config.json" "200" "JSON extension"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/data/settings.json" "200" "JSON extension in subfolder"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/style.css" "200" "CSS extension"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/assets/main.css" "200" "CSS extension in subfolder"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
echo ""
|
||||
echo "Testing AsyncURIMatcher factory methods:"
|
||||
|
||||
# Factory exact match (should NOT match subpaths)
|
||||
if test_route "/factory/exact" "200" "Factory exact match"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Factory prefix match
|
||||
if test_route "/factory/prefix" "200" "Factory prefix base"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/prefix-test" "200" "Factory prefix extended"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/prefix/sub" "200" "Factory prefix subpath"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Factory directory match (should NOT match the directory itself)
|
||||
if test_route "/factory/dir/users" "200" "Factory directory match"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/dir/sub/path" "200" "Factory directory deep"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Factory extension match
|
||||
if test_route "/factory/files/doc.txt" "200" "Factory extension match"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/files/sub/readme.txt" "200" "Factory extension deep"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
echo ""
|
||||
echo "Testing case insensitive matching:"
|
||||
|
||||
# Case insensitive exact
|
||||
if test_route "/case/exact" "200" "Case exact lowercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/CASE/EXACT" "200" "Case exact uppercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/Case/Exact" "200" "Case exact mixed"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Case insensitive prefix
|
||||
if test_route "/case/prefix" "200" "Case prefix lowercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/CASE/PREFIX-test" "200" "Case prefix uppercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/Case/Prefix/sub" "200" "Case prefix mixed"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Case insensitive directory
|
||||
if test_route "/case/dir/users" "200" "Case dir lowercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/CASE/DIR/admin" "200" "Case dir uppercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/Case/Dir/settings" "200" "Case dir mixed"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Case insensitive extension
|
||||
if test_route "/case/files/doc.pdf" "200" "Case ext lowercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/CASE/FILES/DOC.PDF" "200" "Case ext uppercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/Case/Files/Doc.Pdf" "200" "Case ext mixed"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
echo ""
|
||||
echo "Testing special matchers:"
|
||||
|
||||
# Test POST to catch-all (all() matcher)
|
||||
echo -n "Testing POST /any/path (all matcher) ... "
|
||||
response=$(curl -s -X POST -w "HTTPSTATUS:%{http_code}" "$BASE_URL/any/path" 2>/dev/null)
|
||||
status_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2)
|
||||
if [ "$status_code" = "200" ]; then
|
||||
echo "✅ PASS ($status_code)"
|
||||
((PASS++))
|
||||
else
|
||||
echo "❌ FAIL (expected 200, got $status_code)"
|
||||
((FAIL++))
|
||||
fi
|
||||
|
||||
# Check if regex is enabled by testing the server
|
||||
echo ""
|
||||
echo "Checking for regex support..."
|
||||
regex_test=$(curl -s "$BASE_URL/user/123" 2>/dev/null)
|
||||
if curl -s -w "%{http_code}" "$BASE_URL/user/123" 2>/dev/null | grep -q "200"; then
|
||||
echo "Regex support detected - testing traditional regex routes:"
|
||||
if test_route "/user/123" "200" "Traditional regex user ID"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/user/456" "200" "Traditional regex user ID 2"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/blog/2023/10/15" "200" "Traditional regex blog date"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/blog/2024/12/25" "200" "Traditional regex blog date 2"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
echo "Testing AsyncURIMatcher regex factory methods:"
|
||||
if test_route "/factory/user/123" "200" "Factory regex user ID"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/user/789" "200" "Factory regex user ID 2"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/blog/2023/10/15" "200" "Factory regex blog date"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/blog/2024/12/31" "200" "Factory regex blog date 2"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Case insensitive regex
|
||||
if test_route "/factory/search/hello" "200" "Factory regex search lowercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/FACTORY/SEARCH/WORLD" "200" "Factory regex search uppercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/Factory/Search/Test" "200" "Factory regex search mixed"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Test regex validation
|
||||
if test_route "/user/abc" "404" "Invalid regex (letters instead of numbers)"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/blog/23/10/15" "404" "Invalid regex (2-digit year)"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/factory/user/abc" "404" "Factory regex invalid (letters)"; then ((PASS++)); else ((FAIL++)); fi
|
||||
else
|
||||
echo "Regex support not detected (compile with ASYNCWEBSERVER_REGEX to enable)"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Testing routes that should fail (404 Not Found):"
|
||||
|
||||
if test_route "/nonexistent" "404" "Non-existent route"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Test factory exact vs traditional behavior difference
|
||||
if test_route "/factory/exact/sub" "404" "Factory exact should NOT match subpaths"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Test factory directory requires trailing slash
|
||||
if test_route "/factory/dir" "404" "Factory directory should NOT match without trailing slash"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Test extension mismatch
|
||||
if test_route "/factory/files/doc.pdf" "404" "Factory extension mismatch (.pdf vs .txt)"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
# Test case sensitive when flag not used
|
||||
if test_route "/exact" "200" "Traditional exact lowercase"; then ((PASS++)); else ((FAIL++)); fi
|
||||
if test_route "/EXACT" "404" "Traditional exact should be case sensitive"; then ((PASS++)); else ((FAIL++)); fi
|
||||
|
||||
echo ""
|
||||
echo "=================================="
|
||||
echo "Test Results:"
|
||||
echo "✅ Passed: $PASS"
|
||||
echo "❌ Failed: $FAIL"
|
||||
echo "Total: $((PASS + FAIL))"
|
||||
|
||||
if [ $FAIL -eq 0 ]; then
|
||||
echo ""
|
||||
echo "🎉 All tests passed! URI matching is working correctly."
|
||||
exit 0
|
||||
else
|
||||
echo ""
|
||||
echo "❌ Some tests failed. Check the server and routes."
|
||||
exit 1
|
||||
fi
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Demo text, binary and file upload
|
||||
@@ -31,7 +31,7 @@ void setup() {
|
||||
LittleFS.begin();
|
||||
}
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -63,6 +63,7 @@ void setup() {
|
||||
if (!buffer->reserve(size)) {
|
||||
delete buffer;
|
||||
request->abort();
|
||||
return;
|
||||
}
|
||||
request->_tempObject = buffer;
|
||||
}
|
||||
@@ -100,6 +101,7 @@ void setup() {
|
||||
|
||||
if (!request->_tempFile) {
|
||||
request->send(400, "text/plain", "File not available for writing");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (len) {
|
||||
@@ -141,6 +143,7 @@ void setup() {
|
||||
|
||||
// first pass ?
|
||||
if (!index) {
|
||||
// Note: using content type to determine size is not reliable!
|
||||
size_t size = request->header("Content-Length").toInt();
|
||||
if (!size) {
|
||||
request->send(400, "text/plain", "No Content-Length");
|
||||
@@ -150,6 +153,7 @@ void setup() {
|
||||
if (!buffer) {
|
||||
// not enough memory
|
||||
request->abort();
|
||||
return;
|
||||
} else {
|
||||
request->_tempObject = buffer;
|
||||
}
|
||||
|
||||
158
examples/UploadFlash/UploadFlash.ino
Normal file
158
examples/UploadFlash/UploadFlash.ino
Normal file
@@ -0,0 +1,158 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// Demo to upload a firmware and filesystem image via multipart form data
|
||||
//
|
||||
|
||||
#include <Arduino.h>
|
||||
#if defined(ESP32) || defined(LIBRETINY)
|
||||
#include <AsyncTCP.h>
|
||||
#include <WiFi.h>
|
||||
#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>
|
||||
#include <StreamString.h>
|
||||
#include <LittleFS.h>
|
||||
|
||||
// ESP32 example ONLY
|
||||
#ifdef ESP32
|
||||
#include <Update.h>
|
||||
#endif
|
||||
|
||||
static AsyncWebServer server(80);
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
if (!LittleFS.begin()) {
|
||||
LittleFS.format();
|
||||
LittleFS.begin();
|
||||
}
|
||||
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
// ESP32 example ONLY
|
||||
#ifdef ESP32
|
||||
|
||||
// Shows how to get the fw and fs (names) and filenames from a multipart upload,
|
||||
// and also how to handle multiple file uploads in a single request.
|
||||
//
|
||||
// This example also shows how to pass and handle different parameters having the same name in query string, post form and content-disposition.
|
||||
//
|
||||
// Execute in the terminal, in order:
|
||||
//
|
||||
// 1. Build firmware: pio run -e arduino-3
|
||||
// 2. Build FS image: pio run -e arduino-3 -t buildfs
|
||||
// 3. Flash both at the same time: curl -v -F "name=Bob" -F "fw=@.pio/build/arduino-3/firmware.bin" -F "fs=@.pio/build/arduino-3/littlefs.bin" http://192.168.4.1/flash?name=Bill
|
||||
//
|
||||
server.on(
|
||||
"/flash", HTTP_POST,
|
||||
[](AsyncWebServerRequest *request) {
|
||||
if (request->getResponse()) {
|
||||
// response already created
|
||||
return;
|
||||
}
|
||||
|
||||
// list all parameters
|
||||
Serial.println("Request parameters:");
|
||||
const size_t params = request->params();
|
||||
for (size_t i = 0; i < params; i++) {
|
||||
const AsyncWebParameter *p = request->getParam(i);
|
||||
Serial.printf("Param[%u]: %s=%s, isPost=%d, isFile=%d, size=%u\n", i, p->name().c_str(), p->value().c_str(), p->isPost(), p->isFile(), p->size());
|
||||
}
|
||||
|
||||
Serial.println("Flash / Filesystem upload completed");
|
||||
|
||||
request->send(200, "text/plain", "Upload complete");
|
||||
},
|
||||
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
|
||||
Serial.printf("Upload[%s]: index=%u, len=%u, final=%d\n", filename.c_str(), index, len, final);
|
||||
|
||||
if (request->getResponse() != nullptr) {
|
||||
// upload aborted
|
||||
return;
|
||||
}
|
||||
|
||||
// start a new content-disposition upload
|
||||
if (!index) {
|
||||
// list all parameters
|
||||
const size_t params = request->params();
|
||||
for (size_t i = 0; i < params; i++) {
|
||||
const AsyncWebParameter *p = request->getParam(i);
|
||||
Serial.printf("Param[%u]: %s=%s, isPost=%d, isFile=%d, size=%u\n", i, p->name().c_str(), p->value().c_str(), p->isPost(), p->isFile(), p->size());
|
||||
}
|
||||
|
||||
// get the content-disposition parameter
|
||||
const AsyncWebParameter *p = request->getParam(asyncsrv::T_name, true, true);
|
||||
if (p == nullptr) {
|
||||
request->send(400, "text/plain", "Missing content-disposition 'name' parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
// determine upload type based on the parameter name
|
||||
if (p->value() == "fs") {
|
||||
Serial.printf("Filesystem image upload for file: %s\n", filename.c_str());
|
||||
if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) {
|
||||
Update.printError(Serial);
|
||||
request->send(400, "text/plain", "Update begin failed");
|
||||
return;
|
||||
}
|
||||
|
||||
} else if (p->value() == "fw") {
|
||||
Serial.printf("Firmware image upload for file: %s\n", filename.c_str());
|
||||
if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) {
|
||||
Update.printError(Serial);
|
||||
request->send(400, "text/plain", "Update begin failed");
|
||||
return;
|
||||
}
|
||||
|
||||
} else {
|
||||
Serial.printf("Unknown upload type for file: %s\n", filename.c_str());
|
||||
request->send(400, "text/plain", "Unknown upload type");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// some bytes to write ?
|
||||
if (len) {
|
||||
if (Update.write(data, len) != len) {
|
||||
Update.printError(Serial);
|
||||
Update.end();
|
||||
request->send(400, "text/plain", "Update write failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// finish the content-disposition upload
|
||||
if (final) {
|
||||
if (!Update.end(true)) {
|
||||
Update.printError(Serial);
|
||||
request->send(400, "text/plain", "Update end failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// success response is created in the final request handler when all uploads are completed
|
||||
Serial.printf("Upload success of file %s\n", filename.c_str());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
#endif
|
||||
|
||||
server.begin();
|
||||
}
|
||||
|
||||
// not needed
|
||||
void loop() {
|
||||
delay(100);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// WebSocket example
|
||||
@@ -19,17 +19,58 @@
|
||||
|
||||
#include <ESPAsyncWebServer.h>
|
||||
|
||||
static const char *htmlContent PROGMEM = R"(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebSocket</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebSocket Example</h1>
|
||||
<p>Open your browser console!</p>
|
||||
<input type="text" id="message" placeholder="Type a message">
|
||||
<button onclick='sendMessage()'>Send</button>
|
||||
<script>
|
||||
var ws = new WebSocket('ws://192.168.4.1/ws');
|
||||
ws.onopen = function() {
|
||||
console.log("WebSocket connected");
|
||||
};
|
||||
ws.onmessage = function(event) {
|
||||
console.log("WebSocket message: " + event.data);
|
||||
};
|
||||
ws.onclose = function() {
|
||||
console.log("WebSocket closed");
|
||||
};
|
||||
ws.onerror = function(error) {
|
||||
console.log("WebSocket error: " + error);
|
||||
};
|
||||
function sendMessage() {
|
||||
var message = document.getElementById("message").value;
|
||||
ws.send(message);
|
||||
console.log("WebSocket sent: " + message);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
)";
|
||||
static const size_t htmlContentLength = strlen_P(htmlContent);
|
||||
|
||||
static AsyncWebServer server(80);
|
||||
static AsyncWebSocket ws("/ws");
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
|
||||
// serves root html page
|
||||
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
|
||||
request->send(200, "text/html", (const uint8_t *)htmlContent, htmlContentLength);
|
||||
});
|
||||
|
||||
//
|
||||
// Run in terminal 1: websocat ws://192.168.4.1/ws => should stream data
|
||||
// Run in terminal 2: websocat ws://192.168.4.1/ws => should stream data
|
||||
@@ -66,6 +107,7 @@ void setup() {
|
||||
if (info->opcode == WS_TEXT) {
|
||||
data[len] = 0;
|
||||
Serial.printf("ws text: %s\n", (char *)data);
|
||||
client->ping();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||||
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
|
||||
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
|
||||
|
||||
//
|
||||
// WebSocket example using the easy to use AsyncWebSocketMessageHandler handler that only supports unfragmented messages
|
||||
@@ -40,7 +40,7 @@ static const char *htmlContent PROGMEM = R"(
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebSocket Example</h1>
|
||||
<>Open your browser console!</p>
|
||||
<p>Open your browser console!</p>
|
||||
<input type="text" id="message" placeholder="Type a message">
|
||||
<button onclick='sendMessage()'>Send</button>
|
||||
<script>
|
||||
@@ -71,7 +71,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent);
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
|
||||
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
|
||||
#if ASYNCWEBSERVER_WIFI_SUPPORTED
|
||||
WiFi.mode(WIFI_AP);
|
||||
WiFi.softAP("esp-captive");
|
||||
#endif
|
||||
@@ -97,6 +97,7 @@ void setup() {
|
||||
|
||||
wsHandler.onMessage([](AsyncWebSocket *server, AsyncWebSocketClient *client, const uint8_t *data, size_t len) {
|
||||
Serial.printf("Client %" PRIu32 " data: %s\n", client->id(), (const char *)data);
|
||||
server->textAll(data, len);
|
||||
});
|
||||
|
||||
wsHandler.onFragment([](AsyncWebSocket *server, AsyncWebSocketClient *client, const AwsFrameInfo *frameInfo, const uint8_t *data, size_t len) {
|
||||
|
||||
Reference in New Issue
Block a user