|
DeterministicESPAsyncWebServer 1.2.0
Zero-allocation, bounded-execution async HTTP server for ESP32
|
An HTTP/1.1 web server for ESP32 with a fully deterministic memory footprint, RFC 7230 compliant request parsing, and an OSI-layered architecture.
FS (LittleFS, SPIFFS, SD)MAX_MULTIPART_PARTS parts#define; illegal combinations produce #error messagesDETWS_ENABLE_DIAG build-config dump, disabled by default for securityrestart()** — hard-resets all connections and reinitialises on the same port without touching the WiFi/TCP stackEvery byte of memory the library uses is accounted for at compile time:
| Storage | Location |
|---|---|
conn_pool[MAX_CONNS] — TCP connections + ring buffers | BSS |
http_pool[MAX_CONNS] — HTTP request structs | BSS |
ws_pool[MAX_WS_CONNS] — WebSocket connection state | BSS |
sse_pool[MAX_SSE_CONNS] — SSE connection state | BSS |
_queue_storage[EVT_QUEUE_DEPTH * sizeof(TcpEvt)] — event queue backing store | BSS |
_queue_struct — FreeRTOS StaticQueue_t | BSS |
Route table _routes[MAX_ROUTES] | BSS (inside DetWebServer) |
begin() calls xQueueCreateStatic() — no pvPortMalloc, no fragmentation risk. DetWebServer::heap_needed() returns 0 and heap_available() returns true.
The only post-begin() allocation that can occur is inside fs::File construction in serve_file(), which is an Arduino FS implementation detail outside the library's control.
Define these before including the library header. Any flag set to 0 strips the corresponding code and its includes from the build entirely.
Illegal combinations (e.g. MAX_WS_CONNS + MAX_SSE_CONNS > MAX_CONNS) produce #error messages at compile time with a descriptive reason string.
All constants can be overridden via build flags or #define before the library include. Defaults live in src/DetWebServerConfig.h.
| Constant | Default | Description |
|---|---|---|
MAX_CONNS | 4 | Simultaneous TCP connections (1–255) |
EVT_QUEUE_DEPTH | 16 | FreeRTOS event queue depth; must be ≥ MAX_CONNS * 4 |
RX_BUF_SIZE | 1024 | Ring buffer bytes per connection |
BODY_BUF_SIZE | 256 | Request body bytes; must be ≤ RX_BUF_SIZE |
MAX_ROUTES | 16 | Registered route handlers |
MAX_HEADERS | 8 | Headers stored per request |
MAX_PATH_LEN | 64 | URL path bytes including leading / |
MAX_KEY_LEN | 24 | Header field-name bytes |
MAX_VAL_LEN | 48 | Header field-value bytes |
MAX_QUERY_LEN | 128 | Raw query string bytes (after ?) |
MAX_QUERY_PARAMS | 8 | Parsed query key=value pairs |
QUERY_KEY_LEN | 24 | Query parameter key bytes |
QUERY_VAL_LEN | 48 | Query parameter value bytes |
| Constant | Default | Minimum | Description |
|---|---|---|---|
RESP_HDR_BUF_SIZE | 512 | 128 | Stack buffer for HTTP response headers |
WS_HDR_BUF_SIZE | 256 | 128 | Stack buffer for WebSocket 101 response |
CORS_HDR_BUF_SIZE | 192 | 64 | Buffer for pre-built CORS header block; must be ≤ RESP_HDR_BUF_SIZE |
| Constant | Default | Description |
|---|---|---|
MAX_WS_CONNS | 2 | WebSocket slots; each consumes one MAX_CONNS slot |
WS_FRAME_SIZE | 512 | Max WebSocket frame payload bytes |
| Constant | Default | Description |
|---|---|---|
MAX_SSE_CONNS | 2 | SSE slots; each consumes one MAX_CONNS slot |
SSE_BUF_SIZE | 256 | Stack buffer for one formatted SSE event |
| Constant | Default | Description |
|---|---|---|
FILE_CHUNK_SIZE | 512 | Bytes read from FS per tcp_write() call; must be ≤ RX_BUF_SIZE |
| Constant | Default | Description |
|---|---|---|
MAX_AUTH_LEN | 32 | Max username or password length including null terminator |
| Constant | Default | Description |
|---|---|---|
MAX_MULTIPART_PARTS | 4 | Max form parts per request |
MAX_BOUNDARY_LEN | 72 | Max MIME boundary length |
The connection idle timeout can be changed without a rebuild:
Pass nullptr (or omit) to use the compile-time default CONN_TIMEOUT_MS (5000 ms).
See examples/ConfigurationExample/ConfigurationExample.ino for a full reference of every configurable flag and constant.
| Method | Description |
|---|---|
begin(port, cfg = nullptr) | Bind and listen. Returns +1 on success, -1 on lwIP error. |
stop() | Abort all connections, close listener, reset all pools. |
restart(cfg = nullptr) | stop() + begin() on the same port. Returns -1 if called before begin(). |
handle() | Call every loop(). Runs timeout sweep, event drain, and dispatch. |
static heap_needed() | Returns 0 — no heap allocation. |
static heap_available() | Returns true — always safe to call begin(). |
| Method | Description |
|---|---|
on(path, method, handler) | Register a route. Trailing * enables prefix matching. |
on(path, method, handler, realm, user, pass) | Same, with Basic Auth (DETWS_ENABLE_AUTH). |
on_not_found(handler) | Fallback handler; default sends 404. |
set_cors(origin) | Enable CORS and answer OPTIONS with 204. Pass "" to disable. |
send(slot_id, code, type, body) | Send a response with body and close the connection. |
send_empty(slot_id, code) | Send a headers-only response and close the connection. |
serve_file(slot_id, fs, path, type) | Stream a file from an Arduino FS (DETWS_ENABLE_FILE_SERVING). |
| Method | Description |
|---|---|
on_ws(path, on_connect, on_message, on_close) | Register a WebSocket route. |
ws_send_text(ws_id, text) | Send a UTF-8 text frame to a client. |
ws_send_binary(ws_id, data, len) | Send a binary frame to a client. |
ws_disconnect(ws_id) | Send Close frame and mark slot for cleanup. |
In on_message, read the received payload from ws_pool[ws_id].buf (length in ws_pool[ws_id].payload_len).
| Method | Description |
|---|---|
on_sse(path, on_connect) | Register an SSE route. |
sse_send(sse_id, data, event = nullptr, id = nullptr) | Push an event to one client. |
sse_broadcast(path, data, event = nullptr, id = nullptr) | Push an event to all clients on a path. |
| Method | Description |
|---|---|
diag(slot_id) | Send a JSON object with all active feature flags and configuration constants. Disable in production. |
| Field | Type | Description |
|---|---|---|
method | char[8] | HTTP method string, e.g. "GET" |
path | char[MAX_PATH_LEN] | URL path, e.g. "/api/status" |
version | HttpVersion | HTTP_10, HTTP_11, or HTTP_UNKNOWN |
query | char[MAX_QUERY_LEN] | Raw query string (everything after ?) |
query_params | QueryParam[MAX_QUERY_PARAMS] | Parsed key=value pairs |
query_count | uint8_t | Valid entries in query_params[] |
headers | Header[MAX_HEADERS] | Captured header fields |
header_count | uint8_t | Valid entries in headers[] |
content_length | size_t | Value of Content-Length header (0 if absent) |
body | uint8_t[BODY_BUF_SIZE+1] | Request body, always null-terminated |
body_len | size_t | Bytes stored in body[] |
The HTTP/1.1 parser enforces RFC 7230 rules byte-by-byte during parsing:
| Field | Allowed characters | RFC reference | Violation response |
|---|---|---|---|
| Method | tchar (‘ALPHA DIGIT ! # $ % & ’ * + - . ^ _ ` | ~) \ilinebr </td> <td class="markdownTableBodyNone"> §3.1.1 \ilinebr </td> <td class="markdownTableBodyNone"> 400 \ilinebr </td> </tr> <tr class="markdownTableRowEven"> <td class="markdownTableBodyNone"> Path / Query \ilinebr </td> <td class="markdownTableBodyNone">VCHAR(x21–7E) \ilinebr </td> <td class="markdownTableBodyNone"> RFC 3986 §3.3 \ilinebr </td> <td class="markdownTableBodyNone"> 400 \ilinebr </td> </tr> <tr class="markdownTableRowOdd"> <td class="markdownTableBodyNone"> Header field-name \ilinebr </td> <td class="markdownTableBodyNone">tchar\ilinebr </td> <td class="markdownTableBodyNone"> §3.2 \ilinebr </td> <td class="markdownTableBodyNone"> 400 \ilinebr </td> </tr> <tr class="markdownTableRowEven"> <td class="markdownTableBodyNone"> Header field-value \ilinebr </td> <td class="markdownTableBodyNone">VCHAR, SP, HTAB, obs-text (x80–FF) \ilinebr </td> <td class="markdownTableBodyNone"> §3.2 \ilinebr </td> <td class="markdownTableBodyNone"> 400 \ilinebr </td> </tr> <tr class="markdownTableRowOdd"> <td class="markdownTableBodyNone"> Path length \ilinebr </td> <td class="markdownTableBodyNone"> ≤MAX_PATH_LEN − 1bytes \ilinebr </td> <td class="markdownTableBodyNone"> §3.1.1 \ilinebr </td> <td class="markdownTableBodyNone"> 414 \ilinebr </td> </tr> <tr class="markdownTableRowEven"> <td class="markdownTableBodyNone"> Body size \ilinebr </td> <td class="markdownTableBodyNone"> ≤BODY_BUF_SIZEbytes (viaContent-Length) \ilinebr </td> <td class="markdownTableBodyNone"> §3.3.2 \ilinebr </td> <td class="markdownTableBodyNone"> 413 \ilinebr </td> </tr> <tr class="markdownTableRowOdd"> <td class="markdownTableBodyNone"> Transfer-Encoding \ilinebr </td> <td class="markdownTableBodyNone"> Not supported — rejected at dispatch \ilinebr </td> <td class="markdownTableBodyNone"> §3.3.1 \ilinebr </td> <td class="markdownTableBodyNone"> 501 \ilinebr </td> </tr> <tr class="markdownTableRowEven"> <td class="markdownTableBodyNone"> HTTP version \ilinebr </td> <td class="markdownTableBodyNone"> FNV-1a hash match; setsHttpReq::version\ilinebr </td> <td class="markdownTableBodyNone"> §2.6 \ilinebr </td> <td class="markdownTableBodyNone">HTTP_UNKNOWN` |
Additional behaviors:
MAX_HEADERS are consumed and discarded, not rejectedhandle() sends these before dispatching to any route handler:
| Parser state | Response | Trigger |
|---|---|---|
PARSE_ERROR | 400 Bad Request | Any RFC 7230 character violation or malformed CRLF |
PARSE_ENTITY_TOO_LARGE | 413 Payload Too Large | Content-Length > BODY_BUF_SIZE |
PARSE_URI_TOO_LONG | 414 URI Too Long | Path exceeds MAX_PATH_LEN − 1 bytes |
handle() also sends these during dispatch:
| Condition | Response |
|---|---|
Transfer-Encoding header present | 501 Not Implemented |
No matching route, no on_not_found handler | 404 Not Found |
| WebSocket upgrade on a non-WS route | 400 Bad Request |
| WebSocket or SSE pool full | 503 Service Unavailable |
321 Unity tests across nine suites, all runnable on a native x86/x64 host:
| Suite | Tests | Coverage |
|---|---|---|
test_http_parser | 79 | All parser states, RFC 7230 compliance, 413/414, version hash |
test_presentation | 61 | Parser integration via ring buffer, race condition simulations |
test_transport | 25 | Ring buffer integrity, timeouts, pool lifecycle |
test_session | 19 | Event queue drain, slot lifecycle, millis wraparound |
test_websocket | 38 | Frame parser, masking, control frames, error paths |
test_sse | 21 | Pool lifecycle, event formatting, broadcast |
test_auth | 31 | Base64 decode, credential matching, 401 responses |
test_file_serving | 18 | Chunked send, 404 on missing file, FS stub |
test_multipart | 29 | Boundary parsing, field extraction, stress |
Full API documentation generated by Doxygen: https://dstroy0.github.io/DeterministicESPAsyncWebServer/
To build locally:
PlatformIO:
Arduino IDE: Download the repository as a ZIP and use Sketch → Include Library → Add .ZIP Library.
AGPL-3.0-or-later. See LICENSE for details.