DeterministicESPAsyncWebServer 1.2.0
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
DeterministicESPAsyncWebServer.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 DeterministicESPAsyncWebServer.h
6 * @brief Layer 7 (Application) — public HTTP routing API.
7 *
8 * This is the only header most application code needs to include.
9 * The full OSI include chain is pulled in automatically:
10 * @code
11 * DeterministicESPAsyncWebServer.h
12 * └── network_drivers/presentation.h (Layer 6)
13 * ├── network_drivers/transport.h (Layer 4)
14 * │ └── DetWebServerConfig.h (compile-time constants + feature flags)
15 * └── network_drivers/http_parser.h (parser types)
16 * └── network_drivers/session.h (Layer 5 — event queue)
17 * @endcode
18 *
19 * **Feature flags** — define any of these to 0 before including to strip
20 * the feature from the build entirely:
21 * @code
22 * #define DETWS_ENABLE_WEBSOCKET 0
23 * #define DETWS_ENABLE_SSE 0
24 * #define DETWS_ENABLE_MULTIPART 0
25 * #define DETWS_ENABLE_FILE_SERVING 0
26 * #define DETWS_ENABLE_AUTH 0
27 * #include <DeterministicESPAsyncWebServer.h>
28 * @endcode
29 *
30 * **Determinism guarantees**
31 * - All buffers are statically allocated; no heap usage after begin().
32 * - Every operation has O(1) or O(MAX_ROUTES) worst-case time.
33 * - `handle()` is safe to call every Arduino `loop()` iteration.
34 *
35 * @author Douglas Quigg (dstroy0)
36 * @date 2026
37 * @copyright Copyright (C) 2026 Douglas Quigg (dstroy0). AGPL-3.0-or-later.
38 */
39
40#ifndef DETERMINISTICESPASYNCWEBSERVER_H
41#define DETERMINISTICESPASYNCWEBSERVER_H
42
45#if DETWS_ENABLE_WEBSOCKET
47#endif
48#if DETWS_ENABLE_SSE
49#include "network_drivers/sse.h"
50#endif
51#if DETWS_ENABLE_MULTIPART
53#endif
54#include <Arduino.h>
55#if DETWS_ENABLE_FILE_SERVING
56#ifdef ARDUINO
57#include <FS.h>
58#else
59#include "FS.h"
60#endif
61#endif
62
63// ---------------------------------------------------------------------------
64// HTTP method enumeration
65// ---------------------------------------------------------------------------
66
67/**
68 * @brief HTTP request methods supported by the router.
69 *
70 * Pass one of these values to DetWebServer::on() to bind a route to a
71 * specific method. PATCH, HEAD, and OPTIONS were added in v1.0 alongside
72 * CORS preflight support.
73 */
75{
76 HTTP_GET, ///< Safe, idempotent read
77 HTTP_POST, ///< Non-idempotent create / action
78 HTTP_PUT, ///< Idempotent replace
79 HTTP_DELETE, ///< Idempotent delete
80 HTTP_PATCH, ///< Partial update
81 HTTP_HEAD, ///< Same as GET but no response body
82 HTTP_OPTIONS ///< Capability query / CORS preflight
83};
84
85// ---------------------------------------------------------------------------
86// Handler and route types
87// ---------------------------------------------------------------------------
88
89/**
90 * @brief Callback signature for HTTP request handlers.
91 *
92 * The callback receives the connection slot index and a pointer to the
93 * fully-parsed request. Call DetWebServer::send() or DetWebServer::send_empty()
94 * from inside the callback to write a response.
95 *
96 * @param slot_id Index into the connection pool (0 … MAX_CONNS-1).
97 * @param request Pointer to the parsed HTTP request. Valid only during the
98 * callback; do not cache this pointer.
99 *
100 * @note If the callback returns without calling send(), the framework will
101 * reset the slot automatically (no response is sent to the client).
102 */
103typedef void (*Handler)(uint8_t slot_id, HttpReq *request);
104
105#if DETWS_ENABLE_WEBSOCKET
106/**
107 * @brief Callback fired when a WebSocket connection is established.
108 *
109 * @param ws_id Index into ws_pool[] for this connection.
110 */
111typedef void (*WsConnectHandler)(uint8_t ws_id);
112
113/**
114 * @brief Callback fired when a WebSocket text or binary frame arrives.
115 *
116 * The payload is in ws_pool[ws_id].buf, null-terminated. Length is in
117 * ws_pool[ws_id].payload_len. Opcode is in ws_pool[ws_id].opcode.
118 *
119 * @param ws_id Index into ws_pool[].
120 */
121typedef void (*WsMessageHandler)(uint8_t ws_id);
122
123/**
124 * @brief Callback fired when a WebSocket connection closes.
125 *
126 * @param ws_id Index into ws_pool[] (slot is still valid during callback).
127 */
128typedef void (*WsCloseHandler)(uint8_t ws_id);
129#endif // DETWS_ENABLE_WEBSOCKET
130
131#if DETWS_ENABLE_SSE
132/**
133 * @brief Callback fired when a new SSE client connects.
134 *
135 * Use sse_send() inside this callback to push an initial event if needed.
136 *
137 * @param sse_id Index into sse_pool[] for this connection.
138 */
139typedef void (*SseConnectHandler)(uint8_t sse_id);
140#endif // DETWS_ENABLE_SSE
141
142// ---------------------------------------------------------------------------
143// Route type discriminator
144// ---------------------------------------------------------------------------
145
146/** @brief Discriminates between HTTP, WebSocket, and SSE route entries. */
148{
149 ROUTE_HTTP, ///< Standard HTTP request/response.
150#if DETWS_ENABLE_WEBSOCKET
151 ROUTE_WS, ///< WebSocket upgrade route.
152#endif
153#if DETWS_ENABLE_SSE
154 ROUTE_SSE ///< Server-Sent Events route.
155#endif
156};
157
158/**
159 * @brief Internal route entry stored in the routing table.
160 *
161 * Populated by DetWebServer::on(), on_ws(), or on_sse().
162 * Application code does not interact with this struct directly.
163 */
164struct Route
165{
166 char path[MAX_PATH_LEN]; ///< Null-terminated path pattern.
167 RouteType type; ///< HTTP, WS, or SSE.
168 HttpMethod method; ///< HTTP method (ROUTE_HTTP only).
169 Handler callback; ///< HTTP handler (ROUTE_HTTP only).
170
171#if DETWS_ENABLE_WEBSOCKET
172 WsConnectHandler ws_connect; ///< Fired on upgrade success.
173 WsMessageHandler ws_message; ///< Fired on each data frame.
174 WsCloseHandler ws_close; ///< Fired on close.
175#endif
176
177#if DETWS_ENABLE_SSE
178 SseConnectHandler sse_connect; ///< Fired when client subscribes.
179#endif
180
181#if DETWS_ENABLE_AUTH
182 bool auth_required; ///< True when this route requires Basic Auth.
183 char auth_realm[MAX_AUTH_LEN]; ///< WWW-Authenticate realm string.
184 char auth_user[MAX_AUTH_LEN]; ///< Required username.
185 char auth_pass[MAX_AUTH_LEN]; ///< Required password.
186#endif
187
188 bool is_active; ///< `false` for unused table slots.
189 bool is_wildcard; ///< `true` when path ends with `*`.
190};
191
192// ---------------------------------------------------------------------------
193// DetWebServer — the main application class
194// ---------------------------------------------------------------------------
195
196/**
197 * @class DetWebServer
198 * @brief Single-port HTTP server with deterministic, zero-allocation execution.
199 *
200 * ## Typical usage
201 * @code
202 * DetWebServer server;
203 *
204 * void handle_api(uint8_t slot_id, HttpReq *req) {
205 * server.send(slot_id, 200, "application/json", "{\"ok\":true}");
206 * }
207 *
208 * void setup() {
209 * WiFi.begin("SSID", "PASSWORD");
210 * server.on("/api/*", HTTP_GET, handle_api);
211 * server.set_cors("*");
212 * int32_t result = server.begin(80);
213 * if (result < 0) { } // abs(result) == heap bytes needed
214 * }
215 *
216 * void loop() {
217 * server.handle(); // call every iteration — O(MAX_CONNS) per call
218 * }
219 * @endcode
220 *
221 * ## Design constraints
222 * - Maximum simultaneous connections: `MAX_CONNS` (default 4).
223 * - Maximum registered routes: `MAX_ROUTES` (default 16).
224 * - Responses are sent synchronously and the TCP connection is closed
225 * immediately after every response (HTTP/1.0 close semantics).
226 */
228{
229 private:
230 Route _routes[MAX_ROUTES]; ///< Flat routing table; searched linearly.
231 uint8_t _route_count; ///< Number of active entries in _routes.
232
233 uint16_t _port; ///< TCP port passed to begin(); 0 before first begin().
234 Handler _not_found_handler; ///< Called when no route matches; may be null.
235 bool _cors_enabled; ///< True after a non-empty set_cors() call.
236
237 /**
238 * @brief Pre-built CORS header block injected into every response.
239 *
240 * Built once by set_cors() to avoid repeated snprintf at dispatch time.
241 */
242 char _cors_header_buf[CORS_HDR_BUF_SIZE];
243
244 /**
245 * @brief Evaluate whether a route pattern matches a request path.
246 *
247 * Wildcard routes end with `*`; the `*` is replaced by a prefix match.
248 * Exact routes use strcmp.
249 *
250 * @param route Null-terminated route pattern.
251 * @param is_wildcard True if route ends with `*`.
252 * @param req_path Null-terminated path from the parsed request.
253 * @return True if the route matches the request path.
254 */
255 static bool path_matches(const char *route, bool is_wildcard, const char *req_path);
256
257#if DETWS_ENABLE_AUTH
258 static bool check_basic_auth(uint8_t slot_id, HttpReq *req, const Route *r);
259 void send_unauth(uint8_t slot_id, const char *realm);
260#endif
261
262 /**
263 * @brief Look up and invoke the first matching route for the given slot.
264 *
265 * If CORS is enabled and the method is OPTIONS, the preflight is
266 * short-circuited here with a 204 response. If no route matches, the
267 * not-found handler is invoked (or a default 404 is sent).
268 *
269 * @param slot_id Connection slot to dispatch.
270 */
271 void match_and_execute(uint8_t slot_id);
272
273 public:
274 /**
275 * @brief Bytes of contiguous heap that begin() will allocate.
276 *
277 * The event queue is the library's only dynamic allocation. Compare
278 * this value against heap_caps_get_largest_free_block(MALLOC_CAP_8BIT)
279 * to verify a suitable block exists before calling begin().
280 *
281 * @code
282 * if (!DetWebServer::heap_available()) {
283 * Serial.printf("need %u contiguous bytes, largest block is %u\n",
284 * DetWebServer::heap_needed(),
285 * heap_caps_get_largest_free_block(MALLOC_CAP_8BIT));
286 * return;
287 * }
288 * server.begin(80);
289 * @endcode
290 */
291 static size_t heap_needed();
292
293 /**
294 * @brief True if the largest contiguous free heap block >= heap_needed().
295 *
296 * A false return means begin() will fail; check heap fragmentation or
297 * reduce EVT_QUEUE_DEPTH.
298 */
299 static bool heap_available();
300
301 /**
302 * @brief Construct a DetWebServer with an empty routing table.
303 *
304 * All route slots are marked inactive. CORS is disabled. The
305 * not-found handler is null (falls back to built-in 404 response).
306 */
307 DetWebServer();
308
309 /**
310 * @brief Initialise all connection slots and open the TCP listener.
311 *
312 * Resets the HTTP parser pool and delegates to DeterministicAsyncTCP::init().
313 * Pass a WebServerConfig to tune runtime parameters (timeout, etc.) at
314 * init time; the config may live in PROGMEM (flash) or RAM.
315 *
316 * @param port TCP port to listen on (typically 80).
317 * @param cfg Optional runtime configuration. Pass nullptr for defaults.
318 * @return Positive value on success; negative value whose absolute value is
319 * the number of heap bytes needed when initialisation fails.
320 */
321 int32_t begin(uint16_t port, const WebServerConfig *cfg = nullptr);
322
323 /**
324 * @brief Gracefully stop the server.
325 *
326 * Aborts all active connections, closes the listener, frees the event
327 * queue, and resets all HTTP parser slots. The WiFi and TCP/IP stack
328 * remain active. Call begin() or restart() to bring the server back up.
329 */
330 void stop();
331
332 /**
333 * @brief Hard-reset all connections and reinitialise the server on the
334 * same port that was passed to begin().
335 *
336 * Equivalent to stop() followed by begin() with the original port and an
337 * optional new runtime config. The WiFi and TCP/IP stack are not touched.
338 * Returns the same values as begin(): positive on success, negative whose
339 * absolute value is the heap bytes needed on failure.
340 *
341 * Calling restart() before begin() has no effect and returns -1.
342 *
343 * @param cfg Optional new runtime configuration. Pass nullptr to reuse
344 * the compile-time default (CONN_TIMEOUT_MS).
345 */
346 int32_t restart(const WebServerConfig *cfg = nullptr);
347
348 /**
349 * @brief Register a route handler.
350 *
351 * Routes are matched in registration order (first match wins).
352 * A trailing `*` in @p path enables prefix matching:
353 * `"/api/*"` matches `"/api/users"`, `"/api/devices"`, etc.
354 *
355 * @param path URL path pattern, e.g. `"/api/status"` or `"/files/*"`.
356 * Must be ≤ `MAX_PATH_LEN - 1` characters.
357 * @param method HTTP method this route accepts.
358 * @param callback Function called when this route is matched.
359 *
360 * @note Registering more than MAX_ROUTES routes silently drops extras.
361 */
362 void on(const char *path, HttpMethod method, Handler callback);
363
364#if DETWS_ENABLE_AUTH
365 /**
366 * @brief Register a route handler protected by HTTP Basic Authentication.
367 *
368 * If the request does not include valid credentials, the library sends
369 * `401 Unauthorized` with a `WWW-Authenticate: Basic realm="<realm>"`
370 * header automatically; the callback is not invoked.
371 *
372 * @param path URL path pattern.
373 * @param method HTTP method.
374 * @param callback Handler invoked only on successful authentication.
375 * @param realm WWW-Authenticate realm displayed by the browser.
376 * @param user Required username.
377 * @param pass Required password.
378 */
379 void on(const char *path, HttpMethod method, Handler callback,
380 const char *realm, const char *user, const char *pass);
381#endif // DETWS_ENABLE_AUTH
382
383#if DETWS_ENABLE_FILE_SERVING
384 /**
385 * @brief Serve a file from any Arduino-compatible filesystem.
386 *
387 * Opens @p fs_path on @p file_sys, sends HTTP 200 with the appropriate
388 * headers (Content-Type, Content-Length), and streams the file body in
389 * FILE_CHUNK_SIZE chunks via tcp_write(). Sends 404 if the file cannot
390 * be opened.
391 *
392 * @param slot_id Connection slot index.
393 * @param file_sys Filesystem reference (e.g. SPIFFS, LittleFS).
394 * @param fs_path Path to the file on the filesystem.
395 * @param content_type MIME type string, e.g. "text/html".
396 */
397 void serve_file(uint8_t slot_id, fs::FS &file_sys,
398 const char *fs_path, const char *content_type);
399#endif // DETWS_ENABLE_FILE_SERVING
400
401 /**
402 * @brief Register a fallback handler for unmatched requests.
403 *
404 * Called instead of sending a built-in 404 when no route matches.
405 * The callback may call send() to return a custom error page.
406 *
407 * @param callback Handler to invoke on a 404 condition.
408 */
409 void on_not_found(Handler callback);
410
411 /**
412 * @brief Enable CORS by pre-building the Access-Control headers.
413 *
414 * Once called, every response produced by send() and send_empty()
415 * includes the CORS headers. OPTIONS requests are intercepted and
416 * answered with 204 automatically (preflight short-circuit).
417 *
418 * @param origin `Access-Control-Allow-Origin` value, e.g. `"*"` or
419 * `"https://example.com"`. Pass `""` to disable CORS.
420 */
421 void set_cors(const char *origin);
422
423 /**
424 * @brief Drive the server — call every Arduino `loop()` iteration.
425 *
426 * Internally this:
427 * 1. Calls `DeterministicAsyncTCP::check_timeouts()` to kill stale
428 * connections.
429 * 2. Drains the event queue (connections, data, disconnects, errors).
430 * 3. Scans all connection slots for `PARSE_COMPLETE` requests and
431 * dispatches them to the matching route handler.
432 * 4. Auto-sends 400 for any slot stuck in `PARSE_ERROR`.
433 * 5. Auto-sends 413 for any slot stuck in `PARSE_ENTITY_TOO_LARGE`.
434 * 6. Auto-sends 414 for any slot stuck in `PARSE_URI_TOO_LONG`.
435 */
436 void handle();
437
438 /**
439 * @brief Send an HTTP response with a body and close the connection.
440 *
441 * Writes status line, Content-Type, Content-Length, optional CORS
442 * headers, and the payload; then calls tcp_close (tcp_abort on failure).
443 * Always calls http_reset() at the end to free the parser slot.
444 *
445 * @param slot_id Connection slot index returned by the router.
446 * @param code HTTP status code (200, 404, 500, …).
447 * @param content_type MIME type string, e.g. `"application/json"`.
448 * @param payload Null-terminated response body.
449 *
450 * @note If the underlying PCB has already been freed (e.g. by a
451 * concurrent timeout), this function is a no-op that just
452 * resets the slot.
453 */
454 void send(uint8_t slot_id, int code, const char *content_type, const char *payload);
455
456 /**
457 * @brief Send a headers-only HTTP response and close the connection.
458 *
459 * Equivalent to send() with an empty body and Content-Length: 0.
460 * Useful for 204 No Content, 304 Not Modified, HEAD responses, and
461 * CORS preflight replies.
462 *
463 * @param slot_id Connection slot index.
464 * @param code HTTP status code.
465 */
466 void send_empty(uint8_t slot_id, int code);
467
468#if DETWS_ENABLE_DIAG
469 /**
470 * @brief Send the diagnostic JSON and close the connection.
471 *
472 * Responds with 200 application/json containing the compile-time feature
473 * flags and all capacity constants. Only available when
474 * DETWS_ENABLE_DIAG is set to 1 — disable before deploying to production.
475 *
476 * @param slot_id Connection slot index.
477 */
478 void diag(uint8_t slot_id);
479#endif
480
481#if DETWS_ENABLE_WEBSOCKET
482 // -----------------------------------------------------------------------
483 // WebSocket API
484 // -----------------------------------------------------------------------
485
486 /**
487 * @brief Register a WebSocket upgrade route.
488 *
489 * When a GET request arrives for @p path with `Upgrade: websocket`, the
490 * library performs the RFC 6455 handshake automatically and fires
491 * @p on_connect. Subsequent frames fire @p on_message. Closing the
492 * connection fires @p on_close.
493 *
494 * Ping frames are answered with Pong automatically; no handler needed.
495 *
496 * @param path URL path the client connects to, e.g. `"/ws"`.
497 * @param on_connect Fired once when the handshake completes. May be nullptr.
498 * @param on_message Fired for each text or binary frame. Must not be nullptr.
499 * @param on_close Fired when the connection closes. May be nullptr.
500 */
501 void on_ws(const char *path,
502 WsConnectHandler on_connect,
503 WsMessageHandler on_message,
504 WsCloseHandler on_close);
505
506 /**
507 * @brief Send a text frame to a WebSocket client.
508 *
509 * @param ws_id Index into ws_pool[] (from the WsConnectHandler or WsMessageHandler).
510 * @param text Null-terminated UTF-8 string to send.
511 */
512 void ws_send_text(uint8_t ws_id, const char *text);
513
514 /**
515 * @brief Send a binary frame to a WebSocket client.
516 *
517 * @param ws_id Index into ws_pool[].
518 * @param data Payload bytes.
519 * @param len Payload length in bytes; must be <= WS_FRAME_SIZE.
520 */
521 void ws_send_binary(uint8_t ws_id, const uint8_t *data, uint16_t len);
522
523 /**
524 * @brief Initiate a graceful WebSocket close.
525 *
526 * Sends a Close frame with WS_CLOSE_NORMAL and marks the slot WS_CLOSED.
527 * The on_close handler fires on the next handle() call.
528 *
529 * @param ws_id Index into ws_pool[].
530 */
531 void ws_disconnect(uint8_t ws_id);
532#endif // DETWS_ENABLE_WEBSOCKET
533
534#if DETWS_ENABLE_SSE
535 // -----------------------------------------------------------------------
536 // Server-Sent Events API
537 // -----------------------------------------------------------------------
538
539 /**
540 * @brief Register a Server-Sent Events endpoint.
541 *
542 * When a GET request arrives for @p path, the library sends the SSE
543 * headers and keeps the connection open. @p on_connect fires so the
544 * handler can push an initial event with sse_send().
545 *
546 * @param path URL path, e.g. `"/events"`.
547 * @param on_connect Fired when a client subscribes. May be nullptr.
548 */
549 void on_sse(const char *path, SseConnectHandler on_connect);
550
551 /**
552 * @brief Push an event to one SSE client.
553 *
554 * Formats and sends `event: ...\ndata: ...\nid: ...\n\n` to the client
555 * on @p sse_id. Any field may be nullptr to omit it from the output.
556 * The data field is required; passing nullptr sends nothing.
557 *
558 * @param sse_id Index into sse_pool[].
559 * @param data Event data string (required).
560 * @param event Optional event name (sets the `event:` field).
561 * @param id Optional event ID (sets the `id:` field).
562 */
563 void sse_send(uint8_t sse_id, const char *data,
564 const char *event = nullptr, const char *id = nullptr);
565
566 /**
567 * @brief Push an event to all connected SSE clients on a given path.
568 *
569 * Iterates sse_pool[] and calls sse_send() for every active client
570 * whose path matches @p path.
571 *
572 * @param path SSE endpoint path, e.g. `"/events"`.
573 * @param data Event data string.
574 * @param event Optional event name.
575 * @param id Optional event ID.
576 */
577 void sse_broadcast(const char *path, const char *data,
578 const char *event = nullptr, const char *id = nullptr);
579#endif // DETWS_ENABLE_SSE
580};
581
582#endif
#define MAX_PATH_LEN
Maximum URL path length (including leading /).
#define MAX_AUTH_LEN
Maximum username or password length for HTTP Basic Authentication.
#define CORS_HDR_BUF_SIZE
Size of the pre-built CORS header block stored in DetWebServer.
#define MAX_ROUTES
Maximum simultaneously registered routes.
RouteType
Discriminates between HTTP, WebSocket, and SSE route entries.
@ ROUTE_HTTP
Standard HTTP request/response.
HttpMethod
HTTP request methods supported by the router.
@ HTTP_PUT
Idempotent replace.
@ HTTP_DELETE
Idempotent delete.
@ HTTP_GET
Safe, idempotent read.
@ HTTP_OPTIONS
Capability query / CORS preflight.
@ HTTP_POST
Non-idempotent create / action.
@ HTTP_HEAD
Same as GET but no response body.
@ HTTP_PATCH
Partial update.
void(* Handler)(uint8_t slot_id, HttpReq *request)
Callback signature for HTTP request handlers.
Single-port HTTP server with deterministic, zero-allocation execution.
void on(const char *path, HttpMethod method, Handler callback)
Register a route handler.
void handle()
Drive the server — call every Arduino loop() iteration.
int32_t restart(const WebServerConfig *cfg=nullptr)
Hard-reset all connections and reinitialise the server on the same port that was passed to begin().
int32_t begin(uint16_t port, const WebServerConfig *cfg=nullptr)
Initialise all connection slots and open the TCP listener.
void send(uint8_t slot_id, int code, const char *content_type, const char *payload)
Send an HTTP response with a body and close the connection.
void on_not_found(Handler callback)
Register a fallback handler for unmatched requests.
void send_empty(uint8_t slot_id, int code)
Send a headers-only HTTP response and close the connection.
void set_cors(const char *origin)
Enable CORS by pre-building the Access-Control headers.
static bool heap_available()
True if the largest contiguous free heap block >= heap_needed().
static size_t heap_needed()
Bytes of contiguous heap that begin() will allocate.
DetWebServer()
Construct a DetWebServer with an empty routing table.
void stop()
Gracefully stop the server.
In-place multipart/form-data parser (RFC 7578).
Layer 6 (Presentation) — wires the transport ring buffer to the HTTP parser.
Layer 5 (Session) — event queue dispatcher and session lifecycle.
Layer 6 (Presentation) – Server-Sent Events connection pool.
Fully-parsed HTTP/1.1 request.
Internal route entry stored in the routing table.
RouteType type
HTTP, WS, or SSE.
Handler callback
HTTP handler (ROUTE_HTTP only).
HttpMethod method
HTTP method (ROUTE_HTTP only).
bool is_active
false for unused table slots.
bool is_wildcard
true when path ends with *.
char path[MAX_PATH_LEN]
Null-terminated path pattern.
Runtime-tunable server parameters.
void ws_close(WsConn *ws, WsCloseCode code)
Send a Close frame and mark the slot WS_CLOSED.
Layer 6 (Presentation) – WebSocket frame parser and connection pool.