diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..02f38c3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,2 @@ +# https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository +blank_issues_enabled: false diff --git a/.github/ISSUE_TEMPLATE/issue-report.yml b/.github/ISSUE_TEMPLATE/issue-report.yml new file mode 100644 index 0000000..d48e5ba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/issue-report.yml @@ -0,0 +1,91 @@ +name: 🐛 Bug Report +description: File a bug report +labels: ["Status: Awaiting triage"] +body: + - type: markdown + attributes: + value: > + ### ⚠️ Please remember: issues are for *bugs* only! + + - type: markdown + attributes: + value: | + #### Unsure? Have a questions? 👉 [Start a new discussion](https://github.com/ESP32Async/ESPAsyncWebServer/discussions/new) + + #### Before opening a new issue, please make sure you have searched: + + - In the [documentation](https://github.com/ESP32Async/ESPAsyncWebServer) + - In the [discussions](https://github.com/ESP32Async/ESPAsyncWebServer/discussions) + - In the [issues](https://github.com/ESP32Async/ESPAsyncWebServer/issues) + - In the [examples](https://github.com/ESP32Async/ESPAsyncWebServer/tree/main/examples) + + #### Make sure you are using: + + - The [latest version of ESPAsyncWebServer](https://github.com/ESP32Async/ESPAsyncWebServer/releases) + - The [latest version of AsyncTCP](https://github.com/ESP32Async/AsyncTCP/releases) (for ESP32) + + - type: dropdown + id: platform + attributes: + label: Platform + options: + - ESP32 + - ESP8266 + - RP2040 + validations: + required: true + + - type: dropdown + id: tooling + attributes: + label: IDE / Tooling + options: + - Arduino (IDE/CLI) + - pioarduino + - ESP-IDF + - PlatformIO + validations: + required: true + + - type: textarea + id: what-happened + attributes: + label: What happened? + description: A clear and concise description of the issue. + placeholder: A clear and concise description of the issue. + validations: + required: true + + - type: textarea + id: stack-trace + attributes: + label: Stack Trace + description: Please provide a debug message or error message. If you have a Guru Meditation Error or Backtrace, [please decode it](https://maximeborges.github.io/esp-stacktrace-decoder/). + placeholder: For Arduino IDE, Enable Core debug level - Debug on tools menu, then put the serial output here. + validations: + required: true + + - type: textarea + id: how-to-reproduce + attributes: + label: Minimal Reproductible Example (MRE) + description: Post the code or the steps to reproduce the issue. + placeholder: Post the code or the steps to reproduce the issue. + validations: + required: true + + - type: checkboxes + id: confirmation + attributes: + label: "I confirm that:" + options: + - label: I have read the documentation. + required: true + - label: I have searched for similar discussions. + required: true + - label: I have searched for similar issues. + required: true + - label: I have looked at the examples. + required: true + - label: I have upgraded to the lasted version of ESPAsyncWebServer (and AsyncTCP for ESP32). + required: true diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..dfd0e30 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +# Set update schedule for GitHub Actions + +version: 2 +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + # Check for updates to GitHub Actions every week + interval: "weekly" diff --git a/.github/scripts/update-version.sh b/.github/scripts/update-version.sh new file mode 100755 index 0000000..de8de77 --- /dev/null +++ b/.github/scripts/update-version.sh @@ -0,0 +1,40 @@ + +#!/bin/bash +# shellcheck disable=SC2002 + +# fail the script if any command unexpectedly fails +set -e + +if [ ! $# -eq 3 ]; then + echo "Bad number of arguments: $#" >&2 + echo "usage: $0 " >&2 + exit 1 +fi + +re='^[0-9]+$' +if [[ ! $1 =~ $re ]] || [[ ! $2 =~ $re ]] || [[ ! $3 =~ $re ]] ; then + echo "error: Not a valid version: $1.$2.$3" >&2 + echo "usage: $0 " >&2 + exit 1 +fi + +ASYNCWEBSERVER_VERSION_MAJOR="$1" +ASYNCWEBSERVER_VERSION_MINOR="$2" +ASYNCWEBSERVER_VERSION_PATCH="$3" +ASYNCWEBSERVER_VERSION="$ASYNCWEBSERVER_VERSION_MAJOR.$ASYNCWEBSERVER_VERSION_MINOR.$ASYNCWEBSERVER_VERSION_PATCH" + +echo "New AsyncTCP version: $ASYNCWEBSERVER_VERSION" + +echo "Updating library.properties..." +cat library.properties | sed "s/version=.*/version=$ASYNCWEBSERVER_VERSION/g" > __library.properties && mv __library.properties library.properties + +echo "Updating library.json..." +cat library.json | sed "s/^ \"version\":.*/ \"version\": \"$ASYNCWEBSERVER_VERSION\",/g" > __library.json && mv __library.json library.json + +echo "Updating src/AsyncWebServerVersion.h..." +cat src/AsyncWebServerVersion.h | \ +sed "s/#define ASYNCWEBSERVER_VERSION_MAJOR.*/#define ASYNCWEBSERVER_VERSION_MAJOR $ASYNCWEBSERVER_VERSION_MAJOR/g" | \ +sed "s/#define ASYNCWEBSERVER_VERSION_MINOR.*/#define ASYNCWEBSERVER_VERSION_MINOR $ASYNCWEBSERVER_VERSION_MINOR/g" | \ +sed "s/#define ASYNCWEBSERVER_VERSION_PATCH.*/#define ASYNCWEBSERVER_VERSION_PATCH $ASYNCWEBSERVER_VERSION_PATCH/g" > src/__AsyncWebServerVersion.h && mv src/__AsyncWebServerVersion.h src/AsyncWebServerVersion.h + +exit 0 \ No newline at end of file diff --git a/.github/workflows/arduino-lint.yml b/.github/workflows/arduino-lint.yml new file mode 100644 index 0000000..8abd606 --- /dev/null +++ b/.github/workflows/arduino-lint.yml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: Arduino Lint + +on: + workflow_dispatch: + push: + branches: + - main + - release/* + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + arduino-lint: + name: Arduino Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: arduino/arduino-lint-action@v2 + with: + library-manager: update diff --git a/.github/workflows/build-esp32.yml b/.github/workflows/build-esp32.yml new file mode 100644 index 0000000..9be30b7 --- /dev/null +++ b/.github/workflows/build-esp32.yml @@ -0,0 +1,194 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: Build (ESP32) + +on: + workflow_dispatch: + push: + branches: + - main + - release/* + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + arduino-esp32: + name: ESP32 (arduino-cli) - Release + runs-on: ubuntu-latest + steps: + - name: Install arduino-cli + run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh + + - name: Update core index + run: arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json + + - name: Install core + run: arduino-cli core install --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_index.json esp32:esp32 + + - name: Install ArduinoJson + run: arduino-cli lib install ArduinoJson + + - name: Install AsyncTCP (ESP32) + run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/AsyncTCP#v3.4.10 + + - name: Checkout + uses: actions/checkout@v5 + + - name: Build Examples + run: | + for i in `ls examples`; do + echo "=============================================================" + echo "Building examples/$i..." + echo "=============================================================" + arduino-cli compile --library . --warnings none -b esp32:esp32:esp32 "examples/$i/$i.ino" + done + + arduino-esp32-dev: + name: ESP32 (arduino-cli) - Dev + runs-on: ubuntu-latest + steps: + - name: Install arduino-cli + run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh + + - name: Update core index + run: arduino-cli core update-index --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json + + - name: Install core + run: arduino-cli core install --additional-urls https://espressif.github.io/arduino-esp32/package_esp32_dev_index.json esp32:esp32 + + - name: Install ArduinoJson + run: arduino-cli lib install ArduinoJson + + - name: Install AsyncTCP (ESP32) + run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/AsyncTCP#v3.4.10 + + - name: Checkout + uses: actions/checkout@v5 + + - name: Build Examples + run: | + for i in `ls examples`; do + echo "=============================================================" + echo "Building examples/$i..." + echo "=============================================================" + arduino-cli compile --library . --warnings none -b esp32:esp32:esp32 "examples/$i/$i.ino" + done + + platformio-esp32-arduino2: + name: ESP32 (pio) - Arduino 2 + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: + - esp32dev + - esp32-s2-saola-1 + - esp32-s3-devkitc-1 + - esp32-c3-devkitc-02 + + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "latest" + enable-cache: false + - name: Install platformio + run: | + uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip + + - name: Build Examples + run: | + for i in `ls examples`; do + echo "=============================================================" + echo "Building examples/$i..." + echo "=============================================================" + PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-arduino-2 + done + + platformio-esp32-arduino-3: + name: ESP32 (pio) - Arduino 3 + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: + - esp32dev + - esp32-s2-saola-1 + - esp32-s3-devkitc-1 + - esp32-c3-devkitc-02 + - esp32-c6-devkitc-1 + - esp32-h2-devkitm-1 + - esp32-p4 + + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "latest" + enable-cache: false + - name: Install platformio + run: | + uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip + + - name: Build Examples + run: | + for i in `ls examples`; do + echo "=============================================================" + echo "Building examples/$i..." + echo "=============================================================" + PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-arduino-3 + done + + platformio-specific-envs: + name: ESP32 (pio) - Specific Envs + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + env: + - ci-latest-asynctcp + - ci-no-json + - ci-no-chunk-inflight + - ci-arduino-2-esp-idf-log + - ci-arduino-3-esp-idf-log + - ci-regex + + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "latest" + enable-cache: false + - name: Install platformio + run: | + uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip + + - name: Build Examples + run: | + for i in `ls examples`; do + echo "=============================================================" + echo "Building examples/$i..." + echo "=============================================================" + PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=esp32dev pio run -e ${{ matrix.env }} + done diff --git a/.github/workflows/build-esp8266.yml b/.github/workflows/build-esp8266.yml new file mode 100644 index 0000000..0899b8e --- /dev/null +++ b/.github/workflows/build-esp8266.yml @@ -0,0 +1,82 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: Build (8266) + +on: + workflow_dispatch: + push: + branches: + - main + - release/* + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + arduino-esp8266: + name: ESP8266 (arduino-cli) + runs-on: ubuntu-latest + steps: + - name: Install arduino-cli + run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh + + - name: Update core index + run: arduino-cli core update-index --additional-urls https://arduino.esp8266.com/stable/package_esp8266com_index.json + + - name: Install core + run: arduino-cli core install --additional-urls https://arduino.esp8266.com/stable/package_esp8266com_index.json esp8266:esp8266 + + - name: Install ArduinoJson + run: arduino-cli lib install ArduinoJson + + - name: Install ESPAsyncTCP (ESP8266) + run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ESP32Async/ESPAsyncTCP#v2.0.0 + + - name: Checkout + uses: actions/checkout@v5 + + - name: Build Examples + run: | + for i in `ls examples`; do + echo "=============================================================" + echo "Building examples/$i..." + echo "=============================================================" + arduino-cli compile --library . --warnings none -b esp8266:esp8266:huzzah "examples/$i/$i.ino" + done + + platformio-esp8266: + name: ESP8266 (pio) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: + - huzzah + - d1_mini + + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "latest" + enable-cache: false + - name: Install platformio + run: | + uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip + + - name: Build Examples + run: | + for i in `ls examples`; do + echo "=============================================================" + echo "Building examples/$i..." + echo "=============================================================" + PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-esp8266 + done diff --git a/.github/workflows/build-libretiny.yml b/.github/workflows/build-libretiny.yml new file mode 100644 index 0000000..fcc408a --- /dev/null +++ b/.github/workflows/build-libretiny.yml @@ -0,0 +1,51 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: Build (LibreTiny) + +on: + workflow_dispatch: + push: + branches: + - main + - release/* + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + platformio-libretiny: + name: LibreTiny (pio) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: + - generic-bk7231n-qfn32-tuya + - generic-rtl8710bn-2mb-788k + + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "latest" + enable-cache: false + - name: Install platformio + run: | + uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip + + - name: Build Examples + run: | + for i in AsyncResponseStream Auth Headers; do + echo "=============================================================" + echo "Building examples/$i..." + echo "=============================================================" + PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-libretiny + done diff --git a/.github/workflows/build-rpi.yml b/.github/workflows/build-rpi.yml new file mode 100644 index 0000000..eca7fc5 --- /dev/null +++ b/.github/workflows/build-rpi.yml @@ -0,0 +1,89 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: Build (RPI) + +on: + workflow_dispatch: + push: + branches: + - main + - release/* + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + arduino-rpi: + name: RPI (arduino-cli) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: + - rpipicow + - rpipico2w + + steps: + - name: Install arduino-cli + run: curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh + + - name: Update core index + run: arduino-cli core update-index --additional-urls https://github.com/earlephilhower/arduino-pico/releases/download/4.4.4/package_rp2040_index.json + + - name: Install core + run: arduino-cli core install --additional-urls https://github.com/earlephilhower/arduino-pico/releases/download/4.4.4/package_rp2040_index.json rp2040:rp2040 + + - name: Install ArduinoJson + run: arduino-cli lib install ArduinoJson + + - name: Install RPAsyncTCP + run: ARDUINO_LIBRARY_ENABLE_UNSAFE_INSTALL=true arduino-cli lib install --git-url https://github.com/ayushsharma82/RPAsyncTCP#v1.3.2 + + - name: Checkout + uses: actions/checkout@v5 + + - name: Build Examples + run: | + for i in `ls examples`; do + echo "=============================================================" + echo "Building examples/$i..." + echo "=============================================================" + arduino-cli compile --library . --warnings none -b rp2040:rp2040:${{ matrix.board }} "examples/$i/$i.ino" + done + + platformio-rpi: + name: RPI (pio) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + board: + - rpipicow + - rpipico2w + + steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: "latest" + enable-cache: false + - name: Install platformio + run: | + uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.18.zip + + - name: Build Examples + run: | + for i in `ls examples`; do + echo "=============================================================" + echo "Building examples/$i..." + echo "=============================================================" + PLATFORMIO_SRC_DIR=examples/$i PIO_BOARD=${{ matrix.board }} pio run -e ci-raspberrypi + done diff --git a/.github/workflows/cpplint.yml b/.github/workflows/cpplint.yml new file mode 100644 index 0000000..0f905a0 --- /dev/null +++ b/.github/workflows/cpplint.yml @@ -0,0 +1,46 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: Cpplint + +on: + workflow_dispatch: + push: + branches: + - main + - release/* + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + cpplint: + name: cpplint + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Cache + uses: actions/cache@v4 + with: + key: ${{ runner.os }}-cpplint + path: ~/.cache/pip + + - name: Pyhton + uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: cpplint + run: | + python -m pip install --upgrade pip + pip install --upgrade cpplint + cpplint \ + --repository=. \ + --recursive \ + --filter=-build/c++11,-build/namespaces,-readability/braces,-readability/casting,-readability/todo,-runtime/explicit,-runtime/indentation_namespace,-runtime/int,-runtime/references,-whitespace/blank_line,,-whitespace/braces,-whitespace/comments,-whitespace/indent,-whitespace/line_length,-whitespace/newline,-whitespace/parens \ + lib \ + include \ + src diff --git a/.github/workflows/pre-commit-status.yml b/.github/workflows/pre-commit-status.yml new file mode 100644 index 0000000..d006066 --- /dev/null +++ b/.github/workflows/pre-commit-status.yml @@ -0,0 +1,64 @@ +# This needs to be in a separate workflow because it requires higher permissions than the calling workflow +name: Report Pre-commit Check Status + +on: + workflow_run: + workflows: [Pre-commit hooks] + types: + - completed + +permissions: + statuses: write + +jobs: + report-success: + name: Report pre-commit success + if: github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + steps: + - name: Report success + uses: actions/github-script@v7 + with: + script: | + const owner = '${{ github.repository_owner }}'; + const repo = '${{ github.repository }}'.split('/')[1]; + const sha = '${{ github.event.workflow_run.head_sha }}'; + core.debug(`owner: ${owner}`); + core.debug(`repo: ${repo}`); + core.debug(`sha: ${sha}`); + const { context: name, state } = (await github.rest.repos.createCommitStatus({ + context: 'Pre-commit checks', + description: 'Pre-commit checks successful', + owner: owner, + repo: repo, + sha: sha, + state: 'success', + target_url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}' + })).data; + core.info(`${name} is ${state}`); + + report-pending: + name: Report pre-commit pending + if: github.event.workflow_run.conclusion != 'success' + runs-on: ubuntu-latest + steps: + - name: Report pending + uses: actions/github-script@v7 + with: + script: | + const owner = '${{ github.repository_owner }}'; + const repo = '${{ github.repository }}'.split('/')[1]; + const sha = '${{ github.event.workflow_run.head_sha }}'; + core.debug(`owner: ${owner}`); + core.debug(`repo: ${repo}`); + core.debug(`sha: ${sha}`); + const { context: name, state } = (await github.rest.repos.createCommitStatus({ + context: 'Pre-commit checks', + description: 'The pre-commit checks need to be successful before merging', + owner: owner, + repo: repo, + sha: sha, + state: 'pending', + target_url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}' + })).data; + core.info(`${name} is ${state}`); diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..bf82de7 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,80 @@ +name: Pre-commit hooks + +on: + workflow_dispatch: + push: + branches: + - main + pull_request: + types: [opened, reopened, synchronize, labeled] + +concurrency: + group: pre-commit-${{github.event.pull_request.number || github.ref}} + cancel-in-progress: true + +jobs: + lint: + if: | + github.event_name != 'pull_request' || + contains(github.event.pull_request.labels.*.name, 'Status: Pending Merge') || + contains(github.event.pull_request.labels.*.name, 'Re-trigger Pre-commit') + + name: Check if fixes are needed + runs-on: ubuntu-latest + steps: + - name: Checkout latest commit + uses: actions/checkout@v5 + with: + fetch-depth: 2 + + - name: Remove Label + if: contains(github.event.pull_request.labels.*.name, 'Re-trigger Pre-commit') + run: gh pr edit ${{ github.event.number }} --remove-label 'Re-trigger Pre-commit' + env: + GH_TOKEN: ${{ github.token }} + + - name: Set up Python 3 + uses: actions/setup-python@v6 + with: + cache-dependency-path: pre-commit.requirements.txt + cache: "pip" + python-version: "3.13" + + - name: Get Python version hash + run: | + echo "Using $(python -VV)" + echo "PY_HASH=$(python -VV | sha256sum | cut -d' ' -f1)" >> $GITHUB_ENV + + - name: Restore pre-commit cache + uses: actions/cache/restore@v4 + id: restore-cache + with: + path: | + ~/.cache/pre-commit + key: pre-commit-${{ env.PY_HASH }}-${{ hashFiles('.pre-commit-config.yaml', '.github/workflows/pre-commit.yml', 'pre-commit.requirements.txt') }} + + - name: Install python dependencies + run: python -m pip install -r pre-commit.requirements.txt + + - name: Get changed files + id: changed-files + uses: tj-actions/changed-files@v42.0.2 + + - name: Run pre-commit hooks in changed files + run: pre-commit run --color=always --show-diff-on-failure --files ${{ steps.changed-files.outputs.all_changed_files }} + + - name: Save pre-commit cache + uses: actions/cache/save@v4 + if: ${{ always() && steps.restore-cache.outputs.cache-hit != 'true' }} + continue-on-error: true + with: + path: | + ~/.cache/pre-commit + key: ${{ steps.restore-cache.outputs.cache-primary-key }} + + - name: Push changes using pre-commit-ci-lite + uses: pre-commit-ci/lite-action@v1.1.0 + # Only push changes in PRs + if: ${{ always() && github.event_name == 'pull_request' }} + with: + msg: "ci(pre-commit): Apply automatic fixes" diff --git a/.github/workflows/publish-pio-registry.yml b/.github/workflows/publish-pio-registry.yml new file mode 100644 index 0000000..c0591ee --- /dev/null +++ b/.github/workflows/publish-pio-registry.yml @@ -0,0 +1,62 @@ +# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json + +name: Publish to PlatformIO + +on: + workflow_dispatch: + inputs: + tag_name: + description: "Tag name of the release to publish (use 'latest' for the most recent tag)" + required: true + default: "latest" + +jobs: + publish: + name: Publish + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-tags: true + fetch-depth: 0 # Ensure all commits and tags are fetched + + - name: Show latest tag + run: git tag --sort=-creatordate | head -n 1 + + - name: Download release zip + run: | + if [ "${{ inputs.tag_name }}" == "latest" ]; then + TAG_NAME=$(git tag --sort=-creatordate | head -n 1) + if [ -z "$TAG_NAME" ]; then + echo "Error: No tags found in the repository." + exit 1 + fi + else + TAG_NAME="${{ inputs.tag_name }}" + fi + echo "Downloading tag: $TAG_NAME" + curl -L -o project.zip "https://github.com/${{ github.repository }}/archive/refs/tags/$TAG_NAME.zip" + + # - name: Cache PlatformIO + # uses: actions/cache@v4 + # with: + # key: ${{ runner.os }}-pio + # path: | + # ~/.cache/pip + # ~/.platformio + + - name: Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: Install PlatformIO CLI + run: | + python -m pip install --upgrade pip + pip install --upgrade platformio + + - name: Publish to PlatformIO + run: pio pkg publish --no-interactive --owner ${{ github.repository_owner }} project.zip + env: + PLATFORMIO_AUTH_TOKEN: ${{ secrets.PLATFORMIO_AUTH_TOKEN }} diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml new file mode 100644 index 0000000..14ceb43 --- /dev/null +++ b/.github/workflows/stale.yaml @@ -0,0 +1,24 @@ +name: "Close stale issues and PRs" +on: + schedule: + - cron: "30 1 * * *" + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days." + stale-pr-message: "This PR is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days." + close-issue-message: "This issue was closed because it has been stalled for 7 days with no activity." + close-pr-message: "This PR was closed because it has been stalled for 7 days with no activity." + days-before-issue-stale: 30 + days-before-pr-stale: 30 + days-before-issue-close: 7 + days-before-pr-close: 7 + +permissions: + # contents: write # only for delete-branch option + issues: write + pull-requests: write diff --git a/.github/workflows/upload-idf-component.yml b/.github/workflows/upload-idf-component.yml new file mode 100644 index 0000000..2ab44b4 --- /dev/null +++ b/.github/workflows/upload-idf-component.yml @@ -0,0 +1,50 @@ +name: Publish ESP-IDF Component + +on: + workflow_dispatch: + inputs: + tag: + description: 'Component version (1.2.3, 1.2.3-rc1 or 1.2.3.4)' + required: true + git_ref: + description: 'Git ref with the source (branch, tag or commit)' + required: true + +permissions: + contents: read + +jobs: + upload_components: + runs-on: ubuntu-latest + steps: + - name: Get the release tag + env: + head_branch: ${{ inputs.tag || github.event.workflow_run.head_branch }} + run: | + # Read and sanitize the branch/tag name + branch=$(echo "$head_branch" | tr -cd '[:alnum:]/_.-') + + if [[ $branch == refs/tags/* ]]; then + tag="${branch#refs/tags/}" + elif [[ $branch =~ ^[v]*[0-9]+\.[0-9]+\.[0-9]+.*$ ]]; then + tag=$branch + else + echo "Tag not found in $branch. Exiting..." + exit 1 + fi + + echo "Tag: $tag" + echo "RELEASE_TAG=$tag" >> $GITHUB_ENV + + - uses: actions/checkout@v5 + with: + ref: ${{ inputs.git_ref || env.RELEASE_TAG }} + submodules: "recursive" + + - name: Upload components to the component registry + uses: espressif/upload-components-ci-action@v1 + with: + name: espasyncwebserver + version: ${{ env.RELEASE_TAG }} + namespace: esp32async + api_token: ${{ secrets.IDF_COMPONENT_API_TOKEN }} diff --git a/data/README.md b/data/README.md new file mode 100644 index 0000000..96a2ee4 --- /dev/null +++ b/data/README.md @@ -0,0 +1,48 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod +rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper +arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit +accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. +Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo +dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod +rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper +arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit +accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. +Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo +dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod +rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper +arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit +accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. +Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo +dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod +rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper +arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit +accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. +Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo +dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod +rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper +arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit +accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. +Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo +dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod +rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper +arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit +accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. +Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo +dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod +rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper +arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit +accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. +Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo +dapibus elit, id varius sem dui id lacus.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin euismod, purus a euismod +rhoncus, urna ipsum cursus massa, eu dictum tellus justo ac justo. Quisque ullamcorper +arcu nec tortor ullamcorper, vel fermentum justo fermentum. Vivamus sed velit ut elit +accumsan congue ut ut enim. Ut eu justo eu lacus varius gravida ut a tellus. Nulla facilisi. +Integer auctor consectetur ultricies. Fusce feugiat, mi sit amet bibendum viverra, orci leo +dapibus elit, id varius sem dui id lacus. diff --git a/examples/AsyncResponseStream/AsyncResponseStream.ino b/examples/AsyncResponseStream/AsyncResponseStream.ino index 451bb1a..33b68db 100644 --- a/examples/AsyncResponseStream/AsyncResponseStream.ino +++ b/examples/AsyncResponseStream/AsyncResponseStream.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles #include #if defined(ESP32) || defined(LIBRETINY) @@ -20,7 +20,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/AsyncTunnel/AsyncTunnel.ino b/examples/AsyncTunnel/AsyncTunnel.ino index b63c056..2022450 100644 --- a/examples/AsyncTunnel/AsyncTunnel.ino +++ b/examples/AsyncTunnel/AsyncTunnel.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to trigger an async client request from a browser request and send the client response back to the browser through websocket @@ -70,7 +70,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.begin(WIFI_SSID, WIFI_PASSWORD); while (WiFi.status() != WL_CONNECTED) { delay(500); diff --git a/examples/Auth/Auth.ino b/examples/Auth/Auth.ino index 8f5b535..b2dfb89 100644 --- a/examples/Auth/Auth.ino +++ b/examples/Auth/Auth.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Authentication and authorization middlewares @@ -29,30 +29,36 @@ static AsyncAuthenticationMiddleware basicAuthHash; static AsyncAuthenticationMiddleware digestAuth; static AsyncAuthenticationMiddleware digestAuthHash; +static AsyncAuthenticationMiddleware bearerAuthSharedKey; +static AsyncAuthenticationMiddleware bearerAuthJWT; + // complex authentication which adds request attributes for the next middlewares and handler static AsyncMiddlewareFunction complexAuth([](AsyncWebServerRequest *request, ArMiddlewareNext next) { - if (!request->authenticate("user", "password")) { + if (request->authenticate("Mathieu", "password")) { + request->setAttribute("user", "Mathieu"); + } else if (request->authenticate("Bob", "password")) { + request->setAttribute("user", "Bob"); + } else { return request->requestAuthentication(); } - // add attributes to the request for the next middlewares and handler - request->setAttribute("user", "Mathieu"); - request->setAttribute("role", "staff"); - if (request->hasParam("token")) { - request->setAttribute("token", request->getParam("token")->value().c_str()); + if (request->getAttribute("user") == "Mathieu") { + request->setAttribute("role", "staff"); + } else { + request->setAttribute("role", "user"); } next(); }); static AsyncAuthorizationMiddleware authz([](AsyncWebServerRequest *request) { - return request->getAttribute("token") == "123"; + return request->getAttribute("role") == "staff"; }); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -87,6 +93,36 @@ void setup() { digestAuthHash.setAuthFailureMessage("Authentication failed"); digestAuthHash.setAuthType(AsyncAuthType::AUTH_DIGEST); + // bearer authentication with shared key + bearerAuthSharedKey.setAuthType(AsyncAuthType::AUTH_BEARER); + bearerAuthSharedKey.setToken("shared-secret-key"); + + // bearer authentication with a JWT token + bearerAuthJWT.setAuthType(AsyncAuthType::AUTH_BEARER); + bearerAuthJWT.setAuthentificationFunction([](AsyncWebServerRequest *request) { + const String &token = request->authChallenge(); + // 1. decode base64 token + // 2. decrypt token + const String &decrypted = "..."; // TODO + // 3. validate token (check signature, expiration, etc) + bool valid = token == "" || token == ""; + if (!valid) { + return false; + } + // 4. extract user info from token and set request attributes + if (token == "") { + request->setAttribute("user", "Mathieu"); + request->setAttribute("role", "staff"); + return true; // return true if token is valid, false otherwise + } + if (token == "") { + request->setAttribute("user", "Bob"); + request->setAttribute("role", "user"); + return true; // return true if token is valid, false otherwise + } + return false; + }); + // basic authentication method // curl -v -u admin:admin http://192.168.4.1/auth-basic server @@ -132,9 +168,9 @@ void setup() { .addMiddleware(&digestAuthHash); // test digest auth custom authorization middleware - // curl -v --digest -u user:password http://192.168.4.1/auth-custom?token=123 => OK - // curl -v --digest -u user:password http://192.168.4.1/auth-custom?token=456 => 403 - // curl -v --digest -u user:FAILED http://192.168.4.1/auth-custom?token=456 => 401 + // curl -v --digest -u Mathieu:password http://192.168.4.1/auth-custom => OK + // curl -v --digest -u Bob:password http://192.168.4.1/auth-custom => 403 + // curl -v --digest -u any:password http://192.168.4.1/auth-custom => 401 server .on( "/auth-custom", HTTP_GET, @@ -148,6 +184,32 @@ void setup() { ) .addMiddlewares({&complexAuth, &authz}); + // Bearer authentication with a shared key + // curl -v -H "Authorization: Bearer shared-secret-key" http://192.168.4.1/auth-bearer-shared-key => OK + server + .on( + "/auth-bearer-shared-key", HTTP_GET, + [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "Hello, world!"); + } + ) + .addMiddleware(&bearerAuthSharedKey); + + // Bearer authentication with a JWT token + // curl -v -H "Authorization: Bearer " http://192.168.4.1/auth-bearer-jwt => OK + // curl -v -H "Authorization: Bearer " http://192.168.4.1/auth-bearer-jwt => 403 Forbidden + // curl -v -H "Authorization: Bearer invalid-token" http://192.168.4.1/auth-bearer-jwt => 401 Unauthorized + server + .on( + "/auth-bearer-jwt", HTTP_GET, + [](AsyncWebServerRequest *request) { + Serial.println("User: " + request->getAttribute("user")); + Serial.println("Role: " + request->getAttribute("role")); + request->send(200, "text/plain", "Hello, world!"); + } + ) + .addMiddlewares({&bearerAuthJWT, &authz}); + server.begin(); } diff --git a/examples/CORS/CORS.ino b/examples/CORS/CORS.ino index 647d555..e912c6e 100644 --- a/examples/CORS/CORS.ino +++ b/examples/CORS/CORS.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // How to use CORS middleware @@ -25,7 +25,7 @@ static AsyncCorsMiddleware cors; void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/CaptivePortal/CaptivePortal.ino b/examples/CaptivePortal/CaptivePortal.ino index 0b8c317..820ac03 100644 --- a/examples/CaptivePortal/CaptivePortal.ino +++ b/examples/CaptivePortal/CaptivePortal.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles #include #if defined(ESP32) || defined(LIBRETINY) @@ -28,7 +28,7 @@ public: response->print("Captive Portal"); response->print("

