ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
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 PC: template rendering, chunked/streaming responses,
7 * response headers + cookies, MIME typing, and the stats / Prometheus-metrics endpoints.
8 *
9 * Split out of protocore.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 protocore.cpp. Behavior is identical to the pre-split code.
14 */
15
16#include "network_drivers/transport/tcp.h" // conn_pool, pc_conn_send, TcpConn/ConnState
17#include "protocore.h"
18#include "server/protocore_internal.h" // status_text, req_is_head, SendCtx s_send
19#include "shared_primitives/hex.h" // pc_hex_u32 (chunk size-line writer)
20#include "shared_primitives/mime.h" // PC_MIME_*, mime tables
21#include "shared_primitives/strbuf.h" // pc_sb frame builder (replaces snprintf)
22#include <stdio.h>
23#include <string.h>
24#if PC_ENABLE_METRICS || PC_ENABLE_STATS
25#include "network_drivers/application/web_assets.h" // PC_STATS_JSON / PC_METRICS_PROM (generated)
26
27// Render @p v as decimal into the fixed field @p dst. Both exposition snapshots below fill a
28// dozen of these. Unlike snprintf, pc_sb_finish() does NOT terminate when the value would not
29// fit - it reports 0 and leaves the buffer untouched - so an over-long value must be turned
30// into an empty field explicitly, or the exposition would serve the PREVIOUS snapshot's digits.
31static void num_field(char *dst, size_t cap, uint32_t v)
32{
33 pc_sb b = {dst, cap, 0, true};
34 pc_sb_u32(&b, v);
35 if (pc_sb_finish(&b) == 0)
36 {
37 dst[0] = '\0';
38 }
39}
40#endif
41
42// ---------------------------------------------------------------------------
43// Template rendering
44//
45// Walk a template once: when @p pcb is null only the output length is summed
46// (pass 1); when @p pcb is set each literal run and resolved {{name}} value is
47// written to it (pass 2). Walking twice avoids buffering the whole body, so
48// memory use is constant. The resolver must be deterministic across the two
49// passes. A "{{" with no matching "}}", or a name longer than 32 chars, is
50// emitted literally.
51// ---------------------------------------------------------------------------
52// Consume one "{{name}}" placeholder at @p p (advancing it), sizing into @p total and, when
53// @p emit, streaming the resolved value. An unterminated or over-long (> 32 char) name is emitted
54// as a literal "{{" and the scan resumes just past it.
55static void tmpl_take_placeholder(uint8_t slot, const char *&p, TemplateVar resolver, bool emit, size_t &total)
56{
57 const char *end = strstr(p + 2, "}}");
58 size_t nlen = end ? (size_t)(end - (p + 2)) : 0;
59 if (!end || nlen > 32)
60 {
61 // Unterminated or over-long placeholder: emit "{{" literally.
62 total += 2;
63 if (emit)
64 {
65 pc_conn_send(slot, "{{", 2);
66 }
67 p += 2;
68 return;
69 }
70 char name[33];
71 memcpy(name, p + 2, nlen);
72 name[nlen] = '\0';
73 const char *val = resolver ? resolver(name) : nullptr;
74 if (!val)
75 {
76 val = "";
77 }
78 size_t vlen = strnlen(val, 0xFFFF);
79 total += vlen;
80 if (emit && vlen)
81 {
82 pc_conn_send(slot, val, (u16_t)vlen);
83 }
84 p = end + 2;
85}
86
87// Two-pass: pass 1 sizes the body (emit=false), pass 2 streams it (emit=true).
88static size_t tmpl_walk(uint8_t slot, const char *tmpl, TemplateVar resolver, bool emit)
89{
90 size_t total = 0;
91 const char *p = tmpl;
92 while (*p)
93 {
94 if (p[0] == '{' && p[1] == '{')
95 {
96 tmpl_take_placeholder(slot, p, resolver, emit, total);
97 continue;
98 }
99
100 // Literal run up to the next "{{".
101 const char *run = p;
102 while (*p && !(p[0] == '{' && p[1] == '{'))
103 {
104 p++;
105 }
106 size_t rlen = (size_t)(p - run);
107 total += rlen;
108 // GCOVR_EXCL_BR_START rlen == 0 cannot fire: control only reaches here when p is NOT at a
109 // "{{", so the scan loop above always advances p at least one byte and a literal run is
110 // always >= 1. (The vlen test in tmpl_take_placeholder, which CAN be 0, is exercised.)
111 if (emit && rlen)
112 {
113 pc_conn_send(slot, run, (u16_t)rlen);
114 }
115 // GCOVR_EXCL_BR_STOP
116 }
117 return total;
118}
119
120void PC::send_template(uint8_t slot_id, int code, const char *content_type, const char *tmpl, TemplateVar resolver)
121{
122 if (slot_id >= MAX_CONNS)
123 {
124 return;
125 }
126 if (!pc_conn_active(slot_id))
127 {
128 http_reset(slot_id);
129 return;
130 }
131
132 // Pass 1: size the rendered body (no writes).
133 size_t body_len = tmpl_walk(slot_id, tmpl, resolver, false);
134
135 bool keep;
136 const char *cl = pc_resp_conn_hdr(slot_id, &keep);
137
138 char header[RESP_HDR_BUF_SIZE];
139 pc_sb hb = {header, RESP_HDR_BUF_SIZE, 0, true};
140 pc_sb_lit(&hb, "HTTP/1.1 ");
141 pc_sb_u32(&hb, (uint32_t)code);
142 pc_sb_lit(&hb, " ");
143 pc_sb_put(&hb, status_text(code));
144 pc_sb_lit(&hb, "\r\nContent-Type: ");
145 pc_sb_put(&hb, content_type);
146 pc_sb_lit(&hb, "\r\nContent-Length: ");
147 pc_sb_u32(&hb, (uint32_t)body_len);
148 pc_sb_lit(&hb, "\r\n");
149 int hlen = (int)pc_sb_finish(&hb);
150 hlen = append_resp_trailer(header, RESP_HDR_BUF_SIZE, hlen, slot_id, cl);
151
152 bool head = req_is_head(slot_id);
153
154 pc_conn_send(slot_id, header, (u16_t)hlen);
155 // Pass 2: stream the rendered body (HEAD carries headers only).
156 if (!head && body_len > 0)
157 {
158 tmpl_walk(slot_id, tmpl, resolver, true);
159 }
160
161 pc_resp_end(slot_id, code, (int)body_len, keep);
162}
163
164// ---------------------------------------------------------------------------
165// Chunked (streaming) responses
166//
167// send_chunked() writes the headers, then pulls the body from a ChunkSource one
168// piece at a time, emitting each as an HTTP/1.1 chunk ("<hexlen>\r\n<data>\r\n",
169// RFC 7230 ยง4.1) and finally the terminating "0\r\n\r\n". Like the file pump, the
170// body pages across worker loops as the TCP send window drains (chunk_send_pump,
171// resumed by the sent callback), so a response is unbounded in constant memory and
172// never truncated at the window. The source's ctx must outlive the response (see
173// ChunkSource). One chunked response per slot at a time.
174// ---------------------------------------------------------------------------
175
176void PC::send_chunked(uint8_t slot_id, int code, const char *content_type, ChunkSource source, void *ctx)
177{
178 if (slot_id >= MAX_CONNS)
179 {
180 return;
181 }
182 if (!pc_conn_active(slot_id))
183 {
184 http_reset(slot_id);
185 return;
186 }
187
188 bool keep;
189 const char *cl = pc_resp_conn_hdr(slot_id, &keep);
190
191 // RFC 7230 3.3.1: chunked is an HTTP/1.1 transfer-coding - it MUST NOT be sent
192 // to an HTTP/1.0 (or unknown-version) client. Fall back to a close-delimited
193 // body: omit Transfer-Encoding, force Connection: close, stream the body
194 // unframed, and signal its end by closing the connection (RFC 7230 3.3.3).
195 bool raw = (http_pool[slot_id].version != HttpVersion::HTTP_11);
196
197 char header[RESP_HDR_BUF_SIZE];
198 pc_sb hb2 = {header, RESP_HDR_BUF_SIZE, 0, true};
199 if (raw)
200 {
201 keep = false; // close-delimited: the connection close IS the message boundary
202 cl = "Connection: close\r\n";
203 pc_sb_put(&hb2, "HTTP/1.0 ");
204 }
205 else
206 {
207 pc_sb_put(&hb2, "HTTP/1.1 ");
208 }
209 pc_sb_u32(&hb2, (uint32_t)code);
210 pc_sb_put(&hb2, " ");
211 pc_sb_put(&hb2, status_text(code));
212 pc_sb_put(&hb2, "\r\nContent-Type: ");
213 pc_sb_put(&hb2, content_type);
214 pc_sb_put(&hb2, raw ? "\r\n" : "\r\nTransfer-Encoding: chunked\r\n");
215 int hlen = (int)pc_sb_finish(&hb2);
216 hlen = append_resp_trailer(header, RESP_HDR_BUF_SIZE, hlen, slot_id, cl);
217
218 pc_conn_send(slot_id, header, (u16_t)hlen);
219
220 // HEAD carries the headers but no body or terminator.
221 if (req_is_head(slot_id) || !source)
222 {
223 pc_resp_end(slot_id, code, 0, keep);
224 return;
225 }
226
227 ChunkSend &s = s_send.chunk[slot_id];
228 s.source = source;
229 s.ctx = ctx;
230 s.status = code;
231 s.total = 0;
232 s.keep = keep;
233 s.active = true;
234 s.raw = raw;
235 chunk_send_pump(slot_id);
236}
237
238// Page a pending chunked response: pull pieces from the source and frame them into
239// the send window each worker loop, resuming on later loops as the window drains.
240void PC::chunk_send_pump(uint8_t slot_id)
241{
242 ChunkSend &s = s_send.chunk[slot_id];
243 // GCOVR_EXCL_START unreachable: both callers already established the state - send_chunked() sets
244 // s.active immediately before its call, and the poll loop in protocore.cpp only pumps a slot whose
245 // s_send.chunk[i].active is set. Kept so the pump is safe to call unconditionally.
246 if (!s.active)
247 {
248 return;
249 }
250 // GCOVR_EXCL_STOP
251
252 if (!pc_conn_active(slot_id))
253 {
254 s.active = false; // connection gone mid-stream
255 return;
256 }
257
258 // A body still being paged out is active, not idle: keep the CONN_TIMEOUT_MS idle sweep off
259 // it so a transient send stall on a large stream cannot reap the slot mid-transfer.
260 pc_conn_touch_active(slot_id);
261
262 // Frame each chunk in ONE buffer so it goes out in a single tcpip_thread round-trip (was three -
263 // size line, body, CRLF - each a ~23 us marshal on-device). Reserve CHUNK_HDR_RESERVE bytes ahead
264 // of the body for the "<hex>\r\n" size line and 2 after for the trailing CRLF, so the source writes
265 // the body in place and the whole "<hex>\r\n<body>\r\n" is one pc_conn_send with no extra copy.
266 // FRAME reserves send-window room for that framing; the raw (HTTP/1.0) path sends the body verbatim.
267 static const u16_t CHUNK_HDR_RESERVE = 8; // "<hex>\r\n" is <= 6 bytes for a chunk <= 0xFFFF
268 const u16_t FRAME = s.raw ? 0 : 12;
269 uint8_t framed[CHUNK_HDR_RESERVE + CHUNK_BUF_SIZE + 2];
270 for (;;)
271 {
272 u16_t avail = pc_conn_sndbuf(slot_id);
273 if (avail <= FRAME)
274 {
275 pc_conn_flush(slot_id); // no room for a useful chunk; resume next loop
276 return;
277 }
278 size_t cap = (size_t)(avail - FRAME);
279 if (cap > CHUNK_BUF_SIZE)
280 {
281 cap = CHUNK_BUF_SIZE;
282 }
283
284 uint8_t *body = framed + CHUNK_HDR_RESERVE;
285 size_t n = s.source(body, cap, s.ctx);
286 if (n == 0)
287 {
288 if (!s.raw)
289 {
290 pc_conn_send(slot_id, "0\r\n\r\n", 5); // terminating chunk (1.1 only)
291 }
292 pc_conn_flush(slot_id);
293 s.active = false;
294 pc_resp_end(slot_id, s.status, s.total, s.keep); // raw: keep==false -> connection close ends the body
295 return;
296 }
297 if (n > cap)
298 {
299 n = cap; // defensive: a misbehaving source must not overrun the window
300 }
301
302 if (s.raw)
303 {
304 pc_conn_send(slot_id, body, (u16_t)n); // close-delimited: no chunk framing
305 }
306 else
307 {
308 // Prepend the size line (right-justified against the body) + append the trailing CRLF,
309 // then send the framed chunk in one call. The size line is a hand-written hex (pc_hex_u32),
310 // not snprintf("%x") - the format-string parse dwarfed the few nibble writes on the hot
311 // per-chunk path (performance_benching/server/send_pump: ~9x on the host, more on the ESP32).
312 char digits[8];
313 size_t nd = pc_hex_u32((uint32_t)n, digits);
314 size_t sn = nd + 2; // "<hex>\r\n"
315 uint8_t *start = body - sn;
316 memcpy(start, digits, nd);
317 start[nd] = '\r';
318 start[nd + 1] = '\n';
319 body[n] = '\r';
320 body[n + 1] = '\n';
321 pc_conn_send(slot_id, start, (u16_t)(sn + n + 2));
322 }
323 s.total += (int)n;
324 }
325}
326
327// ---------------------------------------------------------------------------
328// Custom response headers / cookies
329//
330// Appended to a fixed per-slot buffer during a handler and injected into the
331// send paths above. A header that would overflow the buffer is dropped whole
332// (the buffer is rewound to its prior length) so a malformed half-line never
333// reaches the wire.
334// ---------------------------------------------------------------------------
335
336void PC::add_response_header(uint8_t slot_id, const char *name, const char *value)
337{
338 if (slot_id >= MAX_CONNS || name == nullptr || value == nullptr)
339 {
340 return;
341 }
342
343 char *buf = _extra_hdr[slot_id];
344 size_t used = strnlen(buf, EXTRA_HDR_BUF_SIZE);
345 size_t room = EXTRA_HDR_BUF_SIZE - used;
346 pc_sb hb3 = {buf + used, room, 0, true};
347 pc_sb_put(&hb3, name);
348 pc_sb_put(&hb3, ": ");
349 pc_sb_put(&hb3, value);
350 pc_sb_put(&hb3, "\r\n");
351 // A latched builder may have written the pieces that did fit, so rewinding to `used` is what
352 // drops the header whole - the same contract snprintf's truncation test enforced.
353 if (pc_sb_finish(&hb3) == 0)
354 {
355 buf[used] = '\0';
356 }
357}
358
359void PC::set_cookie(uint8_t slot_id, const char *name, const char *value, const char *attrs)
360{
361 if (slot_id >= MAX_CONNS || name == nullptr || value == nullptr)
362 {
363 return;
364 }
365
366 char *buf = _extra_hdr[slot_id];
367 size_t used = strnlen(buf, EXTRA_HDR_BUF_SIZE);
368 size_t room = EXTRA_HDR_BUF_SIZE - used;
369 pc_sb cb = {buf + used, room, 0, true};
370 pc_sb_put(&cb, "Set-Cookie: ");
371 pc_sb_put(&cb, name);
372 pc_sb_put(&cb, "=");
373 pc_sb_put(&cb, value);
374 if (attrs != nullptr && attrs[0] != '\0')
375 {
376 pc_sb_put(&cb, "; ");
377 pc_sb_put(&cb, attrs);
378 }
379 pc_sb_put(&cb, "\r\n");
380 if (pc_sb_finish(&cb) == 0)
381 {
382 buf[used] = '\0'; // would not fit: drop this cookie entirely
383 }
384}
385
386void PC::clear_response_headers(uint8_t slot_id)
387{
388 if (slot_id >= MAX_CONNS)
389 {
390 return;
391 }
392 _extra_hdr[slot_id][0] = '\0';
393}
394
395// ---------------------------------------------------------------------------
396// MIME type lookup by extension
397// ---------------------------------------------------------------------------
398
399const char *PC::mime_type(const char *path)
400{
401 if (!path)
402 {
403 return PC_MIME_OCTET_STREAM;
404 }
405
406 // Find the last '.' after the last '/'.
407 const char *dot = nullptr;
408 for (const char *p = path; *p; p++)
409 {
410 if (*p == '/')
411 {
412 dot = nullptr;
413 }
414 else if (*p == '.')
415 {
416 dot = p;
417 }
418 }
419 if (!dot || dot[1] == '\0')
420 {
421 return PC_MIME_OCTET_STREAM;
422 }
423 const char *ext = dot + 1;
424
425 // Case-insensitive compare against a small static table.
426 static const struct
427 {
428 const char *ext;
429 const char *type;
430 } table[] = {
431 {"html", PC_MIME_TEXT_HTML}, {"htm", PC_MIME_TEXT_HTML}, {"css", "text/css"},
432 {"js", PC_MIME_JAVASCRIPT}, {"mjs", PC_MIME_JAVASCRIPT}, {"json", PC_MIME_JSON},
433 {"xml", "application/xml"}, {"txt", PC_MIME_TEXT_PLAIN}, {"csv", "text/csv"},
434 {"svg", "image/svg+xml"}, {"png", "image/png"}, {"jpg", "image/jpeg"},
435 {"jpeg", "image/jpeg"}, {"gif", "image/gif"}, {"ico", "image/x-icon"},
436 {"webp", "image/webp"}, {"wasm", "application/wasm"}, {"woff", "font/woff"},
437 {"woff2", "font/woff2"}, {"ttf", "font/ttf"}, {"pdf", "application/pdf"},
438 {"gz", "application/gzip"},
439 };
440 for (size_t i = 0; i < sizeof(table) / sizeof(table[0]); i++)
441 {
442 const char *a = ext;
443 const char *b = table[i].ext;
444 bool eq = true;
445 while (*a && *b)
446 {
447 char ca = (*a >= 'A' && *a <= 'Z') ? (char)(*a + 32) : *a;
448 char cb = *b; // table is already lowercase
449 if (ca != cb)
450 {
451 eq = false;
452 break;
453 }
454 a++;
455 b++;
456 }
457 if (eq && *a == '\0' && *b == '\0')
458 {
459 return table[i].type;
460 }
461 }
462 return PC_MIME_OCTET_STREAM;
463}
464
465// ---------------------------------------------------------------------------
466// Runtime stats endpoint
467// ---------------------------------------------------------------------------
468
469#if PC_ENABLE_STATS
470// The stats body is an editable template asset (web_assets/input/PC_STATS_JSON.json)
471// rendered through the {{name}} engine, like /metrics - values are substituted by
472// name, with no printf-format coupling. Snapshot into statics just before the
473// (twice-invoked, size + emit) resolver runs.
474struct StatsCtx
475{
476 char uptime[12];
477 char requests[12];
478 char n2xx[12];
479 char n4xx[12];
480 char n5xx[12];
481 char active[8];
482 char heap[12];
483};
484static StatsCtx s_stats;
485
486static const char *stats_var(const char *name)
487{
488 if (!strcmp(name, "uptime_ms"))
489 {
490 return s_stats.uptime;
491 }
492 if (!strcmp(name, "requests"))
493 {
494 return s_stats.requests;
495 }
496 if (!strcmp(name, "http_2xx"))
497 {
498 return s_stats.n2xx;
499 }
500 if (!strcmp(name, "http_4xx"))
501 {
502 return s_stats.n4xx;
503 }
504 if (!strcmp(name, "http_5xx"))
505 {
506 return s_stats.n5xx;
507 }
508 if (!strcmp(name, "active_conns"))
509 {
510 return s_stats.active;
511 }
512 // The not-found tail is unreachable: stats_var is only ever invoked by stats() against
513 // PC_STATS_JSON, and that asset's seven placeholders are exactly the seven names tested here,
514 // so the last one always matches. Kept because the resolver has to answer an unknown name.
515 if (!strcmp(name, "free_heap")) // GCOVR_EXCL_BR_LINE always matches (see above)
516 {
517 return s_stats.heap;
518 }
519 return nullptr; // GCOVR_EXCL_LINE unreachable: every PC_STATS_JSON name resolves above
520}
521
522void PC::stats(uint8_t slot_id)
523{
524 int active = pc_conn_active_count();
525
526 unsigned long up = millis();
527#ifdef ARDUINO
528 uint32_t heap = ESP.getFreeHeap();
529#else
530 uint32_t heap = 0;
531#endif
532
533 // millis() is a 32-bit tick counter, so the uptime field wraps with it exactly as before.
534 num_field(s_stats.uptime, sizeof(s_stats.uptime), (uint32_t)up);
535 num_field(s_stats.requests, sizeof(s_stats.requests), (uint32_t)_stat_requests);
536 num_field(s_stats.n2xx, sizeof(s_stats.n2xx), (uint32_t)_stat_2xx);
537 num_field(s_stats.n4xx, sizeof(s_stats.n4xx), (uint32_t)_stat_4xx);
538 num_field(s_stats.n5xx, sizeof(s_stats.n5xx), (uint32_t)_stat_5xx);
539 num_field(s_stats.active, sizeof(s_stats.active), (uint32_t)(active < 0 ? 0 : active));
540 num_field(s_stats.heap, sizeof(s_stats.heap), heap);
541
542 send_template(slot_id, 200, PC_MIME_JSON, PC_STATS_JSON, stats_var);
543}
544#endif // PC_ENABLE_STATS
545
546#if PC_ENABLE_METRICS
547// The Prometheus exposition is an editable template asset (web_assets/input/
548// PC_METRICS_PROM.txt) rendered through the {{name}} engine, so values are
549// substituted by name (no printf format coupling). metrics() snapshots the live
550// values into these statics just before send_template(), which invokes the
551// resolver twice (size + emit) - deterministic because the snapshot is fixed.
552struct MetricsCtx
553{
554 char uptime[12];
555 char requests[12];
556 char n2xx[12];
557 char n4xx[12];
558 char n5xx[12];
559 char active[8];
560 char max[8];
561 char heap[12];
562 char minheap[12];
563 char heapsize[12];
564 char maxalloc[12];
565};
566static MetricsCtx s_metrics;
567
568static const char *metrics_var(const char *name)
569{
570 if (!strcmp(name, "uptime_seconds"))
571 {
572 return s_metrics.uptime;
573 }
574 if (!strcmp(name, "requests_total"))
575 {
576 return s_metrics.requests;
577 }
578 if (!strcmp(name, "resp_2xx"))
579 {
580 return s_metrics.n2xx;
581 }
582 if (!strcmp(name, "resp_4xx"))
583 {
584 return s_metrics.n4xx;
585 }
586 if (!strcmp(name, "resp_5xx"))
587 {
588 return s_metrics.n5xx;
589 }
590 if (!strcmp(name, "active_conns"))
591 {
592 return s_metrics.active;
593 }
594 if (!strcmp(name, "max_conns"))
595 {
596 return s_metrics.max;
597 }
598 if (!strcmp(name, "free_heap"))
599 {
600 return s_metrics.heap;
601 }
602 if (!strcmp(name, "min_free_heap"))
603 {
604 return s_metrics.minheap;
605 }
606 if (!strcmp(name, "heap_size"))
607 {
608 return s_metrics.heapsize;
609 }
610 // The not-found tail is unreachable: metrics_var is only ever driven by the placeholders in
611 // PC_METRICS_PROM.txt, and every one of the 11 resolves to a case above. That is not an
612 // assumption - test_metrics_emits_prometheus asserts every emitted sample line carries a
613 // value, which fails the moment a placeholder stops resolving (as three of them silently did
614 // until the resolver names were aligned with the template).
615 if (!strcmp(name, "max_alloc_heap")) // GCOVR_EXCL_BR_LINE - no placeholder falls past here
616 {
617 return s_metrics.maxalloc;
618 }
619 return nullptr; // GCOVR_EXCL_LINE - see above
620}
621
622void PC::metrics(uint8_t slot_id)
623{
624 int active = pc_conn_active_count();
625
626 unsigned long up = millis();
627#ifdef ARDUINO
628 uint32_t heap = ESP.getFreeHeap();
629 uint32_t min_heap = ESP.getMinFreeHeap();
630 uint32_t heap_size = ESP.getHeapSize();
631 uint32_t max_alloc = ESP.getMaxAllocHeap();
632#else
633 uint32_t heap = 0;
634 uint32_t min_heap = 0;
635 uint32_t heap_size = 0;
636 uint32_t max_alloc = 0;
637#endif
638
639 num_field(s_metrics.uptime, sizeof(s_metrics.uptime), (uint32_t)(up / 1000UL));
640 num_field(s_metrics.requests, sizeof(s_metrics.requests), (uint32_t)_stat_requests);
641 num_field(s_metrics.n2xx, sizeof(s_metrics.n2xx), (uint32_t)_stat_2xx);
642 num_field(s_metrics.n4xx, sizeof(s_metrics.n4xx), (uint32_t)_stat_4xx);
643 num_field(s_metrics.n5xx, sizeof(s_metrics.n5xx), (uint32_t)_stat_5xx);
644 num_field(s_metrics.active, sizeof(s_metrics.active), (uint32_t)(active < 0 ? 0 : active));
645 num_field(s_metrics.max, sizeof(s_metrics.max), (uint32_t)MAX_CONNS);
646 num_field(s_metrics.heap, sizeof(s_metrics.heap), heap);
647 num_field(s_metrics.minheap, sizeof(s_metrics.minheap), min_heap);
648 num_field(s_metrics.heapsize, sizeof(s_metrics.heapsize), heap_size);
649 num_field(s_metrics.maxalloc, sizeof(s_metrics.maxalloc), max_alloc);
650
651 send_template(slot_id, 200, "text/plain; version=0.0.4; charset=utf-8", PC_METRICS_PROM, metrics_var);
652}
653#endif // PC_ENABLE_METRICS
#define MAX_CONNS
Definition c2_defaults.h:47
void clear_response_headers(uint8_t slot_id)
Discard any headers/cookies queued for this slot.
Definition response.cpp:386
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:176
static const char * mime_type(const char *path)
Guess a Content-Type from a path's file extension.
Definition response.cpp:399
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:336
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:120
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:359
Base-16 conversion between raw bytes and their ASCII digits.
uint8_t pc_hex_u32(uint32_t v, char *out)
Write v into out as lowercase hex digits, most significant first, and return the digit count (1....
Definition hex.h:61
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.
const char * status_text(int code)
Convert an HTTP status code to its standard reason phrase.
SendCtx s_send
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 protocore.h:310
const char *(* TemplateVar)(const char *name)
Resolver for {{name}} template placeholders used by send_template().
Definition protocore.h:116
#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 EXTRA_HDR_BUF_SIZE
Per-connection buffer for app-supplied custom response headers and cookies.
Library-private declarations shared between protocore.cpp and the src/server/*.cpp request-handler tr...
Bounded no-heap string builder that fails closed on overflow (one shared copy).
size_t pc_sb_finish(pc_sb *b)
NUL-terminate and return the built length, or 0 if the build overflowed.
Definition strbuf.h:648
void pc_sb_lit(pc_sb *b, const char(&s)[N])
Append a string literal. The array parameter deduces N, so the length is a constant.
Definition strbuf.h:76
void pc_sb_put(pc_sb *b, const char *s)
Append NUL-terminated s; leaves the buffer untouched and clears ok if it would not fit.
Definition strbuf.h:60
void pc_sb_u32(pc_sb *b, uint32_t v)
Append v as decimal (no leading zeros; "0" for zero).
Definition strbuf.h:303
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]
Bump-append target; ok latches false once an append would overflow cap.
Definition strbuf.h:30
void pc_conn_touch_active(uint8_t slot_id)
Refresh slot's idle-timeout timestamp while a response body is in flight.
Definition tcp.cpp:945
void pc_conn_flush(uint8_t slot)
Flush queued bytes / finish the send on slot (TLS-aware).
Definition tcp.cpp:561
u16_t pc_conn_sndbuf(uint8_t slot)
Bytes that can currently be queued for sending on slot.
Definition tcp.cpp:542
bool pc_conn_send(uint8_t slot, const void *data, u16_t len)
Send len bytes on connection slot (copies data; TLS-aware).
Definition tcp.cpp:500
uint8_t pc_conn_active_count()
Number of server connection slots currently in the CONN_ACTIVE state.
Definition tcp.cpp:859
Layer 4 (Transport) - TCP connection pool, ring buffers, and lwIP integration.
const char PC_STATS_JSON[]
Runtime stats JSON (rendered via send_template); add/rename fields to taste.
const char PC_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 web_assets/input/.