DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
Loading...
Searching...
No Matches
response.cpp
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 response.cpp
6 * @brief Response building for DetWebServer: template rendering, chunked/streaming responses,
7 * response headers + cookies, MIME typing, and the stats / Prometheus-metrics endpoints.
8 *
9 * Split out of dwserver.cpp (single-purpose server files). The two-pass {{name}} template walk,
10 * the cross-loop chunked-send pump (paged like the file pump so a body is unbounded in constant
11 * memory), the per-response header/cookie buffer API, mime_type(), and the built-in stats/metrics
12 * handlers (rendered through the template engine from generated web assets). The chunked pump
13 * shares the per-slot SendCtx owned by dwserver.cpp. Behaviour is identical to the pre-split code.
14 */
15
16#include "dwserver.h"
17#include "network_drivers/transport/tcp.h" // conn_pool, det_conn_send, TcpConn/ConnState
18#include "server/dwserver_internal.h" // status_text, req_is_head, SendCtx s_send
19#include "shared_primitives/mime.h" // DET_MIME_*, mime tables
20#include <stdio.h>
21#include <string.h>
22#if DETWS_ENABLE_METRICS || DETWS_ENABLE_STATS
23#include "network_drivers/application/web_assets.h" // DETWS_STATS_JSON / DETWS_METRICS_PROM (generated)
24#endif
25
26// ---------------------------------------------------------------------------
27// Template rendering
28//
29// Walk a template once: when @p pcb is null only the output length is summed
30// (pass 1); when @p pcb is set each literal run and resolved {{name}} value is
31// written to it (pass 2). Walking twice avoids buffering the whole body, so
32// memory use is constant. The resolver must be deterministic across the two
33// passes. A "{{" with no matching "}}", or a name longer than 32 chars, is
34// emitted literally.
35// ---------------------------------------------------------------------------
36// Consume one "{{name}}" placeholder at @p p (advancing it), sizing into @p total and, when
37// @p emit, streaming the resolved value. An unterminated or over-long (> 32 char) name is emitted
38// as a literal "{{" and the scan resumes just past it.
39static void tmpl_take_placeholder(uint8_t slot, const char *&p, TemplateVar resolver, bool emit, size_t &total)
40{
41 const char *end = strstr(p + 2, "}}");
42 size_t nlen = end ? (size_t)(end - (p + 2)) : 0;
43 if (!end || nlen > 32)
44 {
45 // Unterminated or over-long placeholder: emit "{{" literally.
46 total += 2;
47 if (emit)
48 det_conn_send(slot, "{{", 2);
49 p += 2;
50 return;
51 }
52 char name[33];
53 memcpy(name, p + 2, nlen);
54 name[nlen] = '\0';
55 const char *val = resolver ? resolver(name) : nullptr;
56 if (!val)
57 val = "";
58 size_t vlen = strnlen(val, 0xFFFF);
59 total += vlen;
60 if (emit && vlen)
61 det_conn_send(slot, val, (u16_t)vlen);
62 p = end + 2;
63}
64
65// Two-pass: pass 1 sizes the body (emit=false), pass 2 streams it (emit=true).
66static size_t tmpl_walk(uint8_t slot, const char *tmpl, TemplateVar resolver, bool emit)
67{
68 size_t total = 0;
69 const char *p = tmpl;
70 while (*p)
71 {
72 if (p[0] == '{' && p[1] == '{')
73 {
74 tmpl_take_placeholder(slot, p, resolver, emit, total);
75 continue;
76 }
77
78 // Literal run up to the next "{{".
79 const char *run = p;
80 while (*p && !(p[0] == '{' && p[1] == '{'))
81 p++;
82 size_t rlen = (size_t)(p - run);
83 total += rlen;
84 if (emit && rlen)
85 det_conn_send(slot, run, (u16_t)rlen);
86 }
87 return total;
88}
89
90void DetWebServer::send_template(uint8_t slot_id, int code, const char *content_type, const char *tmpl,
91 TemplateVar resolver)
92{
93 if (slot_id >= MAX_CONNS)
94 return;
95 if (!det_conn_active(slot_id))
96 {
97 http_reset(slot_id);
98 return;
99 }
100
101 // Pass 1: size the rendered body (no writes).
102 size_t body_len = tmpl_walk(slot_id, tmpl, resolver, false);
103
104 bool keep;
105 const char *cl = resp_conn_hdr(slot_id, &keep);
106
107 char header[RESP_HDR_BUF_SIZE];
108 int hlen = snprintf(header, sizeof(header),
109 "HTTP/1.1 %d %s\r\n"
110 "Content-Type: %s\r\n"
111 "Content-Length: %d\r\n",
112 code, status_text(code), content_type, (int)body_len);
113 hlen = append_resp_trailer(header, sizeof(header), hlen, slot_id, cl);
114
115 bool head = req_is_head(slot_id);
116
117 det_conn_send(slot_id, header, (u16_t)hlen);
118 // Pass 2: stream the rendered body (HEAD carries headers only).
119 if (!head && body_len > 0)
120 tmpl_walk(slot_id, tmpl, resolver, true);
121
122 resp_end(slot_id, code, (int)body_len, keep);
123}
124
125// ---------------------------------------------------------------------------
126// Chunked (streaming) responses
127//
128// send_chunked() writes the headers, then pulls the body from a ChunkSource one
129// piece at a time, emitting each as an HTTP/1.1 chunk ("<hexlen>\r\n<data>\r\n",
130// RFC 7230 §4.1) and finally the terminating "0\r\n\r\n". Like the file pump, the
131// body pages across worker loops as the TCP send window drains (chunk_send_pump,
132// resumed by the sent callback), so a response is unbounded in constant memory and
133// never truncated at the window. The source's ctx must outlive the response (see
134// ChunkSource). One chunked response per slot at a time.
135// ---------------------------------------------------------------------------
136
137void DetWebServer::send_chunked(uint8_t slot_id, int code, const char *content_type, ChunkSource source, void *ctx)
138{
139 if (slot_id >= MAX_CONNS)
140 return;
141 if (!det_conn_active(slot_id))
142 {
143 http_reset(slot_id);
144 return;
145 }
146
147 bool keep;
148 const char *cl = resp_conn_hdr(slot_id, &keep);
149
150 // RFC 7230 3.3.1: chunked is an HTTP/1.1 transfer-coding - it MUST NOT be sent
151 // to an HTTP/1.0 (or unknown-version) client. Fall back to a close-delimited
152 // body: omit Transfer-Encoding, force Connection: close, stream the body
153 // unframed, and signal its end by closing the connection (RFC 7230 3.3.3).
154 bool raw = (http_pool[slot_id].version != HttpVersion::HTTP_11);
155
156 char header[RESP_HDR_BUF_SIZE];
157 int hlen;
158 if (raw)
159 {
160 keep = false; // close-delimited: the connection close IS the message boundary
161 cl = "Connection: close\r\n";
162 hlen = snprintf(header, sizeof(header),
163 "HTTP/1.0 %d %s\r\n"
164 "Content-Type: %s\r\n",
165 code, status_text(code), content_type);
166 }
167 else
168 hlen = snprintf(header, sizeof(header),
169 "HTTP/1.1 %d %s\r\n"
170 "Content-Type: %s\r\n"
171 "Transfer-Encoding: chunked\r\n",
172 code, status_text(code), content_type);
173 hlen = append_resp_trailer(header, sizeof(header), hlen, slot_id, cl);
174
175 det_conn_send(slot_id, header, (u16_t)hlen);
176
177 // HEAD carries the headers but no body or terminator.
178 if (req_is_head(slot_id) || !source)
179 {
180 resp_end(slot_id, code, 0, keep);
181 return;
182 }
183
184 ChunkSend &s = s_send.chunk[slot_id];
185 s.source = source;
186 s.ctx = ctx;
187 s.status = code;
188 s.total = 0;
189 s.keep = keep;
190 s.active = true;
191 s.raw = raw;
192 chunk_send_pump(slot_id);
193}
194
195// Page a pending chunked response: pull pieces from the source and frame them into
196// the send window each worker loop, resuming on later loops as the window drains.
197void DetWebServer::chunk_send_pump(uint8_t slot_id)
198{
199 ChunkSend &s = s_send.chunk[slot_id];
200 if (!s.active)
201 return;
202
203 if (!det_conn_active(slot_id))
204 {
205 s.active = false; // connection gone mid-stream
206 return;
207 }
208
209 // A body still being paged out is active, not idle: keep the CONN_TIMEOUT_MS idle sweep off
210 // it so a transient send stall on a large stream cannot reap the slot mid-transfer.
211 det_conn_touch_active(slot_id);
212
213 // Frame each chunk in ONE buffer so it goes out in a single tcpip_thread round-trip (was three -
214 // size line, body, CRLF - each a ~23 us marshal on-device). Reserve CHUNK_HDR_RESERVE bytes ahead
215 // of the body for the "<hex>\r\n" size line and 2 after for the trailing CRLF, so the source writes
216 // the body in place and the whole "<hex>\r\n<body>\r\n" is one det_conn_send with no extra copy.
217 // FRAME reserves send-window room for that framing; the raw (HTTP/1.0) path sends the body verbatim.
218 static const u16_t CHUNK_HDR_RESERVE = 8; // "<hex>\r\n" is <= 6 bytes for a chunk <= 0xFFFF
219 const u16_t FRAME = s.raw ? 0 : 12;
220 uint8_t framed[CHUNK_HDR_RESERVE + CHUNK_BUF_SIZE + 2];
221 for (;;)
222 {
223 u16_t avail = det_conn_sndbuf(slot_id);
224 if (avail <= FRAME)
225 {
226 det_conn_flush(slot_id); // no room for a useful chunk; resume next loop
227 return;
228 }
229 size_t cap = (size_t)(avail - FRAME);
230 if (cap > CHUNK_BUF_SIZE)
231 cap = CHUNK_BUF_SIZE;
232
233 uint8_t *body = framed + CHUNK_HDR_RESERVE;
234 size_t n = s.source(body, cap, s.ctx);
235 if (n == 0)
236 {
237 if (!s.raw)
238 det_conn_send(slot_id, "0\r\n\r\n", 5); // terminating chunk (1.1 only)
239 det_conn_flush(slot_id);
240 s.active = false;
241 resp_end(slot_id, s.status, s.total, s.keep); // raw: keep==false -> connection close ends the body
242 return;
243 }
244 if (n > cap)
245 n = cap; // defensive: a misbehaving source must not overrun the window
246
247 if (s.raw)
248 {
249 det_conn_send(slot_id, body, (u16_t)n); // close-delimited: no chunk framing
250 }
251 else
252 {
253 // Prepend the size line (right-justified against the body) + append the trailing CRLF,
254 // then send the framed chunk in one call.
255 char sz[8];
256 int sn = snprintf(sz, sizeof(sz), "%x\r\n", (unsigned)n);
257 uint8_t *start = body - sn;
258 memcpy(start, sz, (size_t)sn);
259 body[n] = '\r';
260 body[n + 1] = '\n';
261 det_conn_send(slot_id, start, (u16_t)((size_t)sn + n + 2));
262 }
263 s.total += (int)n;
264 }
265}
266
267// ---------------------------------------------------------------------------
268// Custom response headers / cookies
269//
270// Appended to a fixed per-slot buffer during a handler and injected into the
271// send paths above. A header that would overflow the buffer is dropped whole
272// (the buffer is rewound to its prior length) so a malformed half-line never
273// reaches the wire.
274// ---------------------------------------------------------------------------
275
276void DetWebServer::add_response_header(uint8_t slot_id, const char *name, const char *value)
277{
278 if (slot_id >= MAX_CONNS || name == nullptr || value == nullptr)
279 return;
280
281 char *buf = _extra_hdr[slot_id];
282 size_t used = strnlen(buf, EXTRA_HDR_BUF_SIZE);
283 size_t room = EXTRA_HDR_BUF_SIZE - used;
284 int n = snprintf(buf + used, room, "%s: %s\r\n", name, value);
285 if (n < 0 || (size_t)n >= room)
286 buf[used] = '\0'; // would not fit: drop this header entirely
287}
288
289void DetWebServer::set_cookie(uint8_t slot_id, const char *name, const char *value, const char *attrs)
290{
291 if (slot_id >= MAX_CONNS || name == nullptr || value == nullptr)
292 return;
293
294 char *buf = _extra_hdr[slot_id];
295 size_t used = strnlen(buf, EXTRA_HDR_BUF_SIZE);
296 size_t room = EXTRA_HDR_BUF_SIZE - used;
297 int n;
298 if (attrs != nullptr && attrs[0] != '\0')
299 n = snprintf(buf + used, room, "Set-Cookie: %s=%s; %s\r\n", name, value, attrs);
300 else
301 n = snprintf(buf + used, room, "Set-Cookie: %s=%s\r\n", name, value);
302 if (n < 0 || (size_t)n >= room)
303 buf[used] = '\0'; // would not fit: drop this cookie entirely
304}
305
307{
308 if (slot_id >= MAX_CONNS)
309 return;
310 _extra_hdr[slot_id][0] = '\0';
311}
312
313// ---------------------------------------------------------------------------
314// MIME type lookup by extension
315// ---------------------------------------------------------------------------
316
317const char *DetWebServer::mime_type(const char *path)
318{
319 if (!path)
320 return DET_MIME_OCTET_STREAM;
321
322 // Find the last '.' after the last '/'.
323 const char *dot = nullptr;
324 for (const char *p = path; *p; p++)
325 {
326 if (*p == '/')
327 dot = nullptr;
328 else if (*p == '.')
329 dot = p;
330 }
331 if (!dot || dot[1] == '\0')
332 return DET_MIME_OCTET_STREAM;
333 const char *ext = dot + 1;
334
335 // Case-insensitive compare against a small static table.
336 static const struct
337 {
338 const char *ext;
339 const char *type;
340 } table[] = {
341 {"html", DET_MIME_TEXT_HTML}, {"htm", DET_MIME_TEXT_HTML}, {"css", "text/css"},
342 {"js", DET_MIME_JAVASCRIPT}, {"mjs", DET_MIME_JAVASCRIPT}, {"json", DET_MIME_JSON},
343 {"xml", "application/xml"}, {"txt", DET_MIME_TEXT_PLAIN}, {"csv", "text/csv"},
344 {"svg", "image/svg+xml"}, {"png", "image/png"}, {"jpg", "image/jpeg"},
345 {"jpeg", "image/jpeg"}, {"gif", "image/gif"}, {"ico", "image/x-icon"},
346 {"webp", "image/webp"}, {"wasm", "application/wasm"}, {"woff", "font/woff"},
347 {"woff2", "font/woff2"}, {"ttf", "font/ttf"}, {"pdf", "application/pdf"},
348 {"gz", "application/gzip"},
349 };
350 for (size_t i = 0; i < sizeof(table) / sizeof(table[0]); i++)
351 {
352 const char *a = ext;
353 const char *b = table[i].ext;
354 bool eq = true;
355 while (*a && *b)
356 {
357 char ca = (*a >= 'A' && *a <= 'Z') ? (char)(*a + 32) : *a;
358 char cb = *b; // table is already lowercase
359 if (ca != cb)
360 {
361 eq = false;
362 break;
363 }
364 a++;
365 b++;
366 }
367 if (eq && *a == '\0' && *b == '\0')
368 return table[i].type;
369 }
370 return DET_MIME_OCTET_STREAM;
371}
372
373// ---------------------------------------------------------------------------
374// Runtime stats endpoint
375// ---------------------------------------------------------------------------
376
377#if DETWS_ENABLE_STATS
378// The stats body is an editable template asset (src/web/input/DETWS_STATS_JSON.json)
379// rendered through the {{name}} engine, like /metrics - values are substituted by
380// name, with no printf-format coupling. Snapshot into statics just before the
381// (twice-invoked, size + emit) resolver runs.
382struct StatsCtx
383{
384 char uptime[12];
385 char requests[12];
386 char n2xx[12];
387 char n4xx[12];
388 char n5xx[12];
389 char active[8];
390 char heap[12];
391};
392static StatsCtx s_stats;
393
394static const char *stats_var(const char *name)
395{
396 if (!strcmp(name, "uptime_ms"))
397 return s_stats.uptime;
398 if (!strcmp(name, "requests"))
399 return s_stats.requests;
400 if (!strcmp(name, "http_2xx"))
401 return s_stats.n2xx;
402 if (!strcmp(name, "http_4xx"))
403 return s_stats.n4xx;
404 if (!strcmp(name, "http_5xx"))
405 return s_stats.n5xx;
406 if (!strcmp(name, "active_conns"))
407 return s_stats.active;
408 if (!strcmp(name, "free_heap"))
409 return s_stats.heap;
410 return nullptr;
411}
412
413void DetWebServer::stats(uint8_t slot_id)
414{
415 int active = det_conn_active_count();
416
417 unsigned long up = millis();
418#ifdef ARDUINO
419 uint32_t heap = ESP.getFreeHeap();
420#else
421 uint32_t heap = 0;
422#endif
423
424 snprintf(s_stats.uptime, sizeof(s_stats.uptime), "%lu", up);
425 snprintf(s_stats.requests, sizeof(s_stats.requests), "%lu", (unsigned long)_stat_requests);
426 snprintf(s_stats.n2xx, sizeof(s_stats.n2xx), "%lu", (unsigned long)_stat_2xx);
427 snprintf(s_stats.n4xx, sizeof(s_stats.n4xx), "%lu", (unsigned long)_stat_4xx);
428 snprintf(s_stats.n5xx, sizeof(s_stats.n5xx), "%lu", (unsigned long)_stat_5xx);
429 snprintf(s_stats.active, sizeof(s_stats.active), "%d", active);
430 snprintf(s_stats.heap, sizeof(s_stats.heap), "%u", (unsigned)heap);
431
432 send_template(slot_id, 200, DET_MIME_JSON, DETWS_STATS_JSON, stats_var);
433}
434#endif // DETWS_ENABLE_STATS
435
436#if DETWS_ENABLE_METRICS
437// The Prometheus exposition is an editable template asset (src/web/input/
438// DETWS_METRICS_PROM.txt) rendered through the {{name}} engine, so values are
439// substituted by name (no printf format coupling). metrics() snapshots the live
440// values into these statics just before send_template(), which invokes the
441// resolver twice (size + emit) - deterministic because the snapshot is fixed.
442struct MetricsCtx
443{
444 char uptime[12];
445 char requests[12];
446 char n2xx[12];
447 char n4xx[12];
448 char n5xx[12];
449 char active[8];
450 char max[8];
451 char heap[12];
452 char minheap[12];
453 char heapsize[12];
454 char maxalloc[12];
455};
456static MetricsCtx s_metrics;
457
458static const char *metrics_var(const char *name)
459{
460 if (!strcmp(name, "uptime_seconds"))
461 return s_metrics.uptime;
462 if (!strcmp(name, "requests_total"))
463 return s_metrics.requests;
464 if (!strcmp(name, "resp_2xx"))
465 return s_metrics.n2xx;
466 if (!strcmp(name, "resp_4xx"))
467 return s_metrics.n4xx;
468 if (!strcmp(name, "resp_5xx"))
469 return s_metrics.n5xx;
470 if (!strcmp(name, "active_conns"))
471 return s_metrics.active;
472 if (!strcmp(name, "max_conns"))
473 return s_metrics.max;
474 if (!strcmp(name, "free_heap"))
475 return s_metrics.heap;
476 if (!strcmp(name, "min_free_heap"))
477 return s_metrics.minheap;
478 if (!strcmp(name, "heap_size"))
479 return s_metrics.heapsize;
480 if (!strcmp(name, "max_alloc_heap"))
481 return s_metrics.maxalloc;
482 return nullptr;
483}
484
485void DetWebServer::metrics(uint8_t slot_id)
486{
487 int active = det_conn_active_count();
488
489 unsigned long up = millis();
490#ifdef ARDUINO
491 uint32_t heap = ESP.getFreeHeap();
492 uint32_t min_heap = ESP.getMinFreeHeap();
493 uint32_t heap_size = ESP.getHeapSize();
494 uint32_t max_alloc = ESP.getMaxAllocHeap();
495#else
496 uint32_t heap = 0;
497 uint32_t min_heap = 0;
498 uint32_t heap_size = 0;
499 uint32_t max_alloc = 0;
500#endif
501
502 snprintf(s_metrics.uptime, sizeof(s_metrics.uptime), "%lu", up / 1000UL);
503 snprintf(s_metrics.requests, sizeof(s_metrics.requests), "%lu", (unsigned long)_stat_requests);
504 snprintf(s_metrics.n2xx, sizeof(s_metrics.n2xx), "%lu", (unsigned long)_stat_2xx);
505 snprintf(s_metrics.n4xx, sizeof(s_metrics.n4xx), "%lu", (unsigned long)_stat_4xx);
506 snprintf(s_metrics.n5xx, sizeof(s_metrics.n5xx), "%lu", (unsigned long)_stat_5xx);
507 snprintf(s_metrics.active, sizeof(s_metrics.active), "%d", active);
508 snprintf(s_metrics.max, sizeof(s_metrics.max), "%d", (int)MAX_CONNS);
509 snprintf(s_metrics.heap, sizeof(s_metrics.heap), "%u", (unsigned)heap);
510 snprintf(s_metrics.minheap, sizeof(s_metrics.minheap), "%u", (unsigned)min_heap);
511 snprintf(s_metrics.heapsize, sizeof(s_metrics.heapsize), "%u", (unsigned)heap_size);
512 snprintf(s_metrics.maxalloc, sizeof(s_metrics.maxalloc), "%u", (unsigned)max_alloc);
513
514 send_template(slot_id, 200, "text/plain; version=0.0.4; charset=utf-8", DETWS_METRICS_PROM, metrics_var);
515}
516#endif // DETWS_ENABLE_METRICS
#define CHUNK_BUF_SIZE
Per-chunk staging buffer for send_chunked()'s ChunkSource (max bytes a source produces per call,...
#define RESP_HDR_BUF_SIZE
Stack buffer for HTTP response header lines in send() / send_empty() / send_unauth() / serve_file().
#define MAX_CONNS
Maximum simultaneous TCP connections (fixed static pool; ~3.95 KB of internal RAM per slot).
#define EXTRA_HDR_BUF_SIZE
Per-connection buffer for app-supplied custom response headers and cookies.
void set_cookie(uint8_t slot_id, const char *name, const char *value, const char *attrs=nullptr)
Queue a Set-Cookie response header for the next send on this slot.
Definition response.cpp:289
void add_response_header(uint8_t slot_id, const char *name, const char *value)
Queue a custom response header for the next send on this slot.
Definition response.cpp:276
void clear_response_headers(uint8_t slot_id)
Discard any headers/cookies queued for this slot.
Definition response.cpp:306
void send_chunked(uint8_t slot_id, int code, const char *content_type, ChunkSource source, void *ctx=nullptr)
Stream a response body of unknown length via chunked transfer.
Definition response.cpp:137
static const char * mime_type(const char *path)
Guess a Content-Type from a path's file extension.
Definition response.cpp:317
void send_template(uint8_t slot_id, int code, const char *content_type, const char *tmpl, TemplateVar resolver)
Send a response body with {{name}} placeholders substituted.
Definition response.cpp:90
const char * status_text(int code)
Convert an HTTP status code to its standard reason phrase.
Definition dwserver.cpp:112
SendCtx s_send
Definition dwserver.cpp:97
bool req_is_head(uint8_t slot_id)
True if the request in slot slot_id used the HEAD method (send headers, no body).
Layer 7 (Application) - public HTTP routing API.
size_t(* ChunkSource)(uint8_t *buf, size_t cap, void *ctx)
Source callback that produces a chunked response body incrementally.
Definition dwserver.h:310
const char *(* TemplateVar)(const char *name)
Resolver for {{name}} template placeholders used by send_template().
Definition dwserver.h:116
Library-private declarations shared between dwserver.cpp and the src/server/*.cpp request-handler tra...
HttpReq http_pool[CONN_POOL_SLOTS]
Pool of parser contexts, one per connection-pool slot (incl. reserved dispatch slots).
@ HTTP_11
HTTP/1.1 - persistent connection by default.
Shared HTTP Content-Type ("MIME") string constants (one source of truth).
void http_reset(uint8_t slot_id)
Reset the HTTP parser for a connection slot.
ChunkSource source
body generator (active==false means none).
int status
response status, for note_response.
void * ctx
caller state passed to source (must outlive the send).
bool raw
HTTP/1.0 client: stream the body unframed, close-delimited (no chunk wrapping).
bool active
a chunked response is in progress on this slot.
int total
body bytes emitted so far (excludes framing).
bool keep
keep-alive vs close at completion.
HttpVersion version
Protocol version parsed from the request line.
ChunkSend chunk[MAX_CONNS]
bool det_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:362
void det_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
Definition tcp.cpp:413
u16_t det_conn_sndbuf(uint8_t slot)
Bytes that can currently be queued for sending on slot.
Definition tcp.cpp:398
void det_conn_touch_active(uint8_t slot_id)
Refresh slot's idle-timeout timestamp while a response body is in flight.
Definition tcp.cpp:718
uint8_t det_conn_active_count()
Number of server connection slots currently in the CONN_ACTIVE state.
Definition tcp.cpp:646
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
const char DETWS_STATS_JSON[]
Runtime stats JSON (rendered via send_template); add/rename fields to taste.
const char DETWS_METRICS_PROM[]
All available Prometheus metrics; comment a value line out with a leading # to drop it.
Layer 7 (Application) - embedded web assets generated from src/web/input/.