This is our captive portal front page.

"); response->printf("

You were trying to reach: http://%s%s

", request->host().c_str(), request->url().c_str()); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED response->printf("

Try opening this link instead

", WiFi.softAPIP().toString().c_str()); #endif response->print(""); @@ -41,7 +41,7 @@ void setup() { Serial.println(); Serial.println("Configuring access point..."); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED if (!WiFi.softAP("esp-captive")) { Serial.println("Soft AP creation failed."); while (1); diff --git a/examples/CatchAllHandler/CatchAllHandler.ino b/examples/CatchAllHandler/CatchAllHandler.ino index fb01410..ad3fcf1 100644 --- a/examples/CatchAllHandler/CatchAllHandler.ino +++ b/examples/CatchAllHandler/CatchAllHandler.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to catch all requests and send a 404 Not Found response @@ -86,7 +86,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/ChunkResponse/ChunkResponse.ino b/examples/ChunkResponse/ChunkResponse.ino index 52c31c0..2877e68 100644 --- a/examples/ChunkResponse/ChunkResponse.ino +++ b/examples/ChunkResponse/ChunkResponse.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Chunk response with caching example @@ -86,7 +86,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -94,11 +94,11 @@ void setup() { // first time: serves the file and cache headers // curl -N -v http://192.168.4.1/ --output - // - // secodn time: serves 304 + // second time: serves 304 // curl -N -v -H "if-none-match: 4272" http://192.168.4.1/ --output - // server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { - String etag = String(htmlContentLength); + String etag = "\"" + String(htmlContentLength) + "\""; // RFC9110: ETag must be enclosed in double quotes if (request->header(asyncsrv::T_INM) == etag) { request->send(304); diff --git a/examples/ChunkRetryResponse/ChunkRetryResponse.ino b/examples/ChunkRetryResponse/ChunkRetryResponse.ino index 4e67edb..0b01800 100644 --- a/examples/ChunkRetryResponse/ChunkRetryResponse.ino +++ b/examples/ChunkRetryResponse/ChunkRetryResponse.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to wait in a chunk response for incoming data @@ -19,12 +19,6 @@ #include -#if __has_include("ArduinoJson.h") -#include -#include -#include -#endif - static const char *htmlContent PROGMEM = R"( @@ -96,7 +90,7 @@ static int key = -1; void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -107,7 +101,7 @@ void setup() { server.addMiddleware(&requestLogger); -#if __has_include("ArduinoJson.h") +#if ASYNC_JSON_SUPPORT == 1 // // HOW TO RUN THIS EXAMPLE: @@ -174,7 +168,7 @@ void setup() { return 0; // 0 means we are done } - // log_d("UART answered!"); + // async_ws_log_d("UART answered!"); String answer = "You typed: "; answer.concat((char)key); @@ -193,10 +187,10 @@ void setup() { }, NULL, // upload handler is not used so it should be NULL [](AsyncWebServerRequest *request, uint8_t *data, size_t len, size_t index, size_t total) { - // log_d("Body: index: %u, len: %u, total: %u", index, len, total); + // async_ws_log_d("Body: index: %u, len: %u, total: %u", index, len, total); if (!index) { - // log_d("Start body parsing"); + // async_ws_log_d("Start body parsing"); request->_tempObject = new String(); // cast request->_tempObject pointer to String and reserve total size ((String *)request->_tempObject)->reserve(total); @@ -204,7 +198,7 @@ void setup() { request->client()->setRxTimeout(30); } - // log_d("Append body data"); + // async_ws_log_d("Append body data"); ((String *)request->_tempObject)->concat((const char *)data, len); } ); @@ -217,13 +211,13 @@ void setup() { void loop() { if (triggerUART.length() && key == -1) { Serial.println(triggerUART); - // log_d("Waiting for UART input..."); + // async_ws_log_d("Waiting for UART input..."); while (!Serial.available()) { delay(100); } key = Serial.read(); Serial.flush(); - // log_d("UART input: %c", key); + // async_ws_log_d("UART input: %c", key); triggerUART = emptyString; } } diff --git a/examples/EndBegin/EndBegin.ino b/examples/EndBegin/EndBegin.ino index 8e91fcf..08bbedd 100644 --- a/examples/EndBegin/EndBegin.ino +++ b/examples/EndBegin/EndBegin.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // https://github.com/ESP32Async/ESPAsyncWebServer/discussions/23 @@ -24,7 +24,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -39,6 +39,10 @@ void setup() { Serial.println("end()"); server.end(); + + Serial.println("waiting before restarting server..."); + delay(100); + server.begin(); Serial.println("begin() - run: curl -v http://192.168.4.1/ => should succeed"); } diff --git a/examples/Filters/Filters.ino b/examples/Filters/Filters.ino index bcdb5b3..582345c 100644 --- a/examples/Filters/Filters.ino +++ b/examples/Filters/Filters.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to use setFilter to route requests to different handlers based on WiFi mode @@ -32,7 +32,7 @@ public: response->print("Captive Portal"); response->print("

