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