From c98a476228180ad2ece5e4094ecf8250bd9423f7 Mon Sep 17 00:00:00 2001
From: Pablo2048
Date: Tue, 10 Feb 2026 12:34:12 +0100
Subject: [PATCH] Update to version 3.9.6
---
.github/ISSUE_TEMPLATE/config.yml | 2 +
.github/ISSUE_TEMPLATE/issue-report.yml | 91 +++
.github/dependabot.yml | 10 +
.github/scripts/update-version.sh | 40 ++
.github/workflows/arduino-lint.yml | 25 +
.github/workflows/build-esp32.yml | 194 +++++++
.github/workflows/build-esp8266.yml | 82 +++
.github/workflows/build-libretiny.yml | 51 ++
.github/workflows/build-rpi.yml | 89 +++
.github/workflows/cpplint.yml | 46 ++
.github/workflows/pre-commit-status.yml | 64 ++
.github/workflows/pre-commit.yml | 80 +++
.github/workflows/publish-pio-registry.yml | 62 ++
.github/workflows/stale.yaml | 24 +
.github/workflows/upload-idf-component.yml | 50 ++
data/README.md | 48 ++
.../AsyncResponseStream.ino | 4 +-
examples/AsyncTunnel/AsyncTunnel.ino | 4 +-
examples/Auth/Auth.ino | 86 ++-
examples/CORS/CORS.ino | 4 +-
examples/CaptivePortal/CaptivePortal.ino | 6 +-
examples/CatchAllHandler/CatchAllHandler.ino | 4 +-
examples/ChunkResponse/ChunkResponse.ino | 8 +-
.../ChunkRetryResponse/ChunkRetryResponse.ino | 24 +-
examples/EndBegin/EndBegin.ino | 8 +-
examples/Filters/Filters.ino | 18 +-
examples/FlashResponse/FlashResponse.ino | 4 +-
.../HeaderManipulation/HeaderManipulation.ino | 4 +-
examples/Headers/Headers.ino | 4 +-
examples/Json/Json.ino | 48 +-
examples/LargeResponse/LargeResponse.ino | 178 ++++++
examples/Logging/Logging.ino | 4 +-
examples/MessagePack/MessagePack.ino | 42 +-
examples/Middleware/Middleware.ino | 4 +-
examples/Params/Params.ino | 4 +-
.../PartitionDownloader.ino | 4 +-
examples/PerfTests/PerfTests.ino | 15 +-
examples/RateLimit/RateLimit.ino | 4 +-
examples/Redirect/Redirect.ino | 4 +-
.../RequestContinuation.ino | 4 +-
.../RequestContinuationComplete.ino | 4 +-
.../ResumableDownload/ResumableDownload.ino | 4 +-
examples/Rewrite/Rewrite.ino | 4 +-
.../ServerSentEvents/ServerSentEvents.ino | 8 +-
.../ServerSentEvents_PR156.ino | 4 +-
examples/ServerState/ServerState.ino | 4 +-
.../SkipServerMiddleware.ino | 4 +-
.../SlowChunkResponse/SlowChunkResponse.ino | 9 +-
examples/StaticFile/StaticFile.ino | 4 +-
examples/Templates/Templates.ino | 89 ++-
examples/URIMatcher/README.md | 349 +++++++++++
examples/URIMatcher/URIMatcher.ino | 276 +++++++++
examples/URIMatcherTest/URIMatcherTest.ino | 165 ++++++
examples/URIMatcherTest/test_routes.sh | 174 ++++++
examples/Upload/Upload.ino | 8 +-
examples/UploadFlash/UploadFlash.ino | 158 +++++
examples/WebSocket/WebSocket.ino | 46 +-
examples/WebSocketEasy/WebSocketEasy.ino | 7 +-
idf_component.yml | 2 +-
idf_component_examples/catchall/main/main.cpp | 4 +-
.../serversentevents/main/main.cpp | 4 +-
.../websocket/main/main.cpp | 4 +-
library.json_ => library.json | 4 +-
library.properties | 2 +-
.../IncreaseMaxSockets/.gitignore | 1 +
.../IncreaseMaxSockets/platformio.ini | 19 +-
.../IncreaseMaxSockets/src/main.cpp | 2 +-
platformio.ini | 124 ++--
src/AsyncEventSource.cpp | 107 ++--
src/AsyncEventSource.h | 29 +-
src/AsyncJson.cpp | 96 +--
src/AsyncJson.h | 68 ++-
src/AsyncMessagePack.cpp | 119 ----
src/AsyncMessagePack.h | 125 +---
src/AsyncWebHeader.cpp | 2 +-
src/AsyncWebServerLogging.h | 175 ++++++
src/AsyncWebServerRequest.cpp | 67 ++-
src/AsyncWebServerVersion.h | 6 +-
src/AsyncWebSocket.cpp | 158 +++--
src/AsyncWebSocket.h | 43 +-
src/BackPort_SHA1Builder.h | 7 +-
src/ChunkPrint.cpp | 23 +-
src/ChunkPrint.h | 17 +-
src/ESPAsyncWebServer.h | 401 ++++++++++++-
src/Middleware.cpp | 43 +-
src/WebAuthentication.cpp | 31 +-
src/WebAuthentication.h | 7 +-
src/WebHandlerImpl.h | 34 +-
src/WebHandlers.cpp | 159 +++--
src/WebRequest.cpp | 183 +++---
src/WebResponseImpl.h | 99 +++-
src/WebResponses.cpp | 548 +++++++++---------
src/WebServer.cpp | 160 ++++-
src/literals.h | 390 +++++++------
94 files changed, 4593 insertions(+), 1434 deletions(-)
create mode 100644 .github/ISSUE_TEMPLATE/config.yml
create mode 100644 .github/ISSUE_TEMPLATE/issue-report.yml
create mode 100644 .github/dependabot.yml
create mode 100755 .github/scripts/update-version.sh
create mode 100644 .github/workflows/arduino-lint.yml
create mode 100644 .github/workflows/build-esp32.yml
create mode 100644 .github/workflows/build-esp8266.yml
create mode 100644 .github/workflows/build-libretiny.yml
create mode 100644 .github/workflows/build-rpi.yml
create mode 100644 .github/workflows/cpplint.yml
create mode 100644 .github/workflows/pre-commit-status.yml
create mode 100644 .github/workflows/pre-commit.yml
create mode 100644 .github/workflows/publish-pio-registry.yml
create mode 100644 .github/workflows/stale.yaml
create mode 100644 .github/workflows/upload-idf-component.yml
create mode 100644 data/README.md
create mode 100644 examples/LargeResponse/LargeResponse.ino
create mode 100644 examples/URIMatcher/README.md
create mode 100644 examples/URIMatcher/URIMatcher.ino
create mode 100644 examples/URIMatcherTest/URIMatcherTest.ino
create mode 100755 examples/URIMatcherTest/test_routes.sh
create mode 100644 examples/UploadFlash/UploadFlash.ino
rename library.json_ => library.json (96%)
delete mode 100644 src/AsyncMessagePack.cpp
create mode 100644 src/AsyncWebServerLogging.h
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..02f38c3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,2 @@
+# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository
+blank_issues_enabled: false
diff --git a/.github/ISSUE_TEMPLATE/issue-report.yml b/.github/ISSUE_TEMPLATE/issue-report.yml
new file mode 100644
index 0000000..d48e5ba
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/issue-report.yml
@@ -0,0 +1,91 @@
+name: 🐛 Bug Report
+description: File a bug report
+labels: ["Status: Awaiting triage"]
+body:
+ - type: markdown
+ attributes:
+ value: >
+ ### ⚠️ Please remember: issues are for *bugs* only!
+
+ - type: markdown
+ attributes:
+ value: |
+ #### Unsure? Have a questions? 👉 [Start a new discussion](https://github.com/ESP32Async/ESPAsyncWebServer/discussions/new)
+
+ #### Before opening a new issue, please make sure you have searched:
+
+ - In the [documentation](https://github.com/ESP32Async/ESPAsyncWebServer)
+ - In the [discussions](https://github.com/ESP32Async/ESPAsyncWebServer/discussions)
+ - In the [issues](https://github.com/ESP32Async/ESPAsyncWebServer/issues)
+ - In the [examples](https://github.com/ESP32Async/ESPAsyncWebServer/tree/main/examples)
+
+ #### Make sure you are using:
+
+ - The [latest version of ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer/releases)
+ - The [latest version of AsyncTCP](https://github.com/ESP32Async/AsyncTCP/releases) (for ESP32)
+
+ - type: dropdown
+ id: platform
+ attributes:
+ label: Platform
+ options:
+ - ESP32
+ - ESP8266
+ - RP2040
+ validations:
+ required: true
+
+ - type: dropdown
+ id: tooling
+ attributes:
+ label: IDE / Tooling
+ options:
+ - Arduino (IDE/CLI)
+ - pioarduino
+ - ESP-IDF
+ - PlatformIO
+ validations:
+ required: true
+
+ - type: textarea
+ id: what-happened
+ attributes:
+ label: What happened?
+ description: A clear and concise description of the issue.
+ placeholder: A clear and concise description of the issue.
+ validations:
+ required: true
+
+ - type: textarea
+ id: stack-trace
+ attributes:
+ label: Stack Trace
+ description: Please provide a debug message or error message. If you have a Guru Meditation Error or Backtrace, [please decode it](https://maximeborges.github.io/esp-stacktrace-decoder/).
+ placeholder: For Arduino IDE, Enable Core debug level - Debug on tools menu, then put the serial output here.
+ validations:
+ required: true
+
+ - type: textarea
+ id: how-to-reproduce
+ attributes:
+ label: Minimal Reproductible Example (MRE)
+ description: Post the code or the steps to reproduce the issue.
+ placeholder: Post the code or the steps to reproduce the issue.
+ validations:
+ required: true
+
+ - type: checkboxes
+ id: confirmation
+ attributes:
+ label: "I confirm that:"
+ options:
+ - label: I have read the documentation.
+ required: true
+ - label: I have searched for similar discussions.
+ required: true
+ - label: I have searched for similar issues.
+ required: true
+ - label: I have looked at the examples.
+ required: true
+ - label: I have upgraded to the lasted version of ESPAsyncWebServer (and AsyncTCP for ESP32).
+ required: true
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..dfd0e30
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+# Set update schedule for GitHub Actions
+
+version: 2
+updates:
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ # Check for updates to GitHub Actions every week
+ interval: "weekly"
diff --git a/.github/scripts/update-version.sh b/.github/scripts/update-version.sh
new file mode 100755
index 0000000..de8de77
--- /dev/null
+++ b/.github/scripts/update-version.sh
@@ -0,0 +1,40 @@
+
+#!/bin/bash
+# shellcheck disable=SC2002
+
+# fail the script if any command unexpectedly fails
+set -e
+
+if [ ! $# -eq 3 ]; then
+ echo "Bad number of arguments: $#" >&2
+ echo "usage: $0 " >&2
+ exit 1
+fi
+
+re='^[0-9]+$'
+if [[ ! $1 =~ $re ]] || [[ ! $2 =~ $re ]] || [[ ! $3 =~ $re ]] ; then
+ echo "error: Not a valid version: $1.$2.$3" >&2
+ echo "usage: $0 " >&2
+ exit 1
+fi
+
+ASYNCWEBSERVER_VERSION_MAJOR="$1"
+ASYNCWEBSERVER_VERSION_MINOR="$2"
+ASYNCWEBSERVER_VERSION_PATCH="$3"
+ASYNCWEBSERVER_VERSION="$ASYNCWEBSERVER_VERSION_MAJOR.$ASYNCWEBSERVER_VERSION_MINOR.$ASYNCWEBSERVER_VERSION_PATCH"
+
+echo "New AsyncTCP version: $ASYNCWEBSERVER_VERSION"
+
+echo "Updating library.properties..."
+cat library.properties | sed "s/version=.*/version=$ASYNCWEBSERVER_VERSION/g" > __library.properties && mv __library.properties library.properties
+
+echo "Updating library.json..."
+cat library.json | sed "s/^ \"version\":.*/ \"version\": \"$ASYNCWEBSERVER_VERSION\",/g" > __library.json && mv __library.json library.json
+
+echo "Updating src/AsyncWebServerVersion.h..."
+cat src/AsyncWebServerVersion.h | \
+sed "s/#define ASYNCWEBSERVER_VERSION_MAJOR.*/#define ASYNCWEBSERVER_VERSION_MAJOR $ASYNCWEBSERVER_VERSION_MAJOR/g" | \
+sed "s/#define ASYNCWEBSERVER_VERSION_MINOR.*/#define ASYNCWEBSERVER_VERSION_MINOR $ASYNCWEBSERVER_VERSION_MINOR/g" | \
+sed "s/#define ASYNCWEBSERVER_VERSION_PATCH.*/#define ASYNCWEBSERVER_VERSION_PATCH $ASYNCWEBSERVER_VERSION_PATCH/g" > src/__AsyncWebServerVersion.h && mv src/__AsyncWebServerVersion.h src/AsyncWebServerVersion.h
+
+exit 0
\ No newline at end of file
diff --git a/.github/workflows/arduino-lint.yml b/.github/workflows/arduino-lint.yml
new file mode 100644
index 0000000..8abd606
--- /dev/null
+++ b/.github/workflows/arduino-lint.yml
@@ -0,0 +1,25 @@
+# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
+
+name: Arduino Lint
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ - release/*
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ arduino-lint:
+ name: Arduino Lint
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+ - uses: arduino/arduino-lint-action@v2
+ with:
+ library-manager: update
diff --git a/.github/workflows/build-esp32.yml b/.github/workflows/build-esp32.yml
new file mode 100644
index 0000000..9be30b7
--- /dev/null
+++ b/.github/workflows/build-esp32.yml
@@ -0,0 +1,194 @@
+# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
+
+name: Build (ESP32)
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ - release/*
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ arduino-esp32:
+ name: ESP32 (arduino-cli) - Release
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install arduino-cli
+ run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
+
+ - name: Update core index
+ run: arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json
+
+ - name: Install core
+ run: arduino-cli core install --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json esp32:esp32
+
+ - name: Install ArduinoJson
+ run: arduino-cli lib install ArduinoJson
+
+ - name: Install AsyncTCP (ESP32)
+ run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/AsyncTCP#v3.4.10
+
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - name: Build Examples
+ run: |
+ for i in `ls examples`; do
+ echo "============================================================="
+ echo "Building examples/$i..."
+ echo "============================================================="
+ arduino-cli compile --library . --warnings none -b esp32:esp32:esp32 "examples/$i/$i.ino"
+ done
+
+ arduino-esp32-dev:
+ name: ESP32 (arduino-cli) - Dev
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install arduino-cli
+ run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
+
+ - name: Update core index
+ run: arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json
+
+ - name: Install core
+ run: arduino-cli core install --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json esp32:esp32
+
+ - name: Install ArduinoJson
+ run: arduino-cli lib install ArduinoJson
+
+ - name: Install AsyncTCP (ESP32)
+ run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/AsyncTCP#v3.4.10
+
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - name: Build Examples
+ run: |
+ for i in `ls examples`; do
+ echo "============================================================="
+ echo "Building examples/$i..."
+ echo "============================================================="
+ arduino-cli compile --library . --warnings none -b esp32:esp32:esp32 "examples/$i/$i.ino"
+ done
+
+ platformio-esp32-arduino2:
+ name: ESP32 (pio) - Arduino 2
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ board:
+ - esp32dev
+ - esp32-s2-saola-1
+ - esp32-s3-devkitc-1
+ - esp32-c3-devkitc-02
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ - name: Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+ with:
+ version: "latest"
+ enable-cache: false
+ - name: Install platformio
+ run: |
+ uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
+
+ - name: Build Examples
+ run: |
+ for i in `ls examples`; do
+ echo "============================================================="
+ echo "Building examples/$i..."
+ echo "============================================================="
+ PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-arduino-2
+ done
+
+ platformio-esp32-arduino-3:
+ name: ESP32 (pio) - Arduino 3
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ board:
+ - esp32dev
+ - esp32-s2-saola-1
+ - esp32-s3-devkitc-1
+ - esp32-c3-devkitc-02
+ - esp32-c6-devkitc-1
+ - esp32-h2-devkitm-1
+ - esp32-p4
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ - name: Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+ with:
+ version: "latest"
+ enable-cache: false
+ - name: Install platformio
+ run: |
+ uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
+
+ - name: Build Examples
+ run: |
+ for i in `ls examples`; do
+ echo "============================================================="
+ echo "Building examples/$i..."
+ echo "============================================================="
+ PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-arduino-3
+ done
+
+ platformio-specific-envs:
+ name: ESP32 (pio) - Specific Envs
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ env:
+ - ci-latest-asynctcp
+ - ci-no-json
+ - ci-no-chunk-inflight
+ - ci-arduino-2-esp-idf-log
+ - ci-arduino-3-esp-idf-log
+ - ci-regex
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ - name: Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+ with:
+ version: "latest"
+ enable-cache: false
+ - name: Install platformio
+ run: |
+ uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
+
+ - name: Build Examples
+ run: |
+ for i in `ls examples`; do
+ echo "============================================================="
+ echo "Building examples/$i..."
+ echo "============================================================="
+ PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=esp32dev pio run -e ${{ matrix.env }}
+ done
diff --git a/.github/workflows/build-esp8266.yml b/.github/workflows/build-esp8266.yml
new file mode 100644
index 0000000..0899b8e
--- /dev/null
+++ b/.github/workflows/build-esp8266.yml
@@ -0,0 +1,82 @@
+# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
+
+name: Build (8266)
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ - release/*
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ arduino-esp8266:
+ name: ESP8266 (arduino-cli)
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install arduino-cli
+ run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
+
+ - name: Update core index
+ run: arduino-cli core update-index --additional-urls https://arduino.esp8266.com/stable/package_esp8266com_index.json
+
+ - name: Install core
+ run: arduino-cli core install --additional-urls https://arduino.esp8266.com/stable/package_esp8266com_index.json esp8266:esp8266
+
+ - name: Install ArduinoJson
+ run: arduino-cli lib install ArduinoJson
+
+ - name: Install ESPAsyncTCP (ESP8266)
+ run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/ESPAsyncTCP#v2.0.0
+
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - name: Build Examples
+ run: |
+ for i in `ls examples`; do
+ echo "============================================================="
+ echo "Building examples/$i..."
+ echo "============================================================="
+ arduino-cli compile --library . --warnings none -b esp8266:esp8266:huzzah "examples/$i/$i.ino"
+ done
+
+ platformio-esp8266:
+ name: ESP8266 (pio)
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ board:
+ - huzzah
+ - d1_mini
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ - name: Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+ with:
+ version: "latest"
+ enable-cache: false
+ - name: Install platformio
+ run: |
+ uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
+
+ - name: Build Examples
+ run: |
+ for i in `ls examples`; do
+ echo "============================================================="
+ echo "Building examples/$i..."
+ echo "============================================================="
+ PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-esp8266
+ done
diff --git a/.github/workflows/build-libretiny.yml b/.github/workflows/build-libretiny.yml
new file mode 100644
index 0000000..fcc408a
--- /dev/null
+++ b/.github/workflows/build-libretiny.yml
@@ -0,0 +1,51 @@
+# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
+
+name: Build (LibreTiny)
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ - release/*
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ platformio-libretiny:
+ name: LibreTiny (pio)
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ board:
+ - generic-bk7231n-qfn32-tuya
+ - generic-rtl8710bn-2mb-788k
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ - name: Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+ with:
+ version: "latest"
+ enable-cache: false
+ - name: Install platformio
+ run: |
+ uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
+
+ - name: Build Examples
+ run: |
+ for i in AsyncResponseStream Auth Headers; do
+ echo "============================================================="
+ echo "Building examples/$i..."
+ echo "============================================================="
+ PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-libretiny
+ done
diff --git a/.github/workflows/build-rpi.yml b/.github/workflows/build-rpi.yml
new file mode 100644
index 0000000..eca7fc5
--- /dev/null
+++ b/.github/workflows/build-rpi.yml
@@ -0,0 +1,89 @@
+# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
+
+name: Build (RPI)
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ - release/*
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ arduino-rpi:
+ name: RPI (arduino-cli)
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ board:
+ - rpipicow
+ - rpipico2w
+
+ steps:
+ - name: Install arduino-cli
+ run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
+
+ - name: Update core index
+ run: arduino-cli core update-index --additional-urls https://github.com/earlephilhower/arduino-pico/releases/download/4.4.4/package_rp2040_index.json
+
+ - name: Install core
+ run: arduino-cli core install --additional-urls https://github.com/earlephilhower/arduino-pico/releases/download/4.4.4/package_rp2040_index.json rp2040:rp2040
+
+ - name: Install ArduinoJson
+ run: arduino-cli lib install ArduinoJson
+
+ - name: Install RPAsyncTCP
+ run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ayushsharma82/RPAsyncTCP#v1.3.2
+
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - name: Build Examples
+ run: |
+ for i in `ls examples`; do
+ echo "============================================================="
+ echo "Building examples/$i..."
+ echo "============================================================="
+ arduino-cli compile --library . --warnings none -b rp2040:rp2040:${{ matrix.board }} "examples/$i/$i.ino"
+ done
+
+ platformio-rpi:
+ name: RPI (pio)
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ board:
+ - rpipicow
+ - rpipico2w
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ - name: Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+ - name: Install uv
+ uses: astral-sh/setup-uv@v6
+ with:
+ version: "latest"
+ enable-cache: false
+ - name: Install platformio
+ run: |
+ uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip
+
+ - name: Build Examples
+ run: |
+ for i in `ls examples`; do
+ echo "============================================================="
+ echo "Building examples/$i..."
+ echo "============================================================="
+ PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-raspberrypi
+ done
diff --git a/.github/workflows/cpplint.yml b/.github/workflows/cpplint.yml
new file mode 100644
index 0000000..0f905a0
--- /dev/null
+++ b/.github/workflows/cpplint.yml
@@ -0,0 +1,46 @@
+# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
+
+name: Cpplint
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ - release/*
+ pull_request:
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ cpplint:
+ name: cpplint
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+
+ - name: Cache
+ uses: actions/cache@v4
+ with:
+ key: ${{ runner.os }}-cpplint
+ path: ~/.cache/pip
+
+ - name: Pyhton
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+
+ - name: cpplint
+ run: |
+ python -m pip install --upgrade pip
+ pip install --upgrade cpplint
+ cpplint \
+ --repository=. \
+ --recursive \
+ --filter=-build/c++11,-build/namespaces,-readability/braces,-readability/casting,-readability/todo,-runtime/explicit,-runtime/indentation_namespace,-runtime/int,-runtime/references,-whitespace/blank_line,,-whitespace/braces,-whitespace/comments,-whitespace/indent,-whitespace/line_length,-whitespace/newline,-whitespace/parens \
+ lib \
+ include \
+ src
diff --git a/.github/workflows/pre-commit-status.yml b/.github/workflows/pre-commit-status.yml
new file mode 100644
index 0000000..d006066
--- /dev/null
+++ b/.github/workflows/pre-commit-status.yml
@@ -0,0 +1,64 @@
+# This needs to be in a separate workflow because it requires higher permissions than the calling workflow
+name: Report Pre-commit Check Status
+
+on:
+ workflow_run:
+ workflows: [Pre-commit hooks]
+ types:
+ - completed
+
+permissions:
+ statuses: write
+
+jobs:
+ report-success:
+ name: Report pre-commit success
+ if: github.event.workflow_run.conclusion == 'success'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Report success
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const owner = '${{ github.repository_owner }}';
+ const repo = '${{ github.repository }}'.split('/')[1];
+ const sha = '${{ github.event.workflow_run.head_sha }}';
+ core.debug(`owner: ${owner}`);
+ core.debug(`repo: ${repo}`);
+ core.debug(`sha: ${sha}`);
+ const { context: name, state } = (await github.rest.repos.createCommitStatus({
+ context: 'Pre-commit checks',
+ description: 'Pre-commit checks successful',
+ owner: owner,
+ repo: repo,
+ sha: sha,
+ state: 'success',
+ target_url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}'
+ })).data;
+ core.info(`${name} is ${state}`);
+
+ report-pending:
+ name: Report pre-commit pending
+ if: github.event.workflow_run.conclusion != 'success'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Report pending
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const owner = '${{ github.repository_owner }}';
+ const repo = '${{ github.repository }}'.split('/')[1];
+ const sha = '${{ github.event.workflow_run.head_sha }}';
+ core.debug(`owner: ${owner}`);
+ core.debug(`repo: ${repo}`);
+ core.debug(`sha: ${sha}`);
+ const { context: name, state } = (await github.rest.repos.createCommitStatus({
+ context: 'Pre-commit checks',
+ description: 'The pre-commit checks need to be successful before merging',
+ owner: owner,
+ repo: repo,
+ sha: sha,
+ state: 'pending',
+ target_url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}'
+ })).data;
+ core.info(`${name} is ${state}`);
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
new file mode 100644
index 0000000..bf82de7
--- /dev/null
+++ b/.github/workflows/pre-commit.yml
@@ -0,0 +1,80 @@
+name: Pre-commit hooks
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - main
+ pull_request:
+ types: [opened, reopened, synchronize, labeled]
+
+concurrency:
+ group: pre-commit-${{github.event.pull_request.number || github.ref}}
+ cancel-in-progress: true
+
+jobs:
+ lint:
+ if: |
+ github.event_name != 'pull_request' ||
+ contains(github.event.pull_request.labels.*.name, 'Status: Pending Merge') ||
+ contains(github.event.pull_request.labels.*.name, 'Re-trigger Pre-commit')
+
+ name: Check if fixes are needed
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout latest commit
+ uses: actions/checkout@v5
+ with:
+ fetch-depth: 2
+
+ - name: Remove Label
+ if: contains(github.event.pull_request.labels.*.name, 'Re-trigger Pre-commit')
+ run: gh pr edit ${{ github.event.number }} --remove-label 'Re-trigger Pre-commit'
+ env:
+ GH_TOKEN: ${{ github.token }}
+
+ - name: Set up Python 3
+ uses: actions/setup-python@v6
+ with:
+ cache-dependency-path: pre-commit.requirements.txt
+ cache: "pip"
+ python-version: "3.13"
+
+ - name: Get Python version hash
+ run: |
+ echo "Using $(python -VV)"
+ echo "PY_HASH=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV
+
+ - name: Restore pre-commit cache
+ uses: actions/cache/restore@v4
+ id: restore-cache
+ with:
+ path: |
+ ~/.cache/pre-commit
+ key: pre-commit-${{ env.PY_HASH }}-${{ hashFiles('.pre-commit-config.yaml', '.github/workflows/pre-commit.yml', 'pre-commit.requirements.txt') }}
+
+ - name: Install python dependencies
+ run: python -m pip install -r pre-commit.requirements.txt
+
+ - name: Get changed files
+ id: changed-files
+ uses: tj-actions/changed-files@v42.0.2
+
+ - name: Run pre-commit hooks in changed files
+ run: pre-commit run --color=always --show-diff-on-failure --files ${{ steps.changed-files.outputs.all_changed_files }}
+
+ - name: Save pre-commit cache
+ uses: actions/cache/save@v4
+ if: ${{ always() && steps.restore-cache.outputs.cache-hit != 'true' }}
+ continue-on-error: true
+ with:
+ path: |
+ ~/.cache/pre-commit
+ key: ${{ steps.restore-cache.outputs.cache-primary-key }}
+
+ - name: Push changes using pre-commit-ci-lite
+ uses: pre-commit-ci/lite-action@v1.1.0
+ # Only push changes in PRs
+ if: ${{ always() && github.event_name == 'pull_request' }}
+ with:
+ msg: "ci(pre-commit): Apply automatic fixes"
diff --git a/.github/workflows/publish-pio-registry.yml b/.github/workflows/publish-pio-registry.yml
new file mode 100644
index 0000000..c0591ee
--- /dev/null
+++ b/.github/workflows/publish-pio-registry.yml
@@ -0,0 +1,62 @@
+# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
+
+name: Publish to PlatformIO
+
+on:
+ workflow_dispatch:
+ inputs:
+ tag_name:
+ description: "Tag name of the release to publish (use 'latest' for the most recent tag)"
+ required: true
+ default: "latest"
+
+jobs:
+ publish:
+ name: Publish
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ with:
+ fetch-tags: true
+ fetch-depth: 0 # Ensure all commits and tags are fetched
+
+ - name: Show latest tag
+ run: git tag --sort=-creatordate | head -n 1
+
+ - name: Download release zip
+ run: |
+ if [ "${{ inputs.tag_name }}" == "latest" ]; then
+ TAG_NAME=$(git tag --sort=-creatordate | head -n 1)
+ if [ -z "$TAG_NAME" ]; then
+ echo "Error: No tags found in the repository."
+ exit 1
+ fi
+ else
+ TAG_NAME="${{ inputs.tag_name }}"
+ fi
+ echo "Downloading tag: $TAG_NAME"
+ curl -L -o project.zip "https://github.com/${{ github.repository }}/archive/refs/tags/$TAG_NAME.zip"
+
+ # - name: Cache PlatformIO
+ # uses: actions/cache@v4
+ # with:
+ # key: ${{ runner.os }}-pio
+ # path: |
+ # ~/.cache/pip
+ # ~/.platformio
+
+ - name: Python
+ uses: actions/setup-python@v6
+ with:
+ python-version: "3.13"
+
+ - name: Install PlatformIO CLI
+ run: |
+ python -m pip install --upgrade pip
+ pip install --upgrade platformio
+
+ - name: Publish to PlatformIO
+ run: pio pkg publish --no-interactive --owner ${{ github.repository_owner }} project.zip
+ env:
+ PLATFORMIO_AUTH_TOKEN: ${{ secrets.PLATFORMIO_AUTH_TOKEN }}
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
new file mode 100644
index 0000000..14ceb43
--- /dev/null
+++ b/.github/workflows/stale.yaml
@@ -0,0 +1,24 @@
+name: "Close stale issues and PRs"
+on:
+ schedule:
+ - cron: "30 1 * * *"
+
+jobs:
+ stale:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/stale@v9
+ with:
+ stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days."
+ stale-pr-message: "This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days."
+ close-issue-message: "This issue was closed because it has been stalled for 7 days with no activity."
+ close-pr-message: "This PR was closed because it has been stalled for 7 days with no activity."
+ days-before-issue-stale: 30
+ days-before-pr-stale: 30
+ days-before-issue-close: 7
+ days-before-pr-close: 7
+
+permissions:
+ # contents: write # only for delete-branch option
+ issues: write
+ pull-requests: write
diff --git a/.github/workflows/upload-idf-component.yml b/.github/workflows/upload-idf-component.yml
new file mode 100644
index 0000000..2ab44b4
--- /dev/null
+++ b/.github/workflows/upload-idf-component.yml
@@ -0,0 +1,50 @@
+name: Publish ESP-IDF Component
+
+on:
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: 'Component version (1.2.3, 1.2.3-rc1 or 1.2.3.4)'
+ required: true
+ git_ref:
+ description: 'Git ref with the source (branch, tag or commit)'
+ required: true
+
+permissions:
+ contents: read
+
+jobs:
+ upload_components:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Get the release tag
+ env:
+ head_branch: ${{ inputs.tag || github.event.workflow_run.head_branch }}
+ run: |
+ # Read and sanitize the branch/tag name
+ branch=$(echo "$head_branch" | tr -cd '[:alnum:]/_.-')
+
+ if [[ $branch == refs/tags/* ]]; then
+ tag="${branch#refs/tags/}"
+ elif [[ $branch =~ ^[v]*[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then
+ tag=$branch
+ else
+ echo "Tag not found in $branch. Exiting..."
+ exit 1
+ fi
+
+ echo "Tag: $tag"
+ echo "RELEASE_TAG=$tag" >> $GITHUB_ENV
+
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ inputs.git_ref || env.RELEASE_TAG }}
+ submodules: "recursive"
+
+ - name: Upload components to the component registry
+ uses: espressif/upload-components-ci-action@v1
+ with:
+ name: espasyncwebserver
+ version: ${{ env.RELEASE_TAG }}
+ namespace: esp32async
+ api_token: ${{ secrets.IDF_COMPONENT_API_TOKEN }}
diff --git a/data/README.md b/data/README.md
new file mode 100644
index 0000000..96a2ee4
--- /dev/null
+++ b/data/README.md
@@ -0,0 +1,48 @@
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
+rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
+arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
+accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
+Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
+dapibus elit, id varius sem dui id lacus.
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
+rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
+arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
+accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
+Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
+dapibus elit, id varius sem dui id lacus.
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
+rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
+arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
+accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
+Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
+dapibus elit, id varius sem dui id lacus.
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
+rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
+arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
+accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
+Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
+dapibus elit, id varius sem dui id lacus.
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
+rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
+arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
+accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
+Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
+dapibus elit, id varius sem dui id lacus.
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
+rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
+arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
+accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
+Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
+dapibus elit, id varius sem dui id lacus.
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
+rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
+arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
+accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
+Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
+dapibus elit, id varius sem dui id lacus.
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod
+rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper
+arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit
+accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi.
+Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo
+dapibus elit, id varius sem dui id lacus.
diff --git a/examples/AsyncResponseStream/AsyncResponseStream.ino b/examples/AsyncResponseStream/AsyncResponseStream.ino
index 451bb1a..33b68db 100644
--- a/examples/AsyncResponseStream/AsyncResponseStream.ino
+++ b/examples/AsyncResponseStream/AsyncResponseStream.ino
@@ -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
#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
diff --git a/examples/AsyncTunnel/AsyncTunnel.ino b/examples/AsyncTunnel/AsyncTunnel.ino
index b63c056..2022450 100644
--- a/examples/AsyncTunnel/AsyncTunnel.ino
+++ b/examples/AsyncTunnel/AsyncTunnel.ino
@@ -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);
diff --git a/examples/Auth/Auth.ino b/examples/Auth/Auth.ino
index 8f5b535..b2dfb89 100644
--- a/examples/Auth/Auth.ino
+++ b/examples/Auth/Auth.ino
@@ -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 == "";
+ if (!valid) {
+ return false;
+ }
+ // 4. extract user info from token and set request attributes
+ if (token == "") {
+ request->setAttribute("user", "Mathieu");
+ request->setAttribute("role", "staff");
+ return true; // return true if token is valid, false otherwise
+ }
+ if (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 " http://192.168.4.1/auth-bearer-jwt => OK
+ // curl -v -H "Authorization: Bearer " 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();
}
diff --git a/examples/CORS/CORS.ino b/examples/CORS/CORS.ino
index 647d555..e912c6e 100644
--- a/examples/CORS/CORS.ino
+++ b/examples/CORS/CORS.ino
@@ -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
diff --git a/examples/CaptivePortal/CaptivePortal.ino b/examples/CaptivePortal/CaptivePortal.ino
index 0b8c317..820ac03 100644
--- a/examples/CaptivePortal/CaptivePortal.ino
+++ b/examples/CaptivePortal/CaptivePortal.ino
@@ -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
#if defined(ESP32) || defined(LIBRETINY)
@@ -28,7 +28,7 @@ public:
response->print("Captive Portal ");
response->print("This is our captive portal front page.
");
response->printf("You were trying to reach: http://%s%s
", 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("Try opening this link instead
", WiFi.softAPIP().toString().c_str());
#endif
response->print("");
@@ -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);
diff --git a/examples/CatchAllHandler/CatchAllHandler.ino b/examples/CatchAllHandler/CatchAllHandler.ino
index fb01410..ad3fcf1 100644
--- a/examples/CatchAllHandler/CatchAllHandler.ino
+++ b/examples/CatchAllHandler/CatchAllHandler.ino
@@ -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
diff --git a/examples/ChunkResponse/ChunkResponse.ino b/examples/ChunkResponse/ChunkResponse.ino
index 52c31c0..2877e68 100644
--- a/examples/ChunkResponse/ChunkResponse.ino
+++ b/examples/ChunkResponse/ChunkResponse.ino
@@ -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);
diff --git a/examples/ChunkRetryResponse/ChunkRetryResponse.ino b/examples/ChunkRetryResponse/ChunkRetryResponse.ino
index 4e67edb..0b01800 100644
--- a/examples/ChunkRetryResponse/ChunkRetryResponse.ino
+++ b/examples/ChunkRetryResponse/ChunkRetryResponse.ino
@@ -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
-#if __has_include("ArduinoJson.h")
-#include
-#include
-#include
-#endif
-
static const char *htmlContent PROGMEM = R"(
@@ -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;
}
}
diff --git a/examples/EndBegin/EndBegin.ino b/examples/EndBegin/EndBegin.ino
index 8e91fcf..08bbedd 100644
--- a/examples/EndBegin/EndBegin.ino
+++ b/examples/EndBegin/EndBegin.ino
@@ -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");
}
diff --git a/examples/Filters/Filters.ino b/examples/Filters/Filters.ino
index bcdb5b3..582345c 100644
--- a/examples/Filters/Filters.ino
+++ b/examples/Filters/Filters.ino
@@ -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("Captive Portal ");
response->print("This is out captive portal front page.
");
response->printf("You were trying to reach: http://%s%s
", 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("Try opening this link instead
", WiFi.softAPIP().toString().c_str());
#endif
response->print("");
@@ -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) {
diff --git a/examples/FlashResponse/FlashResponse.ino b/examples/FlashResponse/FlashResponse.ino
index 5763f22..e07b1dd 100644
--- a/examples/FlashResponse/FlashResponse.ino
+++ b/examples/FlashResponse/FlashResponse.ino
@@ -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
diff --git a/examples/HeaderManipulation/HeaderManipulation.ino b/examples/HeaderManipulation/HeaderManipulation.ino
index 5b4c9f7..04227ce 100644
--- a/examples/HeaderManipulation/HeaderManipulation.ino
+++ b/examples/HeaderManipulation/HeaderManipulation.ino
@@ -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
diff --git a/examples/Headers/Headers.ino b/examples/Headers/Headers.ino
index eee87ac..7c32b48 100644
--- a/examples/Headers/Headers.ino
+++ b/examples/Headers/Headers.ino
@@ -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
diff --git a/examples/Json/Json.ino b/examples/Json/Json.ino
index a29fec1..eeaab75 100644
--- a/examples/Json/Json.ino
+++ b/examples/Json/Json.ino
@@ -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
-#if __has_include("ArduinoJson.h")
-#include
-#include
-#include
-#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();
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();
+ root["hello"] = json.as()["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
}
diff --git a/examples/LargeResponse/LargeResponse.ino b/examples/LargeResponse/LargeResponse.ino
new file mode 100644
index 0000000..8d9d3bf
--- /dev/null
+++ b/examples/LargeResponse/LargeResponse.ino
@@ -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
+#if defined(ESP32) || defined(LIBRETINY)
+#include
+#include
+#elif defined(ESP8266)
+#include
+#include
+#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
+#include
+#include
+#endif
+
+#include
+
+static AsyncWebServer server(80);
+
+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(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(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(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);
+}
diff --git a/examples/Logging/Logging.ino b/examples/Logging/Logging.ino
index ae504b2..e716f6f 100644
--- a/examples/Logging/Logging.ino
+++ b/examples/Logging/Logging.ino
@@ -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
diff --git a/examples/MessagePack/MessagePack.ino b/examples/MessagePack/MessagePack.ino
index e038d03..678a5a2 100644
--- a/examples/MessagePack/MessagePack.ino
+++ b/examples/MessagePack/MessagePack.ino
@@ -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
-#if __has_include("ArduinoJson.h")
-#include
-#include
-#include
-#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();
- 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();
root["hello"] = json.as()["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();
+ root["hello"] = json.as()["name"];
+ response->setLength();
+ request->send(response);
+ });
#endif
server.begin();
diff --git a/examples/Middleware/Middleware.ino b/examples/Middleware/Middleware.ino
index 992a0a2..c1ecdec 100644
--- a/examples/Middleware/Middleware.ino
+++ b/examples/Middleware/Middleware.ino
@@ -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
diff --git a/examples/Params/Params.ino b/examples/Params/Params.ino
index 416218c..b27fdff 100644
--- a/examples/Params/Params.ino
+++ b/examples/Params/Params.ino
@@ -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
diff --git a/examples/PartitionDownloader/PartitionDownloader.ino b/examples/PartitionDownloader/PartitionDownloader.ino
index 1174640..d45e387 100644
--- a/examples/PartitionDownloader/PartitionDownloader.ino
+++ b/examples/PartitionDownloader/PartitionDownloader.ino
@@ -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
diff --git a/examples/PerfTests/PerfTests.ino b/examples/PerfTests/PerfTests.ino
index 001512c..d79268c 100644
--- a/examples/PerfTests/PerfTests.ino
+++ b/examples/PerfTests/PerfTests.ino
@@ -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
diff --git a/examples/RateLimit/RateLimit.ino b/examples/RateLimit/RateLimit.ino
index 5ae93e7..e87736d 100644
--- a/examples/RateLimit/RateLimit.ino
+++ b/examples/RateLimit/RateLimit.ino
@@ -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
diff --git a/examples/Redirect/Redirect.ino b/examples/Redirect/Redirect.ino
index 8f10557..410a86d 100644
--- a/examples/Redirect/Redirect.ino
+++ b/examples/Redirect/Redirect.ino
@@ -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
diff --git a/examples/RequestContinuation/RequestContinuation.ino b/examples/RequestContinuation/RequestContinuation.ino
index f59322e..43d0fc5 100644
--- a/examples/RequestContinuation/RequestContinuation.ino
+++ b/examples/RequestContinuation/RequestContinuation.ino
@@ -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
diff --git a/examples/RequestContinuationComplete/RequestContinuationComplete.ino b/examples/RequestContinuationComplete/RequestContinuationComplete.ino
index cb4a53f..5465459 100644
--- a/examples/RequestContinuationComplete/RequestContinuationComplete.ino
+++ b/examples/RequestContinuationComplete/RequestContinuationComplete.ino
@@ -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
diff --git a/examples/ResumableDownload/ResumableDownload.ino b/examples/ResumableDownload/ResumableDownload.ino
index 68646e7..a8a4550 100644
--- a/examples/ResumableDownload/ResumableDownload.ino
+++ b/examples/ResumableDownload/ResumableDownload.ino
@@ -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
diff --git a/examples/Rewrite/Rewrite.ino b/examples/Rewrite/Rewrite.ino
index 8dfeedc..533e6bb 100644
--- a/examples/Rewrite/Rewrite.ino
+++ b/examples/Rewrite/Rewrite.ino
@@ -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
diff --git a/examples/ServerSentEvents/ServerSentEvents.ino b/examples/ServerSentEvents/ServerSentEvents.ino
index 5567ec6..bc6718c 100644
--- a/examples/ServerSentEvents/ServerSentEvents.ino
+++ b/examples/ServerSentEvents/ServerSentEvents.ino
@@ -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);
diff --git a/examples/ServerSentEvents_PR156/ServerSentEvents_PR156.ino b/examples/ServerSentEvents_PR156/ServerSentEvents_PR156.ino
index cced715..ad1864a 100644
--- a/examples/ServerSentEvents_PR156/ServerSentEvents_PR156.ino
+++ b/examples/ServerSentEvents_PR156/ServerSentEvents_PR156.ino
@@ -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
diff --git a/examples/ServerState/ServerState.ino b/examples/ServerState/ServerState.ino
index 4ceddbc..e23d497 100644
--- a/examples/ServerState/ServerState.ino
+++ b/examples/ServerState/ServerState.ino
@@ -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
diff --git a/examples/SkipServerMiddleware/SkipServerMiddleware.ino b/examples/SkipServerMiddleware/SkipServerMiddleware.ino
index 0e7f172..bee7561 100644
--- a/examples/SkipServerMiddleware/SkipServerMiddleware.ino
+++ b/examples/SkipServerMiddleware/SkipServerMiddleware.ino
@@ -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
diff --git a/examples/SlowChunkResponse/SlowChunkResponse.ino b/examples/SlowChunkResponse/SlowChunkResponse.ino
index 7844ad6..8859750 100644
--- a/examples/SlowChunkResponse/SlowChunkResponse.ino
+++ b/examples/SlowChunkResponse/SlowChunkResponse.ino
@@ -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();
diff --git a/examples/StaticFile/StaticFile.ino b/examples/StaticFile/StaticFile.ino
index edc2cb2..6e4fb09 100644
--- a/examples/StaticFile/StaticFile.ino
+++ b/examples/StaticFile/StaticFile.ino
@@ -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
diff --git a/examples/Templates/Templates.ino b/examples/Templates/Templates.ino
index fdf4eb6..679188c 100644
--- a/examples/Templates/Templates.ino
+++ b/examples/Templates/Templates.ino
@@ -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);
+ }
}
diff --git a/examples/URIMatcher/README.md b/examples/URIMatcher/README.md
new file mode 100644
index 0000000..be785e3
--- /dev/null
+++ b/examples/URIMatcher/README.md
@@ -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
diff --git a/examples/URIMatcher/URIMatcher.ino b/examples/URIMatcher/URIMatcher.ino
new file mode 100644
index 0000000..ff40d1f
--- /dev/null
+++ b/examples/URIMatcher/URIMatcher.ino
@@ -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
+#if defined(ESP32) || defined(LIBRETINY)
+#include
+#include
+#elif defined(ESP8266)
+#include
+#include
+#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
+#include
+#include
+#endif
+
+#include
+
+static AsyncWebServer server(80);
+
+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"(
+
+
+ AsyncURIMatcher Examples
+
+
+
+ AsyncURIMatcher Examples
+
+
+
+
+
+
+
+)";
+#ifdef ASYNCWEBSERVER_REGEX
+ response += R"(
+
+)";
+#endif
+ response += R"(
+
+
+)";
+ request->send(200, "text/html", response);
+ });
+
+ // =============================================================================
+ // 10. NOT FOUND HANDLER
+ // =============================================================================
+
+ server.onNotFound([](AsyncWebServerRequest *request) {
+ String html = "404 - Not Found ";
+ html += "The requested URL " + request->url() + " was not found.
";
+ html += "← Back to Examples
";
+ 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);
+}
diff --git a/examples/URIMatcherTest/URIMatcherTest.ino b/examples/URIMatcherTest/URIMatcherTest.ino
new file mode 100644
index 0000000..0d6128c
--- /dev/null
+++ b/examples/URIMatcherTest/URIMatcherTest.ino
@@ -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
+#if defined(ESP32) || defined(LIBRETINY)
+#include
+#include
+#elif defined(ESP8266)
+#include
+#include
+#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
+#include
+#include
+#endif
+
+#include
+
+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);
+}
diff --git a/examples/URIMatcherTest/test_routes.sh b/examples/URIMatcherTest/test_routes.sh
new file mode 100755
index 0000000..4586da2
--- /dev/null
+++ b/examples/URIMatcherTest/test_routes.sh
@@ -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
diff --git a/examples/Upload/Upload.ino b/examples/Upload/Upload.ino
index fd80bd7..404a4d3 100644
--- a/examples/Upload/Upload.ino
+++ b/examples/Upload/Upload.ino
@@ -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;
}
diff --git a/examples/UploadFlash/UploadFlash.ino b/examples/UploadFlash/UploadFlash.ino
new file mode 100644
index 0000000..921d28b
--- /dev/null
+++ b/examples/UploadFlash/UploadFlash.ino
@@ -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
+#if defined(ESP32) || defined(LIBRETINY)
+#include
+#include
+#elif defined(ESP8266)
+#include
+#include
+#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
+#include
+#include
+#endif
+
+#include
+#include
+#include
+
+// ESP32 example ONLY
+#ifdef ESP32
+#include
+#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);
+}
diff --git a/examples/WebSocket/WebSocket.ino b/examples/WebSocket/WebSocket.ino
index c8d3727..8e0988c 100644
--- a/examples/WebSocket/WebSocket.ino
+++ b/examples/WebSocket/WebSocket.ino
@@ -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
+static const char *htmlContent PROGMEM = R"(
+
+
+
+ WebSocket
+
+
+ WebSocket Example
+ Open your browser console!
+
+ Send
+
+
+
+ )";
+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();
}
}
}
diff --git a/examples/WebSocketEasy/WebSocketEasy.ino b/examples/WebSocketEasy/WebSocketEasy.ino
index 5229910..ad97253 100644
--- a/examples/WebSocketEasy/WebSocketEasy.ino
+++ b/examples/WebSocketEasy/WebSocketEasy.ino
@@ -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"(
WebSocket Example
- <>Open your browser console!
+ Open your browser console!
Send