This is out captive portal front page.

"); response->printf("

You were trying to reach: http://%s%s

", request->host().c_str(), request->url().c_str()); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED response->printf("

Try opening this link instead

", WiFi.softAPIP().toString().c_str()); #endif response->print(""); @@ -51,17 +51,17 @@ void setup() { "/", HTTP_GET, [](AsyncWebServerRequest *request) { Serial.println("Captive portal request..."); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED Serial.println("WiFi.localIP(): " + WiFi.localIP().toString()); #endif Serial.println("request->client()->localIP(): " + request->client()->localIP().toString()); #if ESP_IDF_VERSION_MAJOR >= 5 -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED Serial.println("WiFi.type(): " + String((int)WiFi.localIP().type())); #endif Serial.println("request->client()->type(): " + String((int)request->client()->localIP().type())); #endif -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED Serial.println(WiFi.localIP() == request->client()->localIP() ? "should be: ON_STA_FILTER" : "should be: ON_AP_FILTER"); Serial.println(WiFi.localIP() == request->client()->localIP()); Serial.println(WiFi.localIP().toString() == request->client()->localIP().toString()); @@ -77,17 +77,17 @@ void setup() { "/", HTTP_GET, [](AsyncWebServerRequest *request) { Serial.println("Website request..."); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED Serial.println("WiFi.localIP(): " + WiFi.localIP().toString()); #endif Serial.println("request->client()->localIP(): " + request->client()->localIP().toString()); #if ESP_IDF_VERSION_MAJOR >= 5 -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED Serial.println("WiFi.type(): " + String((int)WiFi.localIP().type())); #endif Serial.println("request->client()->type(): " + String((int)request->client()->localIP().type())); #endif -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED Serial.println(WiFi.localIP() == request->client()->localIP() ? "should be: ON_STA_FILTER" : "should be: ON_AP_FILTER"); Serial.println(WiFi.localIP() == request->client()->localIP()); Serial.println(WiFi.localIP().toString() == request->client()->localIP().toString()); @@ -113,7 +113,7 @@ void setup() { // dnsServer.stop(); // WiFi.softAPdisconnect(); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.persistent(false); WiFi.begin("IoT"); while (WiFi.status() != WL_CONNECTED) { diff --git a/examples/FlashResponse/FlashResponse.ino b/examples/FlashResponse/FlashResponse.ino index 5763f22..e07b1dd 100644 --- a/examples/FlashResponse/FlashResponse.ino +++ b/examples/FlashResponse/FlashResponse.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to serve a large HTML page from flash memory without copying it to heap in a temporary buffer @@ -86,7 +86,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/HeaderManipulation/HeaderManipulation.ino b/examples/HeaderManipulation/HeaderManipulation.ino index 5b4c9f7..04227ce 100644 --- a/examples/HeaderManipulation/HeaderManipulation.ino +++ b/examples/HeaderManipulation/HeaderManipulation.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Show how to manipulate headers in the request / response @@ -33,7 +33,7 @@ AsyncHeaderFreeMiddleware headerFree; void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Headers/Headers.ino b/examples/Headers/Headers.ino index eee87ac..7c32b48 100644 --- a/examples/Headers/Headers.ino +++ b/examples/Headers/Headers.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Query and send headers @@ -24,7 +24,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Json/Json.ino b/examples/Json/Json.ino index a29fec1..eeaab75 100644 --- a/examples/Json/Json.ino +++ b/examples/Json/Json.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to send and receive Json data @@ -19,27 +19,21 @@ #include -#if __has_include("ArduinoJson.h") -#include -#include -#include -#endif - static AsyncWebServer server(80); -#if __has_include("ArduinoJson.h") +#if ASYNC_JSON_SUPPORT == 1 static AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler("/json2"); #endif void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif -#if __has_include("ArduinoJson.h") +#if ASYNC_JSON_SUPPORT == 1 // // sends JSON using AsyncJsonResponse // @@ -62,8 +56,8 @@ void setup() { JsonDocument doc; JsonObject root = doc.to(); root["foo"] = "bar"; - serializeJson(root, *response); - Serial.println(); + // serializeJson(root, Serial); + // Serial.println(); request->send(response); }); @@ -92,12 +86,38 @@ void setup() { }); server.addHandler(handler); + + // New Json API since 3.8.2, which works for both Json and MessagePack bodies + // curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"You"}' http://192.168.4.1/json3 + + server.on("/json3", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &json) { + Serial.printf("Body request : "); + serializeJson(json, Serial); + Serial.println(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot().to(); + root["hello"] = json.as()["name"]; + response->setLength(); + request->send(response); + }); #endif server.begin(); } -// not needed +static uint32_t lastHeapTime = 0; +static uint32_t lastHeap = 0; + void loop() { - delay(100); +#ifdef ESP32 + uint32_t now = millis(); + if (now - lastHeapTime >= 500) { + uint32_t heap = ESP.getFreeHeap(); + if (heap != lastHeap) { + lastHeap = heap; + async_ws_log_w("Free heap: %" PRIu32, heap); + } + lastHeapTime = now; + } +#endif } diff --git a/examples/LargeResponse/LargeResponse.ino b/examples/LargeResponse/LargeResponse.ino new file mode 100644 index 0000000..8d9d3bf --- /dev/null +++ b/examples/LargeResponse/LargeResponse.ino @@ -0,0 +1,178 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles + +// +// Example to send a large response and control the filling of the buffer. +// +// This is also a MRE for: +// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/242 +// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/315 +// + +#include +#if defined(ESP32) || defined(LIBRETINY) +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include +#include +#endif + +#include + +static AsyncWebServer server(80); + +static const size_t totalResponseSize = 16 * 1000; // 16 KB +static char fillChar = 'A'; + +class CustomResponse : public AsyncAbstractResponse { +public: + explicit CustomResponse() { + _code = 200; + _contentType = "text/plain"; + _sendContentLength = false; + } + + bool _sourceValid() const override { + return true; + } + + size_t _fillBuffer(uint8_t *buf, size_t buflen) override { + if (_sent == RESPONSE_TRY_AGAIN) { + Serial.println("Simulating temporary unavailability of data..."); + _sent = 0; + return RESPONSE_TRY_AGAIN; + } + size_t remaining = totalResponseSize - _sent; + if (remaining == 0) { + return 0; + } + if (buflen > remaining) { + buflen = remaining; + } + Serial.printf("Filling '%c' @ sent: %u, buflen: %u\n", fillChar, _sent, buflen); + std::fill_n(buf, buflen, static_cast(fillChar)); + _sent += buflen; + fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1; + return buflen; + } + +private: + char fillChar = 'A'; + size_t _sent = 0; +}; + +// Code to reproduce issues: +// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/242 +// - https://github.com/ESP32Async/ESPAsyncWebServer/issues/315 +// +// https://github.com/ESP32Async/ESPAsyncWebServer/pull/317#issuecomment-3421141039 +// +// I cracked it. +// So this is how it works: +// That space that _tcp is writing to identified by CONFIG_TCP_SND_BUF_DEFAULT (and is value-matching with default TCP windows size which is very confusing itself). +// The space returned by client()->write() and client->space() somehow might not be atomically/thread synced (had not dived that deep yet). So if first call to _fillBuffer is done via user-code thread and ended up with some small amount of data consumed and second one is done by _poll or _ack? returns full size again! This is where old code fails. +// If you change your class this way it will fail 100%. +class CustomResponseMRE : public AsyncAbstractResponse { +public: + explicit CustomResponseMRE() { + _code = 200; + _contentType = "text/plain"; + _sendContentLength = false; + // add some useless headers + addHeader("Clear-Site-Data", "Clears browsing data (e.g., cookies, storage, cache) associated with the requesting website."); + addHeader( + "No-Vary-Search", "Specifies a set of rules that define how a URL's query parameters will affect cache matching. These rules dictate whether the same " + "URL with different URL parameters should be saved as separate browser cache entries" + ); + } + + bool _sourceValid() const override { + return true; + } + + size_t _fillBuffer(uint8_t *buf, size_t buflen) override { + if (fillChar == NULL) { + fillChar = 'A'; + return RESPONSE_TRY_AGAIN; + } + if (_sent == RESPONSE_TRY_AGAIN) { + Serial.println("Simulating temporary unavailability of data..."); + _sent = 0; + return RESPONSE_TRY_AGAIN; + } + size_t remaining = totalResponseSize - _sent; + if (remaining == 0) { + return 0; + } + if (buflen > remaining) { + buflen = remaining; + } + Serial.printf("Filling '%c' @ sent: %u, buflen: %u\n", fillChar, _sent, buflen); + std::fill_n(buf, buflen, static_cast(fillChar)); + _sent += buflen; + fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1; + return buflen; + } + +private: + char fillChar = NULL; + size_t _sent = 0; +}; + +void setup() { + Serial.begin(115200); + +#if ASYNCWEBSERVER_WIFI_SUPPORTED + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); +#endif + + // Example to use a AwsResponseFiller + // + // curl -v http://192.168.4.1/1 | grep -o '.' | sort | uniq -c + // + // Should output 16000 and a distribution of letters which is the same in ESP32 logs and console + // + server.on("/1", HTTP_GET, [](AsyncWebServerRequest *request) { + fillChar = 'A'; + AsyncWebServerResponse *response = request->beginResponse("text/plain", totalResponseSize, [](uint8_t *buffer, size_t maxLen, size_t index) -> size_t { + size_t remaining = totalResponseSize - index; + size_t toSend = (remaining < maxLen) ? remaining : maxLen; + Serial.printf("Filling '%c' @ index: %u, maxLen: %u, toSend: %u\n", fillChar, index, maxLen, toSend); + std::fill_n(buffer, toSend, static_cast(fillChar)); + fillChar = (fillChar == 'Z') ? 'A' : fillChar + 1; + return toSend; + }); + request->send(response); + }); + + // Example to use a AsyncAbstractResponse + // + // curl -v http://192.168.4.1/2 | grep -o '.' | sort | uniq -c + // + // Should output 16000 and a distribution of letters which is the same in ESP32 logs and console + // + server.on("/2", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(new CustomResponse()); + }); + + // Example to use a AsyncAbstractResponse + // + // curl -v http://192.168.4.1/3 | grep -o '.' | sort | uniq -c + // + // Should output 16000 and a distribution of letters which is the same in ESP32 logs and console + // + server.on("/3", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(new CustomResponseMRE()); + }); + + server.begin(); +} + +void loop() { + delay(100); +} diff --git a/examples/Logging/Logging.ino b/examples/Logging/Logging.ino index ae504b2..e716f6f 100644 --- a/examples/Logging/Logging.ino +++ b/examples/Logging/Logging.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Show how to log the incoming request and response as a curl-like syntax @@ -25,7 +25,7 @@ static AsyncLoggingMiddleware requestLogger; void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/MessagePack/MessagePack.ino b/examples/MessagePack/MessagePack.ino index e038d03..678a5a2 100644 --- a/examples/MessagePack/MessagePack.ino +++ b/examples/MessagePack/MessagePack.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to send and receive Message Pack data @@ -19,27 +19,21 @@ #include -#if __has_include("ArduinoJson.h") -#include -#include -#include -#endif - static AsyncWebServer server(80); -#if __has_include("ArduinoJson.h") -static AsyncCallbackMessagePackWebHandler *handler = new AsyncCallbackMessagePackWebHandler("/msgpack2"); +#if ASYNC_JSON_SUPPORT == 1 +static AsyncCallbackJsonWebHandler *handler = new AsyncCallbackJsonWebHandler("/msgpack2"); #endif void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif -#if __has_include("ArduinoJson.h") +#if ASYNC_JSON_SUPPORT == 1 // // sends MessagePack using AsyncMessagePackResponse // @@ -57,18 +51,26 @@ void setup() { // // curl -v http://192.168.4.1/msgpack2 // + // Save file: curl -v http://192.168.4.1/msgpack2 -o msgpack.bin + // server.on("/msgpack2", HTTP_GET, [](AsyncWebServerRequest *request) { AsyncResponseStream *response = request->beginResponseStream("application/msgpack"); JsonDocument doc; JsonObject root = doc.to(); - root["foo"] = "bar"; + root["name"] = "Bob"; serializeMsgPack(root, *response); request->send(response); }); + // POST file: + // + // curl -v -X POST -H 'Content-Type: application/msgpack' --data-binary @msgpack.bin http://192.168.4.1/msgpack2 + // handler->setMethod(HTTP_POST | HTTP_PUT); handler->onRequest([](AsyncWebServerRequest *request, JsonVariant &json) { + Serial.printf("Body request /msgpack2 : "); // should print: Body request /msgpack2 : {"name":"Bob"} serializeJson(json, Serial); + Serial.println(); AsyncMessagePackResponse *response = new AsyncMessagePackResponse(); JsonObject root = response->getRoot().to(); root["hello"] = json.as()["name"]; @@ -77,6 +79,22 @@ void setup() { }); server.addHandler(handler); + + // New Json API since 3.8.2, which works for both Json and MessagePack bodies + // + // curl -v -X POST -H 'Content-Type: application/json' -d '{"name":"You"}' http://192.168.4.1/msgpack3 + // curl -v -X POST -H 'Content-Type: application/msgpack' --data-binary @msgpack.bin http://192.168.4.1/msgpack3 + // + server.on("/msgpack3", HTTP_POST, [](AsyncWebServerRequest *request, JsonVariant &json) { + Serial.printf("Body request /msgpack3 : "); // should print: Body request /msgpack3 : {"name":"Bob"} + serializeJson(json, Serial); + Serial.println(); + AsyncJsonResponse *response = new AsyncJsonResponse(); + JsonObject root = response->getRoot().to(); + root["hello"] = json.as()["name"]; + response->setLength(); + request->send(response); + }); #endif server.begin(); diff --git a/examples/Middleware/Middleware.ino b/examples/Middleware/Middleware.ino index 992a0a2..c1ecdec 100644 --- a/examples/Middleware/Middleware.ino +++ b/examples/Middleware/Middleware.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Show how to sue Middleware @@ -34,7 +34,7 @@ public: void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Params/Params.ino b/examples/Params/Params.ino index 416218c..b27fdff 100644 --- a/examples/Params/Params.ino +++ b/examples/Params/Params.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Query parameters and body parameters @@ -74,7 +74,7 @@ static const size_t htmlContentLength = strlen_P(htmlContent); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/PartitionDownloader/PartitionDownloader.ino b/examples/PartitionDownloader/PartitionDownloader.ino index 1174640..d45e387 100644 --- a/examples/PartitionDownloader/PartitionDownloader.ino +++ b/examples/PartitionDownloader/PartitionDownloader.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // - Download ESP32 partition by name and/or type and/or subtype @@ -34,7 +34,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/PerfTests/PerfTests.ino b/examples/PerfTests/PerfTests.ino index 001512c..d79268c 100644 --- a/examples/PerfTests/PerfTests.ino +++ b/examples/PerfTests/PerfTests.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Perf tests @@ -91,7 +91,7 @@ static volatile size_t requests = 0; void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -118,9 +118,8 @@ void setup() { // HTTP endpoint // - // > brew install autocannon - // > autocannon -c 10 -w 10 -d 20 http://192.168.4.1 - // > autocannon -c 16 -w 16 -d 20 http://192.168.4.1 + // > autocannon -c 16 -w 16 -d 20 --renderStatusCodes http://192.168.4.1/ + // > ab -c 16 -t 20 http://192.168.4.1/ // server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { // need to cast to uint8_t* @@ -142,6 +141,11 @@ void setup() { // // time curl -N -v -G -d 'd=2000' -d 'l=10000' http://192.168.4.1/slow.html --output - // + // THIS CODE WILL CRASH BECAUSE OF THE WATCHDOG. + // IF YOU REALLY NEED TO DO THIS, YOU MUST DISABLE THE TWDT + // + // CORRECT WAY IS TO USE SSE OR WEBSOCKETS TO DO THE COSTLY PROCESSING ASYNC. + // server.on("/slow.html", HTTP_GET, [](AsyncWebServerRequest *request) { requests = requests + 1; uint32_t d = request->getParam("d")->value().toInt(); @@ -168,7 +172,6 @@ void setup() { // SSS endpoint // // launch 16 concurrent workers for 30 seconds - // > for i in {1..10}; do ( count=$(gtimeout 30 curl -s -N -H "Accept: text/event-stream" http://192.168.4.1/events 2>&1 | grep -c "^data:"); echo "Total: $count events, $(echo "$count / 4" | bc -l) events / second" ) & done; // > for i in {1..16}; do ( count=$(gtimeout 30 curl -s -N -H "Accept: text/event-stream" http://192.168.4.1/events 2>&1 | grep -c "^data:"); echo "Total: $count events, $(echo "$count / 4" | bc -l) events / second" ) & done; // // With AsyncTCP, with 16 workers: a lot of "Event message queue overflow: discard message", no crash diff --git a/examples/RateLimit/RateLimit.ino b/examples/RateLimit/RateLimit.ino index 5ae93e7..e87736d 100644 --- a/examples/RateLimit/RateLimit.ino +++ b/examples/RateLimit/RateLimit.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Show how to rate limit the server or some endpoints @@ -25,7 +25,7 @@ static AsyncRateLimitMiddleware rateLimit; void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Redirect/Redirect.ino b/examples/Redirect/Redirect.ino index 8f10557..410a86d 100644 --- a/examples/Redirect/Redirect.ino +++ b/examples/Redirect/Redirect.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to redirect @@ -24,7 +24,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/RequestContinuation/RequestContinuation.ino b/examples/RequestContinuation/RequestContinuation.ino index f59322e..43d0fc5 100644 --- a/examples/RequestContinuation/RequestContinuation.ino +++ b/examples/RequestContinuation/RequestContinuation.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to use request continuation to pause a request for a long processing task, and be able to resume it later. @@ -34,7 +34,7 @@ static AsyncWebServerRequestPtr gpioRequest; void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/RequestContinuationComplete/RequestContinuationComplete.ino b/examples/RequestContinuationComplete/RequestContinuationComplete.ino index cb4a53f..5465459 100644 --- a/examples/RequestContinuationComplete/RequestContinuationComplete.ino +++ b/examples/RequestContinuationComplete/RequestContinuationComplete.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to use request continuation to pause a request for a long processing task, and be able to resume it later. @@ -94,7 +94,7 @@ static bool processLongRunningOperation(LongRunningOperation *op) { void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/ResumableDownload/ResumableDownload.ino b/examples/ResumableDownload/ResumableDownload.ino index 68646e7..a8a4550 100644 --- a/examples/ResumableDownload/ResumableDownload.ino +++ b/examples/ResumableDownload/ResumableDownload.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Make sure resumable downloads can be implemented (HEAD request / response and Range header) @@ -24,7 +24,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Rewrite/Rewrite.ino b/examples/Rewrite/Rewrite.ino index 8dfeedc..533e6bb 100644 --- a/examples/Rewrite/Rewrite.ino +++ b/examples/Rewrite/Rewrite.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to rewrite URLs @@ -24,7 +24,7 @@ static AsyncWebServer server(80); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/ServerSentEvents/ServerSentEvents.ino b/examples/ServerSentEvents/ServerSentEvents.ino index 5567ec6..bc6718c 100644 --- a/examples/ServerSentEvents/ServerSentEvents.ino +++ b/examples/ServerSentEvents/ServerSentEvents.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // SSE example @@ -58,7 +58,7 @@ static AsyncEventSource events("/events"); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -71,12 +71,12 @@ void setup() { }); events.onConnect([](AsyncEventSourceClient *client) { - Serial.printf("SSE Client connected! ID: %" PRIu32 "\n", client->lastId()); + Serial.printf("SSE Client connected!"); client->send("hello!", NULL, millis(), 1000); }); events.onDisconnect([](AsyncEventSourceClient *client) { - Serial.printf("SSE Client disconnected! ID: %" PRIu32 "\n", client->lastId()); + Serial.printf("SSE Client disconnected!"); }); server.addHandler(&events); diff --git a/examples/ServerSentEvents_PR156/ServerSentEvents_PR156.ino b/examples/ServerSentEvents_PR156/ServerSentEvents_PR156.ino index cced715..ad1864a 100644 --- a/examples/ServerSentEvents_PR156/ServerSentEvents_PR156.ino +++ b/examples/ServerSentEvents_PR156/ServerSentEvents_PR156.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // SSE example @@ -64,7 +64,7 @@ static constexpr uint32_t timeoutClose = 15000; void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/ServerState/ServerState.ino b/examples/ServerState/ServerState.ino index 4ceddbc..e23d497 100644 --- a/examples/ServerState/ServerState.ino +++ b/examples/ServerState/ServerState.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Server state example @@ -25,7 +25,7 @@ static AsyncWebServer server2(80); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/SkipServerMiddleware/SkipServerMiddleware.ino b/examples/SkipServerMiddleware/SkipServerMiddleware.ino index 0e7f172..bee7561 100644 --- a/examples/SkipServerMiddleware/SkipServerMiddleware.ino +++ b/examples/SkipServerMiddleware/SkipServerMiddleware.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Authentication and authorization middlewares @@ -27,7 +27,7 @@ static AsyncLoggingMiddleware logging; void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/SlowChunkResponse/SlowChunkResponse.ino b/examples/SlowChunkResponse/SlowChunkResponse.ino index 7844ad6..8859750 100644 --- a/examples/SlowChunkResponse/SlowChunkResponse.ino +++ b/examples/SlowChunkResponse/SlowChunkResponse.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Simulate a slow response in a chunk response (like file download from SD Card) @@ -89,7 +89,7 @@ static size_t charactersIndex = 0; void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -114,6 +114,11 @@ void setup() { // // time curl -N -v -G -d 'd=2000' -d 'l=10000' http://192.168.4.1/slow.html --output - // + // THIS CODE WILL CRASH BECAUSE OF THE WATCHDOG. + // IF YOU REALLY NEED TO DO THIS, YOU MUST DISABLE THE TWDT + // + // CORRECT WAY IS TO USE SSE OR WEBSOCKETS TO DO THE COSTLY PROCESSING ASYNC. + // server.on("/slow.html", HTTP_GET, [](AsyncWebServerRequest *request) { uint32_t d = request->getParam("d")->value().toInt(); uint32_t l = request->getParam("l")->value().toInt(); diff --git a/examples/StaticFile/StaticFile.ino b/examples/StaticFile/StaticFile.ino index edc2cb2..6e4fb09 100644 --- a/examples/StaticFile/StaticFile.ino +++ b/examples/StaticFile/StaticFile.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to serve a static file @@ -111,7 +111,7 @@ static const size_t index2_html_gz_len = sizeof(index2_html_gz); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif diff --git a/examples/Templates/Templates.ino b/examples/Templates/Templates.ino index fdf4eb6..679188c 100644 --- a/examples/Templates/Templates.ino +++ b/examples/Templates/Templates.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Shows how to serve a static and dynamic template @@ -33,10 +33,23 @@ static const char *htmlContent PROGMEM = R"( static const size_t htmlContentLength = strlen_P(htmlContent); +// Variables used for dynamic cacheable template +static unsigned uptimeInMinutes = 0; +static AsyncStaticWebHandler *uptimeHandler = nullptr; + +// Utility function for performing that update +static void setUptimeInMinutes(unsigned t) { + uptimeInMinutes = t; + // Update caching header with a new value as well + if (uptimeHandler) { + uptimeHandler->setLastModified(); + } +} + void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -56,35 +69,68 @@ void setup() { // Serve the static template file // + // This call will have caching headers automatically added as it is a static file. + // // curl -v http://192.168.4.1/template.html server.serveStatic("/template.html", LittleFS, "/template.html"); - // Serve the static template with a template processor + // Serve a template with dynamic content // - // ServeStatic static is used to serve static output which never changes over time. - // This special endpoints automatically adds caching headers. - // If a template processor is used, it must ensure that the outputted content will always be the same over time and never changes. - // Otherwise, do not use serveStatic. - // Example below: IP never changes. + // serveStatic recognizes that template processing is in use, and will not automatically + // add caching headers. // - // curl -v http://192.168.4.1/index.html - server.serveStatic("/index.html", LittleFS, "/template.html").setTemplateProcessor([](const String &var) -> String { + // curl -v http://192.168.4.1/dynamic.html + server.serveStatic("/dynamic.html", LittleFS, "/template.html").setTemplateProcessor([](const String &var) -> String { if (var == "USER") { - return "Bob"; + return String("Bob ") + millis(); } return emptyString; }); - // Serve a template with dynamic content + // Serve a static template with a template processor // - // to serve a template with dynamic content (output changes over time), use normal - // Example below: content changes over tinme do not use serveStatic. + // By explicitly calling setLastModified() on the handler object, we enable + // sending the caching headers, even when a template is in use. + // This pattern should never be used with template data that can change. + // Example below: USER never changes. // - // curl -v http://192.168.4.1/dynamic.html - server.on("/dynamic.html", HTTP_GET, [](AsyncWebServerRequest *request) { - request->send(LittleFS, "/template.html", "text/html", false, [](const String &var) -> String { + // curl -v http://192.168.4.1/index.html + server.serveStatic("/index.html", LittleFS, "/template.html") + .setTemplateProcessor([](const String &var) -> String { if (var == "USER") { - return String("Bob ") + millis(); + return "Bob"; + } + return emptyString; + }) + .setLastModified("Sun, 28 Sep 2025 01:02:03 GMT"); + + // Serve a template with dynamic content *and* caching + // + // The data used in this template is updated in loop(). loop() is then responsible + // for calling setLastModified() on the handler object to notify any caches that + // the data has changed. + // + // curl -v http://192.168.4.1/uptime.html + uptimeHandler = &server.serveStatic("/uptime.html", LittleFS, "/template.html").setTemplateProcessor([](const String &var) -> String { + if (var == "USER") { + return String("Bob ") + uptimeInMinutes + " minutes"; + } + return emptyString; + }); + + // Serve a template with dynamic content based on user request + // + // In this case, the template is served via a callback request. Data from the request + // is used to generate the template callback. + // + // curl -v -G -d "USER=Bob" http://192.168.4.1/user_request.html + server.on("/user_request.html", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(LittleFS, "/template.html", "text/html", false, [=](const String &var) -> String { + if (var == "USER") { + const AsyncWebParameter *param = request->getParam("USER"); + if (param) { + return param->value(); + } } return emptyString; }); @@ -96,4 +142,11 @@ void setup() { // not needed void loop() { delay(100); + + // Compute uptime + unsigned currentUptimeInMinutes = millis() / (60 * 1000); + + if (currentUptimeInMinutes != uptimeInMinutes) { + setUptimeInMinutes(currentUptimeInMinutes); + } } diff --git a/examples/URIMatcher/README.md b/examples/URIMatcher/README.md new file mode 100644 index 0000000..be785e3 --- /dev/null +++ b/examples/URIMatcher/README.md @@ -0,0 +1,349 @@ +# AsyncURIMatcher Example + +This example demonstrates the comprehensive URI matching capabilities of the ESPAsyncWebServer library using the `AsyncURIMatcher` class. + +## Overview + +The `AsyncURIMatcher` class provides flexible and powerful URL routing mechanisms that go beyond simple string matching. It supports various matching strategies that can be combined to create sophisticated routing rules. + +**Important**: When using plain strings (not `AsyncURIMatcher` objects), the library uses auto-detection (`URIMatchAuto`) which analyzes the URI pattern and applies appropriate matching rules. This is **not** simple exact matching - it combines exact and folder matching by default! + +## What's Demonstrated + +This example includes two Arduino sketches: + +1. **URIMatcher.ino** - Interactive web-based demonstration with a user-friendly homepage +2. **URIMatcherTest.ino** - Comprehensive test suite with automated shell script testing + +Both sketches create a WiFi Access Point (`esp-captive`) for easy testing without network configuration. + +## Auto-Detection Behavior + +When you pass a plain string or `const char*` to `server.on()`, the `URIMatchAuto` flag is used, which: + +1. **Empty URI**: Matches everything +2. **Ends with `*`**: Becomes prefix match (`URIMatchPrefix`) +3. **Contains `/*.ext`**: Becomes extension match (`URIMatchExtension`) +4. **Starts with `^` and ends with `$`**: Becomes regex match (if enabled) +5. **Everything else**: Becomes **both** exact and folder match (`URIMatchPrefixFolder | URIMatchExact`) + +This means traditional string-based routes like `server.on("/path", handler)` will match: + +- `/path` (exact match) +- `/path/` (folder with trailing slash) +- `/path/anything` (folder match) + +But will **NOT** match `/path-suffix` (prefix without folder separator). + +## Features Demonstrated + +### 1. **Auto-Detection (Traditional Behavior)** + +Demonstrates how traditional string-based routing automatically combines exact and folder matching. + +**Examples in URIMatcher.ino:** + +- `/auto` - Matches both `/auto` exactly AND `/auto/sub` as folder +- `/wildcard*` - Auto-detects as prefix match (due to trailing `*`) +- `/auto-images/*.png` - Auto-detects as extension match (due to `/*.ext` pattern) + +**Examples in URIMatcherTest.ino:** + +- `/exact` - Matches `/exact`, `/exact/`, and `/exact/sub` +- `/api/users` - Matches exact path and subpaths under `/api/users/` +- `/*.json` - Matches any `.json` file anywhere +- `/*.css` - Matches any `.css` file anywhere + +### 2. **Exact Matching (Factory Method)** + +Using `AsyncURIMatcher::exact()` matches only the exact URL, **NOT** subpaths. + +**Key difference from auto-detection:** `AsyncURIMatcher::exact("/path")` matches **only** `/path`, while `server.on("/path", ...)` matches both `/path` and `/path/sub`. + +**Examples in URIMatcher.ino:** + +- `AsyncURIMatcher::exact("/exact")` - Matches only `/exact` + +**Examples in URIMatcherTest.ino:** + +- `AsyncURIMatcher::exact("/factory/exact")` - Matches only `/factory/exact` +- Does NOT match `/factory/exact/sub` (404 response) + +### 3. **Prefix Matching** + +Using `AsyncURIMatcher::prefix()` matches URLs that start with the specified pattern. + +**Examples in URIMatcher.ino:** + +- `AsyncURIMatcher::prefix("/service")` - Matches `/service`, `/service-test`, `/service/status` + +**Examples in URIMatcherTest.ino:** + +- `AsyncURIMatcher::prefix("/factory/prefix")` - Matches `/factory/prefix`, `/factory/prefix-test`, `/factory/prefix/sub` +- Traditional: `/api/*` - Matches `/api/data`, `/api/v1/posts` +- Traditional: `/files/*` - Matches `/files/document.pdf`, `/files/images/photo.jpg` + +### 4. **Folder/Directory Matching** + +Using `AsyncURIMatcher::dir()` matches URLs under a directory (automatically adds trailing slash). + +**Important:** Directory matching requires a trailing slash in the URL - it does NOT match the directory itself. + +**Examples in URIMatcher.ino:** + +- `AsyncURIMatcher::dir("/admin")` - Matches `/admin/users`, `/admin/settings` +- Does NOT match `/admin` without trailing slash + +**Examples in URIMatcherTest.ino:** + +- `AsyncURIMatcher::dir("/factory/dir")` - Matches `/factory/dir/users`, `/factory/dir/sub/path` +- Does NOT match `/factory/dir` itself (404 response) + +### 5. **Extension Matching** + +Using `AsyncURIMatcher::ext()` matches files with specific extensions. + +**Examples in URIMatcher.ino:** + +- `AsyncURIMatcher::ext("/images/*.jpg")` - Matches `/images/photo.jpg`, `/images/sub/pic.jpg` + +**Examples in URIMatcherTest.ino:** + +- `AsyncURIMatcher::ext("/factory/files/*.txt")` - Matches `/factory/files/doc.txt`, `/factory/files/sub/readme.txt` +- Does NOT match `/factory/files/doc.pdf` (wrong extension) + +### 6. **Case Insensitive Matching** + +Using `AsyncURIMatcher::CaseInsensitive` flag matches URLs regardless of character case. + +**Examples in URIMatcher.ino:** + +- `AsyncURIMatcher::exact("/case", AsyncURIMatcher::CaseInsensitive)` - Matches `/case`, `/CASE`, `/CaSe` + +**Examples in URIMatcherTest.ino:** + +- Case insensitive exact: `/case/exact`, `/CASE/EXACT`, `/Case/Exact` all work +- Case insensitive prefix: `/case/prefix`, `/CASE/PREFIX-test`, `/Case/Prefix/sub` all work +- Case insensitive directory: `/case/dir/users`, `/CASE/DIR/admin`, `/Case/Dir/settings` all work +- Case insensitive extension: `/case/files/doc.pdf`, `/CASE/FILES/DOC.PDF`, `/Case/Files/Doc.Pdf` all work + +### 7. **Regular Expression Matching** + +Using `AsyncURIMatcher::regex()` for advanced pattern matching (requires `ASYNCWEBSERVER_REGEX`). + +**Examples in URIMatcher.ino:** + +```cpp +#ifdef ASYNCWEBSERVER_REGEX +AsyncURIMatcher::regex("^/user/([0-9]+)$") // Matches /user/123, captures ID +#endif +``` + +**Examples in URIMatcherTest.ino:** + +- Traditional regex: `^/user/([0-9]+)$` - Matches `/user/123`, `/user/456` +- Traditional regex: `^/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$` - Matches `/blog/2023/10/15` +- Factory regex: `AsyncURIMatcher::regex("^/factory/user/([0-9]+)$")` - Matches `/factory/user/123` +- Factory regex with multiple captures: `^/factory/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$` +- Case insensitive regex: `AsyncURIMatcher::regex("^/factory/search/(.+)$", AsyncURIMatcher::CaseInsensitive)` + +### 8. **Combined Flags** + +Multiple matching strategies can be combined using the `|` operator. + +**Examples in URIMatcher.ino:** + +- `AsyncURIMatcher::prefix("/MixedCase", AsyncURIMatcher::CaseInsensitive)` - Prefix match that's case insensitive + +### 9. **Special Matchers** + +**Examples in URIMatcherTest.ino:** + +- `AsyncURIMatcher::all()` - Matches all requests (used with POST method as catch-all) + +## Usage Patterns + +### Traditional String-based Routing (Auto-Detection) + +```cpp +// Auto-detection with exact + folder matching +server.on("/api", handler); // Matches /api AND /api/anything +server.on("/login", handler); // Matches /login AND /login/sub + +// Auto-detection with prefix matching +server.on("/prefix*", handler); // Matches /prefix, /prefix-test, /prefix/sub + +// Auto-detection with extension matching +server.on("/images/*.jpg", handler); // Matches /images/pic.jpg, /images/sub/pic.jpg +``` + +### Explicit AsyncURIMatcher Syntax + +### Explicit AsyncURIMatcher Syntax + +```cpp +// Exact matching only +server.on(AsyncURIMatcher("/path", URIMatchExact), handler); + +// Prefix matching only +server.on(AsyncURIMatcher("/api", URIMatchPrefix), handler); + +// Combined flags +server.on(AsyncURIMatcher("/api", URIMatchPrefix | URIMatchCaseInsensitive), handler); +``` + +### Factory Functions + +```cpp +// More readable and expressive +server.on(AsyncURIMatcher::exact("/login"), handler); +server.on(AsyncURIMatcher::prefix("/api"), handler); +server.on(AsyncURIMatcher::dir("/admin"), handler); +server.on(AsyncURIMatcher::ext("/images/*.jpg"), handler); + +#ifdef ASYNCWEBSERVER_REGEX +server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), handler); +#endif +``` + +## Available Flags + +| Flag | Description | +| ------------------------- | ----------------------------------------------------------- | +| `URIMatchAuto` | Auto-detect match type from pattern (default) | +| `URIMatchExact` | Exact URL match | +| `URIMatchPrefix` | Prefix match | +| `URIMatchPrefixFolder` | Folder prefix match (requires trailing /) | +| `URIMatchExtension` | File extension match pattern | +| `URIMatchCaseInsensitive` | Case insensitive matching | +| `URIMatchRegex` | Regular expression matching (requires ASYNCWEBSERVER_REGEX) | + +## Testing the Example + +1. **Upload the sketch** to your ESP32/ESP8266 +2. **Connect to WiFi AP**: `esp-captive` (no password required) +3. **Navigate to**: `http://192.168.4.1/` +4. **Explore the examples** by clicking the organized test links +5. **Monitor Serial output**: Open Serial Monitor to see detailed debugging information for each matched route + +### Test URLs Available (All Clickable from Homepage) + +**Auto-Detection Examples:** + +- `http://192.168.4.1/auto` (exact + folder match) +- `http://192.168.4.1/auto/sub` (folder match - same handler!) +- `http://192.168.4.1/wildcard-test` (auto-detected prefix) +- `http://192.168.4.1/auto-images/photo.png` (auto-detected extension) + +**Factory Method Examples:** + +- `http://192.168.4.1/exact` (AsyncURIMatcher::exact) +- `http://192.168.4.1/service/status` (AsyncURIMatcher::prefix) +- `http://192.168.4.1/admin/users` (AsyncURIMatcher::dir) +- `http://192.168.4.1/images/photo.jpg` (AsyncURIMatcher::ext) + +**Case Insensitive Examples:** + +- `http://192.168.4.1/case` (lowercase) +- `http://192.168.4.1/CASE` (uppercase) +- `http://192.168.4.1/CaSe` (mixed case) + +**Regex Examples (if ASYNCWEBSERVER_REGEX enabled):** + +- `http://192.168.4.1/user/123` (captures numeric ID) +- `http://192.168.4.1/user/456` (captures numeric ID) + +**Combined Flags Examples:** + +- `http://192.168.4.1/mixedcase-test` (prefix + case insensitive) +- `http://192.168.4.1/MIXEDCASE/sub` (prefix + case insensitive) + +### Console Output + +Each handler provides detailed debugging information via Serial output: + +``` +Auto-Detection Match (Traditional) +Matched URL: /auto +Uses auto-detection: exact + folder matching +``` + +``` +Factory Exact Match +Matched URL: /exact +Uses AsyncURIMatcher::exact() factory function +``` + +``` +Regex Match - User ID +Matched URL: /user/123 +Captured User ID: 123 +This regex matches /user/{number} pattern +``` + +## Compilation Options + +### Enable Regex Support + +To enable regular expression matching, compile with: + +``` +-D ASYNCWEBSERVER_REGEX +``` + +In PlatformIO, add to `platformio.ini`: + +```ini +build_flags = -D ASYNCWEBSERVER_REGEX +``` + +In Arduino IDE, add to your sketch: + +```cpp +#define ASYNCWEBSERVER_REGEX +``` + +## Performance Considerations + +1. **Exact matches** are fastest +2. **Prefix matches** are very efficient +3. **Regex matches** are slower but most flexible +4. **Case insensitive** matching adds minimal overhead +5. **Auto-detection** adds slight parsing overhead at construction time + +## Real-World Applications + +### REST API Design + +```cpp +// API versioning +server.on(AsyncURIMatcher::prefix("/api/v1"), handleAPIv1); +server.on(AsyncURIMatcher::prefix("/api/v2"), handleAPIv2); + +// Resource endpoints with IDs +server.on(AsyncURIMatcher::regex("^/api/users/([0-9]+)$"), handleUserById); +server.on(AsyncURIMatcher::regex("^/api/posts/([0-9]+)/comments$"), handlePostComments); +``` + +### File Serving + +```cpp +// Serve different file types +server.on(AsyncURIMatcher::ext("/assets/*.css"), serveCSSFiles); +server.on(AsyncURIMatcher::ext("/assets/*.js"), serveJSFiles); +server.on(AsyncURIMatcher::ext("/images/*.jpg"), serveImageFiles); +``` + +### Admin Interface + +```cpp +// Admin section with authentication +server.on(AsyncURIMatcher::dir("/admin"), handleAdminPages); +server.on(AsyncURIMatcher::exact("/admin"), redirectToAdminDashboard); +``` + +## See Also + +- [ESPAsyncWebServer Documentation](https://github.com/ESP32Async/ESPAsyncWebServer) +- [Regular Expression Reference](https://en.cppreference.com/w/cpp/regex) +- Other examples in the `examples/` directory diff --git a/examples/URIMatcher/URIMatcher.ino b/examples/URIMatcher/URIMatcher.ino new file mode 100644 index 0000000..ff40d1f --- /dev/null +++ b/examples/URIMatcher/URIMatcher.ino @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles + +// +// AsyncURIMatcher Examples - Advanced URI Matching and Routing +// +// This example demonstrates the various ways to use AsyncURIMatcher class +// for flexible URL routing with different matching strategies: +// +// 1. Exact matching +// 2. Prefix matching +// 3. Folder/directory matching +// 4. Extension matching +// 5. Case insensitive matching +// 6. Regex matching (if ASYNCWEBSERVER_REGEX is enabled) +// 7. Factory functions for common patterns +// +// Test URLs: +// - Exact: http://192.168.4.1/exact +// - Prefix: http://192.168.4.1/prefix-anything +// - Folder: http://192.168.4.1/api/users, http://192.168.4.1/api/posts +// - Extension: http://192.168.4.1/images/photo.jpg, http://192.168.4.1/docs/readme.pdf +// - Case insensitive: http://192.168.4.1/CaSe or http://192.168.4.1/case +// - Wildcard: http://192.168.4.1/wildcard-test + +#include +#if defined(ESP32) || defined(LIBRETINY) +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include +#include +#endif + +#include + +static AsyncWebServer server(80); + +void setup() { + Serial.begin(115200); + Serial.println(); + Serial.println("=== AsyncURIMatcher Example ==="); + +#if ASYNCWEBSERVER_WIFI_SUPPORTED + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); + Serial.print("AP IP address: "); + Serial.println(WiFi.softAPIP()); +#endif + + // ============================================================================= + // 1. AUTO-DETECTION BEHAVIOR - traditional string-based routing + // ============================================================================= + + // Traditional string-based routing with auto-detection + // This uses URIMatchAuto which combines URIMatchPrefixFolder | URIMatchExact + // It will match BOTH "/auto" exactly AND "/auto/" + anything + server.on("/auto", HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Auto-Detection Match (Traditional)"); + Serial.println("Matched URL: " + request->url()); + Serial.println("Uses auto-detection: exact + folder matching"); + request->send(200, "text/plain", "OK - Auto-detection match"); + }); + + // Auto-detection for wildcard patterns (ends with *) + // This auto-detects as URIMatchPrefix + server.on("/wildcard*", HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Auto-Detected Wildcard (Prefix)"); + Serial.println("Matched URL: " + request->url()); + Serial.println("Auto-detected as prefix match due to trailing *"); + request->send(200, "text/plain", "OK - Wildcard prefix match"); + }); + + // Auto-detection for extension patterns (contains /*.ext) + // This auto-detects as URIMatchExtension + server.on("/auto-images/*.png", HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Auto-Detected Extension Pattern"); + Serial.println("Matched URL: " + request->url()); + Serial.println("Auto-detected as extension match due to /*.png pattern"); + request->send(200, "text/plain", "OK - Extension match"); + }); + + // ============================================================================= + // 2. EXACT MATCHING - matches only the exact URL (explicit) + // ============================================================================= + + // Using factory function for exact match + server.on(AsyncURIMatcher::exact("/exact"), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Factory Exact Match"); + Serial.println("Matched URL: " + request->url()); + Serial.println("Uses AsyncURIMatcher::exact() factory function"); + request->send(200, "text/plain", "OK - Factory exact match"); + }); + + // ============================================================================= + // 3. PREFIX MATCHING - matches URLs that start with the pattern + // ============================================================================= + + // Using factory function for prefix match + server.on(AsyncURIMatcher::prefix("/service"), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Service Prefix Match (Factory)"); + Serial.println("Matched URL: " + request->url()); + Serial.println("Uses AsyncURIMatcher::prefix() factory function"); + request->send(200, "text/plain", "OK - Factory prefix match"); + }); + + // ============================================================================= + // 4. FOLDER/DIRECTORY MATCHING - matches URLs in a folder structure + // ============================================================================= + + // Folder match using factory function (automatically adds trailing slash) + server.on(AsyncURIMatcher::dir("/admin"), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Admin Directory Match"); + Serial.println("Matched URL: " + request->url()); + Serial.println("This matches URLs under /admin/ directory"); + Serial.println("Note: /admin (without slash) will NOT match"); + request->send(200, "text/plain", "OK - Directory match"); + }); + + // ============================================================================= + // 5. EXTENSION MATCHING - matches files with specific extensions + // ============================================================================= + + // Image extension matching + server.on(AsyncURIMatcher::ext("/images/*.jpg"), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("JPG Image Handler"); + Serial.println("Matched URL: " + request->url()); + Serial.println("This matches any .jpg file under /images/"); + request->send(200, "text/plain", "OK - Extension match"); + }); + + // ============================================================================= + // 6. CASE INSENSITIVE MATCHING + // ============================================================================= + + // Case insensitive exact match + server.on(AsyncURIMatcher::exact("/case", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Case Insensitive Match"); + Serial.println("Matched URL: " + request->url()); + Serial.println("This matches /case in any case combination"); + request->send(200, "text/plain", "OK - Case insensitive match"); + }); + +#ifdef ASYNCWEBSERVER_REGEX + // ============================================================================= + // 7. REGEX MATCHING (only available if ASYNCWEBSERVER_REGEX is enabled) + // ============================================================================= + + // Regex match for numeric IDs + server.on(AsyncURIMatcher::regex("^/user/([0-9]+)$"), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Regex Match - User ID"); + Serial.println("Matched URL: " + request->url()); + if (request->pathArg(0).length() > 0) { + Serial.println("Captured User ID: " + request->pathArg(0)); + } + Serial.println("This regex matches /user/{number} pattern"); + request->send(200, "text/plain", "OK - Regex match"); + }); +#endif + + // ============================================================================= + // 8. COMBINED FLAGS EXAMPLE + // ============================================================================= + + // Combine multiple flags + server.on(AsyncURIMatcher::prefix("/MixedCase", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Combined Flags Example"); + Serial.println("Matched URL: " + request->url()); + Serial.println("Uses both AsyncURIMatcher::Prefix and AsyncURIMatcher::CaseInsensitive"); + request->send(200, "text/plain", "OK - Combined flags match"); + }); + + // ============================================================================= + // 9. HOMEPAGE WITH NAVIGATION + // ============================================================================= + + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + Serial.println("Homepage accessed"); + String response = R"( + + + AsyncURIMatcher Examples + + + +

