DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
http_parser.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file http_parser.h
6 * @brief Standalone HTTP/1.1 request parser - no transport dependency.
7 *
8 * The parser is a pure byte-stream state machine. It has no knowledge of
9 * ring buffers, TCP PCBs, or FreeRTOS. Feed it bytes one at a time via
10 * `http_parser_feed()` and inspect `HttpReq::parse_state` to know when the
11 * request is ready.
12 *
13 * **State machine**
14 * ```
15 * ParseState::PARSE_METHOD ──space──────► ParseState::PARSE_PATH
16 * ParseState::PARSE_PATH ──space──────► ParseState::PARSE_VERSION
17 * ParseState::PARSE_PATH ──'?'────────► ParseState::PARSE_QUERY
18 * ParseState::PARSE_QUERY ──space──────► ParseState::PARSE_VERSION (calls parse_query_params)
19 * ParseState::PARSE_VERSION ──CR─────────► ParseState::PARSE_EXPECT_LF
20 * ParseState::PARSE_EXPECT_LF ──LF─────────► ParseState::PARSE_HEADER_KEY
21 * ParseState::PARSE_HEADER_KEY ──':'────────► ParseState::PARSE_HEADER_VAL
22 * ParseState::PARSE_HEADER_KEY ──CR─────────► ParseState::PARSE_EXPECT_BODY_LF (blank line)
23 * ParseState::PARSE_HEADER_VAL ──CR─────────► ParseState::PARSE_EXPECT_LF (stores header)
24 * ParseState::PARSE_EXPECT_BODY_LF ──LF (CL=0)──► ParseState::PARSE_COMPLETE
25 * ParseState::PARSE_EXPECT_BODY_LF ──LF (CL>BUF)► ParseState::PARSE_ENTITY_TOO_LARGE (→ 413)
26 * ParseState::PARSE_EXPECT_BODY_LF ──LF (else)──► ParseState::PARSE_BODY
27 * ParseState::PARSE_BODY ──(all read)──► ParseState::PARSE_COMPLETE
28 * ParseState::PARSE_PATH (overflow) ───────────► ParseState::PARSE_URI_TOO_LONG (→ 414)
29 * Any state + protocol error ──────► ParseState::PARSE_ERROR (→ 400)
30 * ```
31 *
32 * @author Douglas Quigg (dstroy0)
33 * @date 2026
34 */
35
36#ifndef DETERMINISTICESPASYNCWEBSERVER_HTTP_PARSER_H
37#define DETERMINISTICESPASYNCWEBSERVER_HTTP_PARSER_H
38
39#include "ServerConfig.h"
40#include <Arduino.h>
41
42// ---------------------------------------------------------------------------
43// Parser state enumeration
44// ---------------------------------------------------------------------------
45
46/**
47 * @brief States of the HTTP/1.1 request parser.
48 *
49 * Advance via http_parser_feed(). The application layer inspects this
50 * after each feed call or after draining a complete chunk.
51 */
52enum class ParseState : uint8_t
53{
54 PARSE_METHOD, ///< Reading the HTTP method (GET, POST, …).
55 PARSE_PATH, ///< Reading the URL path component.
56 PARSE_QUERY, ///< Reading the raw query string (after `?`).
57 PARSE_VERSION, ///< Accumulating `HTTP/1.x` - hashed for validation.
58 PARSE_HEADER_KEY, ///< Reading a header field name.
59 PARSE_HEADER_VAL, ///< Reading a header field value.
60 PARSE_EXPECT_LF, ///< Consuming the LF of a header-line CRLF pair.
61 PARSE_EXPECT_BODY_LF, ///< Consuming the LF of the blank-line CRLF.
62 PARSE_BODY, ///< Reading the request body.
63 PARSE_COMPLETE, ///< Full request parsed; ready for dispatch.
64 PARSE_ERROR, ///< Unrecoverable parse failure → 400.
65 PARSE_ENTITY_TOO_LARGE, ///< Content-Length > BODY_BUF_SIZE → 413.
66 PARSE_URI_TOO_LONG ///< Path exceeds MAX_PATH_LEN → 414.
67};
68
69/**
70 * @brief Parsed HTTP protocol version.
71 *
72 * Populated from the request line (`HTTP/1.0` or `HTTP/1.1`) using an FNV-1a
73 * hash accumulated during `ParseState::PARSE_VERSION`. The application layer may use
74 * this to drive keep-alive semantics: HTTP/1.1 defaults to persistent
75 * connections; HTTP/1.0 defaults to close.
76 */
77enum class HttpVersion : uint8_t
78{
79 HTTP_UNKNOWN = 0, ///< Version string did not match any known token.
80 HTTP_10, ///< HTTP/1.0 - close semantics by default.
81 HTTP_11 ///< HTTP/1.1 - persistent connection by default.
82};
83
84// ---------------------------------------------------------------------------
85// Data types
86// ---------------------------------------------------------------------------
87
88/** @brief A single HTTP header field (key: value). */
89struct Header
90{
91 char key[MAX_KEY_LEN]; ///< Field name, null-terminated.
92 char val[MAX_VAL_LEN]; ///< Field value, null-terminated.
93};
94
95/** @brief A single parsed query-string parameter. */
97{
98 char key[QUERY_KEY_LEN]; ///< Parameter name, null-terminated.
99 char val[QUERY_VAL_LEN]; ///< Parameter value (empty string if absent).
100};
101
102/**
103 * @brief Fully-parsed HTTP/1.1 request.
104 *
105 * Populated incrementally by http_parser_feed(). Valid for dispatch
106 * only when `parse_state == ParseState::PARSE_COMPLETE`.
107 *
108 * Call http_parser_reset() to recycle this struct for the next request.
109 */
111{
112 uint8_t slot_id; ///< Transport slot index (set by presentation layer).
113 ParseState parse_state; ///< Current parser state.
114 HttpVersion version; ///< Protocol version parsed from the request line.
115 uint32_t _version_hash; ///< FNV-1a accumulator for version validation (internal).
116
117 char method[DETWS_METHOD_BUF_SIZE]; ///< HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).
118 char path[MAX_PATH_LEN]; ///< URL path, null-terminated; no query string.
119 size_t path_idx; ///< Write cursor into path[].
120
121 char query[MAX_QUERY_LEN]; ///< Raw query string (after `?`).
122 size_t query_idx; ///< Write cursor into query[].
123 QueryParam query_params[MAX_QUERY_PARAMS]; ///< Parsed key=value pairs.
124 uint8_t query_count; ///< Valid entries in query_params[].
125
126 QueryParam path_params[MAX_PATH_PARAMS]; ///< `:name` captures from the matched route.
127 uint8_t path_param_count; ///< Valid entries in path_params[].
128
129#if DETWS_CAPTURE_AUTH_HEADER
130 char authorization[DETWS_AUTH_HDR_CAP]; ///< Full Authorization header value (Digest/JWT exceed MAX_VAL_LEN).
131 uint16_t auth_idx; ///< Write cursor into authorization[] (parser-internal).
132 bool cur_is_auth; ///< True while parsing an Authorization header value (parser-internal).
133#endif
134
135 Header headers[MAX_HEADERS]; ///< Captured header fields.
136 uint8_t header_count; ///< Valid entries in headers[].
137 size_t current_token_idx; ///< Write cursor shared by key/value sub-states.
138
139 // Scratch copies of the header currently being parsed, populated even for
140 // headers beyond MAX_HEADERS so that Host / Content-Length detection and
141 // counting are independent of the storage cap (RFC 7230 §5.4, §3.3.2).
142 char cur_key[MAX_KEY_LEN]; ///< Field-name of the in-progress header.
143 char cur_val[MAX_VAL_LEN]; ///< Field-value of the in-progress header.
144
145 size_t content_length; ///< Value of Content-Length header (0 if absent).
146 uint8_t content_length_count; ///< Number of Content-Length fields seen (RFC 7230 §3.3.2).
147 uint8_t host_count; ///< Number of Host fields seen (RFC 7230 §5.4).
148 size_t body_bytes_read; ///< Body bytes received (may exceed BODY_BUF_SIZE).
149
150 uint8_t body[BODY_BUF_SIZE + 1]; ///< Stored body bytes, always null-terminated.
151 size_t body_len; ///< Bytes stored in body[] (≤ BODY_BUF_SIZE).
152
153#if DETWS_ENABLE_STREAM_BODY
154 bool body_streaming; ///< True when the body is streamed to a sink, not buffered (OTA / upload).
155#endif
156};
157
158/** @brief Pool of parser contexts, one per connection-pool slot (incl. reserved dispatch slots). */
160
161#if DETWS_ENABLE_STREAM_BODY
162// ---------------------------------------------------------------------------
163// Streaming-body hooks (OTA / file upload) - gated by DETWS_ENABLE_STREAM_BODY.
164//
165// When set, the parser consults @ref HttpStreamBeginCb at end-of-headers (the
166// request line + all headers are parsed, so method/path/Authorization are
167// available). If it returns true, the body is streamed to @ref HttpStreamDataCb
168// in BODY_BUF_SIZE chunks instead of being buffered into body[] (and the
169// BODY_BUF_SIZE / 413 cap is bypassed), enabling multi-MB uploads such as a
170// firmware image fed to the ESP32 Update API or a file written to LittleFS. The
171// matching route handler still runs at ParseState::PARSE_COMPLETE to send the response.
172// ---------------------------------------------------------------------------
173
174/** @brief Decide whether to stream this request's body; begin the sink if so. */
175typedef bool (*HttpStreamBeginCb)(HttpReq *req);
176/** @brief Receive one body chunk for a streamed request (@p req identifies the connection). */
177typedef void (*HttpStreamDataCb)(HttpReq *req, const uint8_t *data, size_t len);
178/**
179 * @brief A streamed request was torn down before ParseState::PARSE_COMPLETE (peer reset,
180 * timeout, parse error). Lets the sink release its resource (close the file,
181 * abort the Update) so a half-sent upload never leaks a handle.
182 */
183typedef void (*HttpStreamAbortCb)(HttpReq *req);
184
185/** @brief Install the streaming-body hooks (pass nullptr to disable; abort optional). */
186void http_parser_set_stream_hooks(HttpStreamBeginCb begin, HttpStreamDataCb data, HttpStreamAbortCb abort = nullptr);
187#endif // DETWS_ENABLE_STREAM_BODY
188
189// ---------------------------------------------------------------------------
190// Parser API
191// ---------------------------------------------------------------------------
192
193/**
194 * @brief Reset a parser context to the initial (ParseState::PARSE_METHOD) state.
195 *
196 * Zeroes all fields and sets `parse_state = ParseState::PARSE_METHOD`. Call before the
197 * first use, after each completed or failed request, and on connection events.
198 *
199 * @param req Parser context to reset. Must not be null.
200 */
201void http_parser_reset(HttpReq *req);
202
203/**
204 * @brief Feed one byte to the parser state machine.
205 *
206 * Returns immediately without modifying state when `parse_state` is already
207 * `ParseState::PARSE_COMPLETE`, `ParseState::PARSE_ERROR`, `ParseState::PARSE_ENTITY_TOO_LARGE`, or
208 * `ParseState::PARSE_URI_TOO_LONG`.
209 *
210 * @param req Parser context for this request.
211 * @param byte Next byte from the HTTP stream.
212 */
213void http_parser_feed(HttpReq *req, uint8_t byte);
214
215/**
216 * @brief Look up a header value by name (case-insensitive).
217 *
218 * @param req Parsed request.
219 * @param key Header field name (e.g. `"Content-Type"`).
220 * @return Pointer to the null-terminated value, or `nullptr` if not found.
221 */
222const char *http_get_header(const HttpReq *req, const char *key);
223
224/**
225 * @brief Read a named cookie from the request `Cookie` header (RFC 6265 4.2.1).
226 *
227 * Parses the `name1=value1; name2=value2` list and copies the value of cookie
228 * @p name (case-sensitive) into @p out (null-terminated, bounded by @p out_size;
229 * a surrounding DQUOTE pair is stripped). Pairs with the session / CSRF / auth
230 * features (e.g. reading a session-id cookie).
231 *
232 * @return true if the cookie was found (value in @p out), false otherwise.
233 */
234bool http_get_cookie(const HttpReq *req, const char *name, char *out, size_t out_size);
235
236/**
237 * @brief Recover the original client from a reverse-proxy `Forwarded` (RFC 7239)
238 * or de-facto `X-Forwarded-For` / `X-Forwarded-Proto` header.
239 *
240 * Writes the leftmost (original-client) address into @p ip_out as its RFC 5952
241 * canonical text (bounded by @p ip_cap; use ::DET_IP_STR_MAX for the widest IPv6),
242 * and sets @p is_https from `proto=https` / `X-Forwarded-Proto: https`. Both IPv4
243 * (with an optional `:port`) and IPv6 (bracketed `for="[2001:db8::1]:port"` or a
244 * bare `X-Forwarded-For` literal) are recovered; the candidate is validated with
245 * det_ip_parse, so `unknown` / obfuscated `_id` identifiers and malformed tokens
246 * are rejected. The CALLER must only trust this when the TCP peer is a configured
247 * trusted upstream - the header is client-spoofable.
248 *
249 * @return true if a valid client address (IPv4 or IPv6) was written to @p ip_out.
250 */
251bool http_forwarded_client(const HttpReq *req, char *ip_out, size_t ip_cap, bool *is_https);
252
253/**
254 * @brief Look up a query parameter value by name (case-sensitive).
255 *
256 * @param req Parsed request.
257 * @param key Parameter name.
258 * @return Pointer to the null-terminated value (empty string if `key=` with
259 * no value), or `nullptr` if the key is absent.
260 */
261const char *http_get_query(const HttpReq *req, const char *key);
262
263/**
264 * @brief Look up an `application/x-www-form-urlencoded` body field by name.
265 *
266 * Parses the request body on demand (no extra per-request storage) when the
267 * `Content-Type` is `application/x-www-form-urlencoded`, and copies the raw
268 * (not percent-decoded, matching http_get_query()) value of @p key into
269 * @p out. Useful for classic HTML form POSTs.
270 *
271 * @param req Parsed request (body must be buffered, i.e. not streamed).
272 * @param key Field name (case-sensitive).
273 * @param out Caller buffer; always null-terminated on a true return.
274 * @param out_size Size of @p out in bytes (must be >= 1).
275 * @return `true` and fills @p out if the field is present; `false` otherwise
276 * (out is set to an empty string).
277 */
278bool http_get_form(const HttpReq *req, const char *key, char *out, size_t out_size);
279
280/**
281 * @brief Look up a captured path parameter by name (case-sensitive).
282 *
283 * Path parameters are the `:name` segments of a matched route pattern
284 * (e.g. route `"/users/:id"` matching `"/users/42"` captures `id`→`"42"`).
285 * Populated by the dispatcher when the route matches; valid for the duration
286 * of the handler.
287 *
288 * @param req Parsed request.
289 * @param key Parameter name without the leading `:`.
290 * @return Pointer to the null-terminated value, or `nullptr` if absent.
291 */
292const char *http_get_param(const HttpReq *req, const char *key);
293
294#endif
User-facing configuration for DeterministicESPAsyncWebServer.
#define CONN_POOL_SLOTS
#define MAX_VAL_LEN
Maximum header field-value length.
#define MAX_QUERY_PARAMS
Maximum number of parsed query-string parameters.
#define MAX_QUERY_LEN
Maximum raw query-string length (everything after ?).
#define DETWS_METHOD_BUF_SIZE
HTTP method-token buffer size (bytes, including the NUL).
#define QUERY_VAL_LEN
Maximum query-parameter value length.
#define BODY_BUF_SIZE
Maximum request body bytes stored in HttpReq::body.
#define MAX_HEADERS
Maximum HTTP headers stored per request.
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define QUERY_KEY_LEN
Maximum query-parameter key length.
#define MAX_KEY_LEN
Maximum header field-name length (e.g. "Content-Type").
#define MAX_PATH_PARAMS
Maximum number of :name path parameters captured per route match.
#define DETWS_AUTH_HDR_CAP
const char * http_get_param(const HttpReq *req, const char *key)
Look up a captured path parameter by name (case-sensitive).
ParseState
States of the HTTP/1.1 request parser.
Definition http_parser.h:53
@ PARSE_QUERY
Reading the raw query string (after ?).
@ PARSE_METHOD
Reading the HTTP method (GET, POST, …).
@ PARSE_ERROR
Unrecoverable parse failure → 400.
@ PARSE_ENTITY_TOO_LARGE
Content-Length > BODY_BUF_SIZE → 413.
@ PARSE_BODY
Reading the request body.
@ PARSE_COMPLETE
Full request parsed; ready for dispatch.
@ PARSE_HEADER_VAL
Reading a header field value.
@ PARSE_VERSION
Accumulating HTTP/1.x - hashed for validation.
@ PARSE_EXPECT_BODY_LF
Consuming the LF of the blank-line CRLF.
@ PARSE_HEADER_KEY
Reading a header field name.
@ PARSE_PATH
Reading the URL path component.
@ PARSE_URI_TOO_LONG
Path exceeds MAX_PATH_LEN → 414.
@ PARSE_EXPECT_LF
Consuming the LF of a header-line CRLF pair.
void http_parser_reset(HttpReq *req)
Reset a parser context to the initial (ParseState::PARSE_METHOD) state.
const char * http_get_header(const HttpReq *req, const char *key)
Look up a header value by name (case-insensitive).
void http_parser_feed(HttpReq *req, uint8_t byte)
Feed one byte to the parser state machine.
HttpReq http_pool[CONN_POOL_SLOTS]
Pool of parser contexts, one per connection-pool slot (incl. reserved dispatch slots).
HttpVersion
Parsed HTTP protocol version.
Definition http_parser.h:78
@ HTTP_10
HTTP/1.0 - close semantics by default.
@ HTTP_11
HTTP/1.1 - persistent connection by default.
@ HTTP_UNKNOWN
Version string did not match any known token.
bool http_get_cookie(const HttpReq *req, const char *name, char *out, size_t out_size)
Read a named cookie from the request Cookie header (RFC 6265 4.2.1).
bool http_forwarded_client(const HttpReq *req, char *ip_out, size_t ip_cap, bool *is_https)
Recover the original client from a reverse-proxy Forwarded (RFC 7239) or de-facto X-Forwarded-For / X...
bool http_get_form(const HttpReq *req, const char *key, char *out, size_t out_size)
Look up an application/x-www-form-urlencoded body field by name.
const char * http_get_query(const HttpReq *req, const char *key)
Look up a query parameter value by name (case-sensitive).
A single HTTP header field (key: value).
Definition http_parser.h:90
char val[MAX_VAL_LEN]
Field value, null-terminated.
Definition http_parser.h:92
char key[MAX_KEY_LEN]
Field name, null-terminated.
Definition http_parser.h:91
Fully-parsed HTTP/1.1 request.
QueryParam query_params[MAX_QUERY_PARAMS]
Parsed key=value pairs.
Header headers[MAX_HEADERS]
Captured header fields.
char query[MAX_QUERY_LEN]
Raw query string (after ?).
char method[DETWS_METHOD_BUF_SIZE]
HTTP method, null-terminated (OPTIONS, or WebDAV methods when enabled).
uint32_t _version_hash
FNV-1a accumulator for version validation (internal).
uint8_t body[BODY_BUF_SIZE+1]
Stored body bytes, always null-terminated.
QueryParam path_params[MAX_PATH_PARAMS]
:name captures from the matched route.
uint8_t header_count
Valid entries in headers[].
size_t current_token_idx
Write cursor shared by key/value sub-states.
ParseState parse_state
Current parser state.
size_t path_idx
Write cursor into path[].
size_t content_length
Value of Content-Length header (0 if absent).
char cur_key[MAX_KEY_LEN]
Field-name of the in-progress header.
uint8_t path_param_count
Valid entries in path_params[].
uint8_t slot_id
Transport slot index (set by presentation layer).
uint8_t content_length_count
Number of Content-Length fields seen (RFC 7230 §3.3.2).
size_t body_len
Bytes stored in body[] (≤ BODY_BUF_SIZE).
HttpVersion version
Protocol version parsed from the request line.
uint8_t host_count
Number of Host fields seen (RFC 7230 §5.4).
char cur_val[MAX_VAL_LEN]
Field-value of the in-progress header.
char path[MAX_PATH_LEN]
URL path, null-terminated; no query string.
uint8_t query_count
Valid entries in query_params[].
size_t body_bytes_read
Body bytes received (may exceed BODY_BUF_SIZE).
size_t query_idx
Write cursor into query[].
A single parsed query-string parameter.
Definition http_parser.h:97
char val[QUERY_VAL_LEN]
Parameter value (empty string if absent).
Definition http_parser.h:99
char key[QUERY_KEY_LEN]
Parameter name, null-terminated.
Definition http_parser.h:98