Update to version 3.9.6

This commit is contained in:
2026-02-10 12:34:12 +01:00
parent 2617252395
commit c98a476228
94 changed files with 4593 additions and 1434 deletions

2
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -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

91
.github/ISSUE_TEMPLATE/issue-report.yml vendored Normal file
View File

@@ -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

10
.github/dependabot.yml vendored Normal file
View File

@@ -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"

40
.github/scripts/update-version.sh vendored Executable file
View File

@@ -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 <major> <minor> <patch>" >&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 <major> <minor> <patch>" >&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

25
.github/workflows/arduino-lint.yml vendored Normal file
View File

@@ -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

194
.github/workflows/build-esp32.yml vendored Normal file
View File

@@ -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

82
.github/workflows/build-esp8266.yml vendored Normal file
View File

@@ -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

51
.github/workflows/build-libretiny.yml vendored Normal file
View File

@@ -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

89
.github/workflows/build-rpi.yml vendored Normal file
View File

@@ -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

46
.github/workflows/cpplint.yml vendored Normal file
View File

@@ -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

64
.github/workflows/pre-commit-status.yml vendored Normal file
View File

@@ -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}`);

80
.github/workflows/pre-commit.yml vendored Normal file
View File

@@ -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"

View File

@@ -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 }}

24
.github/workflows/stale.yaml vendored Normal file
View File

@@ -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

View File

@@ -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 }}

48
data/README.md Normal file
View File

@@ -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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.</p>
<p>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.

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
#include <DNSServer.h>
#if defined(ESP32) || defined(LIBRETINY)
@@ -20,7 +20,7 @@ static AsyncWebServer server(80);
void setup() {
Serial.begin(115200);
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif

View File

@@ -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);

View File

@@ -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");
if (request->getAttribute("user") == "Mathieu") {
request->setAttribute("role", "staff");
if (request->hasParam("token")) {
request->setAttribute("token", request->getParam("token")->value().c_str());
} else {
request->setAttribute("role", "user");
}
next();
});
static AsyncAuthorizationMiddleware authz([](AsyncWebServerRequest *request) {
return request->getAttribute("token") == "123";
return request->getAttribute("role") == "staff";
});
void setup() {
Serial.begin(115200);
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif
@@ -87,6 +93,36 @@ void setup() {
digestAuthHash.setAuthFailureMessage("Authentication failed");
digestAuthHash.setAuthType(AsyncAuthType::AUTH_DIGEST);
// bearer authentication with shared key
bearerAuthSharedKey.setAuthType(AsyncAuthType::AUTH_BEARER);
bearerAuthSharedKey.setToken("shared-secret-key");
// bearer authentication with a JWT token
bearerAuthJWT.setAuthType(AsyncAuthType::AUTH_BEARER);
bearerAuthJWT.setAuthentificationFunction([](AsyncWebServerRequest *request) {
const String &token = request->authChallenge();
// 1. decode base64 token
// 2. decrypt token
const String &decrypted = "..."; // TODO
// 3. validate token (check signature, expiration, etc)
bool valid = token == "<token>" || token == "<another token>";
if (!valid) {
return false;
}
// 4. extract user info from token and set request attributes
if (token == "<token>") {
request->setAttribute("user", "Mathieu");
request->setAttribute("role", "staff");
return true; // return true if token is valid, false otherwise
}
if (token == "<another token>") {
request->setAttribute("user", "Bob");
request->setAttribute("role", "user");
return true; // return true if token is valid, false otherwise
}
return false;
});
// basic authentication method
// curl -v -u admin:admin http://192.168.4.1/auth-basic
server
@@ -132,9 +168,9 @@ void setup() {
.addMiddleware(&digestAuthHash);
// test digest auth custom authorization middleware
// curl -v --digest -u user:password http://192.168.4.1/auth-custom?token=123 => OK
// curl -v --digest -u user:password http://192.168.4.1/auth-custom?token=456 => 403
// curl -v --digest -u user:FAILED http://192.168.4.1/auth-custom?token=456 => 401
// curl -v --digest -u Mathieu:password http://192.168.4.1/auth-custom => OK
// curl -v --digest -u Bob:password http://192.168.4.1/auth-custom => 403
// curl -v --digest -u any:password http://192.168.4.1/auth-custom => 401
server
.on(
"/auth-custom", HTTP_GET,
@@ -148,6 +184,32 @@ void setup() {
)
.addMiddlewares({&complexAuth, &authz});
// Bearer authentication with a shared key
// curl -v -H "Authorization: Bearer shared-secret-key" http://192.168.4.1/auth-bearer-shared-key => OK
server
.on(
"/auth-bearer-shared-key", HTTP_GET,
[](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "Hello, world!");
}
)
.addMiddleware(&bearerAuthSharedKey);
// Bearer authentication with a JWT token
// curl -v -H "Authorization: Bearer <token>" http://192.168.4.1/auth-bearer-jwt => OK
// curl -v -H "Authorization: Bearer <another token>" http://192.168.4.1/auth-bearer-jwt => 403 Forbidden
// curl -v -H "Authorization: Bearer invalid-token" http://192.168.4.1/auth-bearer-jwt => 401 Unauthorized
server
.on(
"/auth-bearer-jwt", HTTP_GET,
[](AsyncWebServerRequest *request) {
Serial.println("User: " + request->getAttribute("user"));
Serial.println("Role: " + request->getAttribute("role"));
request->send(200, "text/plain", "Hello, world!");
}
)
.addMiddlewares({&bearerAuthJWT, &authz});
server.begin();
}

View File

@@ -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

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
#include <DNSServer.h>
#if defined(ESP32) || defined(LIBRETINY)
@@ -28,7 +28,7 @@ public:
response->print("<!DOCTYPE html><html><head><title>Captive Portal</title></head><body>");
response->print("<p>This is our captive portal front page.</p>");
response->printf("<p>You were trying to reach: http://%s%s</p>", request->host().c_str(), request->url().c_str());
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
response->printf("<p>Try opening <a href='http://%s'>this link</a> instead</p>", WiFi.softAPIP().toString().c_str());
#endif
response->print("</body></html>");
@@ -41,7 +41,7 @@ void setup() {
Serial.println();
Serial.println("Configuring access point...");
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
if (!WiFi.softAP("esp-captive")) {
Serial.println("Soft AP creation failed.");
while (1);

View File

@@ -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

View File

@@ -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);

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
//
// Shows how to wait in a chunk response for incoming data
@@ -19,12 +19,6 @@
#include <ESPAsyncWebServer.h>
#if __has_include("ArduinoJson.h")
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <AsyncMessagePack.h>
#endif
static const char *htmlContent PROGMEM = R"(
<!DOCTYPE html>
<html>
@@ -96,7 +90,7 @@ static int key = -1;
void setup() {
Serial.begin(115200);
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif
@@ -107,7 +101,7 @@ void setup() {
server.addMiddleware(&requestLogger);
#if __has_include("ArduinoJson.h")
#if ASYNC_JSON_SUPPORT == 1
//
// HOW TO RUN THIS EXAMPLE:
@@ -174,7 +168,7 @@ void setup() {
return 0; // 0 means we are done
}
// log_d("UART answered!");
// async_ws_log_d("UART answered!");
String answer = "You typed: ";
answer.concat((char)key);
@@ -193,10 +187,10 @@ void setup() {
},
NULL, // upload handler is not used so it should be NULL
[](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
// log_d("Body: index: %u, len: %u, total: %u", index, len, total);
// async_ws_log_d("Body: index: %u, len: %u, total: %u", index, len, total);
if (!index) {
// log_d("Start body parsing");
// async_ws_log_d("Start body parsing");
request->_tempObject = new String();
// cast request->_tempObject pointer to String and reserve total size
((String *)request->_tempObject)->reserve(total);
@@ -204,7 +198,7 @@ void setup() {
request->client()->setRxTimeout(30);
}
// log_d("Append body data");
// async_ws_log_d("Append body data");
((String *)request->_tempObject)->concat((const char *)data, len);
}
);
@@ -217,13 +211,13 @@ void setup() {
void loop() {
if (triggerUART.length() && key == -1) {
Serial.println(triggerUART);
// log_d("Waiting for UART input...");
// async_ws_log_d("Waiting for UART input...");
while (!Serial.available()) {
delay(100);
}
key = Serial.read();
Serial.flush();
// log_d("UART input: %c", key);
// async_ws_log_d("UART input: %c", key);
triggerUART = emptyString;
}
}

View File

@@ -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");
}

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
//
// Shows how to use setFilter to route requests to different handlers based on WiFi mode
@@ -32,7 +32,7 @@ public:
response->print("<!DOCTYPE html><html><head><title>Captive Portal</title></head><body>");
response->print("<p>This is out captive portal front page.</p>");
response->printf("<p>You were trying to reach: http://%s%s</p>", request->host().c_str(), request->url().c_str());
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
response->printf("<p>Try opening <a href='http://%s'>this link</a> instead</p>", WiFi.softAPIP().toString().c_str());
#endif
response->print("</body></html>");
@@ -51,17 +51,17 @@ void setup() {
"/", HTTP_GET,
[](AsyncWebServerRequest *request) {
Serial.println("Captive portal request...");
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
Serial.println("WiFi.localIP(): " + WiFi.localIP().toString());
#endif
Serial.println("request->client()->localIP(): " + request->client()->localIP().toString());
#if ESP_IDF_VERSION_MAJOR >= 5
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
Serial.println("WiFi.type(): " + String((int)WiFi.localIP().type()));
#endif
Serial.println("request->client()->type(): " + String((int)request->client()->localIP().type()));
#endif
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
Serial.println(WiFi.localIP() == request->client()->localIP() ? "should be: ON_STA_FILTER" : "should be: ON_AP_FILTER");
Serial.println(WiFi.localIP() == request->client()->localIP());
Serial.println(WiFi.localIP().toString() == request->client()->localIP().toString());
@@ -77,17 +77,17 @@ void setup() {
"/", HTTP_GET,
[](AsyncWebServerRequest *request) {
Serial.println("Website request...");
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
Serial.println("WiFi.localIP(): " + WiFi.localIP().toString());
#endif
Serial.println("request->client()->localIP(): " + request->client()->localIP().toString());
#if ESP_IDF_VERSION_MAJOR >= 5
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
Serial.println("WiFi.type(): " + String((int)WiFi.localIP().type()));
#endif
Serial.println("request->client()->type(): " + String((int)request->client()->localIP().type()));
#endif
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
Serial.println(WiFi.localIP() == request->client()->localIP() ? "should be: ON_STA_FILTER" : "should be: ON_AP_FILTER");
Serial.println(WiFi.localIP() == request->client()->localIP());
Serial.println(WiFi.localIP().toString() == request->client()->localIP().toString());
@@ -113,7 +113,7 @@ void setup() {
// dnsServer.stop();
// WiFi.softAPdisconnect();
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.persistent(false);
WiFi.begin("IoT");
while (WiFi.status() != WL_CONNECTED) {

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
//
// Shows how to send and receive Json data
@@ -19,27 +19,21 @@
#include <ESPAsyncWebServer.h>
#if __has_include("ArduinoJson.h")
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <AsyncMessagePack.h>
#endif
static AsyncWebServer server(80);
#if __has_include("ArduinoJson.h")
#if ASYNC_JSON_SUPPORT == 1
static AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler("/json2");
#endif
void setup() {
Serial.begin(115200);
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif
#if __has_include("ArduinoJson.h")
#if ASYNC_JSON_SUPPORT == 1
//
// sends JSON using AsyncJsonResponse
//
@@ -62,8 +56,8 @@ void setup() {
JsonDocument doc;
JsonObject root = doc.to<JsonObject>();
root["foo"] = "bar";
serializeJson(root, *response);
Serial.println();
// serializeJson(root, Serial);
// Serial.println();
request->send(response);
});
@@ -92,12 +86,38 @@ void setup() {
});
server.addHandler(handler);
// New Json API since 3.8.2, which works for both Json and MessagePack bodies
// curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"You"}' http://192.168.4.1/json3
server.on("/json3", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &json) {
Serial.printf("Body request : ");
serializeJson(json, Serial);
Serial.println();
AsyncJsonResponse *response = new AsyncJsonResponse();
JsonObject root = response->getRoot().to<JsonObject>();
root["hello"] = json.as<JsonObject>()["name"];
response->setLength();
request->send(response);
});
#endif
server.begin();
}
// not needed
static uint32_t lastHeapTime = 0;
static uint32_t lastHeap = 0;
void loop() {
delay(100);
#ifdef ESP32
uint32_t now = millis();
if (now - lastHeapTime >= 500) {
uint32_t heap = ESP.getFreeHeap();
if (heap != lastHeap) {
lastHeap = heap;
async_ws_log_w("Free heap: %" PRIu32, heap);
}
lastHeapTime = now;
}
#endif
}

View File

@@ -0,0 +1,178 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
//
// Example to send a large response and control the filling of the buffer.
//
// This is also a MRE for:
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/242
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/315
//
#include <Arduino.h>
#if defined(ESP32) || defined(LIBRETINY)
#include <AsyncTCP.h>
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#include <RPAsyncTCP.h>
#include <WiFi.h>
#endif
#include <ESPAsyncWebServer.h>
static AsyncWebServer server(80);
static const size_t totalResponseSize = 16 * 1000; // 16 KB
static char fillChar = 'A';
class CustomResponse : public AsyncAbstractResponse {
public:
explicit CustomResponse() {
_code = 200;
_contentType = "text/plain";
_sendContentLength = false;
}
bool _sourceValid() const override {
return true;
}
size_t _fillBuffer(uint8_t *buf, size_t buflen) override {
if (_sent == RESPONSE_TRY_AGAIN) {
Serial.println("Simulating temporary unavailability of data...");
_sent = 0;
return RESPONSE_TRY_AGAIN;
}
size_t remaining = totalResponseSize - _sent;
if (remaining == 0) {
return 0;
}
if (buflen > remaining) {
buflen = remaining;
}
Serial.printf("Filling '%c' @ sent: %u, buflen: %u\n", fillChar, _sent, buflen);
std::fill_n(buf, buflen, static_cast<uint8_t>(fillChar));
_sent += buflen;
fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1;
return buflen;
}
private:
char fillChar = 'A';
size_t _sent = 0;
};
// Code to reproduce issues:
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/242
// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/315
//
// https://github.com/ESP32Async/ESPAsyncWebServer/pull/317#issuecomment-3421141039
//
// I cracked it.
// So this is how it works:
// That space that _tcp is writing to identified by CONFIG_TCP_SND_BUF_DEFAULT (and is value-matching with default TCP windows size which is very confusing itself).
// The space returned by client()->write() and client->space() somehow might not be atomically/thread synced (had not dived that deep yet). So if first call to _fillBuffer is done via user-code thread and ended up with some small amount of data consumed and second one is done by _poll or _ack? returns full size again! This is where old code fails.
// If you change your class this way it will fail 100%.
class CustomResponseMRE : public AsyncAbstractResponse {
public:
explicit CustomResponseMRE() {
_code = 200;
_contentType = "text/plain";
_sendContentLength = false;
// add some useless headers
addHeader("Clear-Site-Data", "Clears browsing data (e.g., cookies, storage, cache) associated with the requesting website.");
addHeader(
"No-Vary-Search", "Specifies a set of rules that define how a URL's query parameters will affect cache matching. These rules dictate whether the same "
"URL with different URL parameters should be saved as separate browser cache entries"
);
}
bool _sourceValid() const override {
return true;
}
size_t _fillBuffer(uint8_t *buf, size_t buflen) override {
if (fillChar == NULL) {
fillChar = 'A';
return RESPONSE_TRY_AGAIN;
}
if (_sent == RESPONSE_TRY_AGAIN) {
Serial.println("Simulating temporary unavailability of data...");
_sent = 0;
return RESPONSE_TRY_AGAIN;
}
size_t remaining = totalResponseSize - _sent;
if (remaining == 0) {
return 0;
}
if (buflen > remaining) {
buflen = remaining;
}
Serial.printf("Filling '%c' @ sent: %u, buflen: %u\n", fillChar, _sent, buflen);
std::fill_n(buf, buflen, static_cast<uint8_t>(fillChar));
_sent += buflen;
fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1;
return buflen;
}
private:
char fillChar = NULL;
size_t _sent = 0;
};
void setup() {
Serial.begin(115200);
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif
// Example to use a AwsResponseFiller
//
// curl -v http://192.168.4.1/1 | grep -o '.' | sort | uniq -c
//
// Should output 16000 and a distribution of letters which is the same in ESP32 logs and console
//
server.on("/1", HTTP_GET, [](AsyncWebServerRequest *request) {
fillChar = 'A';
AsyncWebServerResponse *response = request->beginResponse("text/plain", totalResponseSize, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
size_t remaining = totalResponseSize - index;
size_t toSend = (remaining < maxLen) ? remaining : maxLen;
Serial.printf("Filling '%c' @ index: %u, maxLen: %u, toSend: %u\n", fillChar, index, maxLen, toSend);
std::fill_n(buffer, toSend, static_cast<uint8_t>(fillChar));
fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1;
return toSend;
});
request->send(response);
});
// Example to use a AsyncAbstractResponse
//
// curl -v http://192.168.4.1/2 | grep -o '.' | sort | uniq -c
//
// Should output 16000 and a distribution of letters which is the same in ESP32 logs and console
//
server.on("/2", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(new CustomResponse());
});
// Example to use a AsyncAbstractResponse
//
// curl -v http://192.168.4.1/3 | grep -o '.' | sort | uniq -c
//
// Should output 16000 and a distribution of letters which is the same in ESP32 logs and console
//
server.on("/3", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(new CustomResponseMRE());
});
server.begin();
}
void loop() {
delay(100);
}

View File

@@ -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

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
//
// Shows how to send and receive Message Pack data
@@ -19,27 +19,21 @@
#include <ESPAsyncWebServer.h>
#if __has_include("ArduinoJson.h")
#include <ArduinoJson.h>
#include <AsyncJson.h>
#include <AsyncMessagePack.h>
#endif
static AsyncWebServer server(80);
#if __has_include("ArduinoJson.h")
static AsyncCallbackMessagePackWebHandler *handler = new AsyncCallbackMessagePackWebHandler("/msgpack2");
#if ASYNC_JSON_SUPPORT == 1
static AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler("/msgpack2");
#endif
void setup() {
Serial.begin(115200);
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif
#if __has_include("ArduinoJson.h")
#if ASYNC_JSON_SUPPORT == 1
//
// sends MessagePack using AsyncMessagePackResponse
//
@@ -57,18 +51,26 @@ void setup() {
//
// curl -v http://192.168.4.1/msgpack2
//
// Save file: curl -v http://192.168.4.1/msgpack2 -o msgpack.bin
//
server.on("/msgpack2", HTTP_GET, [](AsyncWebServerRequest *request) {
AsyncResponseStream *response = request->beginResponseStream("application/msgpack");
JsonDocument doc;
JsonObject root = doc.to<JsonObject>();
root["foo"] = "bar";
root["name"] = "Bob";
serializeMsgPack(root, *response);
request->send(response);
});
// POST file:
//
// curl -v -X POST -H 'Content-Type: application/msgpack' --data-binary @msgpack.bin http://192.168.4.1/msgpack2
//
handler->setMethod(HTTP_POST | HTTP_PUT);
handler->onRequest([](AsyncWebServerRequest *request, JsonVariant &json) {
Serial.printf("Body request /msgpack2 : "); // should print: Body request /msgpack2 : {"name":"Bob"}
serializeJson(json, Serial);
Serial.println();
AsyncMessagePackResponse *response = new AsyncMessagePackResponse();
JsonObject root = response->getRoot().to<JsonObject>();
root["hello"] = json.as<JsonObject>()["name"];
@@ -77,6 +79,22 @@ void setup() {
});
server.addHandler(handler);
// New Json API since 3.8.2, which works for both Json and MessagePack bodies
//
// curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"You"}' http://192.168.4.1/msgpack3
// curl -v -X POST -H 'Content-Type: application/msgpack' --data-binary @msgpack.bin http://192.168.4.1/msgpack3
//
server.on("/msgpack3", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &json) {
Serial.printf("Body request /msgpack3 : "); // should print: Body request /msgpack3 : {"name":"Bob"}
serializeJson(json, Serial);
Serial.println();
AsyncJsonResponse *response = new AsyncJsonResponse();
JsonObject root = response->getRoot().to<JsonObject>();
root["hello"] = json.as<JsonObject>()["name"];
response->setLength();
request->send(response);
});
#endif
server.begin();

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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

View File

@@ -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);
}
}

View File

@@ -0,0 +1,349 @@
# AsyncURIMatcher Example
This example demonstrates the comprehensive URI matching capabilities of the ESPAsyncWebServer library using the `AsyncURIMatcher` class.
## Overview
The `AsyncURIMatcher` class provides flexible and powerful URL routing mechanisms that go beyond simple string matching. It supports various matching strategies that can be combined to create sophisticated routing rules.
**Important**: When using plain strings (not `AsyncURIMatcher` objects), the library uses auto-detection (`URIMatchAuto`) which analyzes the URI pattern and applies appropriate matching rules. This is **not** simple exact matching - it combines exact and folder matching by default!
## What's Demonstrated
This example includes two Arduino sketches:
1. **URIMatcher.ino** - Interactive web-based demonstration with a user-friendly homepage
2. **URIMatcherTest.ino** - Comprehensive test suite with automated shell script testing
Both sketches create a WiFi Access Point (`esp-captive`) for easy testing without network configuration.
## Auto-Detection Behavior
When you pass a plain string or `const char*` to `server.on()`, the `URIMatchAuto` flag is used, which:
1. **Empty URI**: Matches everything
2. **Ends with `*`**: Becomes prefix match (`URIMatchPrefix`)
3. **Contains `/*.ext`**: Becomes extension match (`URIMatchExtension`)
4. **Starts with `^` and ends with `$`**: Becomes regex match (if enabled)
5. **Everything else**: Becomes **both** exact and folder match (`URIMatchPrefixFolder | URIMatchExact`)
This means traditional string-based routes like `server.on("/path", handler)` will match:
- `/path` (exact match)
- `/path/` (folder with trailing slash)
- `/path/anything` (folder match)
But will **NOT** match `/path-suffix` (prefix without folder separator).
## Features Demonstrated
### 1. **Auto-Detection (Traditional Behavior)**
Demonstrates how traditional string-based routing automatically combines exact and folder matching.
**Examples in URIMatcher.ino:**
- `/auto` - Matches both `/auto` exactly AND `/auto/sub` as folder
- `/wildcard*` - Auto-detects as prefix match (due to trailing `*`)
- `/auto-images/*.png` - Auto-detects as extension match (due to `/*.ext` pattern)
**Examples in URIMatcherTest.ino:**
- `/exact` - Matches `/exact`, `/exact/`, and `/exact/sub`
- `/api/users` - Matches exact path and subpaths under `/api/users/`
- `/*.json` - Matches any `.json` file anywhere
- `/*.css` - Matches any `.css` file anywhere
### 2. **Exact Matching (Factory Method)**
Using `AsyncURIMatcher::exact()` matches only the exact URL, **NOT** subpaths.
**Key difference from auto-detection:** `AsyncURIMatcher::exact("/path")` matches **only** `/path`, while `server.on("/path", ...)` matches both `/path` and `/path/sub`.
**Examples in URIMatcher.ino:**
- `AsyncURIMatcher::exact("/exact")` - Matches only `/exact`
**Examples in URIMatcherTest.ino:**
- `AsyncURIMatcher::exact("/factory/exact")` - Matches only `/factory/exact`
- Does NOT match `/factory/exact/sub` (404 response)
### 3. **Prefix Matching**
Using `AsyncURIMatcher::prefix()` matches URLs that start with the specified pattern.
**Examples in URIMatcher.ino:**
- `AsyncURIMatcher::prefix("/service")` - Matches `/service`, `/service-test`, `/service/status`
**Examples in URIMatcherTest.ino:**
- `AsyncURIMatcher::prefix("/factory/prefix")` - Matches `/factory/prefix`, `/factory/prefix-test`, `/factory/prefix/sub`
- Traditional: `/api/*` - Matches `/api/data`, `/api/v1/posts`
- Traditional: `/files/*` - Matches `/files/document.pdf`, `/files/images/photo.jpg`
### 4. **Folder/Directory Matching**
Using `AsyncURIMatcher::dir()` matches URLs under a directory (automatically adds trailing slash).
**Important:** Directory matching requires a trailing slash in the URL - it does NOT match the directory itself.
**Examples in URIMatcher.ino:**
- `AsyncURIMatcher::dir("/admin")` - Matches `/admin/users`, `/admin/settings`
- Does NOT match `/admin` without trailing slash
**Examples in URIMatcherTest.ino:**
- `AsyncURIMatcher::dir("/factory/dir")` - Matches `/factory/dir/users`, `/factory/dir/sub/path`
- Does NOT match `/factory/dir` itself (404 response)
### 5. **Extension Matching**
Using `AsyncURIMatcher::ext()` matches files with specific extensions.
**Examples in URIMatcher.ino:**
- `AsyncURIMatcher::ext("/images/*.jpg")` - Matches `/images/photo.jpg`, `/images/sub/pic.jpg`
**Examples in URIMatcherTest.ino:**
- `AsyncURIMatcher::ext("/factory/files/*.txt")` - Matches `/factory/files/doc.txt`, `/factory/files/sub/readme.txt`
- Does NOT match `/factory/files/doc.pdf` (wrong extension)
### 6. **Case Insensitive Matching**
Using `AsyncURIMatcher::CaseInsensitive` flag matches URLs regardless of character case.
**Examples in URIMatcher.ino:**
- `AsyncURIMatcher::exact("/case", AsyncURIMatcher::CaseInsensitive)` - Matches `/case`, `/CASE`, `/CaSe`
**Examples in URIMatcherTest.ino:**
- Case insensitive exact: `/case/exact`, `/CASE/EXACT`, `/Case/Exact` all work
- Case insensitive prefix: `/case/prefix`, `/CASE/PREFIX-test`, `/Case/Prefix/sub` all work
- Case insensitive directory: `/case/dir/users`, `/CASE/DIR/admin`, `/Case/Dir/settings` all work
- Case insensitive extension: `/case/files/doc.pdf`, `/CASE/FILES/DOC.PDF`, `/Case/Files/Doc.Pdf` all work
### 7. **Regular Expression Matching**
Using `AsyncURIMatcher::regex()` for advanced pattern matching (requires `ASYNCWEBSERVER_REGEX`).
**Examples in URIMatcher.ino:**
```cpp
#ifdef ASYNCWEBSERVER_REGEX
AsyncURIMatcher::regex("^/user/([0-9]+)$") // Matches /user/123, captures ID
#endif
```
**Examples in URIMatcherTest.ino:**
- Traditional regex: `^/user/([0-9]+)$` - Matches `/user/123`, `/user/456`
- Traditional regex: `^/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$` - Matches `/blog/2023/10/15`
- Factory regex: `AsyncURIMatcher::regex("^/factory/user/([0-9]+)$")` - Matches `/factory/user/123`
- Factory regex with multiple captures: `^/factory/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$`
- Case insensitive regex: `AsyncURIMatcher::regex("^/factory/search/(.+)$", AsyncURIMatcher::CaseInsensitive)`
### 8. **Combined Flags**
Multiple matching strategies can be combined using the `|` operator.
**Examples in URIMatcher.ino:**
- `AsyncURIMatcher::prefix("/MixedCase", AsyncURIMatcher::CaseInsensitive)` - Prefix match that's case insensitive
### 9. **Special Matchers**
**Examples in URIMatcherTest.ino:**
- `AsyncURIMatcher::all()` - Matches all requests (used with POST method as catch-all)
## Usage Patterns
### Traditional String-based Routing (Auto-Detection)
```cpp
// Auto-detection with exact + folder matching
server.on("/api", handler); // Matches /api AND /api/anything
server.on("/login", handler); // Matches /login AND /login/sub
// Auto-detection with prefix matching
server.on("/prefix*", handler); // Matches /prefix, /prefix-test, /prefix/sub
// Auto-detection with extension matching
server.on("/images/*.jpg", handler); // Matches /images/pic.jpg, /images/sub/pic.jpg
```
### Explicit AsyncURIMatcher Syntax
### Explicit AsyncURIMatcher Syntax
```cpp
// Exact matching only
server.on(AsyncURIMatcher("/path", URIMatchExact), handler);
// Prefix matching only
server.on(AsyncURIMatcher("/api", URIMatchPrefix), handler);
// Combined flags
server.on(AsyncURIMatcher("/api", URIMatchPrefix | URIMatchCaseInsensitive), handler);
```
### Factory Functions
```cpp
// More readable and expressive
server.on(AsyncURIMatcher::exact("/login"), handler);
server.on(AsyncURIMatcher::prefix("/api"), handler);
server.on(AsyncURIMatcher::dir("/admin"), handler);
server.on(AsyncURIMatcher::ext("/images/*.jpg"), handler);
#ifdef ASYNCWEBSERVER_REGEX
server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), handler);
#endif
```
## Available Flags
| Flag | Description |
| ------------------------- | ----------------------------------------------------------- |
| `URIMatchAuto` | Auto-detect match type from pattern (default) |
| `URIMatchExact` | Exact URL match |
| `URIMatchPrefix` | Prefix match |
| `URIMatchPrefixFolder` | Folder prefix match (requires trailing /) |
| `URIMatchExtension` | File extension match pattern |
| `URIMatchCaseInsensitive` | Case insensitive matching |
| `URIMatchRegex` | Regular expression matching (requires ASYNCWEBSERVER_REGEX) |
## Testing the Example
1. **Upload the sketch** to your ESP32/ESP8266
2. **Connect to WiFi AP**: `esp-captive` (no password required)
3. **Navigate to**: `http://192.168.4.1/`
4. **Explore the examples** by clicking the organized test links
5. **Monitor Serial output**: Open Serial Monitor to see detailed debugging information for each matched route
### Test URLs Available (All Clickable from Homepage)
**Auto-Detection Examples:**
- `http://192.168.4.1/auto` (exact + folder match)
- `http://192.168.4.1/auto/sub` (folder match - same handler!)
- `http://192.168.4.1/wildcard-test` (auto-detected prefix)
- `http://192.168.4.1/auto-images/photo.png` (auto-detected extension)
**Factory Method Examples:**
- `http://192.168.4.1/exact` (AsyncURIMatcher::exact)
- `http://192.168.4.1/service/status` (AsyncURIMatcher::prefix)
- `http://192.168.4.1/admin/users` (AsyncURIMatcher::dir)
- `http://192.168.4.1/images/photo.jpg` (AsyncURIMatcher::ext)
**Case Insensitive Examples:**
- `http://192.168.4.1/case` (lowercase)
- `http://192.168.4.1/CASE` (uppercase)
- `http://192.168.4.1/CaSe` (mixed case)
**Regex Examples (if ASYNCWEBSERVER_REGEX enabled):**
- `http://192.168.4.1/user/123` (captures numeric ID)
- `http://192.168.4.1/user/456` (captures numeric ID)
**Combined Flags Examples:**
- `http://192.168.4.1/mixedcase-test` (prefix + case insensitive)
- `http://192.168.4.1/MIXEDCASE/sub` (prefix + case insensitive)
### Console Output
Each handler provides detailed debugging information via Serial output:
```
Auto-Detection Match (Traditional)
Matched URL: /auto
Uses auto-detection: exact + folder matching
```
```
Factory Exact Match
Matched URL: /exact
Uses AsyncURIMatcher::exact() factory function
```
```
Regex Match - User ID
Matched URL: /user/123
Captured User ID: 123
This regex matches /user/{number} pattern
```
## Compilation Options
### Enable Regex Support
To enable regular expression matching, compile with:
```
-D ASYNCWEBSERVER_REGEX
```
In PlatformIO, add to `platformio.ini`:
```ini
build_flags = -D ASYNCWEBSERVER_REGEX
```
In Arduino IDE, add to your sketch:
```cpp
#define ASYNCWEBSERVER_REGEX
```
## Performance Considerations
1. **Exact matches** are fastest
2. **Prefix matches** are very efficient
3. **Regex matches** are slower but most flexible
4. **Case insensitive** matching adds minimal overhead
5. **Auto-detection** adds slight parsing overhead at construction time
## Real-World Applications
### REST API Design
```cpp
// API versioning
server.on(AsyncURIMatcher::prefix("/api/v1"), handleAPIv1);
server.on(AsyncURIMatcher::prefix("/api/v2"), handleAPIv2);
// Resource endpoints with IDs
server.on(AsyncURIMatcher::regex("^/api/users/([0-9]+)$"), handleUserById);
server.on(AsyncURIMatcher::regex("^/api/posts/([0-9]+)/comments$"), handlePostComments);
```
### File Serving
```cpp
// Serve different file types
server.on(AsyncURIMatcher::ext("/assets/*.css"), serveCSSFiles);
server.on(AsyncURIMatcher::ext("/assets/*.js"), serveJSFiles);
server.on(AsyncURIMatcher::ext("/images/*.jpg"), serveImageFiles);
```
### Admin Interface
```cpp
// Admin section with authentication
server.on(AsyncURIMatcher::dir("/admin"), handleAdminPages);
server.on(AsyncURIMatcher::exact("/admin"), redirectToAdminDashboard);
```
## See Also
- [ESPAsyncWebServer Documentation](https://github.com/ESP32Async/ESPAsyncWebServer)
- [Regular Expression Reference](https://en.cppreference.com/w/cpp/regex)
- Other examples in the `examples/` directory

View File

@@ -0,0 +1,276 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
//
// AsyncURIMatcher Examples - Advanced URI Matching and Routing
//
// This example demonstrates the various ways to use AsyncURIMatcher class
// for flexible URL routing with different matching strategies:
//
// 1. Exact matching
// 2. Prefix matching
// 3. Folder/directory matching
// 4. Extension matching
// 5. Case insensitive matching
// 6. Regex matching (if ASYNCWEBSERVER_REGEX is enabled)
// 7. Factory functions for common patterns
//
// Test URLs:
// - Exact: http://192.168.4.1/exact
// - Prefix: http://192.168.4.1/prefix-anything
// - Folder: http://192.168.4.1/api/users, http://192.168.4.1/api/posts
// - Extension: http://192.168.4.1/images/photo.jpg, http://192.168.4.1/docs/readme.pdf
// - Case insensitive: http://192.168.4.1/CaSe or http://192.168.4.1/case
// - Wildcard: http://192.168.4.1/wildcard-test
#include <Arduino.h>
#if defined(ESP32) || defined(LIBRETINY)
#include <AsyncTCP.h>
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#include <RPAsyncTCP.h>
#include <WiFi.h>
#endif
#include <ESPAsyncWebServer.h>
static AsyncWebServer server(80);
void setup() {
Serial.begin(115200);
Serial.println();
Serial.println("=== AsyncURIMatcher Example ===");
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
Serial.print("AP IP address: ");
Serial.println(WiFi.softAPIP());
#endif
// =============================================================================
// 1. AUTO-DETECTION BEHAVIOR - traditional string-based routing
// =============================================================================
// Traditional string-based routing with auto-detection
// This uses URIMatchAuto which combines URIMatchPrefixFolder | URIMatchExact
// It will match BOTH "/auto" exactly AND "/auto/" + anything
server.on("/auto", HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println("Auto-Detection Match (Traditional)");
Serial.println("Matched URL: " + request->url());
Serial.println("Uses auto-detection: exact + folder matching");
request->send(200, "text/plain", "OK - Auto-detection match");
});
// Auto-detection for wildcard patterns (ends with *)
// This auto-detects as URIMatchPrefix
server.on("/wildcard*", HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println("Auto-Detected Wildcard (Prefix)");
Serial.println("Matched URL: " + request->url());
Serial.println("Auto-detected as prefix match due to trailing *");
request->send(200, "text/plain", "OK - Wildcard prefix match");
});
// Auto-detection for extension patterns (contains /*.ext)
// This auto-detects as URIMatchExtension
server.on("/auto-images/*.png", HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println("Auto-Detected Extension Pattern");
Serial.println("Matched URL: " + request->url());
Serial.println("Auto-detected as extension match due to /*.png pattern");
request->send(200, "text/plain", "OK - Extension match");
});
// =============================================================================
// 2. EXACT MATCHING - matches only the exact URL (explicit)
// =============================================================================
// Using factory function for exact match
server.on(AsyncURIMatcher::exact("/exact"), HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println("Factory Exact Match");
Serial.println("Matched URL: " + request->url());
Serial.println("Uses AsyncURIMatcher::exact() factory function");
request->send(200, "text/plain", "OK - Factory exact match");
});
// =============================================================================
// 3. PREFIX MATCHING - matches URLs that start with the pattern
// =============================================================================
// Using factory function for prefix match
server.on(AsyncURIMatcher::prefix("/service"), HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println("Service Prefix Match (Factory)");
Serial.println("Matched URL: " + request->url());
Serial.println("Uses AsyncURIMatcher::prefix() factory function");
request->send(200, "text/plain", "OK - Factory prefix match");
});
// =============================================================================
// 4. FOLDER/DIRECTORY MATCHING - matches URLs in a folder structure
// =============================================================================
// Folder match using factory function (automatically adds trailing slash)
server.on(AsyncURIMatcher::dir("/admin"), HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println("Admin Directory Match");
Serial.println("Matched URL: " + request->url());
Serial.println("This matches URLs under /admin/ directory");
Serial.println("Note: /admin (without slash) will NOT match");
request->send(200, "text/plain", "OK - Directory match");
});
// =============================================================================
// 5. EXTENSION MATCHING - matches files with specific extensions
// =============================================================================
// Image extension matching
server.on(AsyncURIMatcher::ext("/images/*.jpg"), HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println("JPG Image Handler");
Serial.println("Matched URL: " + request->url());
Serial.println("This matches any .jpg file under /images/");
request->send(200, "text/plain", "OK - Extension match");
});
// =============================================================================
// 6. CASE INSENSITIVE MATCHING
// =============================================================================
// Case insensitive exact match
server.on(AsyncURIMatcher::exact("/case", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println("Case Insensitive Match");
Serial.println("Matched URL: " + request->url());
Serial.println("This matches /case in any case combination");
request->send(200, "text/plain", "OK - Case insensitive match");
});
#ifdef ASYNCWEBSERVER_REGEX
// =============================================================================
// 7. REGEX MATCHING (only available if ASYNCWEBSERVER_REGEX is enabled)
// =============================================================================
// Regex match for numeric IDs
server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println("Regex Match - User ID");
Serial.println("Matched URL: " + request->url());
if (request->pathArg(0).length() > 0) {
Serial.println("Captured User ID: " + request->pathArg(0));
}
Serial.println("This regex matches /user/{number} pattern");
request->send(200, "text/plain", "OK - Regex match");
});
#endif
// =============================================================================
// 8. COMBINED FLAGS EXAMPLE
// =============================================================================
// Combine multiple flags
server.on(AsyncURIMatcher::prefix("/MixedCase", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println("Combined Flags Example");
Serial.println("Matched URL: " + request->url());
Serial.println("Uses both AsyncURIMatcher::Prefix and AsyncURIMatcher::CaseInsensitive");
request->send(200, "text/plain", "OK - Combined flags match");
});
// =============================================================================
// 9. HOMEPAGE WITH NAVIGATION
// =============================================================================
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
Serial.println("Homepage accessed");
String response = R"(<!DOCTYPE html>
<html>
<head>
<title>AsyncURIMatcher Examples</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
h1 { color: #2E86AB; }
.test-link { display: block; margin: 5px 0; padding: 8px; background: #f0f0f0; text-decoration: none; color: #333; border-radius: 4px; }
.test-link:hover { background: #e0e0e0; }
.section { margin: 20px 0; }
</style>
</head>
<body>
<h1>AsyncURIMatcher Examples</h1>
<div class="section">
<h3>Auto-Detection (Traditional String Matching)</h3>
<a href="/auto" class="test-link">/auto (auto-detection: exact + folder)</a>
<a href="/auto/sub" class="test-link">/auto/sub (folder match)</a>
<a href="/wildcard-test" class="test-link">/wildcard-test (auto prefix)</a>
<a href="/auto-images/photo.png" class="test-link">/auto-images/photo.png (auto extension)</a>
</div>
<div class="section">
<h3>Factory Method Examples</h3>
<a href="/exact" class="test-link">/exact (factory exact)</a>
<a href="/service/status" class="test-link">/service/status (factory prefix)</a>
<a href="/admin/users" class="test-link">/admin/users (factory directory)</a>
<a href="/images/photo.jpg" class="test-link">/images/photo.jpg (factory extension)</a>
</div>
<div class="section">
<h3>Case Insensitive Matching</h3>
<a href="/case" class="test-link">/case (lowercase)</a>
<a href="/CASE" class="test-link">/CASE (uppercase)</a>
<a href="/CaSe" class="test-link">/CaSe (mixed case)</a>
</div>
)";
#ifdef ASYNCWEBSERVER_REGEX
response += R"( <div class="section">
<h3>Regex Matching</h3>
<a href="/user/123" class="test-link">/user/123 (regex numeric ID)</a>
<a href="/user/456" class="test-link">/user/456 (regex numeric ID)</a>
</div>
)";
#endif
response += R"( <div class="section">
<h3>Combined Flags</h3>
<a href="/mixedcase-test" class="test-link">/mixedcase-test (prefix + case insensitive)</a>
<a href="/MIXEDCASE/sub" class="test-link">/MIXEDCASE/sub (prefix + case insensitive)</a>
</div>
</body>
</html>)";
request->send(200, "text/html", response);
});
// =============================================================================
// 10. NOT FOUND HANDLER
// =============================================================================
server.onNotFound([](AsyncWebServerRequest *request) {
String html = "<h1>404 - Not Found</h1>";
html += "<p>The requested URL <strong>" + request->url() + "</strong> was not found.</p>";
html += "<p><a href='/'>← Back to Examples</a></p>";
request->send(404, "text/html", html);
});
server.begin();
Serial.println();
Serial.println("=== Server Started ===");
Serial.println("Open your browser and navigate to:");
Serial.println("http://192.168.4.1/ - Main examples page");
Serial.println();
Serial.println("Available test endpoints:");
Serial.println("• Auto-detection: /auto (exact+folder), /wildcard*, /auto-images/*.png");
Serial.println("• Exact matches: /exact");
Serial.println("• Prefix matches: /service*");
Serial.println("• Folder matches: /admin/*");
Serial.println("• Extension matches: /images/*.jpg");
Serial.println("• Case insensitive: /case (try /CASE, /Case)");
#ifdef ASYNCWEBSERVER_REGEX
Serial.println("• Regex matches: /user/123");
#endif
Serial.println("• Combined flags: /mixedcase*");
Serial.println();
}
void loop() {
// Nothing to do here - the server handles everything asynchronously
delay(1000);
}

View File

@@ -0,0 +1,165 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
//
// Test for ESPAsyncWebServer URI matching
//
// Usage: upload, connect to the AP and run test_routes.sh
//
#include <Arduino.h>
#if defined(ESP32) || defined(LIBRETINY)
#include <AsyncTCP.h>
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#include <RPAsyncTCP.h>
#include <WiFi.h>
#endif
#include <ESPAsyncWebServer.h>
AsyncWebServer server(80);
void setup() {
Serial.begin(115200);
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif
// Status endpoint
server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// Exact paths, plus the subpath (/exact matches /exact/sub but not /exact-no-match)
server.on("/exact", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
server.on("/api/users", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// Prefix matching
server.on("/api/*", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
server.on("/files/*", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// Extensions
server.on("/*.json", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "application/json", "{\"status\":\"OK\"}");
});
server.on("/*.css", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/css", "/* OK */");
});
// =============================================================================
// NEW ASYNCURIMATCHER FACTORY METHODS TESTS
// =============================================================================
// Exact match using factory method (does NOT match subpaths like traditional)
server.on(AsyncURIMatcher::exact("/factory/exact"), HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// Prefix match using factory method
server.on(AsyncURIMatcher::prefix("/factory/prefix"), HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// Directory match using factory method (matches /dir/anything but not /dir itself)
server.on(AsyncURIMatcher::dir("/factory/dir"), HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// Extension match using factory method
server.on(AsyncURIMatcher::ext("/factory/files/*.txt"), HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// =============================================================================
// CASE INSENSITIVE MATCHING TESTS
// =============================================================================
// Case insensitive exact match
server.on(AsyncURIMatcher::exact("/case/exact", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// Case insensitive prefix match
server.on(AsyncURIMatcher::prefix("/case/prefix", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// Case insensitive directory match
server.on(AsyncURIMatcher::dir("/case/dir", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// Case insensitive extension match
server.on(AsyncURIMatcher::ext("/case/files/*.PDF", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
#ifdef ASYNCWEBSERVER_REGEX
// Traditional regex patterns (backward compatibility)
server.on("^/user/([0-9]+)$", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
server.on("^/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// =============================================================================
// NEW ASYNCURIMATCHER REGEX FACTORY METHODS
// =============================================================================
// Regex match using factory method
server.on(AsyncURIMatcher::regex("^/factory/user/([0-9]+)$"), HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// Case insensitive regex match using factory method
server.on(AsyncURIMatcher::regex("^/factory/search/(.+)$", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// Complex regex with multiple capture groups
server.on(AsyncURIMatcher::regex("^/factory/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$"), HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
#endif
// =============================================================================
// SPECIAL MATCHERS
// =============================================================================
// Match all POST requests (catch-all before 404)
server.on(AsyncURIMatcher::all(), HTTP_POST, [](AsyncWebServerRequest *request) {
request->send(200, "text/plain", "OK");
});
// 404 handler
server.onNotFound([](AsyncWebServerRequest *request) {
request->send(404, "text/plain", "Not Found");
});
server.begin();
Serial.println("Server ready");
}
// not needed
void loop() {
delay(100);
}

View File

@@ -0,0 +1,174 @@
#!/bin/bash
# URI Matcher Test Script
# Tests all routes defined in URIMatcherTest.ino
SERVER_IP="${1:-192.168.4.1}"
SERVER_PORT="80"
BASE_URL="http://${SERVER_IP}:${SERVER_PORT}"
echo "Testing URI Matcher at $BASE_URL"
echo "=================================="
# Function to test a route
test_route() {
local path="$1"
local expected_status="$2"
local description="$3"
echo -n "Testing $path ... "
response=$(curl -s -w "HTTPSTATUS:%{http_code}" "$BASE_URL$path" 2>/dev/null)
status_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2)
if [ "$status_code" = "$expected_status" ]; then
echo "✅ PASS ($status_code)"
else
echo "❌ FAIL (expected $expected_status, got $status_code)"
return 1
fi
return 0
}
# Test counter
PASS=0
FAIL=0
# Test all routes that should return 200 OK
echo "Testing routes that should work (200 OK):"
if test_route "/status" "200" "Status endpoint"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/exact" "200" "Exact path"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/exact/" "200" "Exact path ending with /"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/exact/sub" "200" "Exact path with subpath /"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/api/users" "200" "Exact API path"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/api/data" "200" "API prefix match"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/api/v1/posts" "200" "API prefix deep"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/files/document.pdf" "200" "Files prefix"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/files/images/photo.jpg" "200" "Files prefix deep"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/config.json" "200" "JSON extension"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/data/settings.json" "200" "JSON extension in subfolder"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/style.css" "200" "CSS extension"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/assets/main.css" "200" "CSS extension in subfolder"; then ((PASS++)); else ((FAIL++)); fi
echo ""
echo "Testing AsyncURIMatcher factory methods:"
# Factory exact match (should NOT match subpaths)
if test_route "/factory/exact" "200" "Factory exact match"; then ((PASS++)); else ((FAIL++)); fi
# Factory prefix match
if test_route "/factory/prefix" "200" "Factory prefix base"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/factory/prefix-test" "200" "Factory prefix extended"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/factory/prefix/sub" "200" "Factory prefix subpath"; then ((PASS++)); else ((FAIL++)); fi
# Factory directory match (should NOT match the directory itself)
if test_route "/factory/dir/users" "200" "Factory directory match"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/factory/dir/sub/path" "200" "Factory directory deep"; then ((PASS++)); else ((FAIL++)); fi
# Factory extension match
if test_route "/factory/files/doc.txt" "200" "Factory extension match"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/factory/files/sub/readme.txt" "200" "Factory extension deep"; then ((PASS++)); else ((FAIL++)); fi
echo ""
echo "Testing case insensitive matching:"
# Case insensitive exact
if test_route "/case/exact" "200" "Case exact lowercase"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/CASE/EXACT" "200" "Case exact uppercase"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/Case/Exact" "200" "Case exact mixed"; then ((PASS++)); else ((FAIL++)); fi
# Case insensitive prefix
if test_route "/case/prefix" "200" "Case prefix lowercase"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/CASE/PREFIX-test" "200" "Case prefix uppercase"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/Case/Prefix/sub" "200" "Case prefix mixed"; then ((PASS++)); else ((FAIL++)); fi
# Case insensitive directory
if test_route "/case/dir/users" "200" "Case dir lowercase"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/CASE/DIR/admin" "200" "Case dir uppercase"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/Case/Dir/settings" "200" "Case dir mixed"; then ((PASS++)); else ((FAIL++)); fi
# Case insensitive extension
if test_route "/case/files/doc.pdf" "200" "Case ext lowercase"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/CASE/FILES/DOC.PDF" "200" "Case ext uppercase"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/Case/Files/Doc.Pdf" "200" "Case ext mixed"; then ((PASS++)); else ((FAIL++)); fi
echo ""
echo "Testing special matchers:"
# Test POST to catch-all (all() matcher)
echo -n "Testing POST /any/path (all matcher) ... "
response=$(curl -s -X POST -w "HTTPSTATUS:%{http_code}" "$BASE_URL/any/path" 2>/dev/null)
status_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2)
if [ "$status_code" = "200" ]; then
echo "✅ PASS ($status_code)"
((PASS++))
else
echo "❌ FAIL (expected 200, got $status_code)"
((FAIL++))
fi
# Check if regex is enabled by testing the server
echo ""
echo "Checking for regex support..."
regex_test=$(curl -s "$BASE_URL/user/123" 2>/dev/null)
if curl -s -w "%{http_code}" "$BASE_URL/user/123" 2>/dev/null | grep -q "200"; then
echo "Regex support detected - testing traditional regex routes:"
if test_route "/user/123" "200" "Traditional regex user ID"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/user/456" "200" "Traditional regex user ID 2"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/blog/2023/10/15" "200" "Traditional regex blog date"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/blog/2024/12/25" "200" "Traditional regex blog date 2"; then ((PASS++)); else ((FAIL++)); fi
echo "Testing AsyncURIMatcher regex factory methods:"
if test_route "/factory/user/123" "200" "Factory regex user ID"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/factory/user/789" "200" "Factory regex user ID 2"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/factory/blog/2023/10/15" "200" "Factory regex blog date"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/factory/blog/2024/12/31" "200" "Factory regex blog date 2"; then ((PASS++)); else ((FAIL++)); fi
# Case insensitive regex
if test_route "/factory/search/hello" "200" "Factory regex search lowercase"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/FACTORY/SEARCH/WORLD" "200" "Factory regex search uppercase"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/Factory/Search/Test" "200" "Factory regex search mixed"; then ((PASS++)); else ((FAIL++)); fi
# Test regex validation
if test_route "/user/abc" "404" "Invalid regex (letters instead of numbers)"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/blog/23/10/15" "404" "Invalid regex (2-digit year)"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/factory/user/abc" "404" "Factory regex invalid (letters)"; then ((PASS++)); else ((FAIL++)); fi
else
echo "Regex support not detected (compile with ASYNCWEBSERVER_REGEX to enable)"
fi
echo ""
echo "Testing routes that should fail (404 Not Found):"
if test_route "/nonexistent" "404" "Non-existent route"; then ((PASS++)); else ((FAIL++)); fi
# Test factory exact vs traditional behavior difference
if test_route "/factory/exact/sub" "404" "Factory exact should NOT match subpaths"; then ((PASS++)); else ((FAIL++)); fi
# Test factory directory requires trailing slash
if test_route "/factory/dir" "404" "Factory directory should NOT match without trailing slash"; then ((PASS++)); else ((FAIL++)); fi
# Test extension mismatch
if test_route "/factory/files/doc.pdf" "404" "Factory extension mismatch (.pdf vs .txt)"; then ((PASS++)); else ((FAIL++)); fi
# Test case sensitive when flag not used
if test_route "/exact" "200" "Traditional exact lowercase"; then ((PASS++)); else ((FAIL++)); fi
if test_route "/EXACT" "404" "Traditional exact should be case sensitive"; then ((PASS++)); else ((FAIL++)); fi
echo ""
echo "=================================="
echo "Test Results:"
echo "✅ Passed: $PASS"
echo "❌ Failed: $FAIL"
echo "Total: $((PASS + FAIL))"
if [ $FAIL -eq 0 ]; then
echo ""
echo "🎉 All tests passed! URI matching is working correctly."
exit 0
else
echo ""
echo "❌ Some tests failed. Check the server and routes."
exit 1
fi

View File

@@ -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;
}

View File

@@ -0,0 +1,158 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
//
// Demo to upload a firmware and filesystem image via multipart form data
//
#include <Arduino.h>
#if defined(ESP32) || defined(LIBRETINY)
#include <AsyncTCP.h>
#include <WiFi.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#include <RPAsyncTCP.h>
#include <WiFi.h>
#endif
#include <ESPAsyncWebServer.h>
#include <StreamString.h>
#include <LittleFS.h>
// ESP32 example ONLY
#ifdef ESP32
#include <Update.h>
#endif
static AsyncWebServer server(80);
void setup() {
Serial.begin(115200);
if (!LittleFS.begin()) {
LittleFS.format();
LittleFS.begin();
}
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif
// ESP32 example ONLY
#ifdef ESP32
// Shows how to get the fw and fs (names) and filenames from a multipart upload,
// and also how to handle multiple file uploads in a single request.
//
// This example also shows how to pass and handle different parameters having the same name in query string, post form and content-disposition.
//
// Execute in the terminal, in order:
//
// 1. Build firmware: pio run -e arduino-3
// 2. Build FS image: pio run -e arduino-3 -t buildfs
// 3. Flash both at the same time: curl -v -F "name=Bob" -F "fw=@.pio/build/arduino-3/firmware.bin" -F "fs=@.pio/build/arduino-3/littlefs.bin" http://192.168.4.1/flash?name=Bill
//
server.on(
"/flash", HTTP_POST,
[](AsyncWebServerRequest *request) {
if (request->getResponse()) {
// response already created
return;
}
// list all parameters
Serial.println("Request parameters:");
const size_t params = request->params();
for (size_t i = 0; i < params; i++) {
const AsyncWebParameter *p = request->getParam(i);
Serial.printf("Param[%u]: %s=%s, isPost=%d, isFile=%d, size=%u\n", i, p->name().c_str(), p->value().c_str(), p->isPost(), p->isFile(), p->size());
}
Serial.println("Flash / Filesystem upload completed");
request->send(200, "text/plain", "Upload complete");
},
[](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) {
Serial.printf("Upload[%s]: index=%u, len=%u, final=%d\n", filename.c_str(), index, len, final);
if (request->getResponse() != nullptr) {
// upload aborted
return;
}
// start a new content-disposition upload
if (!index) {
// list all parameters
const size_t params = request->params();
for (size_t i = 0; i < params; i++) {
const AsyncWebParameter *p = request->getParam(i);
Serial.printf("Param[%u]: %s=%s, isPost=%d, isFile=%d, size=%u\n", i, p->name().c_str(), p->value().c_str(), p->isPost(), p->isFile(), p->size());
}
// get the content-disposition parameter
const AsyncWebParameter *p = request->getParam(asyncsrv::T_name, true, true);
if (p == nullptr) {
request->send(400, "text/plain", "Missing content-disposition 'name' parameter");
return;
}
// determine upload type based on the parameter name
if (p->value() == "fs") {
Serial.printf("Filesystem image upload for file: %s\n", filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) {
Update.printError(Serial);
request->send(400, "text/plain", "Update begin failed");
return;
}
} else if (p->value() == "fw") {
Serial.printf("Firmware image upload for file: %s\n", filename.c_str());
if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) {
Update.printError(Serial);
request->send(400, "text/plain", "Update begin failed");
return;
}
} else {
Serial.printf("Unknown upload type for file: %s\n", filename.c_str());
request->send(400, "text/plain", "Unknown upload type");
return;
}
}
// some bytes to write ?
if (len) {
if (Update.write(data, len) != len) {
Update.printError(Serial);
Update.end();
request->send(400, "text/plain", "Update write failed");
return;
}
}
// finish the content-disposition upload
if (final) {
if (!Update.end(true)) {
Update.printError(Serial);
request->send(400, "text/plain", "Update end failed");
return;
}
// success response is created in the final request handler when all uploads are completed
Serial.printf("Upload success of file %s\n", filename.c_str());
}
}
);
#endif
server.begin();
}
// not needed
void loop() {
delay(100);
}

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
//
// WebSocket example
@@ -19,17 +19,58 @@
#include <ESPAsyncWebServer.h>
static const char *htmlContent PROGMEM = R"(
<!DOCTYPE html>
<html>
<head>
<title>WebSocket</title>
</head>
<body>
<h1>WebSocket Example</h1>
<p>Open your browser console!</p>
<input type="text" id="message" placeholder="Type a message">
<button onclick='sendMessage()'>Send</button>
<script>
var ws = new WebSocket('ws://192.168.4.1/ws');
ws.onopen = function() {
console.log("WebSocket connected");
};
ws.onmessage = function(event) {
console.log("WebSocket message: " + event.data);
};
ws.onclose = function() {
console.log("WebSocket closed");
};
ws.onerror = function(error) {
console.log("WebSocket error: " + error);
};
function sendMessage() {
var message = document.getElementById("message").value;
ws.send(message);
console.log("WebSocket sent: " + message);
}
</script>
</body>
</html>
)";
static const size_t htmlContentLength = strlen_P(htmlContent);
static AsyncWebServer server(80);
static AsyncWebSocket ws("/ws");
void setup() {
Serial.begin(115200);
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif
// serves root html page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
request->send(200, "text/html", (const uint8_t *)htmlContent, htmlContentLength);
});
//
// Run in terminal 1: websocat ws://192.168.4.1/ws => should stream data
// Run in terminal 2: websocat ws://192.168.4.1/ws => should stream data
@@ -66,6 +107,7 @@ void setup() {
if (info->opcode == WS_TEXT) {
data[len] = 0;
Serial.printf("ws text: %s\n", (char *)data);
client->ping();
}
}
}

View File

@@ -1,5 +1,5 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
//
// WebSocket example using the easy to use AsyncWebSocketMessageHandler handler that only supports unfragmented messages
@@ -40,7 +40,7 @@ static const char *htmlContent PROGMEM = R"(
</head>
<body>
<h1>WebSocket Example</h1>
<>Open your browser console!</p>
<p>Open your browser console!</p>
<input type="text" id="message" placeholder="Type a message">
<button onclick='sendMessage()'>Send</button>
<script>
@@ -71,7 +71,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent);
void setup() {
Serial.begin(115200);
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif
@@ -97,6 +97,7 @@ void setup() {
wsHandler.onMessage([](AsyncWebSocket *server, AsyncWebSocketClient *client, const uint8_t *data, size_t len) {
Serial.printf("Client %" PRIu32 " data: %s\n", client->id(), (const char *)data);
server->textAll(data, len);
});
wsHandler.onFragment([](AsyncWebSocket *server, AsyncWebSocketClient *client, const AwsFrameInfo *frameInfo, const uint8_t *data, size_t len) {

View File

@@ -29,7 +29,7 @@ dependencies:
version: "^3.1.1"
require: public
esp32async/asynctcp:
version: "^3.4.7"
version: "^3.4.10"
require: public
bblanchon/arduinojson:
version: "^7.4.2"

View File

@@ -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
@@ -78,7 +78,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 SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI || CONFIG_ESP32_WIFI_ENABLED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif

View File

@@ -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
@@ -50,7 +50,7 @@ static AsyncEventSource events("/events");
void setup() {
Serial.begin(115200);
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI || CONFIG_ESP32_WIFI_ENABLED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif

View File

@@ -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
@@ -17,7 +17,7 @@ static AsyncWebSocket ws("/ws");
void setup() {
Serial.begin(115200);
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI || CONFIG_ESP32_WIFI_ENABLED
WiFi.mode(WIFI_AP);
WiFi.softAP("esp-captive");
#endif

View File

@@ -1,6 +1,6 @@
{
"name": "ESPAsyncWebServer",
"version": "3.8.0",
"version": "3.9.6",
"description": "Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040. Supports: WebSocket, SSE, Authentication, Arduino Json 7, File Upload, Static File serving, URL Rewrite, URL Redirect, etc.",
"keywords": "http,async,websocket,webserver",
"homepage": "https://github.com/ESP32Async/ESPAsyncWebServer",
@@ -25,7 +25,7 @@
{
"owner": "ESP32Async",
"name": "AsyncTCP",
"version": "^3.4.7",
"version": "^3.4.10",
"platforms": [
"espressif32",
"libretiny"

View File

@@ -1,6 +1,6 @@
name=ESP Async WebServer
includes=ESPAsyncWebServer.h
version=3.8.0
version=3.9.6
author=ESP32Async
maintainer=ESP32Async
sentence=Asynchronous HTTP and WebSocket Server Library for ESP32, ESP8266 and RP2040

View File

@@ -9,3 +9,4 @@
/dependencies.lock
/.dummy
/managed_components
/src/idf_component.yml

View File

@@ -1,6 +1,6 @@
[env]
framework = arduino
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
build_flags =
-Og
-Wall -Wextra
@@ -17,8 +17,21 @@ monitor_filters = esp32_exception_decoder, log2file
lib_compat_mode = strict
lib_ldf_mode = chain
lib_deps =
ESP32Async/AsyncTCP @ 3.4.7
ESP32Async/ESpAsyncWebServer @ 3.7.0
ESP32Async/AsyncTCP @ 3.4.10
ESP32Async/ESpAsyncWebServer @ 3.9.5
custom_component_remove =
espressif/esp_hosted
espressif/esp_wifi_remote
espressif/esp-dsp
espressif/esp32-camera
espressif/libsodium
espressif/esp-modbus
espressif/qrcode
espressif/esp_insights
espressif/esp_diag_data_store
espressif/esp_diagnostics
espressif/esp_rainmaker
espressif/rmaker_common
custom_sdkconfig = CONFIG_LWIP_MAX_ACTIVE_TCP=32

View File

@@ -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
//
// This example demonstrates how to increase the maximum number of active TCP connections

View File

@@ -14,6 +14,7 @@ lib_dir = .
; src_dir = examples/FlashResponse
; src_dir = examples/HeaderManipulation
; src_dir = examples/Json
; src_dir = examples/LargeResponse
; src_dir = examples/Logging
; src_dir = examples/MessagePack
; src_dir = examples/Middleware
@@ -33,12 +34,15 @@ src_dir = examples/PerfTests
; src_dir = examples/StaticFile
; src_dir = examples/Templates
; src_dir = examples/Upload
; src_dir = examples/UploadFlash
; src_dir = examples/URIMatcher
; src_dir = examples/URIMatcherTest
; src_dir = examples/WebSocket
; src_dir = examples/WebSocketEasy
[env]
framework = arduino
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.30-2/platform-espressif32.zip
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.36/platform-espressif32.zip
board = esp32dev
build_flags =
-Og
@@ -51,7 +55,14 @@ build_flags =
-D CONFIG_ASYNC_TCP_QUEUE_SIZE=64
-D CONFIG_ASYNC_TCP_RUNNING_CORE=1
-D CONFIG_ASYNC_TCP_STACK_SIZE=4096
; -D ASYNCWEBSERVER_REGEX=1
; -D CONFIG_ASYNC_TCP_USE_WDT=0
; -D CONFIG_ARDUHAL_LOG_COLORS
; -D CORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_VERBOSE
; -D USE_ESP_IDF_LOG=1
; -D TAG=\"core\"
; -D LOG_LOCAL_LEVEL=ESP_LOG_VERBOSE
; -D ASYNCWEBSERVER_LOG_DEBUG
upload_protocol = esptool
monitor_speed = 115200
monitor_filters = esp32_exception_decoder, log2file
@@ -60,10 +71,14 @@ lib_compat_mode = strict
lib_ldf_mode = chain
lib_deps =
bblanchon/ArduinoJson @ 7.4.2
ESP32Async/AsyncTCP @ 3.4.7
; bblanchon/ArduinoJson @ 6.21.5
; bblanchon/ArduinoJson @ 5.13.4
ESP32Async/AsyncTCP @ 3.4.10
board_build.partitions = partitions-4MB.csv
board_build.filesystem = littlefs
; PLATFORMS (ESP32, ESP8266, Raspberry, LibreTiny)
[env:arduino-2]
platform = espressif32@6.12.0
@@ -71,26 +86,6 @@ platform = espressif32@6.12.0
; board = esp32-p4
; board = esp32-h2-devkitm-1
[env:arduino-rc]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20-rc2/platform-espressif32.zip
[env:arduino-3-no-json]
lib_deps =
ESP32Async/AsyncTCP @ 3.4.7
[env:arduino-rc-asynctcp]
lib_deps =
https://github.com/ESP32Async/AsyncTCP
[env:arduino-3-no-chunk-inflight]
build_flags = ${env.build_flags}
-D ASYNCWEBSERVER_USE_CHUNK_INFLIGHT=0
[env:AsyncTCPSock]
lib_deps =
https://github.com/ESP32Async/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip
build_flags = ${env.build_flags}
[env:esp8266]
platform = espressif8266
; board = huzzah
@@ -122,7 +117,42 @@ lib_deps =
; (BK7231 already uses it)
custom_versions.freertos = 9.0.0
; CI
; SPECIFIC ENVS (for testing various configurations)
[env:arduino-2-esp-idf-log]
platform = espressif32@6.12.0
build_flags =
${env.build_flags}
-D USE_ESP_IDF_LOG=1
-D TAG=\"core\"
[env:arduino-3-esp-idf-log]
build_flags =
${env.build_flags}
-D USE_ESP_IDF_LOG=1
[env:no-json]
lib_deps =
ESP32Async/AsyncTCP @ 3.4.10
[env:latest-asynctcp]
lib_deps =
https://github.com/ESP32Async/AsyncTCP
[env:no-chunk-inflight]
build_flags = ${env.build_flags}
-D ASYNCWEBSERVER_USE_CHUNK_INFLIGHT=0
[env:regex]
build_flags = ${env.build_flags}
-D ASYNCWEBSERVER_REGEX=1
[env:AsyncTCPSock]
lib_deps =
https://github.com/ESP32Async/AsyncTCPSock/archive/refs/tags/v1.0.3-dev.zip
build_flags = ${env.build_flags}
; PLATFORM CI (ESP32, ESP8266, Raspberry, LibreTiny)
[env:ci-arduino-2]
platform = espressif32@6.12.0
@@ -131,24 +161,6 @@ board = ${sysenv.PIO_BOARD}
[env:ci-arduino-3]
board = ${sysenv.PIO_BOARD}
[env:ci-arduino-rc]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.20-rc2/platform-espressif32.zip
board = ${sysenv.PIO_BOARD}
[env:ci-arduino-3-no-json]
board = ${sysenv.PIO_BOARD}
lib_deps =
ESP32Async/AsyncTCP @ 3.4.7
[env:ci-arduino-rc-asynctcp]
lib_deps =
https://github.com/ESP32Async/AsyncTCP
[env:ci-arduino-3-no-chunk-inflight]
board = ${sysenv.PIO_BOARD}
build_flags = ${env.build_flags}
-D ASYNCWEBSERVER_USE_CHUNK_INFLIGHT=1
[env:ci-esp8266]
platform = espressif8266
board = ${sysenv.PIO_BOARD}
@@ -177,3 +189,33 @@ lib_deps =
DNSServer
ESP32Async/AsyncTCP @ 3.4.3
custom_versions.freertos = 9.0.0
; CI FOR SPECIFIC CONFIGURATIONS
[env:ci-arduino-2-esp-idf-log]
platform = espressif32@6.12.0
build_flags =
${env.build_flags}
-D USE_ESP_IDF_LOG=1
-D TAG=\"core\"
[env:ci-arduino-3-esp-idf-log]
build_flags =
${env.build_flags}
-D USE_ESP_IDF_LOG=1
[env:ci-no-json]
lib_deps =
ESP32Async/AsyncTCP @ 3.4.10
[env:ci-latest-asynctcp]
lib_deps =
https://github.com/ESP32Async/AsyncTCP
[env:ci-no-chunk-inflight]
build_flags = ${env.build_flags}
-D ASYNCWEBSERVER_USE_CHUNK_INFLIGHT=1
[env:ci-regex]
build_flags = ${env.build_flags}
-D ASYNCWEBSERVER_REGEX=1

View File

@@ -1,11 +1,12 @@
// 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 "Arduino.h"
#if defined(ESP32)
#include <rom/ets_sys.h>
#endif
#include "AsyncEventSource.h"
#include "AsyncWebServerLogging.h"
#include <algorithm>
#include <memory>
#include <utility>
#define ASYNC_SSE_NEW_LINE_CHAR (char)0xa
@@ -25,9 +26,7 @@ static String generateEventMessage(const char *message, const char *event, uint3
len += 42; // give it some overhead
if (!str.reserve(len)) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
return emptyString;
}
@@ -148,7 +147,7 @@ size_t AsyncEventSourceMessage::send(AsyncClient *client) {
// Client
AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server) : _client(request->client()), _server(server) {
AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server) : _client(request->clientRelease()), _server(server) {
if (request->hasHeader(T_Last_Event_ID)) {
_lastId = atoi(request->getHeader(T_Last_Event_ID)->value().c_str());
@@ -186,9 +185,9 @@ AsyncEventSourceClient::AsyncEventSourceClient(AsyncWebServerRequest *request, A
);
_server->_addClient(this);
delete request;
_client->setNoDelay(true);
// delete AsyncWebServerRequest object (and bound response) since we have the ownership on client connection now
delete request;
}
AsyncEventSourceClient::~AsyncEventSourceClient() {
@@ -201,11 +200,7 @@ AsyncEventSourceClient::~AsyncEventSourceClient() {
bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) {
if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) {
#ifdef ESP8266
ets_printf(String(F("ERROR: Too many messages queued\n")).c_str());
#elif defined(ESP32)
log_e("Event message queue overflow: discard message");
#endif
async_ws_log_e("Event message queue overflow: discard message");
return false;
}
@@ -214,7 +209,12 @@ bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) {
std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif
if (_client) {
_messageQueue.emplace_back(message, len);
} else {
_messageQueue.clear();
return false;
}
/*
throttle queue run
@@ -222,7 +222,7 @@ bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) {
forcing Q run will only eat more heap ram and blow the buffer, let's just keep data in our own queue
the queue will be processed at least on each onAck()/onPoll() call from AsyncTCP
*/
if (_messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2 && _client->canSend()) {
if (_client && _client->canSend() && _messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2) {
_runQueue();
}
@@ -231,11 +231,7 @@ bool AsyncEventSourceClient::_queueMessage(const char *message, size_t len) {
bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) {
if (_messageQueue.size() >= SSE_MAX_QUEUED_MESSAGES) {
#ifdef ESP8266
ets_printf(String(F("ERROR: Too many messages queued\n")).c_str());
#elif defined(ESP32)
log_e("Event message queue overflow: discard message");
#endif
async_ws_log_e("Event message queue overflow: discard message");
return false;
}
@@ -244,7 +240,12 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) {
std::lock_guard<std::recursive_mutex> lock(_lockmq);
#endif
if (_client) {
_messageQueue.emplace_back(std::move(msg));
} else {
_messageQueue.clear();
return false;
}
/*
throttle queue run
@@ -252,7 +253,7 @@ bool AsyncEventSourceClient::_queueMessage(AsyncEvent_SharedData_t &&msg) {
forcing Q run will only eat more heap ram and blow the buffer, let's just keep data in our own queue
the queue will be processed at least on each onAck()/onPoll() call from AsyncTCP
*/
if (_messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2 && _client->canSend()) {
if (_client && _client->canSend() && _messageQueue.size() < SSE_MAX_QUEUED_MESSAGES >> 2) {
_runQueue();
}
return true;
@@ -298,7 +299,7 @@ void AsyncEventSourceClient::_onPoll() {
void AsyncEventSourceClient::_onTimeout(uint32_t time __attribute__((unused))) {
if (_client) {
_client->close(true);
_client->close();
}
}
@@ -343,7 +344,7 @@ void AsyncEventSourceClient::_runQueue() {
}
// flush socket
if (total_bytes_written) {
if (_client && total_bytes_written) {
_client->send();
}
}
@@ -419,17 +420,13 @@ size_t AsyncEventSource::avgPacketsWaiting() const {
#ifdef ESP32
std::lock_guard<std::recursive_mutex> lock(_client_queue_lock);
#endif
if (!_clients.size()) {
return 0;
}
for (const auto &c : _clients) {
if (c->connected()) {
aql += c->packetsWaiting();
++nConnectedClients;
}
}
return ((aql) + (nConnectedClients / 2)) / (nConnectedClients); // round up
return nConnectedClients == 0 ? 0 : ((aql) + (nConnectedClients / 2)) / (nConnectedClients); // round up
}
AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const char *event, uint32_t id, uint32_t reconnect) {
@@ -440,12 +437,14 @@ AsyncEventSource::SendStatus AsyncEventSource::send(const char *message, const c
size_t hits = 0;
size_t miss = 0;
for (const auto &c : _clients) {
if (c->connected()) {
if (c->write(shared_msg)) {
++hits;
} else {
++miss;
}
}
}
return hits == 0 ? DISCARDED : (miss == 0 ? ENQUEUED : PARTIALLY_ENQUEUED);
}
@@ -471,20 +470,23 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
request->send(new AsyncEventSourceResponse(this));
}
// list iteration protected by caller's lock
void AsyncEventSource::_adjust_inflight_window() {
if (_clients.size()) {
size_t inflight = SSE_MAX_INFLIGH / _clients.size();
const size_t clientCount = count();
if (clientCount) {
size_t inflight = SSE_MAX_INFLIGH / clientCount;
for (const auto &c : _clients) {
if (c->connected()) {
c->set_max_inflight_bytes(inflight);
}
}
// Serial.printf("adjusted inflight to: %u\n", inflight);
}
}
/* Response */
AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource *server) {
_server = server;
AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource *server) : _server(server) {
_code = 200;
_contentType = T_text_event_stream;
_sendContentLength = false;
@@ -495,13 +497,24 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(AsyncEventSource *server) {
void AsyncEventSourceResponse::_respond(AsyncWebServerRequest *request) {
String out;
_assembleHead(out, request->version());
// unbind client's onAck callback from AsyncWebServerRequest's, we will destroy it on next callback and steal the client,
// can't do it now 'cause now we are in AsyncWebServerRequest::_onAck 's stack actually
// here we are loosing time on one RTT delay, but with current design we can't get rid of Req/Resp objects other way
_request = request;
request->client()->onAck(
[](void *r, AsyncClient *c, size_t len, uint32_t time) {
if (len) {
static_cast<AsyncEventSourceResponse *>(r)->_switchClient();
}
},
this
);
request->client()->write(out.c_str(), _headLength);
_state = RESPONSE_WAIT_ACK;
}
size_t AsyncEventSourceResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time __attribute__((unused))) {
if (len) {
new AsyncEventSourceClient(request, _server);
}
return 0;
}
void AsyncEventSourceResponse::_switchClient() {
// AsyncEventSourceClient c-tor will take the ownership of AsyncTCP's client connection
new AsyncEventSourceClient(_request, _server);
// AsyncEventSourceClient c-tor would also delete _request and *this
};

View File

@@ -1,8 +1,7 @@
// 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
#ifndef ASYNCEVENTSOURCE_H_
#define ASYNCEVENTSOURCE_H_
#pragma once
#include <Arduino.h>
@@ -44,6 +43,10 @@
#endif
#endif
#include <list>
#include <memory>
#include <utility>
class AsyncEventSource;
class AsyncEventSourceResponse;
class AsyncEventSourceClient;
@@ -141,6 +144,13 @@ private:
void _runQueue();
public:
/**
* @brief Construct a new Async Event Source Client object
* @note constructor would take the ownership of of AsyncTCP's client pointer from `request` parameter and call delete on it!
*
* @param request
* @param server
*/
AsyncEventSourceClient(AsyncWebServerRequest *request, AsyncEventSource *server);
~AsyncEventSourceClient();
@@ -305,21 +315,24 @@ public:
// system callbacks (do not call from user code!)
void _addClient(AsyncEventSourceClient *client);
void _handleDisconnect(AsyncEventSourceClient *client);
bool canHandle(AsyncWebServerRequest *request) const override final;
void handleRequest(AsyncWebServerRequest *request) override final;
bool canHandle(AsyncWebServerRequest *request) const final;
void handleRequest(AsyncWebServerRequest *request) final;
};
class AsyncEventSourceResponse : public AsyncWebServerResponse {
private:
AsyncEventSource *_server;
AsyncWebServerRequest *_request;
// this call back will switch AsyncTCP client to SSE
void _switchClient();
public:
AsyncEventSourceResponse(AsyncEventSource *server);
void _respond(AsyncWebServerRequest *request);
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time);
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override {
return 0;
};
bool _sourceValid() const {
return true;
}
};
#endif /* ASYNCEVENTSOURCE_H_ */

View File

@@ -1,10 +1,15 @@
// 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 "AsyncJson.h"
#include "AsyncWebServerLogging.h"
#include <utility>
#if ASYNC_JSON_SUPPORT == 1
// Json content type response classes
#if ARDUINOJSON_VERSION_MAJOR == 5
AsyncJsonResponse::AsyncJsonResponse(bool isArray) : _isValid{false} {
_code = 200;
@@ -56,7 +61,7 @@ size_t AsyncJsonResponse::_fillBuffer(uint8_t *data, size_t len) {
#else
serializeJson(_root, dest);
#endif
return len;
return dest.written();
}
#if ARDUINOJSON_VERSION_MAJOR == 6
@@ -84,15 +89,37 @@ size_t PrettyAsyncJsonResponse::_fillBuffer(uint8_t *data, size_t len) {
#else
serializeJsonPretty(_root, dest);
#endif
return len;
return dest.written();
}
// MessagePack content type response
#if ASYNC_MSG_PACK_SUPPORT == 1
size_t AsyncMessagePackResponse::setLength() {
_contentLength = measureMsgPack(_root);
if (_contentLength) {
_isValid = true;
}
return _contentLength;
}
size_t AsyncMessagePackResponse::_fillBuffer(uint8_t *data, size_t len) {
ChunkPrint dest(data, _sentLength, len);
serializeMsgPack(_root, dest);
return dest.written();
}
#endif
// Body handler supporting both content types: JSON and MessagePack
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(const String &uri, ArJsonRequestHandlerFunction onRequest, size_t maxJsonBufferSize)
: _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {}
AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest, size_t maxJsonBufferSize)
: _uri(std::move(uri)), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize),
_maxContentLength(16384) {}
#else
AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(const String &uri, ArJsonRequestHandlerFunction onRequest)
: _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {}
AsyncCallbackJsonWebHandler::AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest)
: _uri(std::move(uri)), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {}
#endif
bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) const {
@@ -100,15 +127,16 @@ bool AsyncCallbackJsonWebHandler::canHandle(AsyncWebServerRequest *request) cons
return false;
}
if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) {
if (!_uri.matches(request)) {
return false;
}
if (request->method() != HTTP_GET && !request->contentType().equalsIgnoreCase(asyncsrv::T_application_json)) {
return false;
}
return true;
#if ASYNC_MSG_PACK_SUPPORT == 1
return request->method() == HTTP_GET || request->contentType().equalsIgnoreCase(asyncsrv::T_application_json)
|| request->contentType().equalsIgnoreCase(asyncsrv::T_application_msgpack);
#else
return request->method() == HTTP_GET || request->contentType().equalsIgnoreCase(asyncsrv::T_application_json);
#endif
}
void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request) {
@@ -123,9 +151,7 @@ void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request)
// POST / PUT / ... requests:
// check if JSON body is too large, if it is, don't deserialize
if (request->contentLength() > _maxContentLength) {
#ifdef ESP32
log_e("Content length exceeds maximum allowed");
#endif
async_ws_log_e("Content length exceeds maximum allowed");
request->send(413);
return;
}
@@ -137,28 +163,34 @@ void AsyncCallbackJsonWebHandler::handleRequest(AsyncWebServerRequest *request)
}
#if ARDUINOJSON_VERSION_MAJOR == 5
DynamicJsonBuffer jsonBuffer;
JsonVariant json = jsonBuffer.parse((const char *)request->_tempObject);
if (json.success()) {
DynamicJsonBuffer doc;
#elif ARDUINOJSON_VERSION_MAJOR == 6
DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize);
DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject);
if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
DynamicJsonDocument doc(this->maxJsonBufferSize);
#else
JsonDocument jsonBuffer;
DeserializationError error = deserializeJson(jsonBuffer, (const char *)request->_tempObject);
if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
JsonDocument doc;
#endif
#if ARDUINOJSON_VERSION_MAJOR == 5
JsonVariant json = doc.parse((const char *)request->_tempObject);
if (json.success()) {
_onRequest(request, json);
} else {
return;
}
#else
DeserializationError error = request->contentType().equalsIgnoreCase(asyncsrv::T_application_msgpack)
? deserializeMsgPack(doc, (uint8_t *)(request->_tempObject))
: deserializeJson(doc, (const char *)request->_tempObject);
if (!error) {
JsonVariant json = doc.as<JsonVariant>();
_onRequest(request, json);
return;
}
#endif
// error parsing the body
request->send(400);
}
}
}
void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
if (_onRequest) {
@@ -172,9 +204,7 @@ void AsyncCallbackJsonWebHandler::handleBody(AsyncWebServerRequest *request, uin
if (request->_tempObject == NULL) {
request->_tempObject = calloc(total + 1, sizeof(uint8_t)); // null-terminated string
if (request->_tempObject == NULL) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
request->abort();
return;
}

View File

@@ -1,22 +1,12 @@
// 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
#ifndef ASYNC_JSON_H_
#define ASYNC_JSON_H_
#pragma once
#if __has_include("ArduinoJson.h")
#include <ArduinoJson.h>
#if ARDUINOJSON_VERSION_MAJOR >= 5
#define ASYNC_JSON_SUPPORT 1
#else
#define ASYNC_JSON_SUPPORT 0
#endif // ARDUINOJSON_VERSION_MAJOR >= 5
#endif // __has_include("ArduinoJson.h")
#include <ESPAsyncWebServer.h>
#include "ChunkPrint.h"
#if ASYNC_JSON_SUPPORT == 1
#include <ESPAsyncWebServer.h>
#include "ChunkPrint.h"
#if ARDUINOJSON_VERSION_MAJOR == 6
#ifndef DYNAMIC_JSON_DOCUMENT_SIZE
@@ -24,6 +14,8 @@
#endif
#endif
// Json content type response classes
class AsyncJsonResponse : public AsyncAbstractResponse {
protected:
#if ARDUINOJSON_VERSION_MAJOR == 5
@@ -49,11 +41,11 @@ public:
bool _sourceValid() const {
return _isValid;
}
size_t setLength();
virtual size_t setLength();
size_t getSize() const {
return _jsonBuffer.size();
}
size_t _fillBuffer(uint8_t *data, size_t len);
virtual size_t _fillBuffer(uint8_t *data, size_t len);
#if ARDUINOJSON_VERSION_MAJOR >= 6
bool overflowed() const {
return _jsonBuffer.overflowed();
@@ -68,15 +60,35 @@ public:
#else
PrettyAsyncJsonResponse(bool isArray = false);
#endif
size_t setLength();
size_t _fillBuffer(uint8_t *data, size_t len);
size_t setLength() override;
size_t _fillBuffer(uint8_t *data, size_t len) override;
};
typedef std::function<void(AsyncWebServerRequest *request, JsonVariant &json)> ArJsonRequestHandlerFunction;
// MessagePack content type response
#if ASYNC_MSG_PACK_SUPPORT == 1
class AsyncMessagePackResponse : public AsyncJsonResponse {
public:
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncMessagePackResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE) : AsyncJsonResponse(isArray, maxJsonBufferSize) {
_contentType = asyncsrv::T_application_msgpack;
}
#else
AsyncMessagePackResponse(bool isArray = false) : AsyncJsonResponse(isArray) {
_contentType = asyncsrv::T_application_msgpack;
}
#endif
size_t setLength() override;
size_t _fillBuffer(uint8_t *data, size_t len) override;
};
#endif
// Body handler supporting both content types: JSON and MessagePack
class AsyncCallbackJsonWebHandler : public AsyncWebHandler {
protected:
String _uri;
AsyncURIMatcher _uri;
WebRequestMethodComposite _method;
ArJsonRequestHandlerFunction _onRequest;
#if ARDUINOJSON_VERSION_MAJOR == 6
@@ -86,9 +98,9 @@ protected:
public:
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncCallbackJsonWebHandler(const String &uri, ArJsonRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
#else
AsyncCallbackJsonWebHandler(const String &uri, ArJsonRequestHandlerFunction onRequest = nullptr);
AsyncCallbackJsonWebHandler(AsyncURIMatcher uri, ArJsonRequestHandlerFunction onRequest = nullptr);
#endif
void setMethod(WebRequestMethodComposite method) {
@@ -101,18 +113,16 @@ public:
_onRequest = fn;
}
bool canHandle(AsyncWebServerRequest *request) const override final;
void handleRequest(AsyncWebServerRequest *request) override final;
bool canHandle(AsyncWebServerRequest *request) const final;
void handleRequest(AsyncWebServerRequest *request) final;
void handleUpload(
__unused AsyncWebServerRequest *request, __unused const String &filename, __unused size_t index, __unused uint8_t *data, __unused size_t len,
__unused bool final
) override final {}
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final;
bool isRequestHandlerTrivial() const override final {
) final {}
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) final;
bool isRequestHandlerTrivial() const final {
return !_onRequest;
}
};
#endif // ASYNC_JSON_SUPPORT == 1
#endif // ASYNC_JSON_H_

View File

@@ -1,119 +0,0 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov
#include "AsyncMessagePack.h"
#if ASYNC_MSG_PACK_SUPPORT == 1
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncMessagePackResponse::AsyncMessagePackResponse(bool isArray, size_t maxJsonBufferSize) : _jsonBuffer(maxJsonBufferSize), _isValid{false} {
_code = 200;
_contentType = asyncsrv::T_application_msgpack;
if (isArray) {
_root = _jsonBuffer.createNestedArray();
} else {
_root = _jsonBuffer.createNestedObject();
}
}
#else
AsyncMessagePackResponse::AsyncMessagePackResponse(bool isArray) : _isValid{false} {
_code = 200;
_contentType = asyncsrv::T_application_msgpack;
if (isArray) {
_root = _jsonBuffer.add<JsonArray>();
} else {
_root = _jsonBuffer.add<JsonObject>();
}
}
#endif
size_t AsyncMessagePackResponse::setLength() {
_contentLength = measureMsgPack(_root);
if (_contentLength) {
_isValid = true;
}
return _contentLength;
}
size_t AsyncMessagePackResponse::_fillBuffer(uint8_t *data, size_t len) {
ChunkPrint dest(data, _sentLength, len);
serializeMsgPack(_root, dest);
return len;
}
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncCallbackMessagePackWebHandler::AsyncCallbackMessagePackWebHandler(
const String &uri, ArMessagePackRequestHandlerFunction onRequest, size_t maxJsonBufferSize
)
: _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), maxJsonBufferSize(maxJsonBufferSize), _maxContentLength(16384) {}
#else
AsyncCallbackMessagePackWebHandler::AsyncCallbackMessagePackWebHandler(const String &uri, ArMessagePackRequestHandlerFunction onRequest)
: _uri(uri), _method(HTTP_GET | HTTP_POST | HTTP_PUT | HTTP_PATCH), _onRequest(onRequest), _maxContentLength(16384) {}
#endif
bool AsyncCallbackMessagePackWebHandler::canHandle(AsyncWebServerRequest *request) const {
if (!_onRequest || !request->isHTTP() || !(_method & request->method())) {
return false;
}
if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) {
return false;
}
if (request->method() != HTTP_GET && !request->contentType().equalsIgnoreCase(asyncsrv::T_application_msgpack)) {
return false;
}
return true;
}
void AsyncCallbackMessagePackWebHandler::handleRequest(AsyncWebServerRequest *request) {
if (_onRequest) {
if (request->method() == HTTP_GET) {
JsonVariant json;
_onRequest(request, json);
return;
} else if (request->_tempObject != NULL) {
#if ARDUINOJSON_VERSION_MAJOR == 6
DynamicJsonDocument jsonBuffer(this->maxJsonBufferSize);
DeserializationError error = deserializeMsgPack(jsonBuffer, (uint8_t *)(request->_tempObject));
if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
#else
JsonDocument jsonBuffer;
DeserializationError error = deserializeMsgPack(jsonBuffer, (uint8_t *)(request->_tempObject));
if (!error) {
JsonVariant json = jsonBuffer.as<JsonVariant>();
#endif
_onRequest(request, json);
return;
}
}
request->send(_contentLength > _maxContentLength ? 413 : 400);
} else {
request->send(500);
}
}
void AsyncCallbackMessagePackWebHandler::handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) {
if (_onRequest) {
_contentLength = total;
if (total > 0 && request->_tempObject == NULL && total < _maxContentLength) {
request->_tempObject = malloc(total);
if (request->_tempObject == NULL) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
request->abort();
return;
}
}
if (request->_tempObject != NULL) {
memcpy((uint8_t *)(request->_tempObject) + index, data, len);
}
}
}
#endif // ASYNC_MSG_PACK_SUPPORT

View File

@@ -1,126 +1,7 @@
// 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
#pragma once
/*
server.on("/msg_pack", HTTP_ANY, [](AsyncWebServerRequest * request) {
AsyncMessagePackResponse * response = new AsyncMessagePackResponse();
JsonObject& root = response->getRoot();
root["key1"] = "key number one";
JsonObject& nested = root.createNestedObject("nested");
nested["key1"] = "key number one";
response->setLength();
request->send(response);
});
#warning "AsyncMessagePack.h is deprecated, just include ESPAsyncWebServer.h from now on"
--------------------
AsyncCallbackMessagePackWebHandler* handler = new AsyncCallbackMessagePackWebHandler("/msg_pack/endpoint");
handler->onRequest([](AsyncWebServerRequest *request, JsonVariant &json) {
JsonObject jsonObj = json.as<JsonObject>();
// ...
});
server.addHandler(handler);
*/
#if __has_include("ArduinoJson.h")
#include <ArduinoJson.h>
#if ARDUINOJSON_VERSION_MAJOR >= 6
#define ASYNC_MSG_PACK_SUPPORT 1
#else
#define ASYNC_MSG_PACK_SUPPORT 0
#endif // ARDUINOJSON_VERSION_MAJOR >= 6
#endif // __has_include("ArduinoJson.h")
#if ASYNC_MSG_PACK_SUPPORT == 1
#include <ESPAsyncWebServer.h>
#include "ChunkPrint.h"
#if ARDUINOJSON_VERSION_MAJOR == 6
#ifndef DYNAMIC_JSON_DOCUMENT_SIZE
#define DYNAMIC_JSON_DOCUMENT_SIZE 1024
#endif
#endif
class AsyncMessagePackResponse : public AsyncAbstractResponse {
protected:
#if ARDUINOJSON_VERSION_MAJOR == 6
DynamicJsonDocument _jsonBuffer;
#else
JsonDocument _jsonBuffer;
#endif
JsonVariant _root;
bool _isValid;
public:
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncMessagePackResponse(bool isArray = false, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE);
#else
AsyncMessagePackResponse(bool isArray = false);
#endif
JsonVariant &getRoot() {
return _root;
}
bool _sourceValid() const {
return _isValid;
}
size_t setLength();
size_t getSize() const {
return _jsonBuffer.size();
}
size_t _fillBuffer(uint8_t *data, size_t len);
#if ARDUINOJSON_VERSION_MAJOR >= 6
bool overflowed() const {
return _jsonBuffer.overflowed();
}
#endif
};
typedef std::function<void(AsyncWebServerRequest *request, JsonVariant &json)> ArMessagePackRequestHandlerFunction;
class AsyncCallbackMessagePackWebHandler : public AsyncWebHandler {
protected:
String _uri;
WebRequestMethodComposite _method;
ArMessagePackRequestHandlerFunction _onRequest;
size_t _contentLength;
#if ARDUINOJSON_VERSION_MAJOR == 6
size_t maxJsonBufferSize;
#endif
size_t _maxContentLength;
public:
#if ARDUINOJSON_VERSION_MAJOR == 6
AsyncCallbackMessagePackWebHandler(
const String &uri, ArMessagePackRequestHandlerFunction onRequest = nullptr, size_t maxJsonBufferSize = DYNAMIC_JSON_DOCUMENT_SIZE
);
#else
AsyncCallbackMessagePackWebHandler(const String &uri, ArMessagePackRequestHandlerFunction onRequest = nullptr);
#endif
void setMethod(WebRequestMethodComposite method) {
_method = method;
}
void setMaxContentLength(int maxContentLength) {
_maxContentLength = maxContentLength;
}
void onRequest(ArMessagePackRequestHandlerFunction fn) {
_onRequest = fn;
}
bool canHandle(AsyncWebServerRequest *request) const override final;
void handleRequest(AsyncWebServerRequest *request) override final;
void handleUpload(
__unused AsyncWebServerRequest *request, __unused const String &filename, __unused size_t index, __unused uint8_t *data, __unused size_t len,
__unused bool final
) override final {}
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final;
bool isRequestHandlerTrivial() const override final {
return !_onRequest;
}
};
#endif // ASYNC_MSG_PACK_SUPPORT == 1
#include "AsyncJson.h"

View File

@@ -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 <ESPAsyncWebServer.h>

175
src/AsyncWebServerLogging.h Normal file
View File

@@ -0,0 +1,175 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
#pragma once
#ifdef ASYNCWEBSERVER_LOG_CUSTOM
// The user must provide the following macros in AsyncWebServerLoggingCustom.h:
// async_ws_log_e, async_ws_log_w, async_ws_log_i, async_ws_log_d, async_ws_log_v
#include <AsyncWebServerLoggingCustom.h>
#elif defined(ASYNCWEBSERVER_LOG_DEBUG)
// Local Debug logging
#include <HardwareSerial.h>
#define async_ws_log_e(format, ...) Serial.printf("E async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
#define async_ws_log_w(format, ...) Serial.printf("W async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
#define async_ws_log_i(format, ...) Serial.printf("I async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
#define async_ws_log_d(format, ...) Serial.printf("D async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
#define async_ws_log_v(format, ...) Serial.printf("V async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
#else
// Framework-based logging
/**
* LibreTiny specific configurations
*/
#if defined(LIBRETINY)
#include <Arduino.h>
#define async_ws_log_e(format, ...) log_e(format, ##__VA_ARGS__)
#define async_ws_log_w(format, ...) log_w(format, ##__VA_ARGS__)
#define async_ws_log_i(format, ...) log_i(format, ##__VA_ARGS__)
#define async_ws_log_d(format, ...) log_d(format, ##__VA_ARGS__)
#define async_ws_log_v(format, ...) log_v(format, ##__VA_ARGS__)
/**
* Raspberry Pi Pico specific configurations
*/
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#include <HardwareSerial.h>
// define log levels
#define ASYNC_WS_LOG_NONE 0 /*!< No log output */
#define ASYNC_WS_LOG_ERROR 1 /*!< Critical errors, software module can not recover on its own */
#define ASYNC_WS_LOG_WARN 2 /*!< Error conditions from which recovery measures have been taken */
#define ASYNC_WS_LOG_INFO 3 /*!< Information messages which describe normal flow of events */
#define ASYNC_WS_LOG_DEBUG 4 /*!< Extra information which is not necessary for normal use (values, pointers, sizes, etc). */
#define ASYNC_WS_LOG_VERBOSE 5 /*!< Verbose information for debugging purposes */
#define ASYNC_WS_LOG_MAX 6 /*!< Number of levels supported */
// set default log level
#ifndef ASYNCWEBSERVER_LOG_LEVEL
#define ASYNCWEBSERVER_LOG_LEVEL ASYNC_WS_LOG_INFO
#endif
// error
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_ERROR
#define async_ws_log_e(format, ...) Serial.printf("E async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
#else
#define async_ws_log_e(format, ...)
#endif
// warn
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_WARN
#define async_ws_log_w(format, ...) Serial.printf("W async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
#else
#define async_ws_log_w(format, ...)
#endif
// info
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_INFO
#define async_ws_log_i(format, ...) Serial.printf("I async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
#else
#define async_ws_log_i(format, ...)
#endif
// debug
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_DEBUG
#define async_ws_log_d(format, ...) Serial.printf("D async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
#else
#define async_ws_log_d(format, ...)
#endif
// verbose
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_VERBOSE
#define async_ws_log_v(format, ...) Serial.printf("V async_ws %s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__);
#else
#define async_ws_log_v(format, ...)
#endif
/**
* ESP8266 specific configurations
* Uses ets_printf to avoid dependency on global Serial object.
* Format strings are stored in PROGMEM and copied to a stack buffer.
*/
#elif defined(ESP8266)
#include <ets_sys.h>
#include <pgmspace.h>
// define log levels
#define ASYNC_WS_LOG_NONE 0 /*!< No log output */
#define ASYNC_WS_LOG_ERROR 1 /*!< Critical errors, software module can not recover on its own */
#define ASYNC_WS_LOG_WARN 2 /*!< Error conditions from which recovery measures have been taken */
#define ASYNC_WS_LOG_INFO 3 /*!< Information messages which describe normal flow of events */
#define ASYNC_WS_LOG_DEBUG 4 /*!< Extra information which is not necessary for normal use (values, pointers, sizes, etc). */
#define ASYNC_WS_LOG_VERBOSE 5 /*!< Verbose information for debugging purposes */
#define ASYNC_WS_LOG_MAX 6 /*!< Number of levels supported */
// set default log level
#ifndef ASYNCWEBSERVER_LOG_LEVEL
#define ASYNCWEBSERVER_LOG_LEVEL ASYNC_WS_LOG_INFO
#endif
// helper macro to copy PROGMEM format string to stack and call ets_printf
// level is a char literal ('E', 'W', etc.) to avoid RAM usage from string literals
#define _ASYNC_WS_LOG(level, format, ...) \
do { \
static const char __fmt[] PROGMEM = "%c async_ws %d: " format "\n"; \
char __buf[sizeof(__fmt)]; \
strcpy_P(__buf, __fmt); \
ets_printf(__buf, level, __LINE__, ##__VA_ARGS__); \
} while (0)
// error
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_ERROR
#define async_ws_log_e(format, ...) _ASYNC_WS_LOG('E', format, ##__VA_ARGS__)
#else
#define async_ws_log_e(format, ...)
#endif
// warn
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_WARN
#define async_ws_log_w(format, ...) _ASYNC_WS_LOG('W', format, ##__VA_ARGS__)
#else
#define async_ws_log_w(format, ...)
#endif
// info
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_INFO
#define async_ws_log_i(format, ...) _ASYNC_WS_LOG('I', format, ##__VA_ARGS__)
#else
#define async_ws_log_i(format, ...)
#endif
// debug
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_DEBUG
#define async_ws_log_d(format, ...) _ASYNC_WS_LOG('D', format, ##__VA_ARGS__)
#else
#define async_ws_log_d(format, ...)
#endif
// verbose
#if ASYNCWEBSERVER_LOG_LEVEL >= ASYNC_WS_LOG_VERBOSE
#define async_ws_log_v(format, ...) _ASYNC_WS_LOG('V', format, ##__VA_ARGS__)
#else
#define async_ws_log_v(format, ...)
#endif
/**
* Arduino specific configurations
*/
#elif defined(ARDUINO)
#if defined(USE_ESP_IDF_LOG)
#include <esp_log.h>
#define async_ws_log_e(format, ...) ESP_LOGE("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define async_ws_log_w(format, ...) ESP_LOGW("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define async_ws_log_i(format, ...) ESP_LOGI("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define async_ws_log_d(format, ...) ESP_LOGD("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define async_ws_log_v(format, ...) ESP_LOGV("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#else
#include <esp32-hal-log.h>
#define async_ws_log_e(format, ...) log_e(format, ##__VA_ARGS__)
#define async_ws_log_w(format, ...) log_w(format, ##__VA_ARGS__)
#define async_ws_log_i(format, ...) log_i(format, ##__VA_ARGS__)
#define async_ws_log_d(format, ...) log_d(format, ##__VA_ARGS__)
#define async_ws_log_v(format, ...) log_v(format, ##__VA_ARGS__)
#endif // USE_ESP_IDF_LOG
/**
* ESP-IDF specific configurations
*/
#else
#include <esp_log.h>
#define async_ws_log_e(format, ...) ESP_LOGE("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define async_ws_log_w(format, ...) ESP_LOGW("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define async_ws_log_i(format, ...) ESP_LOGI("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define async_ws_log_d(format, ...) ESP_LOGD("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#define async_ws_log_v(format, ...) ESP_LOGV("async_ws", "%s() %d: " format, __FUNCTION__, __LINE__, ##__VA_ARGS__)
#endif // !LIBRETINY && !ARDUINO
#endif // ASYNCWEBSERVER_LOG_CUSTOM

View File

@@ -1,3 +1,6 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles
#include <ESPAsyncWebServer.h>
/**
@@ -29,21 +32,17 @@ void AsyncWebServerRequest::send(FS &fs, const String &path, const char *content
const String gzPath = path + asyncsrv::T__gz;
File gzFile = fs.open(gzPath, fs::FileOpenMode::read);
// ETag validation
if (this->hasHeader(asyncsrv::T_INM)) {
// Generate server ETag from CRC in gzip trailer
char serverETag[11];
if (!_getEtag(gzFile, serverETag)) {
// Compressed file not found or invalid
if (!gzFile.seek(gzFile.size() - 8)) {
send(404);
gzFile.close();
return;
}
// ETag validation
if (this->hasHeader(asyncsrv::T_INM)) {
// Generate server ETag from CRC in gzip trailer
uint8_t crcInTrailer[4];
gzFile.read(crcInTrailer, 4);
char serverETag[9];
_getEtag(crcInTrailer, serverETag);
// Compare with client's ETag
const AsyncWebHeader *inmHeader = this->getHeader(asyncsrv::T_INM);
if (inmHeader && inmHeader->value() == serverETag) {
@@ -59,27 +58,39 @@ void AsyncWebServerRequest::send(FS &fs, const String &path, const char *content
}
/**
* @brief Generates an ETag string from a 4-byte trailer
* @brief Generates an ETag string (enclosed into quotes) from the CRC32 trailer of a GZIP file.
*
* This function converts a 4-byte array into a hexadecimal ETag string enclosed in quotes.
* This function reads the CRC32 checksum (4 bytes) located at the end of a GZIP-compressed file
* and converts it into an 8-character hexadecimal ETag string (enclosed in double quotes and null-terminated).
* Double quotes for ETag value are required by RFC9110 section 8.8.3.
*
* @param trailer[4] Input array of 4 bytes to convert to hexadecimal
* @param serverETag Output buffer to store the ETag
* Must be pre-allocated with minimum 9 bytes (8 hex + 1 null terminator)
* @param gzFile Opened file handle pointing to the GZIP file.
* @param eTag Output buffer to store the generated ETag.
* Must be pre-allocated with at least 11 bytes (8 for hex digits + 2 for quotes + 1 for null terminator).
*
* @return true if the ETag was successfully generated, false otherwise (e.g., file too short or seek failed).
*/
void AsyncWebServerRequest::_getEtag(uint8_t trailer[4], char *serverETag) {
bool AsyncWebServerRequest::_getEtag(File gzFile, char *etag) {
static constexpr char hexChars[] = "0123456789ABCDEF";
uint32_t data;
memcpy(&data, trailer, 4);
serverETag[0] = hexChars[(data >> 4) & 0x0F];
serverETag[1] = hexChars[data & 0x0F];
serverETag[2] = hexChars[(data >> 12) & 0x0F];
serverETag[3] = hexChars[(data >> 8) & 0x0F];
serverETag[4] = hexChars[(data >> 20) & 0x0F];
serverETag[5] = hexChars[(data >> 16) & 0x0F];
serverETag[6] = hexChars[(data >> 28)];
serverETag[7] = hexChars[(data >> 24) & 0x0F];
serverETag[8] = '\0';
if (!gzFile.seek(gzFile.size() - 8)) {
return false;
}
uint32_t crc;
gzFile.read(reinterpret_cast<uint8_t *>(&crc), sizeof(crc));
etag[0] = '"';
etag[1] = hexChars[(crc >> 4) & 0x0F];
etag[2] = hexChars[crc & 0x0F];
etag[3] = hexChars[(crc >> 12) & 0x0F];
etag[4] = hexChars[(crc >> 8) & 0x0F];
etag[5] = hexChars[(crc >> 20) & 0x0F];
etag[6] = hexChars[(crc >> 16) & 0x0F];
etag[7] = hexChars[(crc >> 28)];
etag[8] = hexChars[(crc >> 24) & 0x0F];
etag[9] = '"';
etag[10] = '\0';
return true;
}

View File

@@ -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
#pragma once
@@ -10,9 +10,9 @@ extern "C" {
/** Major version number (X.x.x) */
#define ASYNCWEBSERVER_VERSION_MAJOR 3
/** Minor version number (x.X.x) */
#define ASYNCWEBSERVER_VERSION_MINOR 8
#define ASYNCWEBSERVER_VERSION_MINOR 9
/** Patch version number (x.x.X) */
#define ASYNCWEBSERVER_VERSION_PATCH 0
#define ASYNCWEBSERVER_VERSION_PATCH 6
/**
* Macro to convert version number into an integer

View File

@@ -1,10 +1,8 @@
// 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 "AsyncWebSocket.h"
#include "Arduino.h"
#include <cstring>
#include "AsyncWebServerLogging.h"
#include <libb64/cencode.h>
@@ -21,6 +19,12 @@
#include <mbedtls/sha1.h>
#endif
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <memory>
#include <utility>
using namespace asyncsrv;
size_t webSocketSendFrameWindow(AsyncClient *client) {
@@ -48,10 +52,10 @@ size_t webSocketSendFrame(AsyncClient *client, bool final, uint8_t opcode, bool
uint8_t headLen = 2;
if (len && mask) {
headLen += 4;
mbuf[0] = rand() % 0xFF;
mbuf[1] = rand() % 0xFF;
mbuf[2] = rand() % 0xFF;
mbuf[3] = rand() % 0xFF;
mbuf[0] = rand() % 0xFF; // NOLINT(runtime/threadsafe_fn)
mbuf[1] = rand() % 0xFF; // NOLINT(runtime/threadsafe_fn)
mbuf[2] = rand() % 0xFF; // NOLINT(runtime/threadsafe_fn)
mbuf[3] = rand() % 0xFF; // NOLINT(runtime/threadsafe_fn)
}
if (len > 125) {
headLen += 2;
@@ -68,9 +72,7 @@ size_t webSocketSendFrame(AsyncClient *client, bool final, uint8_t opcode, bool
uint8_t *buf = (uint8_t *)malloc(headLen);
if (buf == NULL) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
client->abort();
return 0;
}
@@ -223,14 +225,10 @@ size_t AsyncWebSocketMessage::send(AsyncClient *client) {
const char *AWSC_PING_PAYLOAD = "ESPAsyncWebServer-PING";
const size_t AWSC_PING_PAYLOAD_LEN = 22;
AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server) : _tempObject(NULL) {
_client = request->client();
_server = server;
_clientId = _server->_getNextId();
_status = WS_CONNECTED;
_pstate = 0;
_lastMessageTime = millis();
_keepAlivePeriod = 0;
AsyncWebSocketClient::AsyncWebSocketClient(AsyncClient *client, AsyncWebSocket *server)
: _client(client), _server(server), _clientId(_server->_getNextId()), _status(WS_CONNECTED), _pstate(0), _lastMessageTime(millis()), _keepAlivePeriod(0),
_tempObject(NULL) {
_client->setRxTimeout(0);
_client->onError(
[](void *r, AsyncClient *c, int8_t error) {
@@ -274,7 +272,6 @@ AsyncWebSocketClient::AsyncWebSocketClient(AsyncWebServerRequest *request, Async
},
this
);
delete request;
memset(&_pinfo, 0, sizeof(_pinfo));
}
@@ -313,12 +310,12 @@ void AsyncWebSocketClient::_onAck(size_t len, uint32_t time) {
#ifdef ESP32
/*
Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock.
Due to _client->close(true) shall call the callback function _onDisconnect()
Due to _client->close() shall call the callback function _onDisconnect()
The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient()
*/
lock.unlock();
#endif
_client->close(true);
_client->close();
}
return;
}
@@ -425,26 +422,18 @@ bool AsyncWebSocketClient::_queueMessage(AsyncWebSocketSharedBuffer buffer, uint
#ifdef ESP32
/*
Unlocking has to be called before return execution otherwise std::unique_lock ::~unique_lock() will get an exception pthread_mutex_unlock.
Due to _client->close(true) shall call the callback function _onDisconnect()
Due to _client->close() shall call the callback function _onDisconnect()
The calling flow _onDisconnect() --> _handleDisconnect() --> ~AsyncWebSocketClient()
*/
lock.unlock();
#endif
_client->close(true);
_client->close();
}
#ifdef ESP8266
ets_printf("AsyncWebSocketClient::_queueMessage: Too many messages queued: closing connection\n");
#elif defined(ESP32)
log_e("Too many messages queued: closing connection");
#endif
async_ws_log_e("Too many messages queued: closing connection");
} else {
#ifdef ESP8266
ets_printf("AsyncWebSocketClient::_queueMessage: Too many messages queued: discarding new message\n");
#elif defined(ESP32)
log_e("Too many messages queued: discarding new message");
#endif
async_ws_log_e("Too many messages queued: discarding new message");
}
return false;
@@ -486,10 +475,8 @@ void AsyncWebSocketClient::close(uint16_t code, const char *message) {
free(buf);
return;
} else {
#ifdef ESP32
log_e("Failed to allocate");
async_ws_log_e("Failed to allocate");
_client->abort();
#endif
}
}
_queueControl(WS_DISCONNECT);
@@ -509,7 +496,7 @@ void AsyncWebSocketClient::_onTimeout(uint32_t time) {
}
// Serial.println("onTime");
(void)time;
_client->close(true);
_client->close();
}
void AsyncWebSocketClient::_onDisconnect() {
@@ -528,12 +515,15 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) {
_pinfo.index = 0;
_pinfo.final = (fdata[0] & 0x80) != 0;
_pinfo.opcode = fdata[0] & 0x0F;
_pinfo.masked = (fdata[1] & 0x80) != 0;
_pinfo.masked = ((fdata[1] & 0x80) != 0) ? 1 : 0;
_pinfo.len = fdata[1] & 0x7F;
// log_d("WS[%" PRIu32 "]: _onData: %" PRIu32, _clientId, plen);
// log_d("WS[%" PRIu32 "]: _status = %" PRIu32, _clientId, _status);
// log_d("WS[%" PRIu32 "]: _pinfo: index: %" PRIu64 ", final: %" PRIu8 ", opcode: %" PRIu8 ", masked: %" PRIu8 ", len: %" PRIu64, _clientId, _pinfo.index, _pinfo.final, _pinfo.opcode, _pinfo.masked, _pinfo.len);
// async_ws_log_w("WS[%" PRIu32 "]: _onData: %" PRIu32, _clientId, plen);
// async_ws_log_w("WS[%" PRIu32 "]: _status = %" PRIu32, _clientId, _status);
// async_ws_log_w(
// "WS[%" PRIu32 "]: _pinfo: index: %" PRIu64 ", final: %" PRIu8 ", opcode: %" PRIu8 ", masked: %" PRIu8 ", len: %" PRIu64, _clientId, _pinfo.index,
// _pinfo.final, _pinfo.opcode, _pinfo.masked, _pinfo.len
// );
data += 2;
plen -= 2;
@@ -549,18 +539,50 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) {
data += 8;
plen -= 8;
}
}
if (_pinfo.masked
&& plen >= 4) { // if ws.close() is called, Safari sends a close frame with plen 2 and masked bit set. We must not decrement plen which is already 0.
memcpy(_pinfo.mask, data, 4);
data += 4;
plen -= 4;
if (_pinfo.masked) {
// Read mask bytes (may be fragmented across packets in Safari)
size_t mask_offset = 0;
// If we're resuming from a previous fragmented read, check _pinfo.index
if (_pstate == 1 && _pinfo.index < 4) {
mask_offset = _pinfo.index;
}
// Read as many mask bytes as available
while (mask_offset < 4 && plen > 0) {
_pinfo.mask[mask_offset++] = *data++;
plen--;
}
// Check if we have all 4 mask bytes
if (mask_offset < 4) {
// Incomplete mask
if (_pinfo.opcode == WS_DISCONNECT && plen == 0) {
// Safari close frame edge case: masked bit set but no mask data
// async_ws_log_w("WS[%" PRIu32 "]: close frame with incomplete mask, treating as unmasked", _clientId);
_pinfo.masked = 0;
_pinfo.index = 0;
} else {
// Wait for more data
// async_ws_log_w("WS[%" PRIu32 "]: waiting for more mask data: read=%zu/4", _clientId, mask_offset);
_pinfo.index = mask_offset; // Save progress
_pstate = 1;
return;
}
} else {
// All mask bytes received
// async_ws_log_w("WS[%" PRIu32 "]: mask complete", _clientId);
_pinfo.index = 0; // Reset index for payload processing
}
}
const size_t datalen = std::min((size_t)(_pinfo.len - _pinfo.index), plen);
const auto datalast = data[datalen];
// async_ws_log_w("WS[%" PRIu32 "]: _processing data: datalen=%" PRIu32 ", plen=%" PRIu32, _clientId, datalen, plen);
if (_pinfo.masked) {
for (size_t i = 0; i < datalen; i++) {
data[i] ^= _pinfo.mask[(_pinfo.index + i) % 4];
@@ -594,7 +616,7 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) {
if (_status == WS_DISCONNECTING) {
_status = WS_DISCONNECTED;
if (_client) {
_client->close(true);
_client->close();
}
} else {
_status = WS_DISCONNECTING;
@@ -619,7 +641,7 @@ void AsyncWebSocketClient::_onData(void *pbuf, size_t plen) {
}
}
} else {
// os_printf("frame error: len: %u, index: %llu, total: %llu\n", datalen, _pinfo.index, _pinfo.len);
// async_ws_log_w("frame error: len: %u, index: %llu, total: %llu\n", datalen, _pinfo.index, _pinfo.len);
// what should we do?
break;
}
@@ -818,7 +840,10 @@ void AsyncWebSocket::_handleEvent(AsyncWebSocketClient *client, AwsEventType typ
AsyncWebSocketClient *AsyncWebSocket::_newClient(AsyncWebServerRequest *request) {
_clients.emplace_back(request, this);
// we've just detached AsyncTCP client from AsyncWebServerRequest
_handleEvent(&_clients.back(), WS_EVT_CONNECT, request, NULL, 0);
// after user code completed CONNECT event callback we can delete req/response objects
delete request;
return &_clients.back();
}
@@ -1230,9 +1255,7 @@ void AsyncWebSocket::handleRequest(AsyncWebServerRequest *request) {
const AsyncWebHeader *key = request->getHeader(WS_STR_KEY);
AsyncWebServerResponse *response = new AsyncWebSocketResponse(key->value(), this);
if (response == NULL) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
request->abort();
return;
}
@@ -1257,8 +1280,7 @@ AsyncWebSocketMessageBuffer *AsyncWebSocket::makeBuffer(const uint8_t *data, siz
* Authentication code from https://github.com/Links2004/arduinoWebSockets/blob/master/src/WebSockets.cpp#L480
*/
AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket *server) {
_server = server;
AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket *server) : _server(server) {
_code = 101;
_sendContentLength = false;
@@ -1270,7 +1292,7 @@ AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket
#else
String k;
if (!k.reserve(key.length() + WS_STR_UUID_LEN)) {
log_e("Failed to allocate");
async_ws_log_e("Failed to allocate");
return;
}
k.concat(key);
@@ -1301,21 +1323,29 @@ AsyncWebSocketResponse::AsyncWebSocketResponse(const String &key, AsyncWebSocket
void AsyncWebSocketResponse::_respond(AsyncWebServerRequest *request) {
if (_state == RESPONSE_FAILED) {
request->client()->close(true);
request->client()->close();
return;
}
// unbind client's onAck callback from AsyncWebServerRequest's, we will destroy it on next callback and steal the client,
// can't do it now 'cause now we are in AsyncWebServerRequest::_onAck 's stack actually
// here we are loosing time on one RTT delay, but with current design we can't get rid of Req/Resp objects other way
_request = request;
request->client()->onAck(
[](void *r, AsyncClient *c, size_t len, uint32_t time) {
if (len) {
static_cast<AsyncWebSocketResponse *>(r)->_switchClient();
}
},
this
);
String out;
_assembleHead(out, request->version());
request->client()->write(out.c_str(), _headLength);
_state = RESPONSE_WAIT_ACK;
}
size_t AsyncWebSocketResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) {
(void)time;
if (len) {
_server->_newClient(request);
}
return 0;
void AsyncWebSocketResponse::_switchClient() {
// detach client from request
_server->_newClient(_request);
// _newClient() would also destruct _request and *this
}

View File

@@ -1,8 +1,7 @@
// 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
#ifndef ASYNCWEBSOCKET_H_
#define ASYNCWEBSOCKET_H_
#pragma once
#include <Arduino.h>
@@ -30,7 +29,11 @@
#endif
#include <ESPAsyncWebServer.h>
#include <AsyncWebServerLogging.h>
#include <cstdio>
#include <deque>
#include <list>
#include <memory>
#ifdef ESP8266
@@ -80,9 +83,7 @@ public:
_data = (uint8_t *)malloc(_len);
if (_data == NULL) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
_len = 0;
} else {
memcpy(_data, data, len);
@@ -212,6 +213,9 @@ private:
AsyncWebSocket *_server;
uint32_t _clientId;
AwsClientStatus _status;
uint8_t _pstate;
uint32_t _lastMessageTime;
uint32_t _keepAlivePeriod;
#ifdef ESP32
mutable std::recursive_mutex _lock;
#endif
@@ -219,12 +223,8 @@ private:
std::deque<AsyncWebSocketMessage> _messageQueue;
bool closeWhenFull = true;
uint8_t _pstate;
AwsFrameInfo _pinfo;
uint32_t _lastMessageTime;
uint32_t _keepAlivePeriod;
bool _queueControl(uint8_t opcode, const uint8_t *data = NULL, size_t len = 0, bool mask = false);
bool _queueMessage(AsyncWebSocketSharedBuffer buffer, uint8_t opcode = WS_TEXT, bool mask = false);
void _runQueue();
@@ -233,7 +233,15 @@ private:
public:
void *_tempObject;
AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server);
AsyncWebSocketClient(AsyncClient *client, AsyncWebSocket *server);
/**
* @brief Construct a new Async Web Socket Client object
* @note constructor would take the ownership of of AsyncTCP's client pointer from `request` parameter and call delete on it!
* @param request
* @param server
*/
AsyncWebSocketClient(AsyncWebServerRequest *request, AsyncWebSocket *server) : AsyncWebSocketClient(request->clientRelease(), server){};
~AsyncWebSocketClient();
// client id increments for the given server
@@ -448,8 +456,8 @@ public:
AsyncWebSocketClient *_newClient(AsyncWebServerRequest *request);
void _handleDisconnect(AsyncWebSocketClient *client);
void _handleEvent(AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len);
bool canHandle(AsyncWebServerRequest *request) const override final;
void handleRequest(AsyncWebServerRequest *request) override final;
bool canHandle(AsyncWebServerRequest *request) const final;
void handleRequest(AsyncWebServerRequest *request) final;
// messagebuffer functions/objects.
AsyncWebSocketMessageBuffer *makeBuffer(size_t size = 0);
@@ -465,11 +473,16 @@ class AsyncWebSocketResponse : public AsyncWebServerResponse {
private:
String _content;
AsyncWebSocket *_server;
AsyncWebServerRequest *_request;
// this call back will switch AsyncTCP client to WebSocket
void _switchClient();
public:
AsyncWebSocketResponse(const String &key, AsyncWebSocket *server);
void _respond(AsyncWebServerRequest *request);
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time);
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override {
return 0;
};
bool _sourceValid() const {
return true;
}
@@ -556,5 +569,3 @@ private:
}
};
};
#endif /* ASYNCWEBSOCKET_H_ */

View File

@@ -12,12 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
#pragma once
#include <Arduino.h>
#if ESP_IDF_VERSION_MAJOR < 5
#ifndef SHA1Builder_h
#define SHA1Builder_h
#include <Stream.h>
#include <WString.h>
@@ -39,6 +38,4 @@ public:
void getBytes(uint8_t *output);
};
#endif // SHA1Builder_h
#endif // ESP_IDF_VERSION_MAJOR < 5

View File

@@ -1,18 +1,23 @@
// 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 <ChunkPrint.h>
ChunkPrint::ChunkPrint(uint8_t *destination, size_t from, size_t len) : _destination(destination), _to_skip(from), _to_write(len), _pos{0} {}
size_t ChunkPrint::write(uint8_t c) {
if (_to_skip > 0) {
_to_skip--;
return 1;
} else if (_to_write > 0) {
_to_write--;
_destination[_pos++] = c;
return 1;
}
// handle case where len is zero
if (!_len) {
return 0;
}
// skip first bytes until from is zero (bytes were already sent by previous chunk)
if (_from) {
_from--;
return 1;
}
// write a maximum of len bytes
if (_len - _index) {
_destination[_index++] = c;
return 1;
}
// we have finished writing len bytes, ignore the rest
return 0;
}

View File

@@ -1,23 +1,24 @@
// 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
#ifndef CHUNKPRINT_H
#define CHUNKPRINT_H
#pragma once
#include <Print.h>
class ChunkPrint : public Print {
private:
uint8_t *_destination;
size_t _to_skip;
size_t _to_write;
size_t _pos;
size_t _from;
size_t _len;
size_t _index;
public:
ChunkPrint(uint8_t *destination, size_t from, size_t len);
ChunkPrint(uint8_t *destination, size_t from, size_t len) : _destination(destination), _from(from), _len(len), _index(0) {}
size_t write(uint8_t c);
size_t write(const uint8_t *buffer, size_t size) {
return this->Print::write(buffer, size);
}
size_t written() const {
return _index;
}
};
#endif

View File

@@ -1,8 +1,7 @@
// 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
#ifndef _ESPAsyncWebServer_H_
#define _ESPAsyncWebServer_H_
#pragma once
#include <Arduino.h>
#include <FS.h>
@@ -12,11 +11,32 @@
#include <deque>
#include <functional>
#include <list>
#include <memory>
#include <tuple>
#include <unordered_map>
#include <utility>
#include <vector>
#if __has_include("ArduinoJson.h")
#include <ArduinoJson.h>
#if ARDUINOJSON_VERSION_MAJOR >= 5
#define ASYNC_JSON_SUPPORT 1
#else
#define ASYNC_JSON_SUPPORT 0
#endif // ARDUINOJSON_VERSION_MAJOR >= 5
#if ARDUINOJSON_VERSION_MAJOR >= 6
#define ASYNC_MSG_PACK_SUPPORT 1
#else
#define ASYNC_MSG_PACK_SUPPORT 0
#endif // ARDUINOJSON_VERSION_MAJOR >= 6
#endif // __has_include("ArduinoJson.h")
#if defined(ESP32) || defined(LIBRETINY)
#include <AsyncTCP.h>
#include <assert.h>
#elif defined(ESP8266)
#include <ESPAsyncTCP.h>
#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
@@ -27,23 +47,27 @@
#error Platform not supported
#endif
#include "literals.h"
#include "AsyncWebServerVersion.h"
#define ASYNCWEBSERVER_FORK_ESP32Async
#ifdef ASYNCWEBSERVER_REGEX
#define ASYNCWEBSERVER_REGEX_ATTRIBUTE
#else
#define ASYNCWEBSERVER_REGEX_ATTRIBUTE __attribute__((warning("ASYNCWEBSERVER_REGEX not defined")))
#include <regex>
#endif
#include "./literals.h"
// See https://github.com/ESP32Async/ESPAsyncWebServer/commit/3d3456e9e81502a477f6498c44d0691499dda8f9#diff-646b25b11691c11dce25529e3abce843f0ba4bd07ab75ec9eee7e72b06dbf13fR388-R392
// This setting slowdown chunk serving but avoids crashing or deadlocks in the case where slow chunk responses are created, like file serving form SD Card
#ifndef ASYNCWEBSERVER_USE_CHUNK_INFLIGHT
#define ASYNCWEBSERVER_USE_CHUNK_INFLIGHT 1
#endif
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI || CONFIG_ESP32_WIFI_ENABLED || defined(ESP8266)
#define ASYNCWEBSERVER_WIFI_SUPPORTED 1
#else
#define ASYNCWEBSERVER_WIFI_SUPPORTED 0
#endif
class AsyncWebServer;
class AsyncWebServerRequest;
class AsyncWebServerResponse;
@@ -204,6 +228,8 @@ class AsyncWebServerRequest {
friend class AsyncWebServer;
friend class AsyncCallbackWebHandler;
friend class AsyncFileResponse;
friend class AsyncStaticWebHandler;
friend class AsyncURIMatcher;
private:
AsyncClient *_client;
@@ -236,7 +262,9 @@ private:
std::list<AsyncWebHeader> _headers;
std::list<AsyncWebParameter> _params;
#ifdef ASYNCWEBSERVER_REGEX
std::list<String> _pathParams;
#endif
std::unordered_map<const char *, String, std::hash<const char *>, std::equal_to<const char *>> _attributes;
@@ -259,8 +287,6 @@ private:
void _onDisconnect();
void _onData(void *buf, size_t len);
void _addPathParam(const char *param);
bool _parseReqHead();
bool _parseReqHeader();
void _parseLine();
@@ -275,7 +301,7 @@ private:
void _send();
void _runMiddlewareChain();
static void _getEtag(uint8_t trailer[4], char *serverETag);
static bool _getEtag(File gzFile, char *eTag);
public:
File _tempFile;
@@ -287,6 +313,19 @@ public:
AsyncClient *client() {
return _client;
}
/**
* @brief release owned AsyncClient object
* AsyncClient pointer will be abandoned in this instance,
* the further ownership of the connection should be managed out of request's life-time scope
* could be used for long lived connection like SSE or WebSockets
* @note do not call this method unless you know what you are doing, otherwise it may lead to
* memory leaks and connections lingering
*
* @return AsyncClient* pointer to released connection object
*/
AsyncClient *clientRelease();
uint8_t version() const {
return _version;
}
@@ -338,6 +377,17 @@ public:
}
void requestAuthentication(AsyncAuthType method, const char *realm = nullptr, const char *_authFailMsg = nullptr);
// detected Authentication type from "Authorization" request header during request parsing
AsyncAuthType authType() const {
return _authMethod;
}
// raw value of "Authorization" request header after the auth type
// For example, for header "Authorization: Bearer <token>", <token> is the value returned
const String &authChallenge() const {
return _authorization;
}
// IMPORTANT: this method is for internal use ONLY
// Please do not use it!
// It can be removed or modified at any time without notice
@@ -586,10 +636,22 @@ public:
bool hasArg(const __FlashStringHelper *data) const; // check if F(argument) exists
#endif
const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(size_t i) const;
const String &ASYNCWEBSERVER_REGEX_ATTRIBUTE pathArg(int i) const {
#ifdef ASYNCWEBSERVER_REGEX
const String &pathArg(size_t i) const {
if (i >= _pathParams.size()) {
return emptyString;
}
auto it = _pathParams.begin();
std::advance(it, i);
return *it;
}
const String &pathArg(int i) const {
return i < 0 ? emptyString : pathArg((size_t)i);
}
#else
const String &pathArg(size_t i) const __attribute__((error("ERR: pathArg() requires -D ASYNCWEBSERVER_REGEX and only works on regex handlers")));
const String &pathArg(int i) const __attribute__((error("ERR: pathArg() requires -D ASYNCWEBSERVER_REGEX and only works on regex handlers")));
#endif
// get request header value by name
const String &header(const char *name) const;
@@ -690,6 +752,235 @@ public:
String urlDecode(const String &text) const;
};
class AsyncURIMatcher {
private:
// Matcher types are internal, not part of public API
enum class Type {
None, // default state: matcher does not match anything
All, // matches everything
Exact, // matches equivalent to regex: ^{_uri}$
Prefix, // matches equivalent to regex: ^{_uri}.*
Extension, // non-regular match: /pattern../*.ext
BackwardCompatible, // matches equivalent to regex: ^{_uri}(/.*)?$
Regex, // matches _url as regex
};
public:
/**
* @brief No special matching behavior (default)
*/
static constexpr uint16_t None = 0;
/**
* @brief Enable case-insensitive URI matching
*
* When CaseInsensitive is specified:
* - The URI pattern is converted to lowercase during construction
* - Incoming request URLs are converted to lowercase before matching
* - For regex matchers, the std::regex::icase flag is used
*
* Example usage:
* ```cpp
* // Matches /login, /LOGIN, /Login, /LoGiN, etc.
* server.on(AsyncURIMatcher::exact("/login", AsyncURIMatcher::CaseInsensitive), handler);
*
* // Matches /api/\*, /API/\*, /Api/\*, etc.
* server.on(AsyncURIMatcher::prefix("/api", AsyncURIMatcher::CaseInsensitive), handler);
*
* // Regex with case insensitive matching
* server.on(AsyncURIMatcher::regex("^/user/([a-z]+)$", AsyncURIMatcher::CaseInsensitive), handler);
* ```
*
* Performance note: Case conversion adds minimal overhead during construction and matching.
*/
static constexpr uint16_t CaseInsensitive = (1 << 0);
// public constructors
AsyncURIMatcher() : AsyncURIMatcher({}, Type::None, None) {}
AsyncURIMatcher(const char *uri, uint16_t modifiers = None) : AsyncURIMatcher(String(uri), modifiers) {}
AsyncURIMatcher(String uri, uint16_t modifiers = None);
#ifdef ASYNCWEBSERVER_REGEX
AsyncURIMatcher(const AsyncURIMatcher &c);
AsyncURIMatcher(AsyncURIMatcher &&c);
~AsyncURIMatcher();
AsyncURIMatcher &operator=(const AsyncURIMatcher &r);
AsyncURIMatcher &operator=(AsyncURIMatcher &&r);
#else
AsyncURIMatcher(const AsyncURIMatcher &) = default;
AsyncURIMatcher(AsyncURIMatcher &&) = default;
~AsyncURIMatcher() = default;
AsyncURIMatcher &operator=(const AsyncURIMatcher &) = default;
AsyncURIMatcher &operator=(AsyncURIMatcher &&) = default;
#endif
bool matches(AsyncWebServerRequest *request) const;
// static factory methods for common match types:
// - AsyncURIMatcher::all() - matches everything
// - AsyncURIMatcher::none() - matches nothing
// - AsyncURIMatcher::exact(uri, modifiers) - exact match
// - AsyncURIMatcher::prefix(uri, modifiers) - prefix match
// - AsyncURIMatcher::dir(uri, modifiers) - directory/folder match (trailing slash added automatically)
// - AsyncURIMatcher::ext(uri, modifiers) - extension match (pattern with wildcard)
// - AsyncURIMatcher::regex(uri, modifiers) - regex match (requires ASYNCWEBSERVER_REGEX)
/**
* @brief Create a matcher that matches all URIs unconditionally
* @return AsyncURIMatcher that accepts any request URL
*
* Usage: server.on(AsyncURIMatcher::all(), handler);
*/
static inline AsyncURIMatcher all() {
return AsyncURIMatcher{{}, Type::All, None};
}
/**
* @brief Create a matcher that matches no URIs (never matches)
* @return AsyncURIMatcher that rejects all request URLs
*
* Usage: server.on(AsyncURIMatcher::none(), handler);
*/
static inline AsyncURIMatcher none() {
return AsyncURIMatcher{{}, Type::None, None};
}
/**
* @brief Create an exact URI matcher
* @param c The exact URI string to match (e.g., "/login", "/api/status")
* @param modifiers Optional modifiers (CaseInsensitive, etc.)
* @return AsyncURIMatcher that matches only the exact URI
*
* Usage: server.on(AsyncURIMatcher::exact("/login"), handler);
* Matches: "/login"
* Doesn't match: "/login/", "/login-page"
* Doesn't match: "/LOGIN" (unless CaseInsensitive flag used)
*/
static inline AsyncURIMatcher exact(String c, uint16_t modifiers = None) {
return AsyncURIMatcher{std::move(c), Type::Exact, modifiers};
}
/**
* @brief Create a prefix URI matcher
* @param c The URI prefix to match (e.g., "/api", "/static")
* @param modifiers Optional modifiers (CaseInsensitive, etc.)
* @return AsyncURIMatcher that matches URIs starting with the prefix
*
* Usage: server.on(AsyncURIMatcher::prefix("/api"), handler);
* Matches: "/api", "/api/users", "/api-v2", "/apitest"
* Note: This is pure prefix matching - does NOT require folder separator
*/
static inline AsyncURIMatcher prefix(String c, uint16_t modifiers = None) {
return AsyncURIMatcher{std::move(c), Type::Prefix, modifiers};
}
/**
* @brief Create a directory/folder URI matcher
* @param c The directory path (trailing slash automatically added if missing)
* @param modifiers Optional modifiers (CaseInsensitive, etc.)
* @return AsyncURIMatcher that matches URIs under the directory
*
* Usage: server.on(AsyncURIMatcher::dir("/admin"), handler);
* Matches: "/admin/users", "/admin/settings", "/admin/sub/path"
* Doesn't match: "/admin" (exact), "/admin-panel" (no folder separator)
*
* The trailing slash is automatically added for convenience and efficiency.
*/
static inline AsyncURIMatcher dir(String c, uint16_t modifiers = None) {
// Pre-calculate folder for efficiency
if (!c.length()) {
return AsyncURIMatcher{"/", Type::Prefix, modifiers};
}
if (c[c.length() - 1] != '/') {
c.concat('/');
}
return AsyncURIMatcher{std::move(c), Type::Prefix, modifiers};
}
/**
* @brief Create a file extension URI matcher
* @param c The pattern with wildcard extension (e.g., "/images/\*.jpg", "/docs/\*.pdf")
* @param modifiers Optional modifiers (CaseInsensitive, etc.)
* @return AsyncURIMatcher that matches files with specific extensions under a path
*
* Usage: server.on(AsyncURIMatcher::ext("/images/\*.jpg"), handler);
* Matches: "/images/photo.jpg", "/images/gallery/pic.jpg"
* Doesn't match: "/images/photo.png", "/img/photo.jpg"
*
* Pattern format: "/path/\*.extension" where "*" is a literal wildcard placeholder.
* The path before "/\*." must match exactly, and the URI must end with the extension.
*/
static inline AsyncURIMatcher ext(String c, uint16_t modifiers = None) {
return AsyncURIMatcher{std::move(c), Type::Extension, modifiers};
}
#ifdef ASYNCWEBSERVER_REGEX
/**
* @brief Create a regular expression URI matcher
* @param c The regex pattern string (e.g., "^/user/([0-9]+)$", "^/blog/([0-9]{4})/([0-9]{2})$")
* @param modifiers Optional modifiers (CaseInsensitive applies to regex compilation)
* @return AsyncURIMatcher that matches URIs using regex with capture groups
*
* Usage: server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), handler);
* Matches: "/user/123", "/user/456"
* Doesn't match: "/user/abc", "/user/123/profile"
*
* Captured groups can be accessed via request->pathArg(index) in the handler.
* Requires ASYNCWEBSERVER_REGEX to be defined during compilation.
* Performance note: Regex matching is slower than other match types.
*/
static inline AsyncURIMatcher regex(String c, uint16_t modifiers = None) {
return AsyncURIMatcher{std::move(c), Type::Regex, modifiers};
}
#endif
private:
// fields
String _value;
union {
intptr_t _flags; // type and flags packed together
#ifdef ASYNCWEBSERVER_REGEX
// Overlay the pattern pointer storage with the type. It is treated as a tagged pointer:
// if any of the LSBs are set, it stores type, as a valid object must be aligned and so
// none of the LSBs can be set in a valid pointer.
std::regex *pattern;
#endif
};
// private constructor called from static factory methods
AsyncURIMatcher(String uri, Type type, uint16_t modifiers);
#ifdef ASYNCWEBSERVER_REGEX
inline bool _isRegex() const {
static_assert(
(std::alignment_of<std::regex>::value % 2) == 0, "Unexpected regex type alignment - please let the ESPAsyncWebServer team know about your platform!"
);
// pattern is non-null pointer with correct alignment.
// We use the _flags view as it's already a integer type.
return _flags && !(_flags & (std::alignment_of<std::regex>::value - 1));
}
#endif
static constexpr intptr_t _toFlags(Type type, uint16_t modifiers) {
// Use lsb to disambiguate from regex pointer in the case where someone has regex activated but uses a non-regex type.
// We always do this shift, even if regex is not enabled, to keep the layout identical and also catch programmatic errors earlier.
// For example a mistake is to set a modifier flag to (1 << 15), which is the msb of the uint16_t.
// This msb is discarded during this shift operation.
// So pay attention to not have more than 15 modifier flags.
return ((uint32_t(modifiers) << 16 | uint16_t(type)) << 1) + 1;
}
static constexpr std::tuple<Type, uint16_t> _fromFlags(intptr_t in_flags) {
// shift off disambiguation bit
// - Type is lower 16 bits
// - Modifiers are upper 16 bits
return std::make_tuple(static_cast<Type>((in_flags >> 1) & 0xFFFF), (in_flags >> 1) >> 16);
}
};
/*
* FILTER :: Callback to filter AsyncWebRewrite and AsyncWebHandler (done by the Server)
* */
@@ -756,10 +1047,35 @@ protected:
// AsyncAuthenticationMiddleware is a middleware that checks if the request is authenticated
class AsyncAuthenticationMiddleware : public AsyncMiddleware {
public:
const String &username() const {
return _username;
}
const String &credentials() const {
return _credentials;
}
const String &realm() const {
return _realm;
}
const String &authFailureMessage() const {
return _authFailMsg;
}
bool isHash() const {
return _hash;
}
AsyncAuthType authType() const {
return _authMethod;
}
void setUsername(const char *username);
void setPassword(const char *password);
void setPasswordHash(const char *hash);
// can be used for Bearer token authentication with a static shared secret
void setToken(const char *token);
void setAuthentificationFunction(std::function<bool(AsyncWebServerRequest *request)> func) {
_authcFunc = func;
}
void setRealm(const char *realm) {
_realm = realm;
}
@@ -802,6 +1118,9 @@ private:
AsyncAuthType _authMethod = AsyncAuthType::AUTH_NONE;
String _authFailMsg;
bool _hasCreds = false;
std::function<bool(AsyncWebServerRequest *request)> _authcFunc = [this](AsyncWebServerRequest *request) {
return request->authenticate(_username.c_str(), _credentials.c_str(), _realm.c_str(), _hash);
};
};
using ArAuthorizeFunction = std::function<bool(AsyncWebServerRequest *request)>;
@@ -891,7 +1210,13 @@ public:
_maxAge = seconds;
}
void addCORSHeaders(AsyncWebServerResponse *response);
#ifndef ESP8266
[[deprecated("Use instead: addCORSHeaders(AsyncWebServerRequest *request, AsyncWebServerResponse *response)")]]
#endif
void addCORSHeaders(AsyncWebServerResponse *response) {
addCORSHeaders(nullptr, response);
}
void addCORSHeaders(AsyncWebServerRequest *request, AsyncWebServerResponse *response);
void run(AsyncWebServerRequest *request, ArMiddlewareNext next);
@@ -1032,15 +1357,18 @@ protected:
bool _sendContentLength;
bool _chunked;
size_t _headLength;
// amount of data sent for content part of the response (excluding all headers)
size_t _sentLength;
size_t _ackedLength;
// amount of response bytes (including all headers) written to sockbuff for delivery
size_t _writtenLength;
WebResponseState _state;
static bool headerMustBePresentOnce(const String &name);
public:
static const char *responseCodeToString(int code);
// Return type changes based on platform (const char* or __FlashStringHelper*)
static STR_RETURN_TYPE responseCodeToString(int code);
public:
AsyncWebServerResponse();
@@ -1090,7 +1418,20 @@ public:
virtual bool _failed() const;
virtual bool _sourceValid() const;
virtual void _respond(AsyncWebServerRequest *request);
virtual size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time);
/**
* @brief write next portion of response data to send buffs
* this method (re)fills tcp send buffers, it could be called either at will
* or from a tcp_recv/tcp_poll callbacks from AsyncTCP
*
* @param request - used to access client object
* @param len - size of acknowledged data from the remote side (TCP window update, not TCP ack!)
* @param time - time passed between last sent and received packet
* @return size_t amount of response data placed to TCP send buffs for delivery (defined by sdkconfig value CONFIG_LWIP_TCP_SND_BUF_DEFAULT)
*/
virtual size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) {
return 0;
};
};
/*
@@ -1102,6 +1443,20 @@ typedef std::function<void(AsyncWebServerRequest *request, const String &filenam
ArUploadHandlerFunction;
typedef std::function<void(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total)> ArBodyHandlerFunction;
#if ASYNC_JSON_SUPPORT == 1
class AsyncCallbackJsonWebHandler;
typedef std::function<void(AsyncWebServerRequest *request, JsonVariant &json)> ArJsonRequestHandlerFunction;
#if ASYNC_MSG_PACK_SUPPORT == 1
#ifndef ESP8266
[[deprecated("Replaced by AsyncCallbackJsonWebHandler")]]
#endif
typedef AsyncCallbackJsonWebHandler AsyncCallbackMessagePackWebHandler;
#endif // ASYNC_MSG_PACK_SUPPORT
#endif
class AsyncWebServer : public AsyncMiddlewareChain {
protected:
AsyncServer _server;
@@ -1174,14 +1529,18 @@ public:
AsyncWebHandler &addHandler(AsyncWebHandler *handler);
bool removeHandler(AsyncWebHandler *handler);
AsyncCallbackWebHandler &on(const char *uri, ArRequestHandlerFunction onRequest) {
return on(uri, HTTP_ANY, onRequest);
AsyncCallbackWebHandler &on(AsyncURIMatcher uri, ArRequestHandlerFunction onRequest) {
return on(std::move(uri), HTTP_ANY, onRequest);
}
AsyncCallbackWebHandler &on(
const char *uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload = nullptr,
AsyncURIMatcher uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload = nullptr,
ArBodyHandlerFunction onBody = nullptr
);
#if ASYNC_JSON_SUPPORT == 1
AsyncCallbackJsonWebHandler &on(AsyncURIMatcher uri, WebRequestMethodComposite method, ArJsonRequestHandlerFunction onBody);
#endif
AsyncStaticWebHandler &serveStatic(const char *uri, fs::FS &fs, const char *path, const char *cache_control = NULL);
void onNotFound(ArRequestHandlerFunction fn); // called when handler is not assigned
@@ -1231,4 +1590,6 @@ public:
#include "WebHandlerImpl.h"
#include "WebResponseImpl.h"
#endif /* _AsyncWebServer_H_ */
#if ASYNC_JSON_SUPPORT == 1
#include <AsyncJson.h>
#endif

View File

@@ -1,9 +1,11 @@
// 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 "WebAuthentication.h"
#include <ESPAsyncWebServer.h>
#include <list>
AsyncMiddlewareChain::~AsyncMiddlewareChain() {
for (AsyncMiddleware *m : _middlewares) {
if (m->_freeOnRemoval) {
@@ -85,6 +87,12 @@ void AsyncAuthenticationMiddleware::setPasswordHash(const char *hash) {
_hasCreds = _username.length() && _credentials.length();
}
void AsyncAuthenticationMiddleware::setToken(const char *token) {
_credentials = token;
_hash = _credentials.length();
_hasCreds = _credentials.length();
}
bool AsyncAuthenticationMiddleware::generateHash() {
// ensure we have all the necessary data
if (!_hasCreds) {
@@ -120,19 +128,15 @@ bool AsyncAuthenticationMiddleware::generateHash() {
}
bool AsyncAuthenticationMiddleware::allowed(AsyncWebServerRequest *request) const {
if (_authMethod == AsyncAuthType::AUTH_NONE) {
return true;
switch (_authMethod) {
case AsyncAuthType::AUTH_NONE: return true;
case AsyncAuthType::AUTH_DENIED: return false;
case AsyncAuthType::AUTH_BEARER: return _authcFunc(request);
case AsyncAuthType::AUTH_OTHER: return _authcFunc(request);
case AsyncAuthType::AUTH_BASIC: return !_hasCreds || _authcFunc(request);
case AsyncAuthType::AUTH_DIGEST: return !_hasCreds || _authcFunc(request);
default: return false;
}
if (_authMethod == AsyncAuthType::AUTH_DENIED) {
return false;
}
if (!_hasCreds) {
return true;
}
return request->authenticate(_username.c_str(), _credentials.c_str(), _realm.c_str(), _hash);
}
void AsyncAuthenticationMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNext next) {
@@ -228,8 +232,13 @@ void AsyncLoggingMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNex
}
}
void AsyncCorsMiddleware::addCORSHeaders(AsyncWebServerResponse *response) {
void AsyncCorsMiddleware::addCORSHeaders(AsyncWebServerRequest *request, AsyncWebServerResponse *response) {
if (request != nullptr && _credentials && _origin == "*") {
// cannot use wildcard when allowing credentials
response->addHeader(asyncsrv::T_CORS_ACAO, request->header(asyncsrv::T_CORS_O).c_str());
} else {
response->addHeader(asyncsrv::T_CORS_ACAO, _origin.c_str());
}
response->addHeader(asyncsrv::T_CORS_ACAM, _methods.c_str());
response->addHeader(asyncsrv::T_CORS_ACAH, _headers.c_str());
response->addHeader(asyncsrv::T_CORS_ACAC, _credentials ? asyncsrv::T_TRUE : asyncsrv::T_FALSE);
@@ -242,7 +251,7 @@ void AsyncCorsMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNext n
// check if this is a preflight request => handle it and return
if (request->method() == HTTP_OPTIONS) {
AsyncWebServerResponse *response = request->beginResponse(200);
addCORSHeaders(response);
addCORSHeaders(request, response);
request->send(response);
return;
}
@@ -251,7 +260,7 @@ void AsyncCorsMiddleware::run(AsyncWebServerRequest *request, ArMiddlewareNext n
next();
AsyncWebServerResponse *response = request->getResponse();
if (response) {
addCORSHeaders(response);
addCORSHeaders(request, response);
}
} else {

View File

@@ -1,14 +1,17 @@
// 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 "WebAuthentication.h"
#include "AsyncWebServerLogging.h"
#include <libb64/cencode.h>
#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
#include <MD5Builder.h>
#else
#include "md5.h"
#include <md5.h>
#endif
#include "literals.h"
#include "./literals.h"
using namespace asyncsrv;
@@ -82,13 +85,11 @@ String genRandomMD5() {
#ifdef ESP8266
uint32_t r = RANDOM_REG32;
#else
uint32_t r = rand();
uint32_t r = rand(); // NOLINT(runtime/threadsafe_fn)
#endif
char *out = (char *)malloc(33);
if (out == NULL || !getMD5((uint8_t *)(&r), 4, out)) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
return emptyString;
}
String res = String(out);
@@ -99,9 +100,7 @@ String genRandomMD5() {
static String stringMD5(const String &in) {
char *out = (char *)malloc(33);
if (out == NULL || !getMD5((uint8_t *)(in.c_str()), in.length(), out)) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
return emptyString;
}
String res = String(out);
@@ -115,17 +114,13 @@ String generateDigestHash(const char *username, const char *password, const char
}
char *out = (char *)malloc(33);
if (out == NULL) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
return emptyString;
}
String in;
if (!in.reserve(strlen(username) + strlen(realm) + strlen(password) + 2)) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
free(out);
return emptyString;
}
@@ -137,9 +132,7 @@ String generateDigestHash(const char *username, const char *password, const char
in.concat(password);
if (!getMD5((uint8_t *)(in.c_str()), in.length(), out)) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
free(out);
return emptyString;
}

View File

@@ -1,8 +1,7 @@
// 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
#ifndef WEB_AUTHENTICATION_H_
#define WEB_AUTHENTICATION_H_
#pragma once
#include "Arduino.h"
@@ -19,5 +18,3 @@ String generateDigestHash(const char *username, const char *password, const char
String generateBasicHash(const char *username, const char *password);
String genRandomMD5();
#endif

View File

@@ -1,16 +1,12 @@
// 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
#ifndef ASYNCWEBSERVERHANDLERIMPL_H_
#define ASYNCWEBSERVERHANDLERIMPL_H_
#pragma once
#include <stddef.h>
#include <time.h>
#include <string>
#ifdef ASYNCWEBSERVER_REGEX
#include <regex>
#endif
#include "stddef.h"
#include <time.h>
class AsyncStaticWebHandler : public AsyncWebHandler {
using File = fs::File;
@@ -33,8 +29,8 @@ protected:
public:
AsyncStaticWebHandler(const char *uri, FS &fs, const char *path, const char *cache_control);
bool canHandle(AsyncWebServerRequest *request) const override final;
void handleRequest(AsyncWebServerRequest *request) override final;
bool canHandle(AsyncWebServerRequest *request) const final;
void handleRequest(AsyncWebServerRequest *request) final;
AsyncStaticWebHandler &setTryGzipFirst(bool value);
AsyncStaticWebHandler &setIsDir(bool isDir);
AsyncStaticWebHandler &setDefaultFile(const char *filename);
@@ -58,7 +54,7 @@ public:
class AsyncCallbackWebHandler : public AsyncWebHandler {
private:
protected:
String _uri;
AsyncURIMatcher _uri;
WebRequestMethodComposite _method;
ArRequestHandlerFunction _onRequest;
ArUploadHandlerFunction _onUpload;
@@ -67,7 +63,7 @@ protected:
public:
AsyncCallbackWebHandler() : _uri(), _method(HTTP_ANY), _onRequest(NULL), _onUpload(NULL), _onBody(NULL), _isRegex(false) {}
void setUri(const String &uri);
void setUri(AsyncURIMatcher uri);
void setMethod(WebRequestMethodComposite method) {
_method = method;
}
@@ -81,13 +77,11 @@ public:
_onBody = fn;
}
bool canHandle(AsyncWebServerRequest *request) const override final;
void handleRequest(AsyncWebServerRequest *request) override final;
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) override final;
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) override final;
bool isRequestHandlerTrivial() const override final {
bool canHandle(AsyncWebServerRequest *request) const final;
void handleRequest(AsyncWebServerRequest *request) final;
void handleUpload(AsyncWebServerRequest *request, const String &filename, size_t index, uint8_t *data, size_t len, bool final) final;
void handleBody(AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) final;
bool isRequestHandlerTrivial() const final {
return !_onRequest;
}
};
#endif /* ASYNCWEBSERVERHANDLERIMPL_H_ */

View File

@@ -1,8 +1,12 @@
// 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 "ESPAsyncWebServer.h"
#include "WebHandlerImpl.h"
#include "AsyncWebServerLogging.h"
#include <cstdio>
#include <utility>
using namespace asyncsrv;
@@ -75,7 +79,7 @@ AsyncStaticWebHandler &AsyncStaticWebHandler::setLastModified(struct tm *last_mo
char result[30];
#ifdef ESP8266
auto formatP = PSTR("%a, %d %b %Y %H:%M:%S GMT");
char format[strlen_P(formatP) + 1];
char format[strlen_P(formatP) + 1]; // NOLINT(runtime/arrays)
strcpy_P(format, formatP);
#else
static constexpr const char *format = "%a, %d %b %Y %H:%M:%S GMT";
@@ -173,9 +177,7 @@ bool AsyncStaticWebHandler::_searchFile(AsyncWebServerRequest *request, const St
size_t pathLen = path.length();
char *_tempPath = (char *)malloc(pathLen + 1);
if (_tempPath == NULL) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
request->abort();
request->_tempFile.close();
return false;
@@ -187,54 +189,74 @@ bool AsyncStaticWebHandler::_searchFile(AsyncWebServerRequest *request, const St
return found;
}
/**
* @brief Handles an incoming HTTP request for a static file.
*
* This method processes a request for serving static files asynchronously.
* It determines the correct ETag (entity tag) for caching, checks if the file
* has been modified, and prepares the appropriate response (file response or 304 Not Modified).
*
* @param request Pointer to the incoming AsyncWebServerRequest object.
*/
void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
// Get the filename from request->_tempObject and free it
String filename((char *)request->_tempObject);
free(request->_tempObject);
request->_tempObject = NULL;
request->_tempObject = nullptr;
if (request->_tempFile != true) {
request->send(404);
return;
}
time_t lw = request->_tempFile.getLastWrite(); // get last file mod time (if supported by FS)
// set etag to lastmod timestamp if available, otherwise to size
String etag;
if (lw) {
setLastModified(lw);
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350)
// time_t == long long int
constexpr size_t len = 1 + 8 * sizeof(time_t);
char buf[len];
char *ret = lltoa(lw ^ request->_tempFile.size(), buf, len, 10);
etag = ret ? String(ret) : String(request->_tempFile.size());
#elif defined(LIBRETINY)
long val = lw ^ request->_tempFile.size();
etag = String(val);
#else
etag = lw ^ request->_tempFile.size(); // etag combines file size and lastmod timestamp
#endif
} else {
#if defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
etag = String(request->_tempFile.size());
#else
etag = request->_tempFile.size();
#endif
// Get server ETag. If file is not GZ and we have a Template Processor, ETag is set to an empty string
char etag[11];
const char *tempFileName = request->_tempFile.name();
const size_t lenFilename = strlen(tempFileName);
if (lenFilename > T__GZ_LEN && memcmp(tempFileName + lenFilename - T__GZ_LEN, T__gz, T__GZ_LEN) == 0) {
//File is a gz, get etag from CRC in trailer
if (!AsyncWebServerRequest::_getEtag(request->_tempFile, etag)) {
// File is corrupted or invalid
async_ws_log_e("File is corrupted or invalid: %s", tempFileName);
request->send(404);
return;
}
bool not_modified = false;
// if-none-match has precedence over if-modified-since
if (request->hasHeader(T_INM)) {
not_modified = request->header(T_INM).equals(etag);
} else if (_last_modified.length()) {
not_modified = request->header(T_IMS).equals(_last_modified);
// Reset file position to the beginning so the file can be served from the start.
request->_tempFile.seek(0);
} else if (_callback == nullptr) {
// We don't have a Template processor
uint32_t etagValue;
time_t lastWrite = request->_tempFile.getLastWrite();
if (lastWrite > 0) {
// Use timestamp-based ETag
etagValue = static_cast<uint32_t>(lastWrite);
} else {
// No timestamp available, use filesize-based ETag
size_t fileSize = request->_tempFile.size();
etagValue = static_cast<uint32_t>(fileSize);
}
// RFC9110 Section-8.8.3: Value of the ETag response must be enclosed in double quotes
snprintf(etag, sizeof(etag), "\"%08" PRIx32 "\"", etagValue);
} else {
etag[0] = '\0';
}
AsyncWebServerResponse *response;
if (not_modified) {
bool notModified = false;
// 1. If the client sent If-None-Match and we have an ETag → compare
if (*etag != '\0' && request->header(T_INM) == etag) {
notModified = true;
}
// 2. Otherwise, if there is no ETag but we have Last-Modified and Last-Modified matches
else if (*etag == '\0' && _last_modified.length() > 0 && request->header(T_IMS) == _last_modified) {
async_ws_log_d("_last_modified: %s", _last_modified.c_str());
notModified = true;
}
if (notModified) {
request->_tempFile.close();
response = new AsyncBasicResponse(304); // Not modified
} else {
@@ -242,20 +264,27 @@ void AsyncStaticWebHandler::handleRequest(AsyncWebServerRequest *request) {
}
if (!response) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
request->abort();
return;
}
response->addHeader(T_ETag, etag.c_str());
if (_last_modified.length()) {
response->addHeader(T_Last_Modified, _last_modified.c_str());
if (!notModified) {
// Set ETag header
if (*etag != '\0') {
response->addHeader(T_ETag, etag, true);
}
// Set Last-Modified header
if (_last_modified.length()) {
response->addHeader(T_Last_Modified, _last_modified.c_str(), true);
}
}
// Set cache control
if (_cache_control.length()) {
response->addHeader(T_Cache_Control, _cache_control.c_str());
response->addHeader(T_Cache_Control, _cache_control.c_str(), false);
} else {
response->addHeader(T_Cache_Control, T_no_cache, false);
}
request->send(response);
@@ -266,47 +295,15 @@ AsyncStaticWebHandler &AsyncStaticWebHandler::setTemplateProcessor(AwsTemplatePr
return *this;
}
void AsyncCallbackWebHandler::setUri(const String &uri) {
_uri = uri;
_isRegex = uri.startsWith("^") && uri.endsWith("$");
void AsyncCallbackWebHandler::setUri(AsyncURIMatcher uri) {
_uri = std::move(uri);
}
bool AsyncCallbackWebHandler::canHandle(AsyncWebServerRequest *request) const {
if (!_onRequest || !request->isHTTP() || !(_method & request->method())) {
return false;
}
#ifdef ASYNCWEBSERVER_REGEX
if (_isRegex) {
std::regex pattern(_uri.c_str());
std::smatch matches;
std::string s(request->url().c_str());
if (std::regex_search(s, matches, pattern)) {
for (size_t i = 1; i < matches.size(); ++i) { // start from 1
request->_addPathParam(matches[i].str().c_str());
}
} else {
return false;
}
} else
#endif
if (_uri.length() && _uri.startsWith("/*.")) {
String uriTemplate = String(_uri);
uriTemplate = uriTemplate.substring(uriTemplate.lastIndexOf("."));
if (!request->url().endsWith(uriTemplate)) {
return false;
}
} else if (_uri.length() && _uri.endsWith("*")) {
String uriTemplate = String(_uri);
uriTemplate = uriTemplate.substring(0, uriTemplate.length() - 1);
if (!request->url().startsWith(uriTemplate)) {
return false;
}
} else if (_uri.length() && (_uri != request->url() && !request->url().startsWith(_uri + "/"))) {
return false;
}
return true;
return _uri.matches(request);
}
void AsyncCallbackWebHandler::handleRequest(AsyncWebServerRequest *request) {

View File

@@ -1,13 +1,21 @@
// 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 "ESPAsyncWebServer.h"
#include "WebAuthentication.h"
#include "WebResponseImpl.h"
#include "literals.h"
#include <cstring>
#include "AsyncWebServerLogging.h"
#define __is_param_char(c) ((c) && ((c) != '{') && ((c) != '[') && ((c) != '&') && ((c) != '='))
#include <algorithm>
#include <cstring>
#include <memory>
#include <utility>
#include "./literals.h"
static inline bool isParamChar(char c) {
return ((c) && ((c) != '{') && ((c) != '[') && ((c) != '&') && ((c) != '='));
}
static void doNotDelete(AsyncWebServerRequest *) {}
@@ -29,72 +37,68 @@ AsyncWebServerRequest::AsyncWebServerRequest(AsyncWebServer *s, AsyncClient *c)
c->onError(
[](void *r, AsyncClient *c, int8_t error) {
(void)c;
// log_e("AsyncWebServerRequest::_onError");
AsyncWebServerRequest *req = (AsyncWebServerRequest *)r;
req->_onError(error);
// async_ws_log_e("AsyncWebServerRequest::_onError");
static_cast<AsyncWebServerRequest *>(r)->_onError(error);
},
this
);
c->onAck(
[](void *r, AsyncClient *c, size_t len, uint32_t time) {
(void)c;
// log_e("AsyncWebServerRequest::_onAck");
AsyncWebServerRequest *req = (AsyncWebServerRequest *)r;
req->_onAck(len, time);
// async_ws_log_e("AsyncWebServerRequest::_onAck");
static_cast<AsyncWebServerRequest *>(r)->_onAck(len, time);
},
this
);
c->onDisconnect(
[](void *r, AsyncClient *c) {
// log_e("AsyncWebServerRequest::_onDisconnect");
AsyncWebServerRequest *req = (AsyncWebServerRequest *)r;
req->_onDisconnect();
delete c;
// async_ws_log_e("AsyncWebServerRequest::_onDisconnect");
static_cast<AsyncWebServerRequest *>(r)->_onDisconnect();
},
this
);
c->onTimeout(
[](void *r, AsyncClient *c, uint32_t time) {
(void)c;
// log_e("AsyncWebServerRequest::_onTimeout");
AsyncWebServerRequest *req = (AsyncWebServerRequest *)r;
req->_onTimeout(time);
// async_ws_log_e("AsyncWebServerRequest::_onTimeout");
static_cast<AsyncWebServerRequest *>(r)->_onTimeout(time);
},
this
);
c->onData(
[](void *r, AsyncClient *c, void *buf, size_t len) {
(void)c;
// log_e("AsyncWebServerRequest::_onData");
AsyncWebServerRequest *req = (AsyncWebServerRequest *)r;
req->_onData(buf, len);
// async_ws_log_e("AsyncWebServerRequest::_onData");
static_cast<AsyncWebServerRequest *>(r)->_onData(buf, len);
},
this
);
c->onPoll(
[](void *r, AsyncClient *c) {
(void)c;
// log_e("AsyncWebServerRequest::_onPoll");
AsyncWebServerRequest *req = (AsyncWebServerRequest *)r;
req->_onPoll();
// async_ws_log_e("AsyncWebServerRequest::_onPoll");
static_cast<AsyncWebServerRequest *>(r)->_onPoll();
},
this
);
}
AsyncWebServerRequest::~AsyncWebServerRequest() {
// log_e("AsyncWebServerRequest::~AsyncWebServerRequest");
if (_client) {
// usually it is _client's disconnect triggers object destruct, but for completeness we define behavior
// if for some reason *this will be destructed while client is still connected
_client->onDisconnect(nullptr);
delete _client;
_client = nullptr;
}
if (_response) {
delete _response;
_response = nullptr;
}
_this.reset();
_headers.clear();
_pathParams.clear();
AsyncWebServerResponse *r = _response;
_response = NULL;
delete r;
if (_tempObject != NULL) {
free(_tempObject);
}
@@ -112,9 +116,7 @@ void AsyncWebServerRequest::_onData(void *buf, size_t len) {
// SSL/TLS handshake detection
#ifndef ASYNC_TCP_SSL_ENABLED
if (_parseState == PARSE_REQ_START && len && ((uint8_t *)buf)[0] == 0x16) { // 0x16 indicates a Handshake message (SSL/TLS).
#ifdef ESP32
log_d("SSL/TLS handshake detected: resetting connection");
#endif
async_ws_log_d("SSL/TLS handshake detected: resetting connection");
_parseState = PARSE_REQ_FAIL;
abort();
return;
@@ -142,9 +144,7 @@ void AsyncWebServerRequest::_onData(void *buf, size_t len) {
char ch = str[len - 1];
str[len - 1] = 0;
if (!_temp.reserve(_temp.length() + len)) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
_parseState = PARSE_REQ_FAIL;
abort();
return;
@@ -183,9 +183,12 @@ void AsyncWebServerRequest::_onData(void *buf, size_t len) {
if (_parsedLength == 0) {
if (_contentType.startsWith(T_app_xform_urlencoded)) {
_isPlainPost = true;
} else if (_contentType == T_text_plain && __is_param_char(((char *)buf)[0])) {
} else if (_contentType == T_text_plain && isParamChar(((char *)buf)[0])) {
size_t i = 0;
while (i < len && __is_param_char(((char *)buf)[i++]));
char ch;
do {
ch = ((char *)buf)[i];
} while (i++ < len && isParamChar(ch));
if (i < len && ((char *)buf)[i - 1] == '=') {
_isPlainPost = true;
}
@@ -219,31 +222,26 @@ void AsyncWebServerRequest::_onData(void *buf, size_t len) {
void AsyncWebServerRequest::_onPoll() {
// os_printf("p\n");
if (_response != NULL && _client != NULL && _client->canSend()) {
if (!_response->_finished()) {
if (_response && _client && _client->canSend()) {
_response->_ack(this, 0, 0);
} else {
AsyncWebServerResponse *r = _response;
_response = NULL;
delete r;
_client->close();
}
}
}
void AsyncWebServerRequest::_onAck(size_t len, uint32_t time) {
// os_printf("a:%u:%u\n", len, time);
if (_response != NULL) {
if (!_response) {
return;
}
if (!_response->_finished()) {
_response->_ack(this, len, time);
} else if (_response->_finished()) {
AsyncWebServerResponse *r = _response;
_response = NULL;
delete r;
_client->close();
// recheck if response has just completed, close connection
if (_response->_finished()) {
_client->close(); // this will trigger _onDisconnect() and object destruction
}
} else {
// this will close responses that were complete via a single _send() call
_client->close(); // this will trigger _onDisconnect() and object destruction
}
}
@@ -269,10 +267,6 @@ void AsyncWebServerRequest::_onDisconnect() {
_server->_handleDisconnect(this);
}
void AsyncWebServerRequest::_addPathParam(const char *p) {
_pathParams.emplace_back(p);
}
void AsyncWebServerRequest::_addGetParams(const String &params) {
size_t start = 0;
while (start < params.length()) {
@@ -526,6 +520,16 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
_itemFilename = nameVal;
_itemIsFile = true;
}
// Add the parameters from the content-disposition header to the param list, flagged as POST and File,
// so that they can be retrieved using getParam(name, isPost=true, isFile=true)
// in the upload handler to correctly handle multiple file uploads within the same request.
// Example: Content-Disposition: form-data; name="fw"; filename="firmware.bin"
// See: https://github.com/ESP32Async/ESPAsyncWebServer/discussions/328
if (_itemIsFile && _itemName.length() && _itemFilename.length()) {
// add new parameters for this content-disposition
_params.emplace_back(T_name, _itemName, true, true);
_params.emplace_back(T_filename, _itemFilename, true, true);
}
}
_temp = emptyString;
} else {
@@ -540,9 +544,7 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
}
_itemBuffer = (uint8_t *)malloc(RESPONSE_STREAM_BUFFER_SIZE);
if (_itemBuffer == NULL) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
_multiParseState = PARSE_ERROR;
abort();
return;
@@ -596,13 +598,15 @@ void AsyncWebServerRequest::_parseMultipartPostByte(uint8_t data, bool last) {
if (!_itemIsFile) {
_params.emplace_back(_itemName, _itemValue, true);
} else {
if (_itemSize) {
if (_handler) {
_handler->handleUpload(this, _itemFilename, _itemSize - _itemBufferIndex, _itemBuffer, _itemBufferIndex, true);
}
_itemBufferIndex = 0;
_params.emplace_back(_itemName, _itemFilename, true, true, _itemSize);
}
// remove previous occurrence(s) of content-disposition parameters for this upload
_params.remove_if([this](const AsyncWebParameter &p) {
return p.isPost() && p.isFile() && (p.name() == T_name || p.name() == T_filename);
});
free(_itemBuffer);
_itemBuffer = NULL;
}
@@ -707,7 +711,7 @@ void AsyncWebServerRequest::_runMiddlewareChain() {
void AsyncWebServerRequest::_send() {
if (!_sent && !_paused) {
// log_d("AsyncWebServerRequest::_send()");
// async_ws_log_d("AsyncWebServerRequest::_send()");
// user did not create a response ?
if (!_response) {
@@ -743,7 +747,7 @@ void AsyncWebServerRequest::abort() {
_sent = true;
_paused = false;
_this.reset();
// log_e("AsyncWebServerRequest::abort");
// async_ws_log_e("AsyncWebServerRequest::abort");
_client->abort();
}
}
@@ -997,15 +1001,13 @@ void AsyncWebServerRequest::requestAuthentication(AsyncAuthType method, const ch
case AsyncAuthType::AUTH_BASIC:
{
String header;
if (header.reserve(strlen(T_BASIC_REALM) + strlen(realm) + 1)) {
if (header.reserve(sizeof(T_BASIC_REALM) - 1 + strlen(realm) + 1)) {
header.concat(T_BASIC_REALM);
header.concat(realm);
header.concat('"');
r->addHeader(T_WWW_AUTH, header.c_str());
} else {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
abort();
}
@@ -1013,7 +1015,7 @@ void AsyncWebServerRequest::requestAuthentication(AsyncAuthType method, const ch
}
case AsyncAuthType::AUTH_DIGEST:
{
size_t len = strlen(T_DIGEST_) + strlen(T_realm__) + strlen(T_auth_nonce) + 32 + strlen(T__opaque) + 32 + 1;
size_t len = sizeof(T_DIGEST_) - 1 + sizeof(T_realm__) - 1 + sizeof(T_auth_nonce) - 1 + 32 + sizeof(T__opaque) - 1 + 32 + 1;
String header;
if (header.reserve(len + strlen(realm))) {
const String nonce = genRandomMD5();
@@ -1029,9 +1031,7 @@ void AsyncWebServerRequest::requestAuthentication(AsyncAuthType method, const ch
header.concat((char)0x22); // '"'
r->addHeader(T_WWW_AUTH, header.c_str());
} else {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
abort();
}
}
@@ -1081,15 +1081,6 @@ const String &AsyncWebServerRequest::argName(size_t i) const {
return getParam(i)->name();
}
const String &AsyncWebServerRequest::pathArg(size_t i) const {
if (i >= _pathParams.size()) {
return emptyString;
}
auto it = _pathParams.begin();
std::advance(it, i);
return *it;
}
const String &AsyncWebServerRequest::header(const char *name) const {
const AsyncWebHeader *h = getHeader(name);
return h ? h->value() : emptyString;
@@ -1118,9 +1109,7 @@ String AsyncWebServerRequest::urlDecode(const String &text) const {
String decoded;
// Allocate the string internal buffer - never longer from source text
if (!decoded.reserve(len)) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
return emptyString;
}
while (i < len) {
@@ -1183,3 +1172,9 @@ bool AsyncWebServerRequest::isExpectedRequestedConnType(RequestedConnectionType
return ((erct1 != RCT_NOT_USED) && (erct1 == _reqconntype)) || ((erct2 != RCT_NOT_USED) && (erct2 == _reqconntype))
|| ((erct3 != RCT_NOT_USED) && (erct3 == _reqconntype));
}
AsyncClient *AsyncWebServerRequest::clientRelease() {
AsyncClient *c = _client;
_client = nullptr;
return c;
}

View File

@@ -1,34 +1,64 @@
// 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
#ifndef ASYNCWEBSERVERRESPONSEIMPL_H_
#define ASYNCWEBSERVERRESPONSEIMPL_H_
#pragma once
#ifdef Arduino_h
// arduino is not compatible with std::vector
#undef min
#undef max
#endif
#include "literals.h"
#include <cbuf.h>
#include <memory>
#include <vector>
#include "./literals.h"
#ifndef CONFIG_LWIP_TCP_MSS
#ifdef TCP_MSS // ESP8266
#define CONFIG_LWIP_TCP_MSS TCP_MSS
#else
// as it is defined for ESP32's Arduino LWIP
#define CONFIG_LWIP_TCP_MSS 1436
#endif
#endif
#define ASYNC_RESPONCE_BUFF_SIZE CONFIG_LWIP_TCP_MSS * 2
// It is possible to restore these defines, but one can use _min and _max instead. Or std::min, std::max.
class AsyncBasicResponse : public AsyncWebServerResponse {
private:
String _content;
// buffer to accumulate all response headers
String _assembled_headers;
// amount of headers buffer writtent to sockbuff
size_t _writtenHeadersLength{0};
public:
explicit AsyncBasicResponse(int code, const char *contentType = asyncsrv::empty, const char *content = asyncsrv::empty);
AsyncBasicResponse(int code, const String &contentType, const String &content = emptyString)
: AsyncBasicResponse(code, contentType.c_str(), content.c_str()) {}
void _respond(AsyncWebServerRequest *request) override final;
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override final;
bool _sourceValid() const override final {
void _respond(AsyncWebServerRequest *request) final;
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) final {
return write_send_buffs(request, len, time);
};
bool _sourceValid() const final {
return true;
}
protected:
/**
* @brief write next portion of response data to send buffs
* this method (re)fills tcp send buffers, it could be called either at will
* or from a tcp_recv/tcp_poll callbacks from AsyncTCP
*
* @param request - used to access client object
* @param len - size of acknowledged data from the remote side (TCP window update, not TCP ack!)
* @param time - time passed between last sent and received packet
* @return size_t amount of response data placed to TCP send buffs for delivery (defined by sdkconfig value CONFIG_LWIP_TCP_SND_BUF_DEFAULT)
*/
size_t write_send_buffs(AsyncWebServerRequest *request, size_t len, uint32_t time);
};
class AsyncAbstractResponse : public AsyncWebServerResponse {
@@ -39,23 +69,43 @@ private:
// in-flight queue credits
size_t _in_flight_credit{2};
#endif
String _head;
// buffer to accumulate all response headers
String _assembled_headers;
// amount of headers buffer writtent to sockbuff
size_t _writtenHeadersLength{0};
// Data is inserted into cache at begin().
// This is inefficient with vector, but if we use some other container,
// we won't be able to access it as contiguous array of bytes when reading from it,
// so by gaining performance in one place, we'll lose it in another.
std::vector<uint8_t> _cache;
// intermediate buffer to copy outbound data to, also it will keep pending data between _send calls
std::unique_ptr<std::array<uint8_t, ASYNC_RESPONCE_BUFF_SIZE> > _send_buffer;
// buffer data size specifiers
size_t _send_buffer_offset{0}, _send_buffer_len{0};
size_t _readDataFromCacheOrContent(uint8_t *data, const size_t len);
size_t _fillBufferAndProcessTemplates(uint8_t *buf, size_t maxLen);
protected:
AwsTemplateProcessor _callback;
/**
* @brief write next portion of response data to send buffs
* this method (re)fills tcp send buffers, it could be called either at will
* or from a tcp_recv/tcp_poll callbacks from AsyncTCP
*
* @param request - used to access client object
* @param len - size of acknowledged data from the remote side (TCP window update, not TCP ack!)
* @param time - time passed between last sent and received packet
* @return size_t amount of response data placed to TCP send buffs for delivery (defined by sdkconfig value CONFIG_LWIP_TCP_SND_BUF_DEFAULT)
*/
size_t write_send_buffs(AsyncWebServerRequest *request, size_t len, uint32_t time);
public:
AsyncAbstractResponse(AwsTemplateProcessor callback = nullptr);
virtual ~AsyncAbstractResponse() {}
void _respond(AsyncWebServerRequest *request) override final;
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) override final;
void _respond(AsyncWebServerRequest *request) final;
size_t _ack(AsyncWebServerRequest *request, size_t len, uint32_t time) final {
return write_send_buffs(request, len, time);
};
virtual bool _sourceValid() const {
return false;
}
@@ -89,10 +139,10 @@ public:
~AsyncFileResponse() {
_content.close();
}
bool _sourceValid() const override final {
bool _sourceValid() const final {
return !!(_content);
}
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
size_t _fillBuffer(uint8_t *buf, size_t maxLen) final;
};
class AsyncStreamResponse : public AsyncAbstractResponse {
@@ -103,10 +153,10 @@ public:
AsyncStreamResponse(Stream &stream, const char *contentType, size_t len, AwsTemplateProcessor callback = nullptr);
AsyncStreamResponse(Stream &stream, const String &contentType, size_t len, AwsTemplateProcessor callback = nullptr)
: AsyncStreamResponse(stream, contentType.c_str(), len, callback) {}
bool _sourceValid() const override final {
bool _sourceValid() const final {
return !!(_content);
}
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
size_t _fillBuffer(uint8_t *buf, size_t maxLen) final;
};
class AsyncCallbackResponse : public AsyncAbstractResponse {
@@ -118,10 +168,10 @@ public:
AsyncCallbackResponse(const char *contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr);
AsyncCallbackResponse(const String &contentType, size_t len, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr)
: AsyncCallbackResponse(contentType.c_str(), len, callback, templateCallback) {}
bool _sourceValid() const override final {
bool _sourceValid() const final {
return !!(_content);
}
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
size_t _fillBuffer(uint8_t *buf, size_t maxLen) final;
};
class AsyncChunkedResponse : public AsyncAbstractResponse {
@@ -133,25 +183,26 @@ public:
AsyncChunkedResponse(const char *contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr);
AsyncChunkedResponse(const String &contentType, AwsResponseFiller callback, AwsTemplateProcessor templateCallback = nullptr)
: AsyncChunkedResponse(contentType.c_str(), callback, templateCallback) {}
bool _sourceValid() const override final {
bool _sourceValid() const final {
return !!(_content);
}
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
size_t _fillBuffer(uint8_t *buf, size_t maxLen) final;
};
class AsyncProgmemResponse : public AsyncAbstractResponse {
private:
const uint8_t *_content;
size_t _readLength;
// offset index (how much we've sent already)
size_t _index;
public:
AsyncProgmemResponse(int code, const char *contentType, const uint8_t *content, size_t len, AwsTemplateProcessor callback = nullptr);
AsyncProgmemResponse(int code, const String &contentType, const uint8_t *content, size_t len, AwsTemplateProcessor callback = nullptr)
: AsyncProgmemResponse(code, contentType.c_str(), content, len, callback) {}
bool _sourceValid() const override final {
bool _sourceValid() const final {
return true;
}
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
size_t _fillBuffer(uint8_t *buf, size_t maxLen) final;
};
class AsyncResponseStream : public AsyncAbstractResponse, public Print {
@@ -161,10 +212,10 @@ private:
public:
AsyncResponseStream(const char *contentType, size_t bufferSize);
AsyncResponseStream(const String &contentType, size_t bufferSize) : AsyncResponseStream(contentType.c_str(), bufferSize) {}
bool _sourceValid() const override final {
bool _sourceValid() const final {
return (_state < RESPONSE_END);
}
size_t _fillBuffer(uint8_t *buf, size_t maxLen) override final;
size_t _fillBuffer(uint8_t *buf, size_t maxLen) final;
size_t write(const uint8_t *data, size_t len);
size_t write(uint8_t data);
/**
@@ -175,5 +226,3 @@ public:
}
using Print::write;
};
#endif /* ASYNCWEBSERVERRESPONSEIMPL_H_ */

View File

@@ -1,8 +1,22 @@
// 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 "ESPAsyncWebServer.h"
#include "WebResponseImpl.h"
#include "AsyncWebServerLogging.h"
#include <algorithm>
#include <memory>
#include <utility>
#ifndef CONFIG_LWIP_TCP_WND_DEFAULT
#ifdef TCP_WND // ESP8266
#define CONFIG_LWIP_TCP_WND_DEFAULT TCP_WND
#else
// as it is defined for esp32's LWIP
#define CONFIG_LWIP_TCP_WND_DEFAULT 5760
#endif
#endif
using namespace asyncsrv;
@@ -11,50 +25,50 @@ using namespace asyncsrv;
*
*/
const char *AsyncWebServerResponse::responseCodeToString(int code) {
STR_RETURN_TYPE AsyncWebServerResponse::responseCodeToString(int code) {
switch (code) {
case 100: return T_HTTP_CODE_100;
case 101: return T_HTTP_CODE_101;
case 200: return T_HTTP_CODE_200;
case 201: return T_HTTP_CODE_201;
case 202: return T_HTTP_CODE_202;
case 203: return T_HTTP_CODE_203;
case 204: return T_HTTP_CODE_204;
case 205: return T_HTTP_CODE_205;
case 206: return T_HTTP_CODE_206;
case 300: return T_HTTP_CODE_300;
case 301: return T_HTTP_CODE_301;
case 302: return T_HTTP_CODE_302;
case 303: return T_HTTP_CODE_303;
case 304: return T_HTTP_CODE_304;
case 305: return T_HTTP_CODE_305;
case 307: return T_HTTP_CODE_307;
case 400: return T_HTTP_CODE_400;
case 401: return T_HTTP_CODE_401;
case 402: return T_HTTP_CODE_402;
case 403: return T_HTTP_CODE_403;
case 404: return T_HTTP_CODE_404;
case 405: return T_HTTP_CODE_405;
case 406: return T_HTTP_CODE_406;
case 407: return T_HTTP_CODE_407;
case 408: return T_HTTP_CODE_408;
case 409: return T_HTTP_CODE_409;
case 410: return T_HTTP_CODE_410;
case 411: return T_HTTP_CODE_411;
case 412: return T_HTTP_CODE_412;
case 413: return T_HTTP_CODE_413;
case 414: return T_HTTP_CODE_414;
case 415: return T_HTTP_CODE_415;
case 416: return T_HTTP_CODE_416;
case 417: return T_HTTP_CODE_417;
case 429: return T_HTTP_CODE_429;
case 500: return T_HTTP_CODE_500;
case 501: return T_HTTP_CODE_501;
case 502: return T_HTTP_CODE_502;
case 503: return T_HTTP_CODE_503;
case 504: return T_HTTP_CODE_504;
case 505: return T_HTTP_CODE_505;
default: return T_HTTP_CODE_ANY;
case 100: return STR(T_HTTP_CODE_100);
case 101: return STR(T_HTTP_CODE_101);
case 200: return STR(T_HTTP_CODE_200);
case 201: return STR(T_HTTP_CODE_201);
case 202: return STR(T_HTTP_CODE_202);
case 203: return STR(T_HTTP_CODE_203);
case 204: return STR(T_HTTP_CODE_204);
case 205: return STR(T_HTTP_CODE_205);
case 206: return STR(T_HTTP_CODE_206);
case 300: return STR(T_HTTP_CODE_300);
case 301: return STR(T_HTTP_CODE_301);
case 302: return STR(T_HTTP_CODE_302);
case 303: return STR(T_HTTP_CODE_303);
case 304: return STR(T_HTTP_CODE_304);
case 305: return STR(T_HTTP_CODE_305);
case 307: return STR(T_HTTP_CODE_307);
case 400: return STR(T_HTTP_CODE_400);
case 401: return STR(T_HTTP_CODE_401);
case 402: return STR(T_HTTP_CODE_402);
case 403: return STR(T_HTTP_CODE_403);
case 404: return STR(T_HTTP_CODE_404);
case 405: return STR(T_HTTP_CODE_405);
case 406: return STR(T_HTTP_CODE_406);
case 407: return STR(T_HTTP_CODE_407);
case 408: return STR(T_HTTP_CODE_408);
case 409: return STR(T_HTTP_CODE_409);
case 410: return STR(T_HTTP_CODE_410);
case 411: return STR(T_HTTP_CODE_411);
case 412: return STR(T_HTTP_CODE_412);
case 413: return STR(T_HTTP_CODE_413);
case 414: return STR(T_HTTP_CODE_414);
case 415: return STR(T_HTTP_CODE_415);
case 416: return STR(T_HTTP_CODE_416);
case 417: return STR(T_HTTP_CODE_417);
case 429: return STR(T_HTTP_CODE_429);
case 500: return STR(T_HTTP_CODE_500);
case 501: return STR(T_HTTP_CODE_501);
case 502: return STR(T_HTTP_CODE_502);
case 503: return STR(T_HTTP_CODE_503);
case 504: return STR(T_HTTP_CODE_504);
case 505: return STR(T_HTTP_CODE_505);
default: return STR(T_HTTP_CODE_ANY);
}
}
@@ -237,13 +251,6 @@ bool AsyncWebServerResponse::_sourceValid() const {
}
void AsyncWebServerResponse::_respond(AsyncWebServerRequest *request) {
_state = RESPONSE_END;
request->client()->close();
}
size_t AsyncWebServerResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) {
(void)request;
(void)len;
(void)time;
return 0;
}
/*
@@ -264,70 +271,61 @@ AsyncBasicResponse::AsyncBasicResponse(int code, const char *contentType, const
void AsyncBasicResponse::_respond(AsyncWebServerRequest *request) {
_state = RESPONSE_HEADERS;
String out;
_assembleHead(out, request->version());
size_t outLen = out.length();
size_t space = request->client()->space();
if (!_contentLength && space >= outLen) {
_writtenLength += request->client()->write(out.c_str(), outLen);
_state = RESPONSE_WAIT_ACK;
} else if (_contentLength && space >= outLen + _contentLength) {
out += _content;
outLen += _contentLength;
_writtenLength += request->client()->write(out.c_str(), outLen);
_state = RESPONSE_WAIT_ACK;
} else if (space && space < outLen) {
String partial = out.substring(0, space);
_content = out.substring(space) + _content;
_contentLength += outLen - space;
_writtenLength += request->client()->write(partial.c_str(), partial.length());
_state = RESPONSE_CONTENT;
} else if (space > outLen && space < (outLen + _contentLength)) {
size_t shift = space - outLen;
outLen += shift;
_sentLength += shift;
out += _content.substring(0, shift);
_content = _content.substring(shift);
_writtenLength += request->client()->write(out.c_str(), outLen);
_state = RESPONSE_CONTENT;
} else {
_content = out + _content;
_contentLength += outLen;
_state = RESPONSE_CONTENT;
}
_assembleHead(_assembled_headers, request->version());
write_send_buffs(request, 0, 0);
}
size_t AsyncBasicResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) {
size_t AsyncBasicResponse::write_send_buffs(AsyncWebServerRequest *request, size_t len, uint32_t time) {
(void)time;
// this is not functionally needed in AsyncBasicResponse itself, but kept for compatibility if some of the derived classes are rely on it somehow
_ackedLength += len;
if (_state == RESPONSE_CONTENT) {
size_t available = _contentLength - _sentLength;
size_t space = request->client()->space();
// we can fit in this packet
if (space > available) {
_writtenLength += request->client()->write(_content.c_str(), available);
_content = emptyString;
_state = RESPONSE_WAIT_ACK;
return available;
size_t payloadlen{0}; // amount of data to be written to tcp sockbuff during this call, used as return value of this method
// send http headers first
if (_state == RESPONSE_HEADERS) {
// copy headers buffer to sock buffer
size_t const pcb_written = request->client()->add(_assembled_headers.c_str() + _writtenHeadersLength, _assembled_headers.length() - _writtenHeadersLength);
_writtenLength += pcb_written;
_writtenHeadersLength += pcb_written;
if (_writtenHeadersLength < _assembled_headers.length()) {
// we were not able to fit all headers in current buff, send this part here and return later for the rest
if (!request->client()->send()) {
// something is wrong, what should we do here?
request->client()->close();
return 0;
}
// send some data, the rest on ack
String out = _content.substring(0, space);
_content = _content.substring(space);
_sentLength += space;
_writtenLength += request->client()->write(out.c_str(), space);
return space;
} else if (_state == RESPONSE_WAIT_ACK) {
if (_ackedLength >= _writtenLength) {
return pcb_written;
}
// otherwise we've added all the (remainder) headers in current buff, go on with content
_state = RESPONSE_CONTENT;
payloadlen += pcb_written;
_assembled_headers = String(); // clear
}
if (_state == RESPONSE_CONTENT) {
size_t const pcb_written = request->client()->write(_content.c_str() + _sentLength, _content.length() - _sentLength);
_writtenLength += pcb_written; // total written data (hdrs + body)
_sentLength += pcb_written; // body written data
payloadlen += pcb_written; // data writtent in current buff
if (_sentLength >= _content.length()) {
// we've just sent all the (remainder) data in current buff, complete the response
_state = RESPONSE_END;
}
}
return 0;
// implicit complete
if (_state == RESPONSE_WAIT_ACK) {
_state = RESPONSE_END;
}
return payloadlen;
}
/*
* Abstract Response
* */
*
*/
AsyncAbstractResponse::AsyncAbstractResponse(AwsTemplateProcessor callback) : _callback(callback) {
// In case of template processing, we're unable to determine real response size
if (callback) {
@@ -339,12 +337,12 @@ AsyncAbstractResponse::AsyncAbstractResponse(AwsTemplateProcessor callback) : _c
void AsyncAbstractResponse::_respond(AsyncWebServerRequest *request) {
addHeader(T_Connection, T_close, false);
_assembleHead(_head, request->version());
_assembleHead(_assembled_headers, request->version());
_state = RESPONSE_HEADERS;
_ack(request, 0, 0);
write_send_buffs(request, 0, 0);
}
size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, uint32_t time) {
size_t AsyncAbstractResponse::write_send_buffs(AsyncWebServerRequest *request, size_t len, uint32_t time) {
(void)time;
if (!_sourceValid()) {
_state = RESPONSE_FAILED;
@@ -353,142 +351,168 @@ size_t AsyncAbstractResponse::_ack(AsyncWebServerRequest *request, size_t len, u
}
#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT
/*
for response payloads with unknown length or length larger than TCP_WND we need to control AsyncTCP's queue and in-flight fragmentation.
Either user callback could fill buffer with very small chunks or long running large response could receive a lot of poll() calls here,
both could flood asynctcp's queue with large number of events to handle and fragment socket buffer space for large responses.
Let's ignore polled acks and acks in case when available window size is less than our used buffer size since we won't be able to fill and send it whole
That way we could balance on having at least half tcp win in-flight while minimizing send/ack events in asynctcp Q
This could decrease sustained bandwidth for one single connection but would drastically improve parallelism and equalize bandwidth sharing
*/
// return a credit for each chunk of acked data (polls does not give any credits)
if (len) {
++_in_flight_credit;
_in_flight -= std::min(len, _in_flight);
}
// for chunked responses ignore acks if there are no _in_flight_credits left
if (_chunked && !_in_flight_credit) {
#ifdef ESP32
log_d("(chunk) out of in-flight credits");
#endif
return 0;
}
_in_flight -= (_in_flight > len) ? len : _in_flight;
// get the size of available sock space
#endif
_ackedLength += len;
size_t space = request->client()->space();
size_t headLen = _head.length();
if (_state == RESPONSE_HEADERS) {
if (space >= headLen) {
_state = RESPONSE_CONTENT;
space -= headLen;
} else {
String out = _head.substring(0, space);
_head = _head.substring(space);
_writtenLength += request->client()->write(out.c_str(), out.length());
#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT
_in_flight += out.length();
--_in_flight_credit; // take a credit
#endif
return out.length();
}
}
if (_state == RESPONSE_CONTENT) {
#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT
// for response data we need to control the queue and in-flight fragmentation. Sending small chunks could give low latency,
// but flood asynctcp's queue and fragment socket buffer space for large responses.
// Let's ignore polled acks and acks in case when we have more in-flight data then the available socket buff space.
// That way we could balance on having half the buffer in-flight while another half is filling up, while minimizing events in asynctcp q
if (_in_flight > space) {
// log_d("defer user call %u/%u", _in_flight, space);
// take the credit back since we are ignoring this ack and rely on other inflight data
if (_chunked || !_sendContentLength || (_sentLength > CONFIG_LWIP_TCP_WND_DEFAULT)) {
if (!_in_flight_credit || (ASYNC_RESPONCE_BUFF_SIZE > request->client()->space())) {
// async_ws_log_d("defer user call in_flight:%u, tcpwin:%u", _in_flight, request->client()->space());
// take the credit back since we are ignoring this ack and rely on other inflight data acks
if (len) {
--_in_flight_credit;
}
return 0;
}
}
#endif
size_t outLen;
if (_chunked) {
if (space <= 8) {
// this is not functionally needed in AsyncAbstractResponse itself, but kept for compatibility if some of the derived classes are rely on it somehow
_ackedLength += len;
size_t payloadlen{0}; // amount of data to be written to tcp sockbuff during this call, used as return value of this method
// send http headers first
if (_state == RESPONSE_HEADERS) {
// copy headers buffer to sock buffer
size_t const pcb_written = request->client()->add(_assembled_headers.c_str() + _writtenHeadersLength, _assembled_headers.length() - _writtenHeadersLength);
_writtenLength += pcb_written;
_writtenHeadersLength += pcb_written;
if (_writtenHeadersLength < _assembled_headers.length()) {
// we were not able to fit all headers in current buff, send this part here and return later for the rest
#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT
_in_flight += pcb_written;
--_in_flight_credit; // take a credit
#endif
if (!request->client()->send()) {
// something is wrong, what should we do here?
request->client()->close();
return 0;
}
return pcb_written;
}
// otherwise we've added all the (remainder) headers in current buff
_state = RESPONSE_CONTENT;
payloadlen += pcb_written;
_assembled_headers = String(); // clear
}
outLen = space;
} else if (!_sendContentLength) {
outLen = space;
// send content body
if (_state == RESPONSE_CONTENT) {
do {
if (_send_buffer_len && _send_buffer) {
// data is pending in buffer from a previous call or previous iteration
size_t const added_len =
request->client()->add(reinterpret_cast<char *>(_send_buffer->data() + _send_buffer_offset), _send_buffer_len - _send_buffer_offset);
if (added_len != _send_buffer_len - _send_buffer_offset) {
// we were not able to add entire buffer's content to tcp buffs, leave it for later
// (this should not happen normally unless connection's TCP window suddenly changed from remote or mem pressure)
_send_buffer_offset += added_len;
break;
} else {
outLen = ((_contentLength - _sentLength) > space) ? space : (_contentLength - _sentLength);
_send_buffer_len = _send_buffer_offset = 0; // consider buffer empty
}
payloadlen += added_len;
}
uint8_t *buf = (uint8_t *)malloc(outLen + headLen);
if (!buf) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
request->abort();
return 0;
auto tcp_win = request->client()->space();
if (tcp_win == 0 || _state == RESPONSE_END) {
break; // no room left or no more data
}
if (headLen) {
memcpy(buf, _head.c_str(), _head.length());
if ((_chunked || !_sendContentLength) && (tcp_win < CONFIG_LWIP_TCP_MSS / 2)) {
// available window size is not enough to send a new chunk sized half of tcp mss, let's wait for better chance and reduce pressure to AsyncTCP's event Q
break;
}
size_t readLen = 0;
if (!_send_buffer) {
auto p = new (std::nothrow) std::array<uint8_t, ASYNC_RESPONCE_BUFF_SIZE>;
if (p) {
_send_buffer.reset(p);
_send_buffer_len = _send_buffer_offset = 0;
} else {
break; // OOM
}
}
if (_chunked) {
// HTTP 1.1 allows leading zeros in chunk length. Or spaces may be added.
// See RFC2616 sections 2, 3.6.1.
readLen = _fillBufferAndProcessTemplates(buf + headLen + 6, outLen - 8);
if (readLen == RESPONSE_TRY_AGAIN) {
free(buf);
return 0;
// See https://datatracker.ietf.org/doc/html/rfc9112#section-7.1
size_t const readLen =
_fillBufferAndProcessTemplates(_send_buffer->data() + 6, std::min(_send_buffer->size(), tcp_win) - 8); // reserve 8 bytes for chunk size data
if (readLen != RESPONSE_TRY_AGAIN) {
// Write 4 hex digits directly without null terminator
static constexpr char hexChars[] = "0123456789abcdef";
_send_buffer->data()[0] = hexChars[(readLen >> 12) & 0xF];
_send_buffer->data()[1] = hexChars[(readLen >> 8) & 0xF];
_send_buffer->data()[2] = hexChars[(readLen >> 4) & 0xF];
_send_buffer->data()[3] = hexChars[readLen & 0xF];
_send_buffer->data()[4] = '\r';
_send_buffer->data()[5] = '\n';
// data (readLen bytes) is already there
_send_buffer->at(readLen + 6) = '\r';
_send_buffer->at(readLen + 7) = '\n';
_send_buffer_len += readLen + 8; // set buffers's size to match added data
_sentLength += readLen; // data is not sent yet, but we won't get a chance to count this later properly for chunked data
if (!readLen) {
// last chunk?
_state = RESPONSE_END;
}
}
outLen = sprintf((char *)buf + headLen, "%04x", readLen) + headLen;
buf[outLen++] = '\r';
buf[outLen++] = '\n';
outLen += readLen;
buf[outLen++] = '\r';
buf[outLen++] = '\n';
} else {
readLen = _fillBufferAndProcessTemplates(buf + headLen, outLen);
if (readLen == RESPONSE_TRY_AGAIN) {
free(buf);
return 0;
}
outLen = readLen + headLen;
// Non-chunked data. We can either have a response:
// - with a known content-length (example: Json response), in that case we pass the remaining length if lower than tcp_win
// - or with unknown content-length (see LargeResponse example, like ESP32Cam with streaming), in that case we just fill as much as tcp_win allows
size_t maxLen = std::min(_send_buffer->size(), tcp_win);
if (_contentLength) {
maxLen = _contentLength > _sentLength ? std::min(maxLen, _contentLength - _sentLength) : 0;
}
if (headLen) {
_head = emptyString;
}
size_t const readLen = _fillBufferAndProcessTemplates(_send_buffer->data(), maxLen);
if (outLen) {
_writtenLength += request->client()->write((const char *)buf, outLen);
if (readLen == 0) {
// no more data to send
_state = RESPONSE_END;
} else if (readLen != RESPONSE_TRY_AGAIN) {
_send_buffer_len += readLen; // set buffers's size to match added data
_sentLength += readLen; // data is not sent yet, but we need it to understand that it would be last block
if (_sendContentLength && (_sentLength == _contentLength)) {
// it was last piece of content
_state = RESPONSE_END;
}
}
}
} while (_send_buffer_len); // go on till we have something in buffer pending to send
// execute sending whatever we have in sock buffs now
request->client()->send();
_writtenLength += payloadlen;
#if ASYNCWEBSERVER_USE_CHUNK_INFLIGHT
_in_flight += outLen;
_in_flight += payloadlen;
--_in_flight_credit; // take a credit
#endif
if (_send_buffer_len == 0) {
// buffer empty, we can release mem, otherwise need to keep it till next run (should not happen under normal conditions)
_send_buffer.reset();
}
return payloadlen;
} // (_state == RESPONSE_CONTENT)
if (_chunked) {
_sentLength += readLen;
} else {
_sentLength += outLen - headLen;
}
free(buf);
if ((_chunked && readLen == 0) || (!_sendContentLength && outLen == 0) || (!_chunked && _sentLength == _contentLength)) {
_state = RESPONSE_WAIT_ACK;
}
return outLen;
} else if (_state == RESPONSE_WAIT_ACK) {
if (!_sendContentLength || _ackedLength >= _writtenLength) {
// implicit check
if (_state == RESPONSE_WAIT_ACK) {
// we do not need to wait for any acks actually if we won't send any more data,
// connection would be closed gracefully with last piece of data (in AsyncWebServerRequest::_onAck)
_state = RESPONSE_END;
if (!_chunked && !_sendContentLength) {
request->client()->close(true);
}
}
}
return 0;
}
@@ -516,8 +540,8 @@ size_t AsyncAbstractResponse::_fillBufferAndProcessTemplates(uint8_t *data, size
// Now we've read 'len' bytes, either from cache or from file
// Search for template placeholders
uint8_t *pTemplateStart = data;
while ((pTemplateStart < &data[len]) && (pTemplateStart = (uint8_t *)memchr(pTemplateStart, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart + 1))
) { // data[0] ... data[len - 1]
while ((pTemplateStart < &data[len]) && (pTemplateStart = (uint8_t *)memchr(pTemplateStart, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart + 1))) {
// data[0] ... data[len - 1]
uint8_t *pTemplateEnd =
(pTemplateStart < &data[len - 1]) ? (uint8_t *)memchr(pTemplateStart + 1, TEMPLATE_PLACEHOLDER, &data[len - 1] - pTemplateStart) : nullptr;
// temporary buffer to hold parameter name
@@ -639,8 +663,8 @@ void AsyncFileResponse::_setContentTypeFromPath(const String &path) {
_contentType = T_text_html;
} else if (strcmp(dot, T__css) == 0) {
_contentType = T_text_css;
} else if (strcmp(dot, T__js) == 0) {
_contentType = T_application_javascript;
} else if (strcmp(dot, T__js) == 0 || strcmp(dot, T__mjs) == 0) {
_contentType = T_text_javascript;
} else if (strcmp(dot, T__json) == 0) {
_contentType = T_application_json;
} else if (strcmp(dot, T__png) == 0) {
@@ -699,29 +723,23 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con
// Try to open the uncompressed version first
_content = fs.open(path, fs::FileOpenMode::read);
if (_content.available()) {
_contentLength = _content.size();
} else {
// Try to open the compressed version (.gz)
if (!_content.available()) {
// If not available try to open the compressed version (.gz)
String gzPath;
uint16_t pathLen = path.length();
gzPath.reserve(pathLen + 3);
gzPath.concat(path);
gzPath.concat(asyncsrv::T__gz);
_content = fs.open(gzPath, fs::FileOpenMode::read);
_contentLength = _content.size();
if (_content.seek(_contentLength - 8)) {
char serverETag[11];
if (AsyncWebServerRequest::_getEtag(_content, serverETag)) {
addHeader(T_Content_Encoding, T_gzip, false);
_callback = nullptr; // Unable to process zipped templates
_sendContentLength = true;
_chunked = false;
// Add ETag and cache headers
uint8_t crcInTrailer[4];
_content.read(crcInTrailer, sizeof(crcInTrailer));
char serverETag[9];
AsyncWebServerRequest::_getEtag(crcInTrailer, serverETag);
addHeader(T_ETag, serverETag, true);
addHeader(T_Cache_Control, T_no_cache, true);
@@ -733,6 +751,8 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con
}
}
_contentLength = _content.size();
if (*contentType == '\0') {
_setContentTypeFromPath(path);
} else {
@@ -742,9 +762,12 @@ AsyncFileResponse::AsyncFileResponse(FS &fs, const String &path, const char *con
if (download) {
// Extract filename from path and set as download attachment
int filenameStart = path.lastIndexOf('/') + 1;
char buf[26 + path.length() - filenameStart];
char *filename = (char *)path.c_str() + filenameStart;
snprintf(buf, sizeof(buf), T_attachment, filename);
const char *filename = path.c_str() + filenameStart;
String buf;
buf.reserve(sizeof(T_attachment) - 1 + strlen(filename) + 2);
buf = T_attachment;
buf += filename;
buf += "\"";
addHeader(T_Content_Disposition, buf, false);
} else {
// Serve file inline (display in browser)
@@ -768,22 +791,26 @@ AsyncFileResponse::AsyncFileResponse(File content, const String &path, const cha
_content = content;
_contentLength = _content.size();
if (strlen(contentType) == 0) {
if (*contentType == '\0') {
_setContentTypeFromPath(path);
} else {
_contentType = contentType;
}
int filenameStart = path.lastIndexOf('/') + 1;
char buf[26 + path.length() - filenameStart];
char *filename = (char *)path.c_str() + filenameStart;
if (download) {
snprintf_P(buf, sizeof(buf), PSTR("attachment; filename=\"%s\""), filename);
} else {
snprintf_P(buf, sizeof(buf), PSTR("inline"));
}
// Extract filename from path and set as download attachment
int filenameStart = path.lastIndexOf('/') + 1;
const char *filename = path.c_str() + filenameStart;
String buf;
buf.reserve(sizeof(T_attachment) - 1 + strlen(filename) + 2);
buf = T_attachment;
buf += filename;
buf += "\"";
addHeader(T_Content_Disposition, buf, false);
} else {
// Serve file inline (display in browser)
addHeader(T_Content_Disposition, T_inline, false);
}
}
size_t AsyncFileResponse::_fillBuffer(uint8_t *data, size_t len) {
@@ -863,24 +890,17 @@ size_t AsyncChunkedResponse::_fillBuffer(uint8_t *data, size_t len) {
* */
AsyncProgmemResponse::AsyncProgmemResponse(int code, const char *contentType, const uint8_t *content, size_t len, AwsTemplateProcessor callback)
: AsyncAbstractResponse(callback) {
: AsyncAbstractResponse(callback), _content(content), _index(0) {
_code = code;
_content = content;
_contentType = contentType;
_contentLength = len;
_readLength = 0;
}
size_t AsyncProgmemResponse::_fillBuffer(uint8_t *data, size_t len) {
size_t left = _contentLength - _readLength;
if (left > len) {
memcpy_P(data, _content + _readLength, len);
_readLength += len;
return len;
}
memcpy_P(data, _content + _readLength, left);
_readLength += left;
return left;
size_t read_size = std::min(len, _contentLength - _index);
memcpy_P(data, _content + _index, read_size);
_index += read_size;
return read_size;
}
/*
@@ -894,9 +914,7 @@ AsyncResponseStream::AsyncResponseStream(const char *contentType, size_t bufferS
// internal buffer will be null on allocation failure
_content = std::unique_ptr<cbuf>(new cbuf(bufferSize));
if (bufferSize && _content->size() < bufferSize) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
}
}
@@ -915,9 +933,7 @@ size_t AsyncResponseStream::write(const uint8_t *data, size_t len) {
// with _content->write: if len is more than the available size in the buffer, only
// the available size will be written
if (len > _content->room()) {
#ifdef ESP32
log_e("Failed to allocate");
#endif
async_ws_log_e("Failed to allocate");
}
}
size_t written = _content->write((const char *)data, len);

View File

@@ -1,9 +1,12 @@
// 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 "ESPAsyncWebServer.h"
#include "WebHandlerImpl.h"
#include <string>
#include <utility>
#if defined(ESP32) || defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) || defined(LIBRETINY)
#include <WiFi.h>
#elif defined(ESP8266)
@@ -15,7 +18,7 @@
using namespace asyncsrv;
bool ON_STA_FILTER(AsyncWebServerRequest *request) {
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
return WiFi.localIP() == request->client()->localIP();
#else
return false;
@@ -23,7 +26,7 @@ bool ON_STA_FILTER(AsyncWebServerRequest *request) {
}
bool ON_AP_FILTER(AsyncWebServerRequest *request) {
#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI
#if ASYNCWEBSERVER_WIFI_SUPPORTED
return WiFi.localIP() != request->client()->localIP();
#else
return false;
@@ -151,10 +154,10 @@ void AsyncWebServer::_attachHandler(AsyncWebServerRequest *request) {
}
AsyncCallbackWebHandler &AsyncWebServer::on(
const char *uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody
AsyncURIMatcher uri, WebRequestMethodComposite method, ArRequestHandlerFunction onRequest, ArUploadHandlerFunction onUpload, ArBodyHandlerFunction onBody
) {
AsyncCallbackWebHandler *handler = new AsyncCallbackWebHandler();
handler->setUri(uri);
handler->setUri(std::move(uri));
handler->setMethod(method);
handler->onRequest(onRequest);
handler->onUpload(onUpload);
@@ -163,6 +166,15 @@ AsyncCallbackWebHandler &AsyncWebServer::on(
return *handler;
}
#if ASYNC_JSON_SUPPORT == 1
AsyncCallbackJsonWebHandler &AsyncWebServer::on(AsyncURIMatcher uri, WebRequestMethodComposite method, ArJsonRequestHandlerFunction onBody) {
AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler(std::move(uri), onBody);
handler->setMethod(method);
addHandler(handler);
return *handler;
}
#endif
AsyncStaticWebHandler &AsyncWebServer::serveStatic(const char *uri, fs::FS &fs, const char *path, const char *cache_control) {
AsyncStaticWebHandler *handler = new AsyncStaticWebHandler(uri, fs, path, cache_control);
addHandler(handler);
@@ -193,3 +205,141 @@ void AsyncWebServer::reset() {
_catchAllHandler->onUpload(NULL);
_catchAllHandler->onBody(NULL);
}
AsyncURIMatcher::AsyncURIMatcher(String uri, uint16_t modifiers) : _value(std::move(uri)) {
#ifdef ASYNCWEBSERVER_REGEX
if (_value.startsWith("^") && _value.endsWith("$")) {
pattern = new std::regex(_value.c_str(), (modifiers & CaseInsensitive) ? (std::regex::icase | std::regex::optimize) : (std::regex::optimize));
return; // no additional processing - flags are overwritten by pattern pointer
}
#endif
if (modifiers & CaseInsensitive) {
_value.toLowerCase();
}
// Inspect _value to set flags
// empty URI matches everything
if (!_value.length()) {
_flags = _toFlags(Type::All, modifiers);
} else if (_value.endsWith("*")) {
// wildcard match with * at the end
_flags = _toFlags(Type::Prefix, modifiers);
_value = _value.substring(0, _value.length() - 1);
} else if (_value.lastIndexOf("/*.") >= 0) {
// prefix match with /*.ext
// matches any path ending with .ext
// e.g. /images/*.png will match /images/pic.png and /images/2023/pic.png but not /img/pic.png
_flags = _toFlags(Type::Extension, modifiers);
} else {
// backward compatible use case: exact match or prefix with trailing /
_flags = _toFlags(Type::BackwardCompatible, modifiers);
}
}
AsyncURIMatcher::AsyncURIMatcher(String uri, Type type, uint16_t modifiers) : _value(std::move(uri)), _flags(_toFlags(type, modifiers)) {
#ifdef ASYNCWEBSERVER_REGEX
if (type == Type::Regex) {
pattern = new std::regex(_value.c_str(), (modifiers & CaseInsensitive) ? (std::regex::icase | std::regex::optimize) : (std::regex::optimize));
return; // no additional processing - flags are overwritten by pattern pointer
}
#endif
if (modifiers & CaseInsensitive) {
_value.toLowerCase();
}
}
#ifdef ASYNCWEBSERVER_REGEX
AsyncURIMatcher::AsyncURIMatcher(const AsyncURIMatcher &c) : _value(c._value), _flags(c._flags) {
if (_isRegex()) {
pattern = new std::regex(*pattern);
}
}
AsyncURIMatcher::AsyncURIMatcher(AsyncURIMatcher &&c) : _value(std::move(c._value)), _flags(c._flags) {
c._flags = _toFlags(Type::None, None);
}
AsyncURIMatcher::~AsyncURIMatcher() {
if (_isRegex()) {
delete pattern;
}
}
AsyncURIMatcher &AsyncURIMatcher::operator=(const AsyncURIMatcher &r) {
_value = r._value;
if (r._isRegex()) {
// Allocate first before we delete our current state
auto p = new std::regex(*r.pattern);
// Safely reassign our pattern
if (_isRegex()) {
delete pattern;
}
pattern = p;
} else {
if (_isRegex()) {
delete pattern;
}
_flags = r._flags;
}
return *this;
}
AsyncURIMatcher &AsyncURIMatcher::operator=(AsyncURIMatcher &&r) {
_value = std::move(r._value);
if (_isRegex()) {
delete pattern;
}
_flags = r._flags;
if (r._isRegex()) {
// We have adopted it
r._flags = _toFlags(Type::None, None);
}
return *this;
}
#endif
bool AsyncURIMatcher::matches(AsyncWebServerRequest *request) const {
#ifdef ASYNCWEBSERVER_REGEX
if (_isRegex()) {
// when type == Type::Regex, or when _value was auto-detected as regex
std::smatch matches;
std::string s(request->url().c_str());
if (std::regex_search(s, matches, *pattern)) {
for (size_t i = 1; i < matches.size(); ++i) {
request->_pathParams.emplace_back(matches[i].str().c_str());
}
return true;
}
return false;
}
#endif
// extract matcher type from _flags
Type type;
uint16_t modifiers;
std::tie(type, modifiers) = _fromFlags(_flags);
// apply modifiers
String path = request->url();
if (modifiers & CaseInsensitive) {
path.toLowerCase();
}
switch (type) {
case Type::All: return true;
case Type::None: return false;
case Type::Exact: return (_value == path);
case Type::Prefix: return path.startsWith(_value);
case Type::Extension:
{
int split = _value.lastIndexOf("/*.");
return (split >= 0 && path.startsWith(_value.substring(0, split)) && path.endsWith(_value.substring(split + 2)));
}
case Type::BackwardCompatible: return (_value == path) || path.startsWith(_value + "/");
default:
// Should never happen - programming error
assert("Invalid type");
return false;
}
}

View File

@@ -1,201 +1,221 @@
// 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
#pragma once
// Include WString.h for F() macro support on Arduino platforms
#ifdef ARDUINO
#include <WString.h>
#endif
// Platform-specific string storage and return type
#ifdef ARDUINO_ARCH_ESP8266
// On ESP8266, use PROGMEM storage and return __FlashStringHelper*
#include <pgmspace.h>
#define DECLARE_STR(name, value) static const char name##_PROGMEM[] PROGMEM = value
#define STR(name) (reinterpret_cast<const __FlashStringHelper *>(name##_PROGMEM))
#define STR_RETURN_TYPE const __FlashStringHelper *
#else
// On other platforms, use regular constexpr for compile-time optimization
#define DECLARE_STR(name, value) static constexpr const char *name = value
#define STR(name) name
#define STR_RETURN_TYPE const char *
#endif
namespace asyncsrv {
static constexpr const char *empty = "";
static constexpr const char empty[] = "";
static constexpr const char *T__opaque = "\", opaque=\"";
static constexpr const char *T_100_CONTINUE = "100-continue";
static constexpr const char *T_13 = "13";
static constexpr const char *T_ACCEPT = "Accept";
static constexpr const char *T_Accept_Ranges = "Accept-Ranges";
static constexpr const char *T_attachment = "attachment; filename=\"%s\"";
static constexpr const char *T_AUTH = "Authorization";
static constexpr const char *T_auth_nonce = "\", qop=\"auth\", nonce=\"";
static constexpr const char *T_BASIC = "Basic";
static constexpr const char *T_BASIC_REALM = "Basic realm=\"";
static constexpr const char *T_BEARER = "Bearer";
static constexpr const char *T_BODY = "body";
static constexpr const char *T_Cache_Control = "Cache-Control";
static constexpr const char *T_chunked = "chunked";
static constexpr const char *T_close = "close";
static constexpr const char *T_cnonce = "cnonce";
static constexpr const char *T_Connection = "Connection";
static constexpr const char *T_Content_Disposition = "Content-Disposition";
static constexpr const char *T_Content_Encoding = "Content-Encoding";
static constexpr const char *T_Content_Length = "Content-Length";
static constexpr const char *T_Content_Type = "Content-Type";
static constexpr const char *T_Content_Location = "Content-Location";
static constexpr const char *T_Cookie = "Cookie";
static constexpr const char *T_CORS_ACAC = "Access-Control-Allow-Credentials";
static constexpr const char *T_CORS_ACAH = "Access-Control-Allow-Headers";
static constexpr const char *T_CORS_ACAM = "Access-Control-Allow-Methods";
static constexpr const char *T_CORS_ACAO = "Access-Control-Allow-Origin";
static constexpr const char *T_CORS_ACMA = "Access-Control-Max-Age";
static constexpr const char *T_CORS_O = "Origin";
static constexpr const char *T_data_ = "data: ";
static constexpr const char *T_Date = "Date";
static constexpr const char *T_DIGEST = "Digest";
static constexpr const char *T_DIGEST_ = "Digest ";
static constexpr const char *T_ETag = "ETag";
static constexpr const char *T_event_ = "event: ";
static constexpr const char *T_EXPECT = "Expect";
static constexpr const char *T_FALSE = "false";
static constexpr const char *T_filename = "filename";
static constexpr const char *T_gzip = "gzip";
static constexpr const char *T_Host = "host";
static constexpr const char *T_HTTP_1_0 = "HTTP/1.0";
static constexpr const char *T_HTTP_100_CONT = "HTTP/1.1 100 Continue\r\n\r\n";
static constexpr const char *T_id__ = "id: ";
static constexpr const char *T_IMS = "If-Modified-Since";
static constexpr const char *T_INM = "If-None-Match";
static constexpr const char *T_inline = "inline";
static constexpr const char *T_keep_alive = "keep-alive";
static constexpr const char *T_Last_Event_ID = "Last-Event-ID";
static constexpr const char *T_Last_Modified = "Last-Modified";
static constexpr const char *T_LOCATION = "Location";
static constexpr const char *T_LOGIN_REQ = "Login Required";
static constexpr const char *T_MULTIPART_ = "multipart/";
static constexpr const char *T_name = "name";
static constexpr const char *T_nc = "nc";
static constexpr const char *T_no_cache = "no-cache";
static constexpr const char *T_nonce = "nonce";
static constexpr const char *T_none = "none";
static constexpr const char *T_opaque = "opaque";
static constexpr const char *T_qop = "qop";
static constexpr const char *T_realm = "realm";
static constexpr const char *T_realm__ = "realm=\"";
static constexpr const char *T_response = "response";
static constexpr const char *T_retry_ = "retry: ";
static constexpr const char *T_retry_after = "Retry-After";
static constexpr const char *T_nn = "\n\n";
static constexpr const char *T_rn = "\r\n";
static constexpr const char *T_rnrn = "\r\n\r\n";
static constexpr const char *T_Server = "Server";
static constexpr const char *T_Transfer_Encoding = "Transfer-Encoding";
static constexpr const char *T_TRUE = "true";
static constexpr const char *T_UPGRADE = "Upgrade";
static constexpr const char *T_uri = "uri";
static constexpr const char *T_username = "username";
static constexpr const char *T_WS = "websocket";
static constexpr const char *T_WWW_AUTH = "WWW-Authenticate";
static constexpr const char T__opaque[] = "\", opaque=\"";
static constexpr const char T_100_CONTINUE[] = "100-continue";
static constexpr const char T_13[] = "13";
static constexpr const char T_ACCEPT[] = "Accept";
static constexpr const char T_Accept_Ranges[] = "Accept-Ranges";
static constexpr const char T_attachment[] = "attachment; filename=\"";
static constexpr const char T_AUTH[] = "Authorization";
static constexpr const char T_auth_nonce[] = "\", qop=\"auth\", nonce=\"";
static constexpr const char T_BASIC[] = "Basic";
static constexpr const char T_BASIC_REALM[] = "Basic realm=\"";
static constexpr const char T_BEARER[] = "Bearer";
static constexpr const char T_BODY[] = "body";
static constexpr const char T_Cache_Control[] = "Cache-Control";
static constexpr const char T_chunked[] = "chunked";
static constexpr const char T_close[] = "close";
static constexpr const char T_cnonce[] = "cnonce";
static constexpr const char T_Connection[] = "Connection";
static constexpr const char T_Content_Disposition[] = "Content-Disposition";
static constexpr const char T_Content_Encoding[] = "Content-Encoding";
static constexpr const char T_Content_Length[] = "Content-Length";
static constexpr const char T_Content_Type[] = "Content-Type";
static constexpr const char T_Content_Location[] = "Content-Location";
static constexpr const char T_Cookie[] = "Cookie";
static constexpr const char T_CORS_ACAC[] = "Access-Control-Allow-Credentials";
static constexpr const char T_CORS_ACAH[] = "Access-Control-Allow-Headers";
static constexpr const char T_CORS_ACAM[] = "Access-Control-Allow-Methods";
static constexpr const char T_CORS_ACAO[] = "Access-Control-Allow-Origin";
static constexpr const char T_CORS_ACMA[] = "Access-Control-Max-Age";
static constexpr const char T_CORS_O[] = "Origin";
static constexpr const char T_data_[] = "data: ";
static constexpr const char T_Date[] = "Date";
static constexpr const char T_DIGEST[] = "Digest";
static constexpr const char T_DIGEST_[] = "Digest ";
static constexpr const char T_ETag[] = "ETag";
static constexpr const char T_event_[] = "event: ";
static constexpr const char T_EXPECT[] = "Expect";
static constexpr const char T_FALSE[] = "false";
static constexpr const char T_filename[] = "filename";
static constexpr const char T_gzip[] = "gzip";
static constexpr const char T_Host[] = "host";
static constexpr const char T_HTTP_1_0[] = "HTTP/1.0";
static constexpr const char T_HTTP_100_CONT[] = "HTTP/1.1 100 Continue\r\n\r\n";
static constexpr const char T_id__[] = "id: ";
static constexpr const char T_IMS[] = "If-Modified-Since";
static constexpr const char T_INM[] = "If-None-Match";
static constexpr const char T_inline[] = "inline";
static constexpr const char T_keep_alive[] = "keep-alive";
static constexpr const char T_Last_Event_ID[] = "Last-Event-ID";
static constexpr const char T_Last_Modified[] = "Last-Modified";
static constexpr const char T_LOCATION[] = "Location";
static constexpr const char T_LOGIN_REQ[] = "Login Required";
static constexpr const char T_MULTIPART_[] = "multipart/";
static constexpr const char T_name[] = "name";
static constexpr const char T_nc[] = "nc";
static constexpr const char T_no_cache[] = "no-cache";
static constexpr const char T_nonce[] = "nonce";
static constexpr const char T_none[] = "none";
static constexpr const char T_opaque[] = "opaque";
static constexpr const char T_qop[] = "qop";
static constexpr const char T_realm[] = "realm";
static constexpr const char T_realm__[] = "realm=\"";
static constexpr const char T_response[] = "response";
static constexpr const char T_retry_[] = "retry: ";
static constexpr const char T_retry_after[] = "Retry-After";
static constexpr const char T_nn[] = "\n\n";
static constexpr const char T_rn[] = "\r\n";
static constexpr const char T_rnrn[] = "\r\n\r\n";
static constexpr const char T_Server[] = "Server";
static constexpr const char T_Transfer_Encoding[] = "Transfer-Encoding";
static constexpr const char T_TRUE[] = "true";
static constexpr const char T_UPGRADE[] = "Upgrade";
static constexpr const char T_uri[] = "uri";
static constexpr const char T_username[] = "username";
static constexpr const char T_WS[] = "websocket";
static constexpr const char T_WWW_AUTH[] = "WWW-Authenticate";
// HTTP Methods
static constexpr const char *T_ANY = "ANY";
static constexpr const char *T_GET = "GET";
static constexpr const char *T_POST = "POST";
static constexpr const char *T_PUT = "PUT";
static constexpr const char *T_DELETE = "DELETE";
static constexpr const char *T_PATCH = "PATCH";
static constexpr const char *T_HEAD = "HEAD";
static constexpr const char *T_OPTIONS = "OPTIONS";
static constexpr const char *T_UNKNOWN = "UNKNOWN";
static constexpr const char T_ANY[] = "ANY";
static constexpr const char T_GET[] = "GET";
static constexpr const char T_POST[] = "POST";
static constexpr const char T_PUT[] = "PUT";
static constexpr const char T_DELETE[] = "DELETE";
static constexpr const char T_PATCH[] = "PATCH";
static constexpr const char T_HEAD[] = "HEAD";
static constexpr const char T_OPTIONS[] = "OPTIONS";
static constexpr const char T_UNKNOWN[] = "UNKNOWN";
// Req content types
static constexpr const char *T_RCT_NOT_USED = "RCT_NOT_USED";
static constexpr const char *T_RCT_DEFAULT = "RCT_DEFAULT";
static constexpr const char *T_RCT_HTTP = "RCT_HTTP";
static constexpr const char *T_RCT_WS = "RCT_WS";
static constexpr const char *T_RCT_EVENT = "RCT_EVENT";
static constexpr const char *T_ERROR = "ERROR";
static constexpr const char T_RCT_NOT_USED[] = "RCT_NOT_USED";
static constexpr const char T_RCT_DEFAULT[] = "RCT_DEFAULT";
static constexpr const char T_RCT_HTTP[] = "RCT_HTTP";
static constexpr const char T_RCT_WS[] = "RCT_WS";
static constexpr const char T_RCT_EVENT[] = "RCT_EVENT";
static constexpr const char T_ERROR[] = "ERROR";
// extensions & MIME-Types
static constexpr const char *T__avif = ".avif"; // AVIF: Highly compressed images. Compatible with all modern browsers.
static constexpr const char *T__csv = ".csv"; // CSV: Data logging and configuration
static constexpr const char *T__css = ".css"; // CSS: Styling for web interfaces
static constexpr const char *T__gif = ".gif"; // GIF: Simple animations. Legacy support
static constexpr const char *T__gz = ".gz"; // GZ: compressed files
static constexpr const char *T__htm = ".htm"; // HTM: Web interface files
static constexpr const char *T__html = ".html"; // HTML: Web interface files
static constexpr const char *T__ico = ".ico"; // ICO: Favicons, system icons. Legacy support
static constexpr const char *T__jpg = ".jpg"; // JPEG/JPG: Photos. Legacy support
static constexpr const char *T__js = ".js"; // JavaScript: Interactive functionality
static constexpr const char *T__json = ".json"; // JSON: Data exchange format
static constexpr const char *T__mp4 = ".mp4"; // MP4: Proprietary format. Worse compression than WEBM.
static constexpr const char *T__opus = ".opus"; // OPUS: High compression audio format
static constexpr const char *T__pdf = ".pdf"; // PDF: Universal document format
static constexpr const char *T__png = ".png"; // PNG: Icons, logos, transparency. Legacy support
static constexpr const char *T__svg = ".svg"; // SVG: Vector graphics, icons (scalable, tiny file sizes)
static constexpr const char *T__ttf = ".ttf"; // TTF: Font file. Legacy support
static constexpr const char *T__txt = ".txt"; // TXT: Plain text files
static constexpr const char *T__webm = ".webm"; // WebM: Video. Open source, optimized for web. Compatible with all modern browsers.
static constexpr const char *T__webp = ".webp"; // WebP: Highly compressed images. Compatible with all modern browsers.
static constexpr const char *T__woff = ".woff"; // WOFF: Font file. Legacy support
static constexpr const char *T__woff2 = ".woff2"; // WOFF2: Better compression. Compatible with all modern browsers.
static constexpr const char *T__xml = ".xml"; // XML: Configuration and data files
static constexpr const char *T_application_javascript = "application/javascript"; // Obsolete type for JavaScript
static constexpr const char *T_application_json = "application/json";
static constexpr const char *T_application_msgpack = "application/msgpack";
static constexpr const char *T_application_octet_stream = "application/octet-stream";
static constexpr const char *T_application_pdf = "application/pdf";
static constexpr const char *T_app_xform_urlencoded = "application/x-www-form-urlencoded";
static constexpr const char *T_audio_opus = "audio/opus";
static constexpr const char *T_font_ttf = "font/ttf";
static constexpr const char *T_font_woff = "font/woff";
static constexpr const char *T_font_woff2 = "font/woff2";
static constexpr const char *T_image_avif = "image/avif";
static constexpr const char *T_image_gif = "image/gif";
static constexpr const char *T_image_jpeg = "image/jpeg";
static constexpr const char *T_image_png = "image/png";
static constexpr const char *T_image_svg_xml = "image/svg+xml";
static constexpr const char *T_image_webp = "image/webp";
static constexpr const char *T_image_x_icon = "image/x-icon";
static constexpr const char *T_text_css = "text/css";
static constexpr const char *T_text_csv = "text/csv";
static constexpr const char *T_text_event_stream = "text/event-stream";
static constexpr const char *T_text_html = "text/html";
static constexpr const char *T_text_javascript = "text/javascript";
static constexpr const char *T_text_plain = "text/plain";
static constexpr const char *T_text_xml = "text/xml";
static constexpr const char *T_video_mp4 = "video/mp4";
static constexpr const char *T_video_webm = "video/webm";
static constexpr const char T__avif[] = ".avif"; // AVIF: Highly compressed images. Compatible with all modern browsers.
static constexpr const char T__csv[] = ".csv"; // CSV: Data logging and configuration
static constexpr const char T__css[] = ".css"; // CSS: Styling for web interfaces
static constexpr const char T__gif[] = ".gif"; // GIF: Simple animations. Legacy support
static constexpr const char T__gz[] = ".gz"; // GZ: compressed files
static constexpr const char T__htm[] = ".htm"; // HTM: Web interface files
static constexpr const char T__html[] = ".html"; // HTML: Web interface files
static constexpr const char T__ico[] = ".ico"; // ICO: Favicons, system icons. Legacy support
static constexpr const char T__jpg[] = ".jpg"; // JPEG/JPG: Photos. Legacy support
static constexpr const char T__js[] = ".js"; // JavaScript: Interactive functionality
static constexpr const char T__json[] = ".json"; // JSON: Data exchange format
static constexpr const char T__mp4[] = ".mp4"; // MP4: Proprietary format. Worse compression than WEBM.
static constexpr const char T__mjs[] = ".mjs"; // MJS: JavaScript module format
static constexpr const char T__opus[] = ".opus"; // OPUS: High compression audio format
static constexpr const char T__pdf[] = ".pdf"; // PDF: Universal document format
static constexpr const char T__png[] = ".png"; // PNG: Icons, logos, transparency. Legacy support
static constexpr const char T__svg[] = ".svg"; // SVG: Vector graphics, icons (scalable, tiny file sizes)
static constexpr const char T__ttf[] = ".ttf"; // TTF: Font file. Legacy support
static constexpr const char T__txt[] = ".txt"; // TXT: Plain text files
static constexpr const char T__webm[] = ".webm"; // WebM: Video. Open source, optimized for web. Compatible with all modern browsers.
static constexpr const char T__webp[] = ".webp"; // WebP: Highly compressed images. Compatible with all modern browsers.
static constexpr const char T__woff[] = ".woff"; // WOFF: Font file. Legacy support
static constexpr const char T__woff2[] = ".woff2"; // WOFF2: Better compression. Compatible with all modern browsers.
static constexpr const char T__xml[] = ".xml"; // XML: Configuration and data files
static constexpr const char T_application_javascript[] = "application/javascript"; // Obsolete type for JavaScript
static constexpr const char T_application_json[] = "application/json";
static constexpr const char T_application_msgpack[] = "application/msgpack";
static constexpr const char T_application_octet_stream[] = "application/octet-stream";
static constexpr const char T_application_pdf[] = "application/pdf";
static constexpr const char T_app_xform_urlencoded[] = "application/x-www-form-urlencoded";
static constexpr const char T_audio_opus[] = "audio/opus";
static constexpr const char T_font_ttf[] = "font/ttf";
static constexpr const char T_font_woff[] = "font/woff";
static constexpr const char T_font_woff2[] = "font/woff2";
static constexpr const char T_image_avif[] = "image/avif";
static constexpr const char T_image_gif[] = "image/gif";
static constexpr const char T_image_jpeg[] = "image/jpeg";
static constexpr const char T_image_png[] = "image/png";
static constexpr const char T_image_svg_xml[] = "image/svg+xml";
static constexpr const char T_image_webp[] = "image/webp";
static constexpr const char T_image_x_icon[] = "image/x-icon";
static constexpr const char T_text_css[] = "text/css";
static constexpr const char T_text_csv[] = "text/csv";
static constexpr const char T_text_event_stream[] = "text/event-stream";
static constexpr const char T_text_html[] = "text/html";
static constexpr const char T_text_javascript[] = "text/javascript";
static constexpr const char T_text_plain[] = "text/plain";
static constexpr const char T_text_xml[] = "text/xml";
static constexpr const char T_video_mp4[] = "video/mp4";
static constexpr const char T_video_webm[] = "video/webm";
// Response codes
static constexpr const char *T_HTTP_CODE_100 = "Continue";
static constexpr const char *T_HTTP_CODE_101 = "Switching Protocols";
static constexpr const char *T_HTTP_CODE_200 = "OK";
static constexpr const char *T_HTTP_CODE_201 = "Created";
static constexpr const char *T_HTTP_CODE_202 = "Accepted";
static constexpr const char *T_HTTP_CODE_203 = "Non-Authoritative Information";
static constexpr const char *T_HTTP_CODE_204 = "No Content";
static constexpr const char *T_HTTP_CODE_205 = "Reset Content";
static constexpr const char *T_HTTP_CODE_206 = "Partial Content";
static constexpr const char *T_HTTP_CODE_300 = "Multiple Choices";
static constexpr const char *T_HTTP_CODE_301 = "Moved Permanently";
static constexpr const char *T_HTTP_CODE_302 = "Found";
static constexpr const char *T_HTTP_CODE_303 = "See Other";
static constexpr const char *T_HTTP_CODE_304 = "Not Modified";
static constexpr const char *T_HTTP_CODE_305 = "Use Proxy";
static constexpr const char *T_HTTP_CODE_307 = "Temporary Redirect";
static constexpr const char *T_HTTP_CODE_400 = "Bad Request";
static constexpr const char *T_HTTP_CODE_401 = "Unauthorized";
static constexpr const char *T_HTTP_CODE_402 = "Payment Required";
static constexpr const char *T_HTTP_CODE_403 = "Forbidden";
static constexpr const char *T_HTTP_CODE_404 = "Not Found";
static constexpr const char *T_HTTP_CODE_405 = "Method Not Allowed";
static constexpr const char *T_HTTP_CODE_406 = "Not Acceptable";
static constexpr const char *T_HTTP_CODE_407 = "Proxy Authentication Required";
static constexpr const char *T_HTTP_CODE_408 = "Request Time-out";
static constexpr const char *T_HTTP_CODE_409 = "Conflict";
static constexpr const char *T_HTTP_CODE_410 = "Gone";
static constexpr const char *T_HTTP_CODE_411 = "Length Required";
static constexpr const char *T_HTTP_CODE_412 = "Precondition Failed";
static constexpr const char *T_HTTP_CODE_413 = "Request Entity Too Large";
static constexpr const char *T_HTTP_CODE_414 = "Request-URI Too Large";
static constexpr const char *T_HTTP_CODE_415 = "Unsupported Media Type";
static constexpr const char *T_HTTP_CODE_416 = "Requested Range Not Satisfiable";
static constexpr const char *T_HTTP_CODE_417 = "Expectation Failed";
static constexpr const char *T_HTTP_CODE_429 = "Too Many Requests";
static constexpr const char *T_HTTP_CODE_500 = "Internal Server Error";
static constexpr const char *T_HTTP_CODE_501 = "Not Implemented";
static constexpr const char *T_HTTP_CODE_502 = "Bad Gateway";
static constexpr const char *T_HTTP_CODE_503 = "Service Unavailable";
static constexpr const char *T_HTTP_CODE_504 = "Gateway Time-out";
static constexpr const char *T_HTTP_CODE_505 = "HTTP Version Not Supported";
static constexpr const char *T_HTTP_CODE_ANY = "Unknown code";
// Response codes - using DECLARE_STR macro for platform-specific storage
DECLARE_STR(T_HTTP_CODE_100, "Continue");
DECLARE_STR(T_HTTP_CODE_101, "Switching Protocols");
DECLARE_STR(T_HTTP_CODE_200, "OK");
DECLARE_STR(T_HTTP_CODE_201, "Created");
DECLARE_STR(T_HTTP_CODE_202, "Accepted");
DECLARE_STR(T_HTTP_CODE_203, "Non-Authoritative Information");
DECLARE_STR(T_HTTP_CODE_204, "No Content");
DECLARE_STR(T_HTTP_CODE_205, "Reset Content");
DECLARE_STR(T_HTTP_CODE_206, "Partial Content");
DECLARE_STR(T_HTTP_CODE_300, "Multiple Choices");
DECLARE_STR(T_HTTP_CODE_301, "Moved Permanently");
DECLARE_STR(T_HTTP_CODE_302, "Found");
DECLARE_STR(T_HTTP_CODE_303, "See Other");
DECLARE_STR(T_HTTP_CODE_304, "Not Modified");
DECLARE_STR(T_HTTP_CODE_305, "Use Proxy");
DECLARE_STR(T_HTTP_CODE_307, "Temporary Redirect");
DECLARE_STR(T_HTTP_CODE_400, "Bad Request");
DECLARE_STR(T_HTTP_CODE_401, "Unauthorized");
DECLARE_STR(T_HTTP_CODE_402, "Payment Required");
DECLARE_STR(T_HTTP_CODE_403, "Forbidden");
DECLARE_STR(T_HTTP_CODE_404, "Not Found");
DECLARE_STR(T_HTTP_CODE_405, "Method Not Allowed");
DECLARE_STR(T_HTTP_CODE_406, "Not Acceptable");
DECLARE_STR(T_HTTP_CODE_407, "Proxy Authentication Required");
DECLARE_STR(T_HTTP_CODE_408, "Request Time-out");
DECLARE_STR(T_HTTP_CODE_409, "Conflict");
DECLARE_STR(T_HTTP_CODE_410, "Gone");
DECLARE_STR(T_HTTP_CODE_411, "Length Required");
DECLARE_STR(T_HTTP_CODE_412, "Precondition Failed");
DECLARE_STR(T_HTTP_CODE_413, "Request Entity Too Large");
DECLARE_STR(T_HTTP_CODE_414, "Request-URI Too Large");
DECLARE_STR(T_HTTP_CODE_415, "Unsupported Media Type");
DECLARE_STR(T_HTTP_CODE_416, "Requested Range Not Satisfiable");
DECLARE_STR(T_HTTP_CODE_417, "Expectation Failed");
DECLARE_STR(T_HTTP_CODE_429, "Too Many Requests");
DECLARE_STR(T_HTTP_CODE_500, "Internal Server Error");
DECLARE_STR(T_HTTP_CODE_501, "Not Implemented");
DECLARE_STR(T_HTTP_CODE_502, "Bad Gateway");
DECLARE_STR(T_HTTP_CODE_503, "Service Unavailable");
DECLARE_STR(T_HTTP_CODE_504, "Gateway Time-out");
DECLARE_STR(T_HTTP_CODE_505, "HTTP Version Not Supported");
DECLARE_STR(T_HTTP_CODE_ANY, "Unknown code");
static constexpr const char *T_only_once_headers[] = {
T_Accept_Ranges, T_Content_Length, T_Content_Type, T_Connection, T_CORS_ACAC, T_CORS_ACAH, T_CORS_ACAM, T_CORS_ACAO,
@@ -203,5 +223,5 @@ static constexpr const char *T_only_once_headers[] = {
T_Transfer_Encoding, T_Content_Location, T_Server, T_WWW_AUTH
};
static constexpr size_t T_only_once_headers_len = sizeof(T_only_once_headers) / sizeof(T_only_once_headers[0]);
static constexpr size_t T__GZ_LEN = sizeof(T__gz) - 1;
} // namespace asyncsrv