AsyncURIMatcher Examples

+ + + + + +
+

Case Insensitive Matching

+ /case (lowercase) + /CASE (uppercase) + /CaSe (mixed case) +
+ +)"; +#ifdef ASYNCWEBSERVER_REGEX + response += R"( + +)"; +#endif + response += R"( + + +)"; + request->send(200, "text/html", response); + }); + + // ============================================================================= + // 10. NOT FOUND HANDLER + // ============================================================================= + + server.onNotFound([](AsyncWebServerRequest *request) { + String html = "

404 - Not Found

"; + html += "

The requested URL " + request->url() + " was not found.

"; + html += "

← Back to Examples

"; + request->send(404, "text/html", html); + }); + + server.begin(); + + Serial.println(); + Serial.println("=== Server Started ==="); + Serial.println("Open your browser and navigate to:"); + Serial.println("http://192.168.4.1/ - Main examples page"); + Serial.println(); + Serial.println("Available test endpoints:"); + Serial.println("• Auto-detection: /auto (exact+folder), /wildcard*, /auto-images/*.png"); + Serial.println("• Exact matches: /exact"); + Serial.println("• Prefix matches: /service*"); + Serial.println("• Folder matches: /admin/*"); + Serial.println("• Extension matches: /images/*.jpg"); + Serial.println("• Case insensitive: /case (try /CASE, /Case)"); +#ifdef ASYNCWEBSERVER_REGEX + Serial.println("• Regex matches: /user/123"); +#endif + Serial.println("• Combined flags: /mixedcase*"); + Serial.println(); +} + +void loop() { + // Nothing to do here - the server handles everything asynchronously + delay(1000); +} diff --git a/examples/URIMatcherTest/URIMatcherTest.ino b/examples/URIMatcherTest/URIMatcherTest.ino new file mode 100644 index 0000000..0d6128c --- /dev/null +++ b/examples/URIMatcherTest/URIMatcherTest.ino @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles + +// +// Test for ESPAsyncWebServer URI matching +// +// Usage: upload, connect to the AP and run test_routes.sh +// + +#include +#if defined(ESP32) || defined(LIBRETINY) +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include +#include +#endif + +#include + +AsyncWebServer server(80); + +void setup() { + Serial.begin(115200); + +#if ASYNCWEBSERVER_WIFI_SUPPORTED + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); +#endif + + // Status endpoint + server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Exact paths, plus the subpath (/exact matches /exact/sub but not /exact-no-match) + server.on("/exact", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + server.on("/api/users", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Prefix matching + server.on("/api/*", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + server.on("/files/*", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Extensions + server.on("/*.json", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "application/json", "{\"status\":\"OK\"}"); + }); + + server.on("/*.css", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/css", "/* OK */"); + }); + + // ============================================================================= + // NEW ASYNCURIMATCHER FACTORY METHODS TESTS + // ============================================================================= + + // Exact match using factory method (does NOT match subpaths like traditional) + server.on(AsyncURIMatcher::exact("/factory/exact"), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Prefix match using factory method + server.on(AsyncURIMatcher::prefix("/factory/prefix"), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Directory match using factory method (matches /dir/anything but not /dir itself) + server.on(AsyncURIMatcher::dir("/factory/dir"), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Extension match using factory method + server.on(AsyncURIMatcher::ext("/factory/files/*.txt"), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // ============================================================================= + // CASE INSENSITIVE MATCHING TESTS + // ============================================================================= + + // Case insensitive exact match + server.on(AsyncURIMatcher::exact("/case/exact", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Case insensitive prefix match + server.on(AsyncURIMatcher::prefix("/case/prefix", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Case insensitive directory match + server.on(AsyncURIMatcher::dir("/case/dir", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Case insensitive extension match + server.on(AsyncURIMatcher::ext("/case/files/*.PDF", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + +#ifdef ASYNCWEBSERVER_REGEX + // Traditional regex patterns (backward compatibility) + server.on("^/user/([0-9]+)$", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + server.on("^/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // ============================================================================= + // NEW ASYNCURIMATCHER REGEX FACTORY METHODS + // ============================================================================= + + // Regex match using factory method + server.on(AsyncURIMatcher::regex("^/factory/user/([0-9]+)$"), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Case insensitive regex match using factory method + server.on(AsyncURIMatcher::regex("^/factory/search/(.+)$", AsyncURIMatcher::CaseInsensitive), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // Complex regex with multiple capture groups + server.on(AsyncURIMatcher::regex("^/factory/blog/([0-9]{4})/([0-9]{2})/([0-9]{2})$"), HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); +#endif + + // ============================================================================= + // SPECIAL MATCHERS + // ============================================================================= + + // Match all POST requests (catch-all before 404) + server.on(AsyncURIMatcher::all(), HTTP_POST, [](AsyncWebServerRequest *request) { + request->send(200, "text/plain", "OK"); + }); + + // 404 handler + server.onNotFound([](AsyncWebServerRequest *request) { + request->send(404, "text/plain", "Not Found"); + }); + + server.begin(); + Serial.println("Server ready"); +} + +// not needed +void loop() { + delay(100); +} diff --git a/examples/URIMatcherTest/test_routes.sh b/examples/URIMatcherTest/test_routes.sh new file mode 100755 index 0000000..4586da2 --- /dev/null +++ b/examples/URIMatcherTest/test_routes.sh @@ -0,0 +1,174 @@ +#!/bin/bash + +# URI Matcher Test Script +# Tests all routes defined in URIMatcherTest.ino + +SERVER_IP="${1:-192.168.4.1}" +SERVER_PORT="80" +BASE_URL="http://${SERVER_IP}:${SERVER_PORT}" + +echo "Testing URI Matcher at $BASE_URL" +echo "==================================" + +# Function to test a route +test_route() { + local path="$1" + local expected_status="$2" + local description="$3" + + echo -n "Testing $path ... " + + response=$(curl -s -w "HTTPSTATUS:%{http_code}" "$BASE_URL$path" 2>/dev/null) + status_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2) + + if [ "$status_code" = "$expected_status" ]; then + echo "✅ PASS ($status_code)" + else + echo "❌ FAIL (expected $expected_status, got $status_code)" + return 1 + fi + return 0 +} + +# Test counter +PASS=0 +FAIL=0 + +# Test all routes that should return 200 OK +echo "Testing routes that should work (200 OK):" + +if test_route "/status" "200" "Status endpoint"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/exact" "200" "Exact path"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/exact/" "200" "Exact path ending with /"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/exact/sub" "200" "Exact path with subpath /"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/api/users" "200" "Exact API path"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/api/data" "200" "API prefix match"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/api/v1/posts" "200" "API prefix deep"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/files/document.pdf" "200" "Files prefix"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/files/images/photo.jpg" "200" "Files prefix deep"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/config.json" "200" "JSON extension"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/data/settings.json" "200" "JSON extension in subfolder"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/style.css" "200" "CSS extension"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/assets/main.css" "200" "CSS extension in subfolder"; then ((PASS++)); else ((FAIL++)); fi + +echo "" +echo "Testing AsyncURIMatcher factory methods:" + +# Factory exact match (should NOT match subpaths) +if test_route "/factory/exact" "200" "Factory exact match"; then ((PASS++)); else ((FAIL++)); fi + +# Factory prefix match +if test_route "/factory/prefix" "200" "Factory prefix base"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/factory/prefix-test" "200" "Factory prefix extended"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/factory/prefix/sub" "200" "Factory prefix subpath"; then ((PASS++)); else ((FAIL++)); fi + +# Factory directory match (should NOT match the directory itself) +if test_route "/factory/dir/users" "200" "Factory directory match"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/factory/dir/sub/path" "200" "Factory directory deep"; then ((PASS++)); else ((FAIL++)); fi + +# Factory extension match +if test_route "/factory/files/doc.txt" "200" "Factory extension match"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/factory/files/sub/readme.txt" "200" "Factory extension deep"; then ((PASS++)); else ((FAIL++)); fi + +echo "" +echo "Testing case insensitive matching:" + +# Case insensitive exact +if test_route "/case/exact" "200" "Case exact lowercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/CASE/EXACT" "200" "Case exact uppercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/Case/Exact" "200" "Case exact mixed"; then ((PASS++)); else ((FAIL++)); fi + +# Case insensitive prefix +if test_route "/case/prefix" "200" "Case prefix lowercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/CASE/PREFIX-test" "200" "Case prefix uppercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/Case/Prefix/sub" "200" "Case prefix mixed"; then ((PASS++)); else ((FAIL++)); fi + +# Case insensitive directory +if test_route "/case/dir/users" "200" "Case dir lowercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/CASE/DIR/admin" "200" "Case dir uppercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/Case/Dir/settings" "200" "Case dir mixed"; then ((PASS++)); else ((FAIL++)); fi + +# Case insensitive extension +if test_route "/case/files/doc.pdf" "200" "Case ext lowercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/CASE/FILES/DOC.PDF" "200" "Case ext uppercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/Case/Files/Doc.Pdf" "200" "Case ext mixed"; then ((PASS++)); else ((FAIL++)); fi + +echo "" +echo "Testing special matchers:" + +# Test POST to catch-all (all() matcher) +echo -n "Testing POST /any/path (all matcher) ... " +response=$(curl -s -X POST -w "HTTPSTATUS:%{http_code}" "$BASE_URL/any/path" 2>/dev/null) +status_code=$(echo "$response" | grep -o "HTTPSTATUS:[0-9]*" | cut -d: -f2) +if [ "$status_code" = "200" ]; then + echo "✅ PASS ($status_code)" + ((PASS++)) +else + echo "❌ FAIL (expected 200, got $status_code)" + ((FAIL++)) +fi + +# Check if regex is enabled by testing the server +echo "" +echo "Checking for regex support..." +regex_test=$(curl -s "$BASE_URL/user/123" 2>/dev/null) +if curl -s -w "%{http_code}" "$BASE_URL/user/123" 2>/dev/null | grep -q "200"; then + echo "Regex support detected - testing traditional regex routes:" + if test_route "/user/123" "200" "Traditional regex user ID"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/user/456" "200" "Traditional regex user ID 2"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/blog/2023/10/15" "200" "Traditional regex blog date"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/blog/2024/12/25" "200" "Traditional regex blog date 2"; then ((PASS++)); else ((FAIL++)); fi + + echo "Testing AsyncURIMatcher regex factory methods:" + if test_route "/factory/user/123" "200" "Factory regex user ID"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/factory/user/789" "200" "Factory regex user ID 2"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/factory/blog/2023/10/15" "200" "Factory regex blog date"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/factory/blog/2024/12/31" "200" "Factory regex blog date 2"; then ((PASS++)); else ((FAIL++)); fi + + # Case insensitive regex + if test_route "/factory/search/hello" "200" "Factory regex search lowercase"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/FACTORY/SEARCH/WORLD" "200" "Factory regex search uppercase"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/Factory/Search/Test" "200" "Factory regex search mixed"; then ((PASS++)); else ((FAIL++)); fi + + # Test regex validation + if test_route "/user/abc" "404" "Invalid regex (letters instead of numbers)"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/blog/23/10/15" "404" "Invalid regex (2-digit year)"; then ((PASS++)); else ((FAIL++)); fi + if test_route "/factory/user/abc" "404" "Factory regex invalid (letters)"; then ((PASS++)); else ((FAIL++)); fi +else + echo "Regex support not detected (compile with ASYNCWEBSERVER_REGEX to enable)" +fi + +echo "" +echo "Testing routes that should fail (404 Not Found):" + +if test_route "/nonexistent" "404" "Non-existent route"; then ((PASS++)); else ((FAIL++)); fi + +# Test factory exact vs traditional behavior difference +if test_route "/factory/exact/sub" "404" "Factory exact should NOT match subpaths"; then ((PASS++)); else ((FAIL++)); fi + +# Test factory directory requires trailing slash +if test_route "/factory/dir" "404" "Factory directory should NOT match without trailing slash"; then ((PASS++)); else ((FAIL++)); fi + +# Test extension mismatch +if test_route "/factory/files/doc.pdf" "404" "Factory extension mismatch (.pdf vs .txt)"; then ((PASS++)); else ((FAIL++)); fi + +# Test case sensitive when flag not used +if test_route "/exact" "200" "Traditional exact lowercase"; then ((PASS++)); else ((FAIL++)); fi +if test_route "/EXACT" "404" "Traditional exact should be case sensitive"; then ((PASS++)); else ((FAIL++)); fi + +echo "" +echo "==================================" +echo "Test Results:" +echo "✅ Passed: $PASS" +echo "❌ Failed: $FAIL" +echo "Total: $((PASS + FAIL))" + +if [ $FAIL -eq 0 ]; then + echo "" + echo "🎉 All tests passed! URI matching is working correctly." + exit 0 +else + echo "" + echo "❌ Some tests failed. Check the server and routes." + exit 1 +fi diff --git a/examples/Upload/Upload.ino b/examples/Upload/Upload.ino index fd80bd7..404a4d3 100644 --- a/examples/Upload/Upload.ino +++ b/examples/Upload/Upload.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // Demo text, binary and file upload @@ -31,7 +31,7 @@ void setup() { LittleFS.begin(); } -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif @@ -63,6 +63,7 @@ void setup() { if (!buffer->reserve(size)) { delete buffer; request->abort(); + return; } request->_tempObject = buffer; } @@ -100,6 +101,7 @@ void setup() { if (!request->_tempFile) { request->send(400, "text/plain", "File not available for writing"); + return; } } if (len) { @@ -141,6 +143,7 @@ void setup() { // first pass ? if (!index) { + // Note: using content type to determine size is not reliable! size_t size = request->header("Content-Length").toInt(); if (!size) { request->send(400, "text/plain", "No Content-Length"); @@ -150,6 +153,7 @@ void setup() { if (!buffer) { // not enough memory request->abort(); + return; } else { request->_tempObject = buffer; } diff --git a/examples/UploadFlash/UploadFlash.ino b/examples/UploadFlash/UploadFlash.ino new file mode 100644 index 0000000..921d28b --- /dev/null +++ b/examples/UploadFlash/UploadFlash.ino @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: LGPL-3.0-or-later +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles + +// +// Demo to upload a firmware and filesystem image via multipart form data +// + +#include +#if defined(ESP32) || defined(LIBRETINY) +#include +#include +#elif defined(ESP8266) +#include +#include +#elif defined(TARGET_RP2040) || defined(TARGET_RP2350) || defined(PICO_RP2040) || defined(PICO_RP2350) +#include +#include +#endif + +#include +#include +#include + +// ESP32 example ONLY +#ifdef ESP32 +#include +#endif + +static AsyncWebServer server(80); + +void setup() { + Serial.begin(115200); + + if (!LittleFS.begin()) { + LittleFS.format(); + LittleFS.begin(); + } + +#if ASYNCWEBSERVER_WIFI_SUPPORTED + WiFi.mode(WIFI_AP); + WiFi.softAP("esp-captive"); +#endif + +// ESP32 example ONLY +#ifdef ESP32 + + // Shows how to get the fw and fs (names) and filenames from a multipart upload, + // and also how to handle multiple file uploads in a single request. + // + // This example also shows how to pass and handle different parameters having the same name in query string, post form and content-disposition. + // + // Execute in the terminal, in order: + // + // 1. Build firmware: pio run -e arduino-3 + // 2. Build FS image: pio run -e arduino-3 -t buildfs + // 3. Flash both at the same time: curl -v -F "name=Bob" -F "fw=@.pio/build/arduino-3/firmware.bin" -F "fs=@.pio/build/arduino-3/littlefs.bin" http://192.168.4.1/flash?name=Bill + // + server.on( + "/flash", HTTP_POST, + [](AsyncWebServerRequest *request) { + if (request->getResponse()) { + // response already created + return; + } + + // list all parameters + Serial.println("Request parameters:"); + const size_t params = request->params(); + for (size_t i = 0; i < params; i++) { + const AsyncWebParameter *p = request->getParam(i); + Serial.printf("Param[%u]: %s=%s, isPost=%d, isFile=%d, size=%u\n", i, p->name().c_str(), p->value().c_str(), p->isPost(), p->isFile(), p->size()); + } + + Serial.println("Flash / Filesystem upload completed"); + + request->send(200, "text/plain", "Upload complete"); + }, + [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final) { + Serial.printf("Upload[%s]: index=%u, len=%u, final=%d\n", filename.c_str(), index, len, final); + + if (request->getResponse() != nullptr) { + // upload aborted + return; + } + + // start a new content-disposition upload + if (!index) { + // list all parameters + const size_t params = request->params(); + for (size_t i = 0; i < params; i++) { + const AsyncWebParameter *p = request->getParam(i); + Serial.printf("Param[%u]: %s=%s, isPost=%d, isFile=%d, size=%u\n", i, p->name().c_str(), p->value().c_str(), p->isPost(), p->isFile(), p->size()); + } + + // get the content-disposition parameter + const AsyncWebParameter *p = request->getParam(asyncsrv::T_name, true, true); + if (p == nullptr) { + request->send(400, "text/plain", "Missing content-disposition 'name' parameter"); + return; + } + + // determine upload type based on the parameter name + if (p->value() == "fs") { + Serial.printf("Filesystem image upload for file: %s\n", filename.c_str()); + if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_SPIFFS)) { + Update.printError(Serial); + request->send(400, "text/plain", "Update begin failed"); + return; + } + + } else if (p->value() == "fw") { + Serial.printf("Firmware image upload for file: %s\n", filename.c_str()); + if (!Update.begin(UPDATE_SIZE_UNKNOWN, U_FLASH)) { + Update.printError(Serial); + request->send(400, "text/plain", "Update begin failed"); + return; + } + + } else { + Serial.printf("Unknown upload type for file: %s\n", filename.c_str()); + request->send(400, "text/plain", "Unknown upload type"); + return; + } + } + + // some bytes to write ? + if (len) { + if (Update.write(data, len) != len) { + Update.printError(Serial); + Update.end(); + request->send(400, "text/plain", "Update write failed"); + return; + } + } + + // finish the content-disposition upload + if (final) { + if (!Update.end(true)) { + Update.printError(Serial); + request->send(400, "text/plain", "Update end failed"); + return; + } + + // success response is created in the final request handler when all uploads are completed + Serial.printf("Upload success of file %s\n", filename.c_str()); + } + } + ); + +#endif + + server.begin(); +} + +// not needed +void loop() { + delay(100); +} diff --git a/examples/WebSocket/WebSocket.ino b/examples/WebSocket/WebSocket.ino index c8d3727..8e0988c 100644 --- a/examples/WebSocket/WebSocket.ino +++ b/examples/WebSocket/WebSocket.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // WebSocket example @@ -19,17 +19,58 @@ #include +static const char *htmlContent PROGMEM = R"( + + + + WebSocket + + +

WebSocket Example

+

Open your browser console!

+ + + + + + )"; +static const size_t htmlContentLength = strlen_P(htmlContent); + static AsyncWebServer server(80); static AsyncWebSocket ws("/ws"); void setup() { Serial.begin(115200); -#if SOC_WIFI_SUPPORTED || CONFIG_ESP_WIFI_REMOTE_ENABLED || LT_ARD_HAS_WIFI +#if ASYNCWEBSERVER_WIFI_SUPPORTED WiFi.mode(WIFI_AP); WiFi.softAP("esp-captive"); #endif + // serves root html page + server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) { + request->send(200, "text/html", (const uint8_t *)htmlContent, htmlContentLength); + }); + // // Run in terminal 1: websocat ws://192.168.4.1/ws => should stream data // Run in terminal 2: websocat ws://192.168.4.1/ws => should stream data @@ -66,6 +107,7 @@ void setup() { if (info->opcode == WS_TEXT) { data[len] = 0; Serial.printf("ws text: %s\n", (char *)data); + client->ping(); } } } diff --git a/examples/WebSocketEasy/WebSocketEasy.ino b/examples/WebSocketEasy/WebSocketEasy.ino index 5229910..ad97253 100644 --- a/examples/WebSocketEasy/WebSocketEasy.ino +++ b/examples/WebSocketEasy/WebSocketEasy.ino @@ -1,5 +1,5 @@ // SPDX-License-Identifier: LGPL-3.0-or-later -// Copyright 2016-2025 Hristo Gochkov, Mathieu Carbou, Emil Muratov +// Copyright 2016-2026 Hristo Gochkov, Mathieu Carbou, Emil Muratov, Will Miles // // WebSocket example using the easy to use AsyncWebSocketMessageHandler handler that only supports unfragmented messages @@ -40,7 +40,7 @@ static const char *htmlContent PROGMEM = R"(

WebSocket Example

- <>Open your browser console!

+

Open your browser console!