cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
http.c
Go to the documentation of this file.
1#if defined(__APPLE__)
2#ifndef _DARWIN_C_SOURCE
3#define _DARWIN_C_SOURCE
4#endif
5#elif !defined(__FreeBSD__) && !defined(__NetBSD__) && !defined(__OpenBSD__) && \
6 !defined(__DragonFly__)
7#define _POSIX_C_SOURCE 200809L
8#endif
10#include <cwist/sys/wasi.h>
18#include "../../sys/io/reactor_rx.h"
19#include "../../sys/io/async_debug.h"
21#include <cwist/core/log.h>
23#include <ttak/mols_control.h>
24#include <ttak/net/lattice.h>
25#include <ttak/priority/scheduler.h>
26#include "simd_parser.h"
27#include "async_internal.h"
28#include "http_live.h"
29#include "sockopts.h"
30
31#include <limits.h>
32#include <stdio.h>
33#include <stdlib.h>
34#include <string.h>
35#include <stdbool.h>
36#include <strings.h>
37#include <errno.h>
38#include <cwist/sys/platform.h>
39#ifndef CWIST_BAREMETAL /* newlib ships no poll/socket/inet headers */
40#include <poll.h>
41#endif
42#ifndef CWIST_PLATFORM_NO_PROC
43#include <signal.h>
44#endif
45#include <time.h>
46#ifdef CWIST_BAREMETAL
47time_t timegm(struct tm *tm); /* newlib has none: src/sys/port/baremetal.c */
48#endif
49
50#include <sys/types.h>
51#include <unistd.h>
52#if defined(CWIST_PLATFORM_HOSTED)
53#include <sys/wait.h>
54#endif
55#include <pthread.h>
56#include <sched.h>
57#include <stdatomic.h>
58#ifndef CWIST_BAREMETAL
59#include <arpa/inet.h>
60#include <sys/socket.h>
61#include <netinet/in.h>
62#include <netinet/tcp.h>
63#endif
64#include <sys/stat.h>
65#ifndef CWIST_PLATFORM_NO_PROC
66#include <sys/resource.h>
67#endif
68#include <fcntl.h>
69#ifdef __linux__
70#include <sys/epoll.h>
71#endif
72#if defined(__linux__)
73#include <sys/sendfile.h>
74#endif
75#if defined(__APPLE__) || defined(__FreeBSD__)
76#include <sys/uio.h>
77#endif
78#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
79#include <sys/event.h>
80#endif
81
82#if defined(_WIN32) || defined(_WIN64)
83#include <windows.h>
84#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
85#include <sys/types.h>
86#include <sys/sysctl.h>
87#else
88#include <unistd.h>
89#endif
90
99long get_cpu_cores(void) {
100#if defined(_WIN32) || defined(_WIN64)
103 return (long)sysinfo.dwNumberOfProcessors;
104#elif defined(__linux__)
107 if (sched_getaffinity(0, sizeof(cpuset), &cpuset) == 0) {
108 int count = CPU_COUNT(&cpuset);
109 if (count > 0) return (long)count;
110 }
111#if defined(_SC_NPROCESSORS_ONLN)
113 if (nproc > 0) return nproc;
114#endif
115 return 1;
116#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__)
117 int mib[2];
118 int nproc = 0;
119 size_t len = sizeof(nproc);
120 mib[0] = CTL_HW;
121#if defined(HW_NCPUONLINE)
122 mib[1] = HW_NCPUONLINE;
123#else
124 mib[1] = HW_NCPU;
125#endif
126 if (sysctl(mib, 2, &nproc, &len, NULL, 0) == 0) {
127 return (long)nproc;
128 }
129 return 4;
130#elif defined(_SC_NPROCESSORS_ONLN)
132 if (nproc > 0) return nproc;
133 return 1;
134#else
135 return 1;
136#endif
137}
138
139static unsigned int g_http_pool_core_limit = 0;
140
146 g_http_pool_core_limit = limit;
147}
148
160 if (g_http_pool_core_limit > 0) {
161 return (long)g_http_pool_core_limit;
162 }
163 const char *env = getenv("CWIST_WORKER_THREADS");
164 if (env && env[0]) {
165 char *end = NULL;
166 long override = strtol(env, &end, 10);
167 if (end != env && *end == '\0' && override > 0) return override;
168 }
169
170 long cores = get_cpu_cores();
171 if (cores < 1) cores = 1;
172
173 long workers = cores;
174 const char *w_env = getenv("CWIST_WORKERS");
175 if (w_env && w_env[0]) {
176 if (strcmp(w_env, "auto") != 0) {
177 char *end = NULL;
178 long parsed = strtol(w_env, &end, 10);
179 if (end != w_env && *end == '\0' && parsed > 0) workers = parsed;
180 }
181 }
182
183 const char *c1m = getenv("CWIST_C1M_MODE");
184 bool is_c1m = !c1m || (c1m[0] != '0' && strcmp(c1m, "false") != 0);
185
186 if (is_c1m) {
187 /* In event-driven C1M mode each reactor thread multiplexes I/O
188 * asynchronously. Target one event loop per CPU across the whole
189 * deployment: extra loops on the same CPU add no parallelism, only
190 * scheduler queueing, and the queueing lands on the tail. The CI
191 * benchmark matrix tracks this configuration on every run.
192 *
193 * `cores` is this process's own CPU budget, not the machine's:
194 * get_cpu_cores() reads sched_getaffinity(), and with workers > 1
195 * every worker has already been pinned to a single CPU by
196 * cwist_app_pin_worker() before the pool is built. So in the
197 * default pinned deployment cores is 1 here and the division
198 * below yields 0, clamped to 1 -- one loop per process. Unpinned
199 * (e.g. the CI benchmark, which deliberately sees the whole
200 * runner), cores is the machine count and the worker processes
201 * share it, so the division spreads the loops evenly across them.
202 *
203 * A higher floor would protect synchronous handoffs (h2c) and
204 * blocking handlers, but extra loops on a shared CPU cannot run
205 * concurrently there either: a blocking handler stalls its own
206 * worker's connections until it yields, while the other workers
207 * (other CPUs, shared listener) keep serving. */
208 long count = workers > 0 ? cores / workers : cores;
209 if (count < 1) count = 1;
210 if (count > 64) count = 64;
211 return count;
212 }
213
214 if (workers == 1) {
215 /* Keep-alive handlers park on their connection, so the pool must
216 * cover many more concurrent connections than there are cores.
217 * Blocked threads are nearly free (futex sleep); undersizing the
218 * pool caps throughput at threads x per-conn rate. */
219 long count = cores * 8;
220 if (count < 32) count = 32;
221 if (count > 256) count = 256;
222 return count;
223 }
224
225 /* Dynamic thread downscaling: distribute thread budget proportionally across forked worker
226 * processes */
227 long threads_per_worker = (cores * 8) / workers;
230 return threads_per_worker;
231}
232
233#ifndef CWIST_PLATFORM_NO_SOCKETS /* socket worker runtime: thread pool, live-connection registry, async connections */
234#define HTTP_TASKS_PER_THREAD 32768
235
236typedef struct {
237 pthread_t thread;
238 cwist_reactor_t *reactor;
239 uint32_t worker_id;
240} http_thread_worker_t;
241
242static size_t g_rr_index = 0;
243static long g_http_thread_count = 0;
244static http_thread_worker_t *g_workers = NULL;
245static _Atomic uint32_t *g_worker_loads = NULL;
246
247static _Atomic long g_http_inflight = 0;
248static _Atomic bool g_http_pool_stopping = false;
249static _Atomic long g_http_continuation_shed = 0;
250
251/* Generation of the C1M worker reactors. cwist_http_pool_destroy() bumps it
252 * before it stops and frees them, so a deferred completion that still holds
253 * a reactor from an earlier generation (cwist_async outlives the server)
254 * closes its connection instead of posting into freed memory. Posters count
255 * themselves in g_reactor_posters around the check and the post; destroy
256 * waits for that count to drain after the bump, so every post that saw the
257 * old generation lands before the reactors' final drain. Both sides use
258 * sequentially consistent atomics, so one of them always sees the other. */
259static _Atomic uint64_t g_reactor_gen = 0;
260static _Atomic long g_reactor_posters = 0;
261
269 return atomic_load(&g_reactor_gen);
270}
271
284 cwist_reactor_post_t *node) {
285 atomic_fetch_add(&g_reactor_posters, 1);
286 bool posted = atomic_load(&g_reactor_gen) == gen && cwist_reactor_post(reactor, node);
287 atomic_fetch_sub(&g_reactor_posters, 1);
288 return posted;
289}
290
294 return atomic_load_explicit(&g_http_continuation_shed, memory_order_relaxed);
295}
296
302 return atomic_load_explicit(&g_http_inflight, memory_order_acquire);
303}
304#define CWIST_HTTP_INFLIGHT_PER_THREAD 32
305#define CWIST_HTTP_INFLIGHT_FD_RESERVE 4096
306
314static long cwist_http_inflight_limit(void) {
315 long base = g_http_thread_count > 0 ? g_http_thread_count : get_optimal_thread_count();
317#ifndef CWIST_PLATFORM_NO_PROC
318 struct rlimit rl;
319 if (getrlimit(RLIMIT_NOFILE, &rl) != 0) return floor;
320 long budget = (rl.rlim_cur == RLIM_INFINITY)
321 ? (1024L * 1024L)
323 return budget > floor ? budget : floor;
324#else
325 /* WASI preview1 has no rlimit; the thread-count floor is enough. */
326 return floor;
327#endif
328}
329
330static const char CWIST_HTTP_503[] =
331 "HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
332
333static _Thread_local http_thread_worker_t *t_current_worker = NULL;
334
340static void *http_pool_worker(void *arg) {
341 http_thread_worker_t *w = (http_thread_worker_t *)arg;
342 t_current_worker = w;
344
345 cwist_reactor_run(w->reactor);
346 return NULL;
347}
348
349/* Dynamic Thread Pool for Classic Mode
350 * Combines pre-allocated idle worker threads with fast job queue and
351 * on-demand scaling to eliminate starvation on compute/keepalive workloads
352 * while avoiding per-connection pthread_create overhead. */
353#define CWIST_POOL_STACK_SIZE (256 * 1024)
354
355typedef struct http_pool_task {
356 int client_fd;
357 void (*handler_func)(int, void *);
358 void *ctx;
359 struct http_pool_task *next;
361
362typedef struct {
364 http_pool_task_t *tail;
365 atomic_long pending_tasks;
366 atomic_long active_workers;
367 atomic_long idle_workers;
368 atomic_long max_workers;
369 atomic_bool running;
370 /* Worker threads that have not returned yet, counted across pool
371 * generations. Workers are detached, so destroy waits on this before it
372 * tears down lock/cond (#351). Never reset by init. */
373 atomic_long live_workers;
374 atomic_long busy_workers; /* Live workers currently inside a handler. */
375 bool primitives_live; /* lock/cond initialised and not destroyed. */
376 pthread_mutex_t lock;
377 pthread_cond_t cond;
378} http_dynamic_pool_t;
379
380static http_dynamic_pool_t g_dyn_pool;
381
382/* RX-uring RECV completion entry for the async connection path; defined
383 * with the async helpers below. Registered as the reactor's rx_cb in
384 * cwist_http_pool_init. */
385static void http_rx_recv_cb(void *conn_ptr, int res);
386
387/* --- Classic connection registry (shutdown) -------------------------------
388 * Detached workers register active connections so cwist_http_pool_destroy()
389 * can close idle sockets immediately, wait up to the drain timeout for
390 * in-flight requests, and then force-close remaining sockets before freeing
391 * the app. Handlers operate on a dup() of the socket while the registry keeps
392 * the original fd until unlinked, preventing fd reuse races during shutdown().
393 */
394typedef struct http_live_conn {
395 int fd; /* Pool descriptor; handler serves a dup. */
396 _Atomic int idle; /* Handler is waiting for the next request. */
397 struct http_live_conn *prev, *next;
399
400#ifndef CWIST_PLATFORM_NO_PROC
401static pthread_mutex_t g_live_lock = PTHREAD_MUTEX_INITIALIZER;
402static http_live_conn_t *g_live_head;
403static _Atomic bool g_draining = false;
404static pthread_mutex_t g_drain_lock = PTHREAD_MUTEX_INITIALIZER;
405static pthread_cond_t g_drain_cond = PTHREAD_COND_INITIALIZER;
406#endif
407static _Atomic long g_live_count;
408static __thread http_live_conn_t *t_live_conn;
409
410#ifndef CWIST_PLATFORM_NO_PROC
411static void http_live_dec(void) {
412 long prev = atomic_fetch_sub_explicit(&g_live_count, 1, memory_order_acq_rel);
413 if (atomic_load_explicit(&g_draining, memory_order_acquire)) {
414 if (prev == 1) {
415 pthread_mutex_lock(&g_drain_lock);
416 pthread_cond_broadcast(&g_drain_cond);
417 pthread_mutex_unlock(&g_drain_lock);
418 }
419 }
420}
421#endif
422
424 http_live_conn_t *c = t_live_conn;
425 if (!c) return true;
426 atomic_store(&c->idle, 1);
427 if (atomic_load(&g_http_pool_stopping)) {
428 atomic_store(&c->idle, 0);
429 return false;
430 }
431 return true;
432}
433
435 if (t_live_conn) atomic_store(&t_live_conn->idle, 0);
436}
437
439static void http_live_serve(int fd, void (*handler)(int, void *), void *ctx) {
440#ifdef CWIST_PLATFORM_NO_PROC
441 handler(fd, ctx);
442#else
443 int hfd = fd >= 0 ? dup(fd) : -1;
444 if (hfd < 0) {
445 handler(fd, ctx);
446 return;
447 }
448 http_live_conn_t live = {.fd = fd};
449 pthread_mutex_lock(&g_live_lock);
450 live.next = g_live_head;
451 if (g_live_head) g_live_head->prev = &live;
452 g_live_head = &live;
454 pthread_mutex_unlock(&g_live_lock);
455
456 t_live_conn = &live;
457 handler(hfd, ctx);
458 t_live_conn = NULL;
459
460 pthread_mutex_lock(&g_live_lock);
461 if (live.prev)
462 live.prev->next = live.next;
463 else
464 g_live_head = live.next;
465 if (live.next) live.next->prev = live.prev;
466 pthread_mutex_unlock(&g_live_lock);
467 close(fd);
468 http_live_dec();
469#endif
470}
471
473static void http_live_kick(int how, bool idle_only) {
474#ifdef CWIST_PLATFORM_NO_PROC
475 (void)how;
477#else
478 pthread_mutex_lock(&g_live_lock);
479 for (http_live_conn_t *c = g_live_head; c; c = c->next) {
480 if (!idle_only || atomic_load(&c->idle)) shutdown(c->fd, how);
481 }
482 pthread_mutex_unlock(&g_live_lock);
483#endif
484}
485
487static void http_live_drain(void) {
488#ifdef CWIST_PLATFORM_NO_PROC
489 return;
490#else
491 if (atomic_load_explicit(&g_live_count, memory_order_acquire) == 0) return;
492
493 pthread_mutex_lock(&g_drain_lock);
495 http_live_kick(SHUT_RD, true);
496
497 struct timespec ts;
500
501 while (atomic_load_explicit(&g_live_count, memory_order_acquire) > 0) {
502 int rc = pthread_cond_timedwait(&g_drain_cond, &g_drain_lock, &ts);
503 if (rc == ETIMEDOUT) break;
504 }
505
506 long left = atomic_load_explicit(&g_live_count, memory_order_acquire);
507 if (left == 0) {
508 atomic_store_explicit(&g_draining, false, memory_order_release);
509 pthread_mutex_unlock(&g_drain_lock);
510 return;
511 }
512
513 CWIST_LOG_WARN("[http] %ld classic connection(s) still busy after the %d s drain; "
514 "shutting them down",
516 http_live_kick(SHUT_RDWR, false);
517
518 /* Wait for handlers stuck in route code before tearing down the app. */
519 long waited_sec = 0;
520 while ((left = atomic_load_explicit(&g_live_count, memory_order_acquire)) > 0) {
522 ts.tv_sec += 5;
523 int rc = pthread_cond_timedwait(&g_drain_cond, &g_drain_lock, &ts);
524 if (rc == ETIMEDOUT &&
525 (left = atomic_load_explicit(&g_live_count, memory_order_acquire)) > 0) {
526 waited_sec += 5;
527 CWIST_LOG_WARN("[http] still waiting for %ld classic handler(s) to return (%ld s)",
529 }
530 }
531 atomic_store_explicit(&g_draining, false, memory_order_release);
532 pthread_mutex_unlock(&g_drain_lock);
533#endif
534}
535
543static void *http_dynamic_worker_loop(void) {
544 while (atomic_load_explicit(&g_dyn_pool.running, memory_order_acquire)) {
546
547 pthread_mutex_lock(&g_dyn_pool.lock);
548 while (atomic_load_explicit(&g_dyn_pool.running, memory_order_acquire) &&
549 !g_dyn_pool.head) {
550 /* Scale-down idle timeout: CWIST_POOL_IDLE_TIMEOUT_MS overrides
551 * the 2s default; 0 parks surplus threads forever (use with
552 * CWIST_POOL_PREWARM to eliminate spawn churn entirely). */
553 static _Atomic long idle_timeout_ms = -1;
554 long ms = atomic_load_explicit(&idle_timeout_ms, memory_order_relaxed);
555 if (ms < 0) {
556 const char *env = getenv("CWIST_POOL_IDLE_TIMEOUT_MS");
557 if (env && *env) {
558 char *end = NULL;
559 long v = strtol(env, &end, 10);
560 ms = (end != env && *end == '\0') ? v : 2000;
561 } else {
562 ms = 2000;
563 }
564 if (ms < 0) ms = 2000;
566 }
567 atomic_fetch_add_explicit(&g_dyn_pool.idle_workers, 1, memory_order_relaxed);
568 int rc;
569 if (ms == 0) {
570 rc = pthread_cond_wait(&g_dyn_pool.cond, &g_dyn_pool.lock);
571 } else {
572 struct timespec ts;
574 ts.tv_sec += ms / 1000;
575 ts.tv_nsec += (ms % 1000) * 1000000L;
576 if (ts.tv_nsec >= 1000000000L) {
577 ts.tv_sec++;
578 ts.tv_nsec -= 1000000000L;
579 }
580 rc = pthread_cond_timedwait(&g_dyn_pool.cond, &g_dyn_pool.lock, &ts);
581 }
582 atomic_fetch_sub_explicit(&g_dyn_pool.idle_workers, 1, memory_order_relaxed);
583 if (rc == ETIMEDOUT && !g_dyn_pool.head) {
584 /* Scale down if idle and above base worker threshold */
585 long current =
586 atomic_load_explicit(&g_dyn_pool.active_workers, memory_order_relaxed);
587 if (current > g_http_thread_count) {
588 atomic_fetch_sub_explicit(&g_dyn_pool.active_workers, 1, memory_order_relaxed);
589 pthread_mutex_unlock(&g_dyn_pool.lock);
590 return NULL;
591 }
592 }
593 }
594
595 if (!atomic_load_explicit(&g_dyn_pool.running, memory_order_acquire)) {
596 pthread_mutex_unlock(&g_dyn_pool.lock);
597 break;
598 }
599
600 task = g_dyn_pool.head;
601 if (task) {
602 g_dyn_pool.head = task->next;
603 if (!g_dyn_pool.head) g_dyn_pool.tail = NULL;
604 atomic_fetch_sub_explicit(&g_dyn_pool.pending_tasks, 1, memory_order_release);
605 }
606 pthread_mutex_unlock(&g_dyn_pool.lock);
607
608 if (task) {
609 int fd = task->client_fd;
610 void (*handler)(int, void *) = task->handler_func;
611 void *ctx = task->ctx;
613
615 atomic_fetch_add_explicit(&g_dyn_pool.busy_workers, 1, memory_order_relaxed);
616 if (handler) http_live_serve(fd, handler, ctx);
617 atomic_fetch_sub_explicit(&g_dyn_pool.busy_workers, 1, memory_order_release);
619 }
620 }
621 return NULL;
622}
623
625static void *http_dynamic_worker_thread(void *arg) {
626 (void)arg;
627 http_dynamic_worker_loop();
628 /* Last access to the pool: after this, destroy may free lock/cond. */
629 atomic_fetch_sub_explicit(&g_dyn_pool.live_workers, 1, memory_order_release);
630 return NULL;
631}
632
641static bool http_spawn_worker(void) {
645 /* Cap worker stack reservation: the default 8MB dwarfs the worker's real
646 * worst case (16KB read_buf + 8KB header_buf + 64KB file-stream fallback
647 * buffer plus call-chain slack). Note glibc rejects sizes smaller than
648 * the process' static TLS footprint (allocatestack.c), which is why the
649 * HTTP/3 batch scratch buffers must stay out of TLS. */
651 CWIST_LOG_WARN("[http] pthread_attr_setstacksize(%d) failed; using default stack",
653 }
655 atomic_fetch_add_explicit(&g_dyn_pool.active_workers, 1, memory_order_relaxed);
656 atomic_fetch_add_explicit(&g_dyn_pool.live_workers, 1, memory_order_relaxed);
657 int rc = pthread_create(&tid, &attr, http_dynamic_worker_thread, NULL);
658 if (rc != 0) {
659 /* Retry with the default stack rather than losing the worker. */
663 rc = pthread_create(&tid, &attr, http_dynamic_worker_thread, NULL);
664 }
666 if (rc != 0) {
667 atomic_fetch_sub_explicit(&g_dyn_pool.active_workers, 1, memory_order_relaxed);
668 atomic_fetch_sub_explicit(&g_dyn_pool.live_workers, 1, memory_order_relaxed);
669 return false;
670 }
671 return true;
672}
673
679 const char *c1m = getenv("CWIST_C1M_MODE");
680 bool use_c1m = true;
681 if (c1m) {
682 if (c1m[0] == '0' || strcmp(c1m, "false") == 0) {
683 use_c1m = false;
684 }
685 }
687}
688
697 atomic_store(&g_http_pool_stopping, false);
698 g_http_thread_count = get_optimal_thread_count();
699
700 if (use_c1m) {
701 /* Pre-allocate reactor workers for async path (CWIST_C1M_MODE=1) */
702 g_workers = cwist_alloc(g_http_thread_count * sizeof(http_thread_worker_t));
703 if (!g_workers) return -1;
704 g_worker_loads = cwist_alloc(g_http_thread_count * sizeof(_Atomic uint32_t));
705 if (!g_worker_loads) {
706 cwist_free(g_workers);
707 g_workers = NULL;
708 return -1;
709 }
710 for (int i = 0; i < g_http_thread_count; i++) atomic_init(&g_worker_loads[i], 0);
711 g_rr_index = 0;
712 memset(g_workers, 0, g_http_thread_count * sizeof(http_thread_worker_t));
713
714 for (int i = 0; i < g_http_thread_count; i++) {
715 g_workers[i].reactor = cwist_reactor_create();
716 if (!g_workers[i].reactor) return -1;
717 /* RX-uring receive path: route tagged RECV completions into the
718 * HTTP connection state machine (no-op where unsupported). */
719 cwist_reactor_set_rx_cb(g_workers[i].reactor, http_rx_recv_cb);
720 g_workers[i].worker_id = (uint32_t)i;
721 if (pthread_create(&g_workers[i].thread, NULL, http_pool_worker, &g_workers[i]) != 0) {
722 return -1;
723 }
724 }
725 } else {
726 /* Initialize dynamic worker pool for classic path (CWIST_C1M_MODE=0) */
727 g_dyn_pool.head = NULL;
728 g_dyn_pool.tail = NULL;
729 atomic_init(&g_dyn_pool.pending_tasks, 0);
730 atomic_init(&g_dyn_pool.active_workers, 0);
731 atomic_init(&g_dyn_pool.idle_workers, 0);
732 atomic_init(&g_dyn_pool.max_workers, 65536);
733 atomic_init(&g_dyn_pool.running, true);
734
735 /* A previous destroy that gave up waiting for its workers left
736 * lock/cond alive for them; keep using those. */
737 if (!g_dyn_pool.primitives_live) {
738 pthread_mutex_init(&g_dyn_pool.lock, NULL);
739 pthread_cond_init(&g_dyn_pool.cond, NULL);
740 g_dyn_pool.primitives_live = true;
741 }
742
743 /* Pre-warm worker threads. CWIST_POOL_PREWARM extends the spawn count
744 * beyond the base pool when the expected concurrency is known, so the
745 * acceptor does not serialize pthread_create + stack mmap during a
746 * connection ramp. */
747 long prewarm = g_http_thread_count;
748 const char *pw = getenv("CWIST_POOL_PREWARM");
749 if (pw && *pw) {
750 char *end = NULL;
751 long parsed = strtol(pw, &end, 10);
752 if (end == pw || *end != '\0') parsed = 0;
753 if (parsed > prewarm) prewarm = parsed;
754 }
755 long max_w = atomic_load_explicit(&g_dyn_pool.max_workers, memory_order_relaxed);
756 if (prewarm > max_w) prewarm = max_w;
757 for (long i = 0; i < prewarm; i++) {
758 if (!http_spawn_worker()) {
759 return -1;
760 }
761 }
762 }
763 return 0;
764}
765
772void cwist_http_pool_submit(int client_fd, void (*handler)(int, void *), void *ctx) {
773 long limit = cwist_http_inflight_limit();
774 long inflight = atomic_fetch_add_explicit(&g_http_inflight, 1, memory_order_acq_rel) + 1;
775 if (inflight > limit) {
777#ifdef MSG_NOSIGNAL
778 send(client_fd, CWIST_HTTP_503, sizeof(CWIST_HTTP_503) - 1, MSG_NOSIGNAL | MSG_DONTWAIT);
779#endif
780 close(client_fd);
781 return;
782 }
783
784 http_pool_task_t *node = cwist_alloc(sizeof(*node));
785 if (!node) {
787 close(client_fd);
788 return;
789 }
790 node->client_fd = client_fd;
791 node->handler_func = handler;
792 node->ctx = ctx;
793 node->next = NULL;
794
795 pthread_mutex_lock(&g_dyn_pool.lock);
796 if (!g_dyn_pool.tail) {
797 g_dyn_pool.head = node;
798 g_dyn_pool.tail = node;
799 } else {
800 g_dyn_pool.tail->next = node;
801 g_dyn_pool.tail = node;
802 }
803 long pending =
804 atomic_fetch_add_explicit(&g_dyn_pool.pending_tasks, 1, memory_order_release) + 1;
805
806 /* Signalled sleepers remain counted idle until they reacquire this lock.
807 * Compare queued demand with that capacity, not merely idle == 0: a
808 * burst can otherwise strand connections behind busy keep-alive handlers.
809 * Keep the cap check and the spawn reservation in this critical section. */
810 long current = atomic_load_explicit(&g_dyn_pool.active_workers, memory_order_relaxed);
811 long idle = atomic_load_explicit(&g_dyn_pool.idle_workers, memory_order_relaxed);
812 long max_w = atomic_load_explicit(&g_dyn_pool.max_workers, memory_order_relaxed);
813 if (pending > idle && current < max_w) {
814 http_spawn_worker();
815 }
816 pthread_cond_signal(&g_dyn_pool.cond);
817 pthread_mutex_unlock(&g_dyn_pool.lock);
818}
819
824bool cwist_http_pool_rearm_current(int client_fd, void (*handler)(int, void *), void *ctx) {
825 if (client_fd < 0 || !handler) return false;
826 cwist_http_pool_submit(client_fd, handler, ctx);
827 return true;
828}
829
837 /* Queued continuations must release, not repost into a dying reactor. */
838 atomic_store(&g_http_pool_stopping, true);
839 atomic_store_explicit(&g_dyn_pool.running, false, memory_order_release);
840 pthread_mutex_lock(&g_dyn_pool.lock);
841 pthread_cond_broadcast(&g_dyn_pool.cond);
842 pthread_mutex_unlock(&g_dyn_pool.lock);
843
844 /* Drain tasks */
845 pthread_mutex_lock(&g_dyn_pool.lock);
846 http_pool_task_t *node = g_dyn_pool.head;
847 g_dyn_pool.head = NULL;
848 g_dyn_pool.tail = NULL;
849 while (node) {
850 http_pool_task_t *next = node->next;
851 if (node->client_fd >= 0) close(node->client_fd);
852 cwist_free(node);
853 node = next;
854 }
855 pthread_mutex_unlock(&g_dyn_pool.lock);
856
857 /* No handler may still be serving a connection once this returns: the
858 * caller destroys the app next. */
859 http_live_drain();
860
861 /* Workers are detached, so lock/cond may only be destroyed once every
862 * worker has returned (#351). Idle workers wake on the broadcast and exit
863 * at once; wait for those, briefly. A worker still inside a handler (a
864 * keep-alive read, a slow route) is not waited for, so shutdown is not
865 * held up: lock/cond then stay alive (static storage) for it, and the
866 * next init reuses them instead of re-initialising under it. */
867 if (g_dyn_pool.primitives_live) {
868 struct timespec start, now;
870 for (;;) {
871 long live = atomic_load_explicit(&g_dyn_pool.live_workers, memory_order_acquire);
872 long busy = atomic_load_explicit(&g_dyn_pool.busy_workers, memory_order_acquire);
873 if (live <= busy) break;
875 if ((now.tv_sec - start.tv_sec) * 1000 + (now.tv_nsec - start.tv_nsec) / 1000000 >=
876 1000)
877 break;
878 pthread_mutex_lock(&g_dyn_pool.lock);
879 pthread_cond_broadcast(&g_dyn_pool.cond);
880 pthread_mutex_unlock(&g_dyn_pool.lock);
881 struct timespec nap = {0, 1000000};
882 nanosleep(&nap, NULL);
883 }
884 if (atomic_load_explicit(&g_dyn_pool.live_workers, memory_order_acquire) == 0) {
885 pthread_cond_destroy(&g_dyn_pool.cond);
886 pthread_mutex_destroy(&g_dyn_pool.lock);
887 g_dyn_pool.primitives_live = false;
888 }
889 }
890
891 /* Retire this generation of reactors before they go; see g_reactor_gen. */
892 atomic_fetch_add(&g_reactor_gen, 1);
893 while (atomic_load(&g_reactor_posters) > 0) sched_yield();
894
895 if (g_workers) {
896 for (int i = 0; i < g_http_thread_count; i++) {
897 if (g_workers[i].reactor) {
898 cwist_reactor_stop(g_workers[i].reactor);
899 }
900 }
901 for (int i = 0; i < g_http_thread_count; i++) {
902 pthread_join(g_workers[i].thread, NULL);
903 if (g_workers[i].reactor) {
904 cwist_reactor_destroy(g_workers[i].reactor);
905 g_workers[i].reactor = NULL;
906 }
907 }
908 cwist_free(g_workers);
909 g_workers = NULL;
910 if (g_worker_loads) {
911 cwist_free(g_worker_loads);
912 g_worker_loads = NULL;
913 }
914 }
915}
916/* --- End Thread Pool --- */
917
918/* --- Async (one-shot, event-driven) connection path ------------------------
919 * See include/cwist/net/http/http.h for the model overview. The connection
920 * shell lives across events; the recv stash is allocated lazily and released
921 * whenever it drains to empty, so an idle keep-alive connection costs only
922 * the shell plus its reactor slot. */
923
924typedef struct {
925 int client_fd;
926 cwist_async_handler_t handler;
927 void *ctx;
928 cwist_reactor_t *reactor;
930} http_async_ctx_t;
931
932/* Defined below; referenced earlier by the RX-uring helpers. */
933static void http_async_event_cb(int fd, void *ctx);
934static bool http_async_stash_grow(cwist_http_async_conn_t *conn, size_t need);
935
941static uint32_t cwist_http_keep_alive_timeout_sec(void) {
942 static _Atomic int cached_timeout = -1;
944 if (v < 0) {
945 const char *env = getenv("CWIST_HTTP_KEEP_ALIVE_TIMEOUT");
946 int val = 0;
947 if (env && *env) {
948 char *end = NULL;
949 long parsed = strtol(env, &end, 10);
950 if (end != env && *end == '\0' && parsed > 0 && parsed <= INT_MAX) val = (int)parsed;
951 }
954 }
955 return (uint32_t)v;
956}
957
958/* Default hysteresis window (seconds) before an async connection's stash is
959 * shrunk at re-arm; see http_async_stash_idle_shrink(). */
960#define CWIST_ASYNC_STASH_IDLE_SEC_DEFAULT 10
961
968static uint32_t http_async_stash_idle_sec(void) {
969 static _Atomic int cached = -1;
971 if (v < 0) {
972 const char *s = getenv("CWIST_ASYNC_STASH_IDLE_SEC");
973 if (s) {
974 char *end = NULL;
975 long parsed = strtol(s, &end, 10);
976 v = (end != s && *end == '\0' && parsed >= 0 && parsed < INT_MAX)
977 ? (int)parsed
979 } else {
981 }
983 }
984 return (uint32_t)v;
985}
986
993static void http_async_conn_release(cwist_http_async_conn_t *conn) {
994 if (!conn) return;
995 if (g_worker_loads && conn->worker_id < (uint32_t)g_http_thread_count) {
996 atomic_fetch_sub_explicit(&g_worker_loads[conn->worker_id], 1, memory_order_relaxed);
997 }
998 cwist_free(conn->rbuf);
999 cwist_free(conn->obuf);
1000 cwist_free(conn);
1001 atomic_fetch_sub_explicit(&g_http_inflight, 1, memory_order_release);
1002}
1003
1004/* Minimum free stash bytes required before arming a RECV SQE. Below this
1005 * floor the legacy POLL + recv-drain path runs the stash growth instead. */
1006#define CWIST_RX_REARM_MIN_AVAIL 1024
1007
1008/* Idle-keep-alive shrink floor for the rbuf/obuf stashes at rearm: an idle
1009 * connection holds ~KB, not 32 KB, while the grow-on-demand paths restore
1010 * capacity on the next busy turn. Must satisfy CWIST_RX_REARM_MIN_AVAIL so
1011 * the RECV-first arm stays available. */
1012#define CWIST_ASYNC_STASH_IDLE 4096
1013
1038static void http_async_stash_idle_shrink(cwist_http_async_conn_t *conn) {
1040 if (now - conn->stash_grown_sec < http_async_stash_idle_sec()) return;
1041 if (conn->len == 0) {
1042 if (conn->cap > 65536) {
1043 cwist_free(conn->rbuf);
1044 conn->rbuf = NULL;
1045 conn->cap = 0;
1046 } else if (conn->cap > CWIST_ASYNC_STASH_IDLE) {
1048 if (nb) {
1049 conn->rbuf = nb;
1051 }
1052 }
1053 }
1054 if (conn->olen == 0 && conn->ocap > CWIST_ASYNC_STASH_IDLE) {
1056 if (nb) {
1057 conn->obuf = nb;
1059 }
1060 }
1061}
1062
1074static bool http_async_arm_wait(int fd, cwist_http_async_conn_t *conn,
1075 cwist_async_handler_t handler, void *ctx,
1076 cwist_reactor_t *reactor) {
1077 http_async_ctx_t next = {
1078 .client_fd = fd,
1079 .handler = handler,
1080 .ctx = ctx,
1081 .reactor = reactor,
1082 .conn = conn,
1083 };
1084 if (cwist_reactor_rx_supported(reactor) && !conn->peer_eof && !conn->rx_recv_inflight &&
1085 !conn->rx_prefers_poll) {
1086 /* Virgin stash: grow to the 4 KiB idle floor, not the 16 KiB read
1087 * buffer. A mostly-idle C1M connection then pays ~4 KiB, not 16 KiB,
1088 * of rbuf on first arm; any request above the floor regrows through
1089 * http_async_stash_grow exactly as a regrown stash does today. */
1090 if (conn->cap == 0 && !http_async_stash_grow(conn, CWIST_ASYNC_STASH_IDLE)) {
1091 /* Stash allocation failed: the legacy fill path retries growth
1092 * when POLL fires and closes on failure, matching today. */
1093 } else if (conn->cap > 0) {
1094 size_t avail = conn->cap - 1 - conn->len;
1096 /* Mark the RECV in flight BEFORE submitting the SQE. The
1097 * acceptor thread is not this reactor's owner, so the kernel
1098 * can complete the SQE and the owner thread can dispatch the
1099 * connection to close (and free it) while this function is
1100 * still running; any conn write after submission is a
1101 * use-after-free. Roll back on submission failure so the
1102 * POLL fallback below sees the un-armed state. */
1103 conn->rx_recv_inflight = true;
1104 if (cwist_reactor_recv_arm(reactor, fd, conn->rbuf + conn->len, (unsigned)avail,
1105 conn, &conn->rx_armed_ns)) {
1106 return true;
1107 }
1108 conn->rx_recv_inflight = false;
1109 }
1110 }
1111 /* Low headroom or arm failure: fall through to the POLL wait. */
1112 }
1113 return cwist_reactor_add(reactor, fd, http_async_event_cb, &next, sizeof(next));
1114}
1115
1119static void http_async_dispatch(int fd, cwist_http_async_conn_t *conn,
1120 cwist_async_handler_t handler, void *ctx,
1121 cwist_reactor_t *reactor) {
1122 cwist_async_action_t action = handler(fd, conn);
1123
1124 if (action == CWIST_ASYNC_DEFER) {
1125 /* The handler parked the request on a cwist_async; the completion
1126 * path owns fd and conn now and re-arms or closes when done.
1127 *
1128 * A deferred connection used to hold its full rbuf/obuf stashes for
1129 * the entire deferral (SSE, long-poll): the idle shrink only ran at
1130 * re-arm, which a parked connection never reaches. Shrink toward the
1131 * idle floor here instead, so a parked connection pays ~KB, not
1132 * ~32 KB, for the whole deferral.
1133 *
1134 * Defer-specific shrink: the shared helper is keep-alive hysteresis-
1135 * gated (it must be: a busy connection would otherwise
1136 * shrink->regrow every request, a P99.999 stall source). That
1137 * hysteresis is exactly wrong at defer -- the stash was grown moments
1138 * ago, yet the connection is now parked indefinitely and the capacity
1139 * is not needed until the completion re-arms (which regrows on
1140 * demand). So age the anchor out before calling it; the resulting
1141 * shrink->regrow pair costs one realloc per defer cycle, not per
1142 * request. RX-uring invariant holds: defer runs with no RECV SQE in
1143 * flight (rx_recv_inflight was cleared by the RECV completion entry,
1144 * or the wait was a POLL), same as at re-arm. */
1145 conn->stash_grown_sec = 0;
1146 http_async_stash_idle_shrink(conn);
1147 return;
1148 }
1149 if (action == CWIST_ASYNC_DETACH) {
1150 /* The handler owns fd now (h2c preface, protocol upgrade). Free the
1151 * shell but never touch the fd. */
1152 if (g_worker_loads && conn->worker_id < (uint32_t)g_http_thread_count) {
1153 atomic_fetch_sub_explicit(&g_worker_loads[conn->worker_id], 1, memory_order_relaxed);
1154 }
1155 cwist_free(conn->rbuf);
1156 cwist_free(conn->obuf);
1157 cwist_free(conn);
1158 atomic_fetch_sub_explicit(&g_http_inflight, 1, memory_order_release);
1159 return;
1160 }
1161 if (action == CWIST_ASYNC_CLOSE) {
1162 close(fd);
1163 http_async_conn_release(conn);
1164 return;
1165 }
1166
1167 /* CWIST_ASYNC_REARM: keep the stashes allocated across keep-alive
1168 * requests to avoid per-request heap churn. Shrinking toward the idle
1169 * floor is gated by the stash_grown_sec hysteresis: a served keep-alive
1170 * connection otherwise holds 16 KiB rbuf + 16 KiB obuf whenever its
1171 * capacity was recently needed (~24 KB RSS per connection at C1M scale);
1172 * a quiet connection still converges to the small floor one
1173 * CWIST_ASYNC_STASH_IDLE_SEC window after its last growth. The
1174 * grow-on-demand paths (http_async_stash_grow, coalesce serialize)
1175 * restore capacity on the next busy turn. */
1176 http_async_stash_idle_shrink(conn);
1177
1178 if (!http_async_arm_wait(fd, conn, handler, ctx, reactor)) {
1179 if (cwist_async_debug_enabled()) {
1180 static _Atomic long dbg_rearm_fail;
1181 long n = atomic_fetch_add(&dbg_rearm_fail, 1) + 1;
1182 if (n <= 5 || n % 10000 == 0)
1183 fprintf(stderr, "[async] rearm failed fd=%d total=%ld\n", fd, n);
1184 }
1185 close(fd);
1186 http_async_conn_release(conn);
1187 }
1188}
1189
1195static void http_rx_recv_cb(void *conn_ptr, int res) {
1197 conn->rx_recv_inflight = false;
1198
1199 if (res == -ECANCELED) {
1200 /* The deferred submission failed (SQ wedged) or the ring tore down;
1201 * legacy arm-failure semantics: close fd and release the shell. */
1202 close(conn->fd);
1203 http_async_conn_release(conn);
1204 return;
1205 }
1206 if (res == 0) {
1207 /* EOF: do not re-arm; the handler drains complete buffered requests
1208 * and then closes through the existing peer_eof path. */
1209 conn->peer_eof = true;
1210 } else if (res < 0 && res != -EAGAIN) {
1211 close(conn->fd);
1212 http_async_conn_release(conn);
1213 return;
1214 } else if (res > 0) {
1215 conn->len += (size_t)res;
1216 conn->rbuf[conn->len] = '\0';
1217 conn->virgin = false;
1218 }
1219 /* res == -EAGAIN: nothing staged; the POLL fallback below resumes the
1220 * wait without running the handler on an empty stash. */
1221
1222 /* Latency probe: rx_armed_ns is only written while the probe is enabled,
1223 * so a zero value means the probe is off and both clock reads below are
1224 * skipped. Snapshot it now: the serve path re-arms the next RECV and
1225 * would overwrite conn->rx_armed_ns before the queue delay is computed. */
1226 uint64_t t0 = 0;
1228 if (armed_ns) {
1229 struct timespec ts;
1231 t0 = (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
1232 }
1234
1235 if (res == -EAGAIN) {
1236 /* Data not ready: hand the wait to the legacy one-shot POLL. Arm
1237 * POLL directly (NOT http_async_arm_wait, which is RECV-first and
1238 * would immediately re-arm a RECV, livelocking on -EAGAIN). When
1239 * POLL fires, http_async_event_cb re-arms a RECV and the completion
1240 * drives from there.
1241 *
1242 * Learn: this client sends one request per idle period (wrk-style
1243 * keepalive), so an armed RECV just burns an SQE before the POLL
1244 * fallback every round. Prefer POLL until a RECV actually stages
1245 * bytes again (pipelining detected), keeping the steady-state op
1246 * count identical to the legacy path. */
1247 conn->rx_prefers_poll = true;
1248 http_async_ctx_t next = {
1249 .client_fd = conn->fd,
1250 .handler = conn->handler,
1251 .ctx = conn->user_ctx,
1252 .reactor = conn->reactor,
1253 .conn = conn,
1254 };
1255 if (!cwist_reactor_add(conn->reactor, conn->fd, http_async_event_cb, &next, sizeof(next))) {
1256 close(conn->fd);
1257 http_async_conn_release(conn);
1258 }
1259 return;
1260 }
1261
1262 /* rx_data_ready tells cwist_http_async_conn_fill the bytes are already
1263 * in the stash (or EOF is already recorded); never recv() on top of a
1264 * RECV completion. Bytes flowed: a pipelining client gets the SQE
1265 * path again. */
1266 conn->rx_data_ready = true;
1267 conn->rx_prefers_poll = false;
1268 http_async_dispatch(conn->fd, conn, conn->handler, conn->user_ctx, conn->reactor);
1269
1270 if (armed_ns) {
1271 uint64_t queue_us = (t0 - armed_ns) / 1000;
1272 struct timespec ts;
1274 uint64_t t1 = (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec;
1275 cwist_reactor_probe_record(conn->reactor, true, queue_us);
1276 cwist_reactor_probe_record(conn->reactor, false, (t1 - t0) / 1000);
1277 }
1278}
1279
1280/* Engine-internal teardown for the async pool: drain pending receive-queue
1281 * bytes so the kernel answers with FIN instead of RST, then close and release.
1282 * Must run on the connection's reactor owner thread (the reactor may still
1283 * have the fd armed); not part of the public API. Defined below. */
1284static void http_async_close(int client_fd, cwist_http_async_conn_t *conn);
1285
1293static void http_async_event_cb(int fd, void *ctx) {
1294 http_async_ctx_t *c = (http_async_ctx_t *)ctx;
1295 cwist_http_async_conn_t *conn = c->conn;
1296 cwist_async_handler_t handler = c->handler;
1297
1299 uint32_t timeout_sec = cwist_http_keep_alive_timeout_sec();
1300
1301 /* Idle connection reaper: close keep-alive sockets that exceeded timeout.
1302 * A client may have pipelined a request into the post-timeout,
1303 * pre-dispatch window; drain before closing so the teardown is a
1304 * graceful FIN, not an RST that flushes the peer's queued bytes. The
1305 * triggering one-shot slot is consumed, so closing here is safe. */
1306 if (conn->last_active_sec > 0 && (now - conn->last_active_sec) > timeout_sec) {
1307 http_async_close(fd, conn);
1308 return;
1309 }
1310 conn->last_active_sec = now;
1311
1312 if (cwist_reactor_rx_supported(c->reactor) && !conn->rx_recv_inflight && !conn->peer_eof &&
1313 conn->len == 0 && !conn->rx_prefers_poll) {
1314 /* POLL is only a readiness trigger on the RX path: with an empty
1315 * stash the wait moves to a RECV SQE whose completion drives the
1316 * state machine. (POLL gets armed only by the -EAGAIN and
1317 * low-headroom fallbacks.) Buffered bytes (continuation posts,
1318 * parked-writer resumes) must be served, not waited on, so they
1319 * fall through to the legacy dispatch below. If the arm fails,
1320 * fall through to the legacy inline drain. */
1321 if (http_async_arm_wait(fd, conn, handler, c->ctx, c->reactor)) return;
1322 }
1323
1324 http_async_dispatch(fd, conn, handler, c->ctx, c->reactor);
1325}
1326
1327typedef struct {
1329 http_async_ctx_t next;
1330} http_async_continuation_t;
1331
1338static void http_async_continue(void *ctx) {
1339 http_async_continuation_t *continuation = ctx;
1340 http_async_ctx_t next = continuation->next;
1342 if (!atomic_load(&g_cwist_running) || atomic_load(&g_http_pool_stopping)) {
1343 http_async_close(next.client_fd, next.conn);
1344 return;
1345 }
1346 http_async_event_cb(next.client_fd, &next);
1347}
1348
1357bool cwist_http_async_rearm(int client_fd, cwist_reactor_t *reactor,
1359 if (client_fd < 0 || !reactor || !conn) return false;
1360 /* A deferred response may finish in reactor_destroy's final drain. Never
1361 * enqueue new work into that final snapshot or the connection is orphaned. */
1362 if (atomic_load(&g_http_pool_stopping)) {
1363 http_async_close(client_fd, conn);
1364 return false;
1365 }
1366 if (conn->peer_eof && conn->len == 0) {
1367 http_async_close(client_fd, conn);
1368 return false;
1369 }
1371 http_async_ctx_t next = {
1372 .client_fd = client_fd,
1373 .handler = conn->handler,
1374 .ctx = conn->user_ctx,
1375 .reactor = reactor,
1376 .conn = conn,
1377 };
1378 if (conn->len > 0) {
1379 /* Pipelined bytes already sit in the stash: waiting for POLLIN would
1380 * hang. Post instead of recursing inline, so ready connections can
1381 * run between bounded HTTP request batches. */
1382 http_async_continuation_t *continuation = cwist_alloc(sizeof(*continuation));
1383 if (!continuation) {
1384 http_async_close(client_fd, conn);
1385 return false;
1386 }
1387 continuation->next = next;
1389 .cb = http_async_continue,
1390 .ctx = continuation,
1391 };
1392 if (!cwist_reactor_post(reactor, &continuation->post)) {
1393 long n =
1394 atomic_fetch_add_explicit(&g_http_continuation_shed, 1, memory_order_relaxed) + 1;
1396 if (cwist_async_debug_enabled() && (n <= 5 || n % 10000 == 0))
1397 fprintf(stderr, "[async] continuation shed fd=%d total=%ld\n", client_fd, n);
1399 http_async_close(client_fd, conn);
1400 return false;
1401 }
1402 return true;
1403 }
1404 /* Same stash shrink as the REARM path in http_async_dispatch, through
1405 * the one shared policy (rbuf > 64 KiB freed entirely, otherwise
1406 * realloc'd to the floor; obuf symmetric). */
1407 http_async_stash_idle_shrink(conn);
1408 /* RX-uring: RECV-first re-arm when the ring supports it (peer_eof was
1409 * handled above, so the wait is a plain receive wait). arm_wait falls
1410 * back to the legacy one-shot POLL on unsupported reactors or arm
1411 * failure. */
1412 if (!http_async_arm_wait(client_fd, conn, conn->handler, conn->user_ctx, reactor)) {
1413 close(client_fd);
1414 http_async_conn_release(conn);
1415 return false;
1416 }
1417 return true;
1418}
1419
1425static void http_async_close(int client_fd, cwist_http_async_conn_t *conn) {
1426 if (client_fd >= 0) {
1427 /* close() on a socket with unread receive-queue data makes the
1428 * kernel answer with RST instead of a graceful FIN. A client can
1429 * pipeline its next request onto a connection the server is
1430 * closing, leaving those bytes unconsumed here; under connection
1431 * churn that turned most teardowns into abortive closes (the RST
1432 * also flushes response bytes the peer had not read yet). Drain
1433 * whatever is pending before closing; the socket is about to be
1434 * destroyed anyway. Mirrors https_connection_teardown(). */
1435 char drain[2048];
1436 ssize_t n;
1437 int guard = 16;
1438 while (guard-- > 0 && (n = recv(client_fd, drain, sizeof(drain), MSG_DONTWAIT)) > 0) {
1439 /* discard */
1440 }
1441 (void)n;
1442 close(client_fd);
1443 }
1444 http_async_conn_release(conn);
1445}
1446
1454 /* The worker that owned conn is gone, and its load slot index may belong
1455 * to a later pool by now: leave the load counters alone. */
1456 if (conn) conn->worker_id = UINT32_MAX;
1457 http_async_close(client_fd, conn);
1458}
1459
1468bool cwist_http_pool_submit_async(int client_fd, cwist_async_handler_t handler, void *ctx) {
1469 long limit = cwist_http_inflight_limit();
1470 long inflight = atomic_fetch_add_explicit(&g_http_inflight, 1, memory_order_acq_rel) + 1;
1471 if (inflight > limit) {
1472 atomic_fetch_sub_explicit(&g_http_inflight, 1, memory_order_release);
1473#ifdef MSG_NOSIGNAL
1474 send(client_fd, CWIST_HTTP_503, sizeof(CWIST_HTTP_503) - 1, MSG_NOSIGNAL | MSG_DONTWAIT);
1475#endif
1476 close(client_fd);
1477 return false;
1478 }
1479
1480 /* The one-shot path must never park the reactor on a blocking recv. */
1481 int fl = fcntl(client_fd, F_GETFL, 0);
1482 if (fl >= 0) fcntl(client_fd, F_SETFL, fl | O_NONBLOCK);
1483
1484 cwist_http_async_conn_t *conn = cwist_alloc(sizeof(*conn));
1485 if (!conn) {
1486 atomic_fetch_sub_explicit(&g_http_inflight, 1, memory_order_release);
1487 close(client_fd);
1488 return false;
1489 }
1490 memset(conn, 0, sizeof(*conn));
1491 conn->fd = client_fd;
1492 conn->user_ctx = ctx;
1493 conn->virgin = true;
1495 conn->stash_grown_sec = conn->last_active_sec;
1496
1497 /* Worker selection: Power of Two Random Choices (P2C) load balancing
1498 * to eliminate queue skew without cache-bouncing work stealing. */
1499 size_t worker_idx;
1500 if (g_worker_loads && g_http_thread_count > 1) {
1501 worker_idx = cwist_sched_p2c_select_worker(g_worker_loads, (uint32_t)g_http_thread_count);
1502 } else {
1503 worker_idx = g_rr_index;
1504 g_rr_index = (g_rr_index + 1) % (size_t)g_http_thread_count;
1505 }
1506 http_thread_worker_t *w = &g_workers[worker_idx];
1507
1508 conn->worker_id = (uint32_t)worker_idx;
1509 if (g_worker_loads) {
1511 }
1512 conn->reactor = w->reactor;
1513 conn->handler = handler;
1514
1515 /* RX-uring: when the worker reactor has a real io_uring ring, the first
1516 * receive wait is a RECV SQE into the (freshly grown) stash whose
1517 * completion drives the state machine; on any arm failure the legacy
1518 * one-shot POLL is armed instead. */
1519 if (http_async_arm_wait(client_fd, conn, handler, ctx, w->reactor)) return true;
1520
1521 if (cwist_async_debug_enabled()) {
1522 static _Atomic long dbg_submit_fail;
1523 long n = atomic_fetch_add(&dbg_submit_fail, 1) + 1;
1524 if (n <= 5 || n % 10000 == 0)
1525 fprintf(stderr, "[async] submit-add failed fd=%d total=%ld worker=%zu\n", client_fd, n,
1526 worker_idx);
1527 }
1528 http_async_conn_release(conn);
1529 close(client_fd);
1530 return false;
1531}
1532
1533/* --- End Async Connection Path --- */
1534#endif /* !CWIST_PLATFORM_NO_SOCKETS (socket worker runtime) */
1535
1546
1547/* --- Helpers --- */
1548
1555 switch (method) {
1556 case CWIST_HTTP_GET: return "GET";
1557 case CWIST_HTTP_POST: return "POST";
1558 case CWIST_HTTP_PUT: return "PUT";
1559 case CWIST_HTTP_DELETE: return "DELETE";
1560 case CWIST_HTTP_PATCH: return "PATCH";
1561 case CWIST_HTTP_HEAD: return "HEAD";
1562 case CWIST_HTTP_OPTIONS: return "OPTIONS";
1563 case CWIST_HTTP_CONNECT: return "CONNECT";
1564 default: return "UNKNOWN";
1565 }
1566}
1567
1568#define MAKE_MAGIC4(a, b, c, d) \
1569 ((uint32_t)(a) | ((uint32_t)(b) << 8) | ((uint32_t)(c) << 16) | ((uint32_t)(d) << 24))
1570
1571#define MAGIC_GET MAKE_MAGIC4('G', 'E', 'T', ' ')
1572#define MAGIC_POST MAKE_MAGIC4('P', 'O', 'S', 'T')
1573#define MAGIC_PUT MAKE_MAGIC4('P', 'U', 'T', ' ')
1574#define MAGIC_DELE MAKE_MAGIC4('D', 'E', 'L', 'E')
1575#define MAGIC_HEAD MAKE_MAGIC4('H', 'E', 'A', 'D')
1576
1585 if (!str || len == 0) return CWIST_HTTP_UNKNOWN;
1586
1587 /* Ultra-fast path: SWAR 4-byte magic lookup for GET, POST, PUT, DELETE, HEAD */
1588 if (len >= 3) {
1589 uint32_t m = 0;
1590 memcpy(&m, str, sizeof(uint32_t));
1591 switch (m) {
1592 case MAGIC_GET: return CWIST_HTTP_GET;
1593 case MAGIC_POST: return CWIST_HTTP_POST;
1594 case MAGIC_PUT: return CWIST_HTTP_PUT;
1595 case MAGIC_DELE:
1596 if (len >= 6 && memcmp(str, "DELETE", 6) == 0) return CWIST_HTTP_DELETE;
1597 break;
1598 case MAGIC_HEAD:
1599 if (len == 4) return CWIST_HTTP_HEAD;
1600 break;
1601 default: break;
1602 }
1603 }
1604
1605 if (len == 3) {
1606 uint32_t v = 0;
1607 memcpy(&v, str, 3);
1608 if ((v & 0x00FFFFFF) == 0x00544547) return CWIST_HTTP_GET;
1609 if ((v & 0x00FFFFFF) == 0x00545550) return CWIST_HTTP_PUT;
1610 } else if (len == 4) {
1611 uint32_t v = 0;
1612 memcpy(&v, str, 4);
1613 if (v == 0x54534F50) return CWIST_HTTP_POST;
1614 if (v == 0x44414548) return CWIST_HTTP_HEAD;
1615 } else if (len == 5) {
1616 if (memcmp(str, "PATCH", 5) == 0) return CWIST_HTTP_PATCH;
1617 } else if (len == 6) {
1618 if (memcmp(str, "DELETE", 6) == 0) return CWIST_HTTP_DELETE;
1619 } else if (len == 7) {
1620 if (memcmp(str, "OPTIONS", 7) == 0) return CWIST_HTTP_OPTIONS;
1621 }
1622 return CWIST_HTTP_UNKNOWN;
1623}
1624
1633
1634/* --- Header Manipulation --- */
1635
1642static void *cwist_http_struct_alloc(cwist_arena_t *arena, size_t size) {
1643 void *p = arena ? cwist_arena_alloc(arena, size) : NULL;
1644 if (p) {
1645 memset(p, 0, size);
1646 return p;
1647 }
1648 return cwist_alloc(size);
1649}
1650
1659static cwist_sstring *cwist_http_sstring_create(cwist_arena_t *arena) {
1660 bool from_arena = false;
1661 cwist_sstring *str = NULL;
1662 if (arena) {
1663 str = (cwist_sstring *)cwist_arena_alloc(arena, sizeof(cwist_sstring));
1664 if (str) from_arena = true;
1665 }
1666 if (!str) {
1667 str = (cwist_sstring *)cwist_alloc(sizeof(cwist_sstring));
1668 }
1669 if (!str) return NULL;
1670
1671 memset(str, 0, sizeof(cwist_sstring));
1672 str->is_fixed = false;
1673 str->owns_storage = !from_arena;
1674 str->size = 0;
1675 str->data = NULL;
1680
1681 return str;
1682}
1683
1692static cwist_error_t cwist_http_sstring_assign_arena(cwist_sstring *str, cwist_arena_t *arena,
1693 const char *data, size_t len) {
1694 if (arena) {
1695 char *buf = (char *)cwist_arena_alloc(arena, len + 1);
1696 if (buf) {
1697 if (str->data && !str->borrows_buffer) {
1698 cwist_free(str->base ? str->base : str->data);
1699 }
1700 if (data && len > 0) memcpy(buf, data, len);
1701 buf[len] = '\0';
1702 str->base = NULL;
1703 str->data = buf;
1704 str->size = len;
1705 str->capacity = 0;
1706 str->borrows_buffer = true;
1708 err.error.err_i8 = ERR_SSTRING_OKAY;
1709 return err;
1710 }
1711 }
1712 return cwist_sstring_assign_len(str, data, len);
1713}
1714
1718static cwist_error_t cwist_http_header_add_ex_len(cwist_http_header_node **head,
1719 cwist_arena_t *arena, const char *key,
1720 size_t key_len, const char *value,
1721 size_t value_len) {
1723
1724 bool from_arena = false;
1726 if (arena) {
1728 if (node) {
1729 memset(node, 0, sizeof(cwist_http_header_node));
1730 from_arena = true;
1731 }
1732 }
1733 if (!node) {
1735 }
1736 if (!node) {
1738 err.error.err_json = cJSON_CreateObject();
1739 cJSON_AddStringToObject(err.error.err_json, "http_error", "Failed to allocate header");
1740 return err;
1741 }
1742 node->arena_owned = from_arena;
1743
1744 node->key = cwist_http_sstring_create(arena);
1745 node->value = cwist_http_sstring_create(arena);
1746 node->next = NULL;
1747
1748 bool ok = node->key && node->value;
1749 if (ok) {
1750 cwist_error_t kerr = cwist_http_sstring_assign_arena(node->key, arena, key, key_len);
1751 ok = cwist_error_is_ok(&kerr);
1753 }
1754 if (ok) {
1755 cwist_error_t verr = cwist_http_sstring_assign_arena(node->value, arena, value, value_len);
1756 ok = cwist_error_is_ok(&verr);
1758 }
1759 if (!ok) {
1760 /* A half-built node would serialize as an empty or missing header;
1761 * release it and report the failure instead. */
1764 err.error.err_json = cJSON_CreateObject();
1765 cJSON_AddStringToObject(err.error.err_json, "http_error",
1766 "Failed to allocate header strings");
1767 return err;
1768 }
1769
1770 node->next = *head;
1771 *head = node;
1772
1773 err.error.err_i16 = 0; // Success
1774 return err;
1775}
1776
1785static cwist_error_t cwist_http_header_add_ex(cwist_http_header_node **head, cwist_arena_t *arena,
1786 const char *key, const char *value) {
1787 return cwist_http_header_add_ex_len(head, arena, key, key ? strlen(key) : 0, value,
1788 value ? strlen(value) : 0);
1789}
1790
1799static cwist_error_t cwist_http_header_add_static(cwist_http_header_node **head,
1800 cwist_arena_t *arena, const char *key,
1801 const char *value) {
1803
1804 bool from_arena = false;
1806 if (arena) {
1808 if (node) {
1809 memset(node, 0, sizeof(cwist_http_header_node));
1810 from_arena = true;
1811 }
1812 }
1813 if (!node) {
1815 }
1816 if (!node) {
1818 err.error.err_json = cJSON_CreateObject();
1819 cJSON_AddStringToObject(err.error.err_json, "http_error", "Failed to allocate header");
1820 return err;
1821 }
1822 node->arena_owned = from_arena;
1823
1824 node->key = cwist_http_sstring_create(arena);
1825 node->value = cwist_http_sstring_create(arena);
1826 node->next = NULL;
1827
1828 if (!node->key || !node->value) {
1829 if (!from_arena) cwist_free(node);
1831 err.error.err_json = cJSON_CreateObject();
1832 cJSON_AddStringToObject(err.error.err_json, "http_error",
1833 "Failed to allocate header strings");
1834 return err;
1835 }
1836
1837 cwist_sstring_borrow(node->key, key, strlen(key));
1838 cwist_sstring_borrow(node->value, value, strlen(value));
1839
1840 node->next = *head;
1841 *head = node;
1842
1843 err.error.err_i16 = 0; // Success
1844 return err;
1845}
1846
1848static bool cwist_http_header_text_is_safe(const char *text) {
1849 return !text || !strpbrk(text, "\r\n");
1850}
1851
1862 const char *value) {
1863 /* The serializer writes key and value verbatim around ": " and CRLF, so a
1864 * CR or LF here would let caller-supplied data end the header early and
1865 * inject further headers or a body into the response. */
1866 if (!head || !cwist_http_header_text_is_safe(key) || !cwist_http_header_text_is_safe(value)) {
1868 err.error.err_i16 = -1;
1869 return err;
1870 }
1871 return cwist_http_header_add_ex(head, NULL, key, value);
1872}
1873
1881 if (!head || !key) return NULL;
1882 size_t klen = strlen(key);
1884 while (curr) {
1885 if (curr->key && curr->key->data && curr->key->size == klen) {
1886 if (strcasecmp(curr->key->data, key) == 0) {
1887 return curr->value ? curr->value->data : NULL;
1888 }
1889 }
1890 curr = curr->next;
1891 }
1892 return NULL;
1893}
1894
1908 if (!head || !key) return 0;
1909 size_t removed = 0;
1910 while (*head) {
1912 if (node->key && node->key->data && strcasecmp(node->key->data, key) == 0) {
1913 *head = node->next;
1916 if (!node->arena_owned) cwist_free(node);
1917 removed++;
1918 } else {
1919 head = &node->next;
1920 }
1921 }
1922 return removed;
1923}
1924
1936 if (!res) return;
1937 cwist_arena_t *arena = (cwist_arena_t *)res->arena;
1938
1939 /* All key/value pairs are compile-time constants: borrow them instead of
1940 * heap-copying, so a default response performs zero heap allocations for
1941 * its security headers (arena carve only). */
1942 if (!cwist_http_header_get(res->headers, "X-Frame-Options")) {
1943 cwist_http_header_add_static(&res->headers, arena, "X-Frame-Options", "DENY");
1944 }
1945 if (!cwist_http_header_get(res->headers, "X-Content-Type-Options")) {
1946 cwist_http_header_add_static(&res->headers, arena, "X-Content-Type-Options", "nosniff");
1947 }
1948 if (!cwist_http_header_get(res->headers, "Referrer-Policy")) {
1949 cwist_http_header_add_static(&res->headers, arena, "Referrer-Policy",
1950 "strict-origin-when-cross-origin");
1951 }
1952 if (!cwist_http_header_get(res->headers, "Content-Security-Policy")) {
1953 cwist_http_header_add_static(
1954 &res->headers, arena, "Content-Security-Policy",
1955 "default-src 'self'; "
1956 "script-src 'self' https://cdnjs.cloudflare.com; "
1957 "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net https://cdnjs.cloudflare.com; "
1958 "font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net; "
1959 "img-src 'self'; "
1960 "connect-src 'self'; "
1961 "frame-ancestors 'none'; "
1962 "base-uri 'self'; "
1963 "form-action 'self'; "
1964 "object-src 'none';");
1965 }
1966 if (!cwist_http_header_get(res->headers, "Cross-Origin-Resource-Policy")) {
1967 cwist_http_header_add_static(&res->headers, arena, "Cross-Origin-Resource-Policy",
1968 "same-origin");
1969 }
1970 /* Permissions-Policy (W3C Permissions Policy Level 2) — deny access to
1971 * sensitive browser APIs that CWIST apps almost never need. Callers that
1972 * require a specific feature can set the header before calling this
1973 * function; the existing-header check below will skip the default. */
1974 if (!cwist_http_header_get(res->headers, "Permissions-Policy")) {
1975 cwist_http_header_add_static(&res->headers, arena, "Permissions-Policy",
1976 "camera=(), microphone=(), geolocation=(), payment=(), "
1977 "usb=(), interest-cohort=()");
1978 }
1979 if (!cwist_http_header_get(res->headers, "Cross-Origin-Opener-Policy")) {
1980 cwist_http_header_add_static(&res->headers, arena, "Cross-Origin-Opener-Policy", "same-origin");
1981 }
1982}
1983
1994 if (!res) return;
1995 cwist_arena_t *arena = (cwist_arena_t *)res->arena;
1996 if (!cwist_http_header_get(res->headers, "Strict-Transport-Security")) {
1997 cwist_http_header_add_static(&res->headers, arena, "Strict-Transport-Security",
1998 "max-age=31536000; includeSubDomains");
1999 }
2000}
2001
2008 while (curr) {
2012 if (!curr->arena_owned) {
2014 }
2015 curr = next;
2016 }
2017}
2018
2029static bool cwist_get_cached_date_header(char out_buf[36]) {
2031 static _Thread_local char date_str[36];
2032
2033 time_t now = time(NULL);
2034 if (now == (time_t)-1) return false;
2035 if (now != cached_sec) {
2036 struct tm gmt;
2037#if defined(_WIN32)
2038 gmtime_s(&gmt, &now);
2039#else
2040 gmtime_r(&now, &gmt);
2041#endif
2042 strftime(date_str, sizeof(date_str), "%a, %d %b %Y %H:%M:%S GMT", &gmt);
2043 cached_sec = now;
2044 }
2045 memcpy(out_buf, date_str, 30);
2046 out_buf[29] = '\0';
2047 return true;
2048}
2049
2050/* --- Request Lifecycle --- */
2051
2058 cwist_http_request *req =
2059 (cwist_http_request *)cwist_http_struct_alloc(arena, sizeof(cwist_http_request));
2060 if (!req) {
2061 cwist_arena_destroy(arena);
2062 return NULL;
2063 }
2064
2065 req->method = CWIST_HTTP_GET; // Default
2066 req->path = cwist_http_sstring_create(arena);
2067 req->query = cwist_http_sstring_create(arena);
2068 req->query_params = NULL;
2069 req->path_params = NULL;
2070 req->version = cwist_http_sstring_create(arena);
2071 req->headers = NULL;
2072 req->body = cwist_http_sstring_create(arena);
2073 req->keep_alive = true;
2074 req->client_fd = -1;
2075 req->app = NULL;
2076 req->db = NULL;
2077 req->flash = NULL;
2078 req->upgraded = false;
2079 req->content_length = 0;
2080 req->stream_id = 0;
2081 req->private_data = NULL;
2083 req->arena = arena;
2084 req->async_conn = NULL;
2085 req->https_conn = NULL;
2086 req->h2_queue = NULL;
2087
2088 // Defaults (borrowed statics; parsing overwrites them via arena/heap assign)
2089 cwist_sstring_borrow(req->version, "HTTP/1.1", 8);
2090 cwist_sstring_borrow(req->path, "/", 1);
2091
2092 return req;
2093}
2094
2095/* --- Request Data Processing */
2096
2108void cwist_http_format_date(time_t t, char *buf, size_t len) {
2109 struct tm tm;
2110 gmtime_r(&t, &tm);
2111 strftime(buf, len, "%a, %d %b %Y %H:%M:%S GMT", &tm);
2112}
2113
2120 if (!str) return (time_t)-1;
2121 struct tm tm = {0};
2122 const char *fmt = "%a, %d %b %Y %H:%M:%S %Z";
2123 if (strptime(str, fmt, &tm) == NULL) {
2124 // Try alternative formats
2125 fmt = "%a, %d-%b-%y %H:%M:%S %Z";
2126 if (strptime(str, fmt, &tm) == NULL) {
2127 fmt = "%a %b %d %H:%M:%S %Y";
2128 if (strptime(str, fmt, &tm) == NULL) {
2129 return (time_t)-1;
2130 }
2131 }
2132 }
2133 return timegm(&tm);
2134}
2135
2143 cwist_sstring_assign(s, "127.0.0.1");
2144 // First, check if fd is available
2145 // If unavailable, return localhost
2146 if (fd <= 0) return s;
2147
2148#ifndef CWIST_PLATFORM_NO_SOCKETS
2149 /* WASI preview1 has no socket peer addresses; the caller only needs a
2150 * placeholder when running under a WASM host. */
2151 struct sockaddr_storage addr;
2152 socklen_t len = sizeof(addr);
2153
2154 // get client info
2155 if (getpeername(fd, (struct sockaddr *)&addr, &len) == -1) {
2156 fprintf(stdout, "[ERROR] Failed to get client info from file descriptor");
2157 return s;
2158 }
2159
2160 char ip[INET6_ADDRSTRLEN];
2161
2162 if (addr.ss_family == AF_INET) {
2163 struct sockaddr_in *s = (struct sockaddr_in *)&addr;
2164 inet_ntop(AF_INET, &s->sin_addr, ip, sizeof(ip));
2165 } else if (addr.ss_family == AF_INET6) {
2166 struct sockaddr_in6 *s = (struct sockaddr_in6 *)&addr;
2167 inet_ntop(AF_INET6, &s->sin6_addr, ip, sizeof(ip));
2168 }
2169
2170 // assign found ip as a value
2171 cwist_sstring_assign(s, ip);
2172#endif
2173 return s;
2174}
2175
2181 if (req) {
2182 cwist_arena_t *arena = (cwist_arena_t *)req->arena;
2191 cwist_free(req->csrf_token);
2193 if (!arena || !cwist_arena_owns(arena, req)) {
2194 cwist_free(req);
2195 }
2196 /* Releases every arena-carved struct (request, sstrings, header
2197 * nodes) in one shot; heap-owned buffers were freed above. */
2198 cwist_arena_destroy(arena);
2199 }
2200}
2201
2202/* --- Response Lifecycle --- */
2203
2208static void cwist_http_response_release_file_stream(cwist_http_response *res) {
2209 if (!res || !res->use_file_stream) return;
2210 if (res->file_stream_auto_close && res->file_stream_fd >= 0) {
2211 close(res->file_stream_fd);
2212 }
2213 res->use_file_stream = false;
2214 res->file_stream_fd = -1;
2215 res->file_stream_len = 0;
2216 res->file_stream_offset = 0;
2217 res->file_stream_auto_close = false;
2218}
2219
2224static void cwist_http_response_release_ptr_body(cwist_http_response *res) {
2225 if (!res || !res->is_ptr_body) return;
2226 if (res->ptr_body_cleanup && res->ptr_body) {
2228 }
2229 res->is_ptr_body = false;
2230 res->ptr_body = NULL;
2231 res->ptr_body_len = 0;
2232 res->ptr_body_cleanup = NULL;
2234}
2235
2246static cwist_http_response *cwist_http_response_create_impl(cwist_arena_t *arena,
2247 bool arena_borrowed) {
2248 cwist_http_response *res =
2249 (cwist_http_response *)cwist_http_struct_alloc(arena, sizeof(cwist_http_response));
2250 if (!res) return NULL;
2251
2252 res->version = cwist_http_sstring_create(arena);
2254 res->status_text = cwist_http_sstring_create(arena);
2255 res->headers = NULL;
2256 res->body = cwist_http_sstring_create(arena);
2258 res->keep_alive = true;
2259 res->is_ptr_body = false;
2260 res->ptr_body = NULL;
2261 res->ptr_body_len = 0;
2262 res->ptr_body_cleanup = NULL;
2264 res->use_file_stream = false;
2265 res->file_stream_fd = -1;
2266 res->file_stream_len = 0;
2267 res->file_stream_offset = 0;
2268 res->file_stream_auto_close = false;
2269 res->stream_mode = false;
2270 res->stream_ended = false;
2271 res->stream_failed = false;
2272 res->stream_buf = cwist_http_sstring_create(arena);
2273 res->stream_flushed = 0;
2274 res->stream_sink = NULL;
2275 res->stream_sink_ctx = NULL;
2276 res->stream_head_sent = false;
2277 res->arena = arena;
2278 res->arena_borrowed = arena_borrowed;
2279
2280 // Defaults (borrowed statics; handlers may overwrite via regular assign)
2281 cwist_sstring_borrow(res->version, "HTTP/1.1", 8);
2282 cwist_sstring_borrow(res->status_text, "OK", 2);
2283
2284 return res;
2285}
2286
2293 cwist_http_response *res = cwist_http_response_create_impl(arena, false);
2294 if (!res) {
2295 cwist_arena_destroy(arena);
2296 return NULL;
2297 }
2298 return res;
2299}
2300
2307 if (!arena) return cwist_http_response_create();
2308 return cwist_http_response_create_impl((cwist_arena_t *)arena, true);
2309}
2310
2316 if (res) {
2318 cwist_arena_t *arena = (cwist_arena_t *)res->arena;
2319 cwist_http_response_release_ptr_body(res);
2320 cwist_http_response_release_file_stream(res);
2326 cwist_free(res->alt_svc);
2327 /* Snapshot before the possible free: when the arena was full the
2328 * struct fell back to the heap, so res may be freed below and any
2329 * later field read is a use-after-free. */
2330 bool borrowed = res->arena_borrowed;
2331 if (!arena || !cwist_arena_owns(arena, res)) {
2332 cwist_free(res);
2333 }
2334 if (!borrowed) {
2335 cwist_arena_destroy(arena);
2336 }
2337 }
2338}
2339
2346void cwist_http_response_set_body_ptr(cwist_http_response *res, const void *ptr, size_t len) {
2348}
2349
2359 cwist_http_body_cleanup_fn cleanup, void *ctx) {
2360 if (!res) return;
2361 cwist_http_response_release_file_stream(res);
2362 cwist_http_response_release_ptr_body(res);
2363 res->is_ptr_body = true;
2364 res->ptr_body = ptr;
2365 res->ptr_body_len = len;
2366 res->ptr_body_cleanup = cleanup;
2367 res->ptr_body_cleanup_ctx = ctx;
2368}
2369
2377 if (!res) return;
2378 if (res->alt_svc) {
2379 cwist_free(res->alt_svc);
2380 res->alt_svc = NULL;
2381 }
2382 if (alt_svc) {
2383 res->alt_svc = cwist_strdup(alt_svc);
2384 }
2385}
2386
2387// ... (request parsing omitted) ...
2388
2395 cwist_http_header_node *curr = headers;
2396 while (curr) {
2397 if (curr->key && curr->key->data && strcasecmp(curr->key->data, "Content-Length") == 0) {
2398 return 1;
2399 }
2400 curr = curr->next;
2401 }
2402 return 0;
2403}
2404
2412static size_t serialize_headers(cwist_http_response *res, char *buf, size_t buf_size) {
2413 size_t body_len = 0;
2414 if (res->use_file_stream) {
2415 body_len = res->file_stream_len;
2416 } else if (res->is_ptr_body) {
2417 body_len = res->ptr_body_len;
2418 } else if (res->body) {
2419 body_len = res->body->size;
2420 }
2421
2422 /* Fast path for default HTTP/1.1 200 OK response with no custom headers.
2423 * Streamed responses take the general path: chunked mode needs the
2424 * Transfer-Encoding header the fast path cannot emit. */
2425 if (res->status_code == 200 && !res->headers && !res->alt_svc && !res->stream_mode &&
2426 buf_size >= 128) {
2427 /* Compact ring/line buffer optimization for high-throughput pipelining */
2428 static const char status_prefix[] = "HTTP/1.1 200 OK\r\nContent-Length: ";
2429 memcpy(buf, status_prefix, sizeof(status_prefix) - 1);
2430 size_t offset = sizeof(status_prefix) - 1;
2431
2432 /* Fast integer to ascii without snprintf overhead */
2433 char num_buf[20];
2434 char *p = num_buf + sizeof(num_buf);
2435 size_t tmp_len = body_len;
2436 do {
2437 *--p = '0' + (tmp_len % 10);
2438 tmp_len /= 10;
2439 } while (tmp_len > 0);
2440 size_t num_len = (num_buf + sizeof(num_buf)) - p;
2441 memcpy(buf + offset, p, num_len);
2442 offset += num_len;
2443
2444 if (res->keep_alive) {
2445 static const char conn_ka[] = "\r\nConnection: keep-alive\r\n\r\n";
2446 memcpy(buf + offset, conn_ka, sizeof(conn_ka) - 1);
2447 offset += sizeof(conn_ka) - 1;
2448 } else {
2449 static const char conn_cl[] = "\r\nConnection: close\r\n\r\n";
2450 memcpy(buf + offset, conn_cl, sizeof(conn_cl) - 1);
2451 offset += sizeof(conn_cl) - 1;
2452 }
2453 return offset;
2454 }
2455
2456 size_t offset = 0;
2457
2458 // Status Line
2459 const char *status_txt =
2460 (res->status_text && res->status_text->data) ? res->status_text->data : NULL;
2461 /* Default status_text is the borrowed "OK"; when a handler only changed
2462 * the numeric code, emit the standard reason phrase for that code. */
2463 if (!status_txt || (res->status_code != CWIST_HTTP_OK && strcmp(status_txt, "OK") == 0)) {
2464 const char *reason = cwist_http_status_reason(res->status_code);
2465 if (reason) status_txt = reason;
2466 }
2467 if (!status_txt) status_txt = "OK";
2468 /* Hand-rolled status line (version SP code SP reason CRLF): snprintf
2469 * here showed up hot because every non-fast-path response pays it. */
2470 {
2471 const char *version =
2472 (res->version && res->version->data) ? res->version->data : "HTTP/1.1";
2473 const size_t vlen = strlen(version);
2474 const size_t rlen = strlen(status_txt);
2475 unsigned int code = (unsigned int)res->status_code;
2476 char code_buf[10];
2477 char *cp = code_buf + sizeof(code_buf);
2478 do {
2479 *--cp = (char)('0' + (code % 10));
2480 code /= 10;
2481 } while (code > 0);
2482 const size_t code_len = (size_t)(code_buf + sizeof(code_buf) - cp);
2483 if (offset + vlen + 1 + code_len + 1 + rlen + 2 <= buf_size) {
2484 memcpy(buf + offset, version, vlen);
2485 offset += vlen;
2486 buf[offset++] = ' ';
2487 memcpy(buf + offset, cp, code_len);
2488 offset += code_len;
2489 buf[offset++] = ' ';
2490 memcpy(buf + offset, status_txt, rlen);
2491 offset += rlen;
2492 buf[offset++] = '\r';
2493 buf[offset++] = '\n';
2494 }
2495 }
2496
2497 // Headers: memcpy with known sstring lengths (no snprintf/strlen overhead),
2498 // detecting Date/Content-Length/Connection in the same single pass with a
2499 // length + first-byte filter before falling back to strcasecmp.
2500 bool have_date = false, have_clen = false, have_conn = false, have_te = false;
2502 while (curr) {
2503 if (curr->key->data && curr->value->data) {
2504 size_t klen = curr->key->size;
2505 size_t vlen = curr->value->size;
2506 if (offset + klen + 2 + vlen + 2 <= buf_size) {
2507 memcpy(buf + offset, curr->key->data, klen);
2508 buf[offset + klen] = ':';
2509 buf[offset + klen + 1] = ' ';
2510 memcpy(buf + offset + klen + 2, curr->value->data, vlen);
2511 buf[offset + klen + 2 + vlen] = '\r';
2512 buf[offset + klen + 2 + vlen + 1] = '\n';
2513 offset += klen + vlen + 4;
2514 }
2515 char k0 = curr->key->data[0];
2516 if (klen == 4 && (k0 == 'D' || k0 == 'd')) {
2517 if (strcasecmp(curr->key->data, "date") == 0) have_date = true;
2518 } else if (klen == 14 && (k0 == 'C' || k0 == 'c')) {
2519 if (strcasecmp(curr->key->data, "content-length") == 0) have_clen = true;
2520 } else if (klen == 10 && (k0 == 'C' || k0 == 'c')) {
2521 if (strcasecmp(curr->key->data, "connection") == 0) have_conn = true;
2522 } else if (klen == 17 && (k0 == 'T' || k0 == 't')) {
2523 if (strcasecmp(curr->key->data, "transfer-encoding") == 0) have_te = true;
2524 }
2525 }
2526 curr = curr->next;
2527 }
2528
2529 char date_str[36];
2530 if (!have_date && offset + 37 <= buf_size && cwist_get_cached_date_header(date_str)) {
2531 memcpy(buf + offset, "Date: ", 6);
2532 memcpy(buf + offset + 6, date_str, 29);
2533 buf[offset + 35] = '\r';
2534 buf[offset + 36] = '\n';
2535 offset += 37;
2536 }
2537
2538 if (res->stream_mode && !have_te && !have_clen) {
2539 static const char te_chunked[] = "Transfer-Encoding: chunked\r\n";
2540 if (offset + sizeof(te_chunked) - 1 <= buf_size) {
2541 memcpy(buf + offset, te_chunked, sizeof(te_chunked) - 1);
2542 offset += sizeof(te_chunked) - 1;
2543 }
2544 }
2545
2546 if (!have_clen && !res->stream_mode) {
2547 /* Fast integer to ascii without snprintf overhead */
2548 char num_buf[20];
2549 char *p = num_buf + sizeof(num_buf);
2550 size_t tmp_len = body_len;
2551 do {
2552 *--p = '0' + (tmp_len % 10);
2553 tmp_len /= 10;
2554 } while (tmp_len > 0);
2555 size_t num_len = (size_t)((num_buf + sizeof(num_buf)) - p);
2556 if (offset + 16 + num_len + 2 <= buf_size) {
2557 memcpy(buf + offset, "Content-Length: ", 16);
2558 memcpy(buf + offset + 16, p, num_len);
2559 buf[offset + 16 + num_len] = '\r';
2560 buf[offset + 16 + num_len + 1] = '\n';
2561 offset += 16 + num_len + 2;
2562 }
2563 }
2564
2565 if (!have_conn) {
2566 if (res->keep_alive) {
2567 static const char conn_ka[] = "Connection: keep-alive\r\n";
2568 if (offset + sizeof(conn_ka) - 1 <= buf_size) {
2569 memcpy(buf + offset, conn_ka, sizeof(conn_ka) - 1);
2570 offset += sizeof(conn_ka) - 1;
2571 }
2572 } else {
2573 static const char conn_cl[] = "Connection: close\r\n";
2574 if (offset + sizeof(conn_cl) - 1 <= buf_size) {
2575 memcpy(buf + offset, conn_cl, sizeof(conn_cl) - 1);
2576 offset += sizeof(conn_cl) - 1;
2577 }
2578 }
2579 }
2580
2581 if (res->alt_svc) {
2582 if (offset < buf_size) {
2583 int n = snprintf(buf + offset, buf_size - offset, "Alt-Svc: %s\r\n", res->alt_svc);
2584 if (n > 0) {
2585 offset += n;
2586 if (offset > buf_size) offset = buf_size;
2587 }
2588 }
2589 }
2590
2591 if (offset + 2 <= buf_size) {
2592 buf[offset++] = '\r';
2593 buf[offset++] = '\n';
2594 }
2595 return offset;
2596}
2597
2610 return serialize_headers(res, buf, buf_size);
2611}
2612
2613#ifndef CWIST_PLATFORM_NO_SOCKETS /* socket send paths; in-memory dispatch serializes instead */
2614#include <sys/uio.h> // For writev and BSD sendfile
2615
2616/* --- Optional TCP_CORK coalescing layer (cleartext HTTP/1.1) -------------
2617 * Enabled at runtime with CWIST_USE_TCP_CORK=1, no source changes needed.
2618 * While corked, headers + body + file bytes accumulate into full segments;
2619 * instead of flushing everything in one giant clump, the file path flushes
2620 * every CWIST_TCP_CORK_BURST bytes (default 256 KiB), which is what keeps
2621 * high-RTT links fed without head-of-queue clumping. TLS is unaffected
2622 * (records are sealed above the TCP layer, so cork buys nothing there). */
2623#if defined(__linux__) && defined(TCP_CORK)
2624#define CWIST_TCP_CORK_DEFAULT_BURST (256 * 1024)
2625
2631bool cwist_tcp_cork_enabled(void) {
2632 static int enabled = -1; /* benign idempotent race on first use */
2633 if (enabled < 0) {
2634 const char *env = getenv("CWIST_USE_TCP_CORK");
2635 if (env && *env) {
2636 char *end = NULL;
2637 long v = strtol(env, &end, 10);
2638 enabled = (end != env && *end == '\0' && v > 0) ? 1 : 0;
2639 } else {
2640 enabled = 0;
2641 }
2642 }
2643 return enabled == 1;
2644}
2645
2650static size_t cwist_tcp_cork_burst(void) {
2651 static size_t burst = 0;
2652 if (burst == 0) {
2653 const char *env = getenv("CWIST_TCP_CORK_BURST");
2654 long v = 0;
2655 if (env && *env) {
2656 char *end = NULL;
2657 long tmp = strtol(env, &end, 10);
2658 if (end != env && *end == '\0') v = tmp;
2659 }
2660 burst = (v >= 16384) ? (size_t)v : (size_t)CWIST_TCP_CORK_DEFAULT_BURST;
2661 }
2662 return burst;
2663}
2664
2669static int cwist_tcp_cork_set(int fd, bool on) {
2670 int v = on ? 1 : 0;
2671 return setsockopt(fd, IPPROTO_TCP, TCP_CORK, &v, sizeof(v));
2672}
2673
2675static void cwist_tcp_cork_flush(int fd) {
2676 cwist_tcp_cork_set(fd, false);
2677 cwist_tcp_cork_set(fd, true);
2678}
2679#else
2685 return false;
2686}
2687#endif
2688
2694static int cwist_http_sendmsg_all(int fd, struct iovec *iov, int iovcnt, int flags) {
2695#ifdef CWIST_WASI_SOCKETS
2696 /* wasi:sockets (0.2) has no sendmsg(2): coalesce the iov into a single
2697 * buffer and drive plain send(). Response iovs are small (headers + one
2698 * body segment), so the copy is cheap. */
2699 size_t total = 0;
2700 for (int i = 0; i < iovcnt; i++) total += iov[i].iov_len;
2701 char *buf = cwist_alloc(total ? total : 1);
2702 if (!buf) return -1;
2703 size_t off = 0;
2704 for (int i = 0; i < iovcnt; i++) {
2705 memcpy(buf + off, iov[i].iov_base, iov[i].iov_len);
2706 off += iov[i].iov_len;
2707 }
2708 size_t sent = 0;
2709 int rc = -1;
2710 while (sent < total) {
2711 ssize_t n = send(fd, buf + sent, total - sent, flags);
2712 if (n < 0) {
2713 if (errno == EINTR) continue;
2714 if (errno == EAGAIN || errno == EWOULDBLOCK) {
2715 struct pollfd pfd = {.fd = fd, .events = POLLOUT};
2716 int pr = poll(&pfd, 1, CWIST_HTTP_TIMEOUT_MS);
2717 if (pr <= 0) goto wasi_done;
2718 if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) goto wasi_done;
2719 continue;
2720 }
2721 goto wasi_done;
2722 }
2723 if (n == 0) goto wasi_done;
2724 sent += (size_t)n;
2725 }
2726 rc = 0;
2727wasi_done:
2728 cwist_free(buf);
2729 return rc;
2730#elif !defined(CWIST_PLATFORM_NO_SOCKETS)
2731 /* Speculative zero-latency fast-path attempt:
2732 * Completes immediately for non-saturated sockets without entering poll() loops. */
2733 size_t fast_sent = 0;
2736 if (fast_st == CWIST_WRITE_DONE) {
2737 return 0;
2738 } else if (fast_st == CWIST_WRITE_ERR) {
2739 return -1;
2740 }
2741
2742 struct iovec *cur = iov;
2743 int curcnt = iovcnt;
2744
2745 /* Advance by bytes already sent in fast-path attempt */
2746 while (curcnt > 0 && fast_sent >= cur->iov_len) {
2747 fast_sent -= cur->iov_len;
2748 cur++;
2749 curcnt--;
2750 }
2751 if (curcnt > 0 && fast_sent > 0) {
2752 cur->iov_base = (char *)cur->iov_base + fast_sent;
2753 cur->iov_len -= fast_sent;
2754 }
2755
2756 while (curcnt > 0) {
2757 struct msghdr msg = {0};
2758 msg.msg_iov = cur;
2759 msg.msg_iovlen = (size_t)curcnt;
2760 ssize_t n = sendmsg(fd, &msg, flags);
2761 if (n < 0) {
2762 if (errno == EINTR) continue;
2763 if (errno == EAGAIN || errno == EWOULDBLOCK) {
2764 struct pollfd pfd = {.fd = fd, .events = POLLOUT};
2765 int ret = poll(&pfd, 1, CWIST_HTTP_TIMEOUT_MS);
2766 if (ret <= 0) return -1;
2767 if (pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) return -1;
2768 continue;
2769 }
2770 return -1;
2771 }
2772 if (n == 0) return -1;
2773
2774 while (curcnt > 0 && (size_t)n >= cur->iov_len) {
2775 n -= (ssize_t)cur->iov_len;
2776 cur++;
2777 curcnt--;
2778 }
2779 if (curcnt > 0) {
2780 cur->iov_base = (char *)cur->iov_base + n;
2781 cur->iov_len -= (size_t)n;
2782 }
2783 }
2784 return 0;
2785#else
2786 /* WASI preview1 has no sockets; sending is impossible. Callers treat
2787 * this as a write error and tear the connection down. */
2788 (void)fd;
2789 (void)iov;
2790 (void)iovcnt;
2791 (void)flags;
2792 return -1;
2793#endif
2794}
2795
2802static bool cwist_http_stream_file_fast(int client_fd, cwist_http_response *res) {
2803 if (!res || !res->use_file_stream || res->file_stream_fd < 0) return false;
2804 size_t remaining = res->file_stream_len;
2805 off_t offset = res->file_stream_offset;
2806#if defined(__linux__)
2807 const bool cork = cwist_tcp_cork_enabled();
2808 const size_t burst = cork ? cwist_tcp_cork_burst() : 0;
2809 size_t since_flush = 0;
2810#endif
2811 while (remaining > 0) {
2812#if defined(__linux__)
2813 /* Cap each sendfile at the remaining burst budget so flushes land on
2814 * moderate boundaries instead of one giant clump. */
2815 size_t quota = remaining;
2817 ssize_t sent = sendfile(client_fd, res->file_stream_fd, &offset, quota);
2818 if (sent < 0) {
2819 if (errno == EINTR) continue;
2820 if (errno == EAGAIN || errno == EWOULDBLOCK) {
2821 struct pollfd pfd = {.fd = client_fd, .events = POLLOUT};
2822 int ret = poll(&pfd, 1, CWIST_HTTP_TIMEOUT_MS);
2823 if (ret <= 0 || (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) return false;
2824 continue;
2825 }
2826 return false;
2827 }
2828 if (sent == 0) break;
2829 remaining -= (size_t)sent;
2830 if (cork) {
2832 if (since_flush >= burst && remaining > 0) {
2833 cwist_tcp_cork_flush(client_fd);
2834 since_flush = 0;
2835 }
2836 }
2837#else
2838 /* Portable read+write fallback for macOS, FreeBSD, and other platforms.
2839 lseek is used to handle the offset since pread avoids modifying the
2840 file descriptor's position across calls. */
2841 char buf[65536];
2842 size_t to_read = remaining < sizeof(buf) ? remaining : sizeof(buf);
2843 ssize_t n = pread(res->file_stream_fd, buf, to_read, offset);
2844 if (n < 0) {
2845 if (errno == EINTR) continue;
2846 return false;
2847 }
2848 if (n == 0) break;
2849 ssize_t nw = 0;
2850 while (nw < n) {
2851 ssize_t w = write(client_fd, buf + nw, (size_t)(n - nw));
2852 if (w < 0) {
2853 if (errno == EINTR) continue;
2854 if (errno == EAGAIN || errno == EWOULDBLOCK) {
2855 struct pollfd pfd = {.fd = client_fd, .events = POLLOUT};
2856 int ret = poll(&pfd, 1, CWIST_HTTP_TIMEOUT_MS);
2857 if (ret <= 0 || (pfd.revents & (POLLERR | POLLHUP | POLLNVAL))) return false;
2858 continue;
2859 }
2860 return false;
2861 }
2862 nw += w;
2863 }
2864 offset += nw;
2865 remaining -= (size_t)nw;
2866#endif
2867 }
2868 res->file_stream_offset = offset;
2869 return remaining == 0;
2870}
2871
2880
2881 if (client_fd < 0 || !res) {
2882 err.error.err_i16 = -1;
2883 return err;
2884 }
2885
2886 // 1. Prepare Headers (On Stack)
2888 size_t header_len = serialize_headers(res, header_buf, sizeof(header_buf));
2889
2890 // 2. Prepare Body
2891 const void *body_ptr = NULL;
2892 size_t body_len = 0;
2893
2894 if (res->is_ptr_body) {
2895 body_ptr = res->ptr_body;
2896 body_len = res->ptr_body_len;
2897 } else if (res->body && res->body->data) {
2898 body_ptr = res->body->data;
2899 body_len = res->body->size;
2900 }
2901
2902 // 3. sendmsg (Scatter/Gather + Flags) - Zero Copy Send
2903 struct iovec iov[2];
2904 int iov_cnt = 1;
2905
2906 iov[0].iov_base = header_buf;
2907 iov[0].iov_len = header_len;
2908
2909 if (!res->use_file_stream && body_len > 0 && body_ptr) {
2910 iov[1].iov_base = (void *)body_ptr;
2911 iov[1].iov_len = body_len;
2912 iov_cnt = 2;
2913 }
2914
2915 int flags = 0;
2916#if defined(MSG_NOSIGNAL)
2917 flags |= MSG_NOSIGNAL;
2918#endif
2919#if defined(MSG_DONTWAIT)
2920 flags |= MSG_DONTWAIT;
2921#endif
2922
2923#if defined(__linux__) && defined(TCP_CORK)
2924 const bool cork = cwist_tcp_cork_enabled();
2925 if (cork) {
2926 cwist_tcp_cork_set(client_fd, true);
2927#if defined(MSG_MORE)
2928 /* File body follows: let the headers merge with the first burst. */
2929 if (res->use_file_stream) flags |= MSG_MORE;
2930#endif
2931 }
2932#endif
2933
2934 if (cwist_http_sendmsg_all(client_fd, iov, iov_cnt, flags) != 0) {
2935 err.error.err_i16 = -1;
2936 } else {
2937 err.error.err_i16 = 0;
2938 if (res->use_file_stream) {
2939 if (!cwist_http_stream_file_fast(client_fd, res)) {
2940 err.error.err_i16 = -1;
2941 }
2942 }
2943 }
2944
2945#if defined(__linux__) && defined(TCP_CORK)
2946 /* Uncorking flushes the final partial burst; runs on every outcome so a
2947 * failed send can never leave the socket corked. */
2948 if (cork) cwist_tcp_cork_set(client_fd, false);
2949#endif
2950
2951 cwist_http_response_release_ptr_body(res);
2952 cwist_http_response_release_file_stream(res);
2953 return err;
2954}
2955
2963 if (client_fd < 0 || !res) {
2964 err.error.err_i16 = -1;
2965 return err;
2966 }
2967
2969 size_t header_len = serialize_headers(res, header_buf, sizeof(header_buf));
2970
2971 struct iovec iov = {.iov_base = header_buf, .iov_len = header_len};
2972 int flags = 0;
2973#if defined(MSG_NOSIGNAL)
2974 flags |= MSG_NOSIGNAL;
2975#endif
2976#if defined(MSG_DONTWAIT)
2977 flags |= MSG_DONTWAIT;
2978#endif
2979
2980 err.error.err_i16 = (cwist_http_sendmsg_all(client_fd, &iov, 1, flags) == 0) ? 0 : -1;
2981
2982 cwist_http_response_release_ptr_body(res);
2983 cwist_http_response_release_file_stream(res);
2984 return err;
2985}
2986
2987/* --- Parked (POLLOUT-resumable) deferred-response writer --------------------
2988 * A deferred completion on the reactor path must never block the reactor
2989 * thread in the poll(POLLOUT) fallback of cwist_http_sendmsg_all. When the
2990 * speculative sendmsg cannot drain the whole response, the unsent remainder
2991 * is deep-copied into an owned buffer (req/res/the arena are freed right
2992 * after the handoff) and parked on a one-shot write-readiness slot; each
2993 * POLLOUT event resumes the send, and the final event re-arms keep-alive or
2994 * closes exactly like the synchronous completion. The slot payload carries
2995 * everything the callback needs, so no extra heap struct is required. */
2996
2997typedef struct {
2998 int file_fd; /* sendfile source (Linux file mode). */
2999 off_t file_off;
3000 size_t file_left;
3001 bool close_file_fd; /* file_fd is our dup() and must be closed. */
3002 const char *body; /* Referenced pointer body (BODYREF mode). */
3003 size_t body_off;
3004 size_t body_left;
3005 cwist_http_body_cleanup_fn body_cleanup; /* Runs once at park finish. */
3006 void *body_cleanup_ctx;
3007} http_parked_write_state_t;
3008
3009enum {
3010 HTTP_PARK_INLINE = 0, /* w.buf holds the full unsent remainder. */
3011 HTTP_PARK_FILE, /* w.buf holds unsent headers; state->file streams. */
3012 HTTP_PARK_BODYREF, /* w.buf holds unsent headers; state->body references. */
3013};
3014
3015typedef struct {
3016 cwist_reactor_t *reactor;
3018 char *buf; /* Owned copy of the unsent header bytes (all modes). */
3019 size_t off;
3020 size_t len;
3021 http_parked_write_state_t *state; /* Heap state for FILE/BODYREF modes. */
3022 uint32_t deadline_sec; /* Absolute write deadline (monotonic sec). */
3023 uint8_t mode;
3024 bool keep_alive;
3025} http_parked_write_t;
3026
3027_Static_assert(sizeof(http_parked_write_t) <= CWIST_REACTOR_PAYLOAD_SIZE,
3028 "parked write state must fit a reactor slot payload");
3029
3033static uint32_t http_parked_write_deadline(void) {
3034 return cwist_fast_monotonic_sec() + cwist_http_keep_alive_timeout_sec();
3035}
3036
3037static void http_parked_write_cb(int fd, void *ctx);
3038
3039/* Re-park on a fresh one-shot POLLOUT slot; the slot copies the payload and
3040 * ownership of buf/state moves with it. False means the caller must finish. */
3046static bool http_parked_rearm(int fd, http_parked_write_t *w) {
3047 return cwist_fast_monotonic_sec() <= w->deadline_sec &&
3048 cwist_reactor_add_out(w->reactor, fd, http_parked_write_cb, w, sizeof(*w));
3049}
3050
3057static void http_parked_write_finish(int fd, http_parked_write_t *w, bool drained) {
3058 cwist_reactor_t *reactor = w->reactor;
3059 cwist_http_async_conn_t *conn = w->conn;
3060 bool keep = w->keep_alive && drained && atomic_load(&g_cwist_running);
3061 cwist_free(w->buf);
3062 if (w->state) {
3063#if defined(__linux__)
3064 if (w->state->close_file_fd && w->state->file_fd >= 0) close(w->state->file_fd);
3065#endif
3066 if (w->state->body_cleanup && w->state->body) {
3067 size_t total = w->state->body_off + w->state->body_left;
3068 w->state->body_cleanup((void *)w->state->body, total, w->state->body_cleanup_ctx);
3069 }
3070 cwist_free(w->state);
3071 }
3072 if (keep) {
3073 cwist_http_async_rearm(fd, reactor, conn);
3074 } else {
3075 http_async_close(fd, conn);
3076 }
3077}
3078
3087static void http_parked_write_cb(int fd, void *ctx) {
3088 http_parked_write_t *w = (http_parked_write_t *)ctx;
3089 int flags = 0;
3090#if defined(MSG_NOSIGNAL)
3091 flags |= MSG_NOSIGNAL;
3092#endif
3093#if defined(MSG_DONTWAIT)
3094 flags |= MSG_DONTWAIT;
3095#endif
3096
3097 /* Unsent header bytes drain first in every mode. */
3098 while (w->off < w->len) {
3099 ssize_t n = send(fd, w->buf + w->off, w->len - w->off, flags);
3100 if (n > 0) {
3101 w->off += (size_t)n;
3102 /* Progress resets the budget so a slow-but-alive client can
3103 * drain a large body; a silent peer hits the absolute deadline. */
3104 w->deadline_sec = http_parked_write_deadline();
3105 continue;
3106 }
3107 if (n < 0 && errno == EINTR) continue;
3108 if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK) && http_parked_rearm(fd, w)) {
3109 /* Payload copied into the new slot; buf/state ownership moves with it. */
3110 return;
3111 }
3112 http_parked_write_finish(fd, w, false);
3113 return;
3114 }
3115 cwist_free(w->buf);
3116 w->buf = NULL;
3117
3118 if (w->mode == HTTP_PARK_FILE) {
3119#if defined(__linux__)
3120 http_parked_write_state_t *f = w->state;
3121 while (f && f->file_left > 0) {
3122 ssize_t n = sendfile(fd, f->file_fd, &f->file_off, f->file_left);
3123 if (n > 0) {
3124 f->file_left -= (size_t)n;
3125 w->deadline_sec = http_parked_write_deadline();
3126 continue;
3127 }
3128 if (n < 0 && errno == EINTR) continue;
3129 if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK) && http_parked_rearm(fd, w)) {
3130 return;
3131 }
3132 break;
3133 }
3134 http_parked_write_finish(fd, w, f && f->file_left == 0);
3135#else
3136 /* Non-Linux never parks FILE mode; defensive close. */
3137 http_parked_write_finish(fd, w, false);
3138#endif
3139 return;
3140 }
3141
3142 if (w->mode == HTTP_PARK_BODYREF) {
3143 http_parked_write_state_t *s = w->state;
3144 while (s && s->body_left > 0) {
3145 ssize_t n = send(fd, s->body + s->body_off, s->body_left, flags);
3146 if (n > 0) {
3147 s->body_off += (size_t)n;
3148 s->body_left -= (size_t)n;
3149 w->deadline_sec = http_parked_write_deadline();
3150 continue;
3151 }
3152 if (n < 0 && errno == EINTR) continue;
3153 if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK) && http_parked_rearm(fd, w)) {
3154 return;
3155 }
3156 break;
3157 }
3158 http_parked_write_finish(fd, w, s && s->body_left == 0);
3159 return;
3160 }
3161
3162 http_parked_write_finish(fd, w, true);
3163}
3164
3180static bool http_park_streamed_body(int client_fd, cwist_reactor_t *reactor,
3182 const char *header_buf, size_t header_len, size_t header_sent,
3183 const void *body_ptr, size_t body_len, bool keep_alive,
3184 bool file_mode) {
3185 http_parked_write_state_t *st = cwist_alloc(sizeof(*st));
3186 if (!st) return false;
3187 memset(st, 0, sizeof(*st));
3188 if (file_mode) {
3189#if defined(__linux__)
3190 st->file_fd = res->file_stream_fd;
3191 st->file_off = res->file_stream_offset;
3192 st->file_left = res->file_stream_len;
3193 if (res->file_stream_auto_close) {
3194 int dupfd = dup(res->file_stream_fd);
3195 if (dupfd < 0) {
3196 cwist_free(st);
3197 return false;
3198 }
3199 st->file_fd = dupfd;
3200 st->close_file_fd = true;
3201 }
3202#else
3203 cwist_free(st);
3204 return false;
3205#endif
3206 } else {
3207 size_t body_sent = header_sent > header_len ? header_sent - header_len : 0;
3208 st->body = (const char *)body_ptr;
3209 st->body_off = body_sent;
3210 st->body_left = body_len - body_sent;
3211 st->body_cleanup = res->ptr_body_cleanup;
3212 st->body_cleanup_ctx = res->ptr_body_cleanup_ctx;
3213 /* Ownership of the pointer body (and its cleanup hook) moves to the
3214 * park state; the caller's release becomes a no-op. */
3215 res->is_ptr_body = false;
3216 res->ptr_body = NULL;
3217 res->ptr_body_len = 0;
3218 res->ptr_body_cleanup = NULL;
3220 }
3221
3222 http_parked_write_t w = {
3223 .reactor = reactor,
3224 .conn = conn,
3225 .buf = NULL,
3226 .off = 0,
3227 .len = 0,
3228 .state = st,
3229 .deadline_sec = http_parked_write_deadline(),
3231 .keep_alive = keep_alive,
3232 };
3233 size_t hd_left = header_sent < header_len ? header_len - header_sent : 0;
3234 if (hd_left > 0) {
3235 w.buf = cwist_alloc(hd_left);
3236 if (!w.buf) goto fail;
3238 w.len = hd_left;
3239 }
3240 if (cwist_reactor_add_out(reactor, client_fd, http_parked_write_cb, &w, sizeof(w))) return true;
3241
3242fail:
3243 cwist_free(w.buf);
3244#if defined(__linux__)
3245 if (st->close_file_fd && st->file_fd >= 0) close(st->file_fd);
3246#endif
3247 if (!file_mode && st->body_cleanup && st->body) {
3248 /* Park failed after ownership moved: run the cleanup here so the
3249 * body is still released exactly once. */
3250 st->body_cleanup((void *)st->body, st->body_off + st->body_left, st->body_cleanup_ctx);
3251 res->is_ptr_body = false;
3252 res->ptr_body = NULL;
3253 res->ptr_body_len = 0;
3254 res->ptr_body_cleanup = NULL;
3256 }
3257 cwist_free(st);
3258 return false;
3259}
3260
3261typedef enum {
3262 CWIST_FILE_BEGIN_FALLBACK = 0, /* nothing sent; caller may use the legacy path */
3263 CWIST_FILE_BEGIN_HANDLED, /* finished, parked, or closed; conn handled */
3265
3279static cwist_file_begin_result_t cwist_http_file_begin(int client_fd, cwist_http_response *res,
3280 cwist_reactor_t *reactor,
3282 bool keep_alive, bool defer,
3284#if !defined(__linux__)
3285 /* The non-blocking burst below relies on Linux sendfile(2) and dup(2).
3286 * Elsewhere (macOS/BSD sendfile has a different signature; Emscripten and
3287 * WASI have neither) nothing has been sent yet, so let the caller use the
3288 * bounded-blocking path, like http_park_streamed_body() does. */
3289 (void)client_fd;
3290 (void)res;
3291 (void)reactor;
3292 (void)conn;
3293 (void)keep_alive;
3294 (void)defer;
3297#else
3299 size_t header_len = serialize_headers(res, header_buf, sizeof(header_buf));
3300
3301 int flags = 0;
3302#if defined(MSG_NOSIGNAL)
3303 flags |= MSG_NOSIGNAL;
3304#endif
3305#if defined(MSG_DONTWAIT)
3306 flags |= MSG_DONTWAIT;
3307#endif
3308
3309 struct iovec iov = {.iov_base = header_buf, .iov_len = header_len};
3310 size_t sent = 0;
3312
3313 if (hst == CWIST_WRITE_PENDING) {
3314 if (http_park_streamed_body(client_fd, reactor, conn, res, header_buf, header_len, sent,
3315 NULL, 0, keep_alive, true)) {
3316 cwist_http_response_release_file_stream(res);
3319 }
3321 }
3322
3323 /* Headers are on the wire (or failed fatally): from here the legacy path
3324 * would resend them, so this function owns the outcome. */
3325 http_parked_write_state_t *st = cwist_alloc(sizeof(*st));
3326 if (!st) {
3327 http_async_close(client_fd, conn);
3330 }
3331 memset(st, 0, sizeof(*st));
3332 st->file_fd = res->file_stream_fd;
3333 st->file_off = res->file_stream_offset;
3334 st->file_left = res->file_stream_len;
3335 if (res->file_stream_auto_close) {
3336 int dupfd = dup(res->file_stream_fd);
3337 if (dupfd < 0) {
3338 cwist_free(st);
3339 http_async_close(client_fd, conn);
3342 }
3343 st->file_fd = dupfd;
3344 st->close_file_fd = true;
3345 }
3346 cwist_http_response_release_file_stream(res);
3347
3348 if (hst == CWIST_WRITE_ERR) {
3349 if (st->close_file_fd) close(st->file_fd);
3350 cwist_free(st);
3351 http_async_close(client_fd, conn);
3354 }
3355
3356 while (st->file_left > 0) {
3357 ssize_t n = sendfile(client_fd, st->file_fd, &st->file_off, st->file_left);
3358 if (n > 0) {
3359 st->file_left -= (size_t)n;
3360 continue;
3361 }
3362 if (n < 0 && errno == EINTR) continue;
3363 if (n < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
3364 http_parked_write_t w = {
3365 .reactor = reactor,
3366 .conn = conn,
3367 .buf = NULL,
3368 .off = 0,
3369 .len = 0,
3370 .state = st,
3371 .deadline_sec = http_parked_write_deadline(),
3372 .mode = HTTP_PARK_FILE,
3373 .keep_alive = keep_alive,
3374 };
3375 if (cwist_reactor_add_out(reactor, client_fd, http_parked_write_cb, &w, sizeof(w))) {
3378 }
3379 break;
3380 }
3381 break;
3382 }
3383
3384 bool drained = st->file_left == 0;
3385 if (st->close_file_fd) close(st->file_fd);
3386 cwist_free(st);
3387 if (drained && keep_alive && atomic_load(&g_cwist_running)) {
3388 if (defer && status_out) {
3390 } else {
3391 cwist_http_async_rearm(client_fd, reactor, conn);
3392 }
3393 } else {
3394 if (defer && status_out) {
3396 } else {
3397 http_async_close(client_fd, conn);
3398 }
3399 }
3401#endif
3402}
3403
3415 bool keep_alive, bool head_only) {
3416 if (client_fd < 0 || !res || !reactor || !conn) {
3417 http_async_close(client_fd, conn);
3418 return;
3419 }
3420
3421 /* File streams: headers via the speculative path, body via non-blocking
3422 * sendfile; EAGAIN parks {fd, offset} state instead of blocking a
3423 * reactor thread in poll(). Falls back to the bounded-blocking path
3424 * only when nothing has been sent yet. */
3425 if (!head_only && res->use_file_stream) {
3426#if defined(__linux__)
3427 if (cwist_http_file_begin(client_fd, res, reactor, conn, keep_alive, false, NULL) ==
3429 return;
3430 }
3431#endif
3432 cwist_error_t err = cwist_http_send_response(client_fd, res);
3433 if (keep_alive && err.error.err_i16 == 0) {
3434 cwist_http_async_rearm(client_fd, reactor, conn);
3435 } else {
3436 http_async_close(client_fd, conn);
3437 }
3438 return;
3439 }
3440
3442 size_t header_len = serialize_headers(res, header_buf, sizeof(header_buf));
3443
3444 const void *body_ptr = NULL;
3445 size_t body_len = 0;
3446 if (!head_only) {
3447 if (res->is_ptr_body) {
3448 body_ptr = res->ptr_body;
3449 body_len = res->ptr_body_len;
3450 } else if (res->body && res->body->data) {
3451 body_ptr = res->body->data;
3452 body_len = res->body->size;
3453 }
3454 }
3455
3456 struct iovec iov[2];
3457 int iov_cnt = 1;
3458 iov[0].iov_base = header_buf;
3459 iov[0].iov_len = header_len;
3460 if (body_len > 0 && body_ptr) {
3461 iov[1].iov_base = (void *)body_ptr;
3462 iov[1].iov_len = body_len;
3463 iov_cnt = 2;
3464 }
3465
3466 int flags = 0;
3467#if defined(MSG_NOSIGNAL)
3468 flags |= MSG_NOSIGNAL;
3469#endif
3470#if defined(MSG_DONTWAIT)
3471 flags |= MSG_DONTWAIT;
3472#endif
3473
3474 size_t sent = 0;
3476
3477 if (st == CWIST_WRITE_PENDING) {
3478 /* A pointer body parks by reference: ownership (and the cleanup
3479 * hook) moves to the park state instead of deep-copying the body. */
3480 if (res->is_ptr_body && body_ptr && body_len > 0 &&
3481 http_park_streamed_body(client_fd, reactor, conn, res, header_buf, header_len, sent,
3482 body_ptr, body_len, keep_alive, false)) {
3483 cwist_http_response_release_ptr_body(res); /* no-op: ownership moved */
3484 cwist_http_response_release_file_stream(res);
3485 return;
3486 }
3487 /* Deep-copy the unsent remainder before releasing the body: the
3488 * completion frees req/res (and the arena) right after we return. */
3489 size_t total = header_len + body_len;
3490 size_t left = total - sent;
3491 http_parked_write_t w = {
3492 .reactor = reactor,
3493 .conn = conn,
3494 .buf = cwist_alloc(left),
3495 .off = 0,
3496 .len = left,
3497 .deadline_sec = http_parked_write_deadline(),
3498 .keep_alive = keep_alive,
3499 };
3500 if (w.buf) {
3501 size_t hd_off = sent < header_len ? sent : header_len;
3502 size_t hd_left = header_len - hd_off;
3503 memcpy(w.buf, header_buf + hd_off, hd_left);
3504 if (left > hd_left) {
3505 size_t body_off = sent > header_len ? sent - header_len : 0;
3506 memcpy(w.buf + hd_left, (const char *)body_ptr + body_off, left - hd_left);
3507 }
3508 }
3509 cwist_http_response_release_ptr_body(res);
3510 cwist_http_response_release_file_stream(res);
3511 if (w.buf &&
3512 cwist_reactor_add_out(reactor, client_fd, http_parked_write_cb, &w, sizeof(w))) {
3513 return;
3514 }
3515 cwist_free(w.buf);
3516 http_async_close(client_fd, conn);
3517 return;
3518 }
3519
3520 cwist_http_response_release_ptr_body(res);
3521 cwist_http_response_release_file_stream(res);
3522 if (st == CWIST_WRITE_DONE && keep_alive && atomic_load(&g_cwist_running)) {
3523 cwist_http_async_rearm(client_fd, reactor, conn);
3524 } else {
3525 http_async_close(client_fd, conn);
3526 }
3527}
3528
3536 bool keep_alive, bool head_only) {
3537 if (client_fd < 0 || !res || !conn) return CWIST_ASYNC_SEND_CLOSE;
3538
3539 if (!head_only && res->use_file_stream) {
3540#if defined(__linux__)
3542 if (cwist_http_file_begin(client_fd, res, conn->reactor, conn, keep_alive, true,
3544 return fstatus;
3545 }
3546#endif
3547 cwist_error_t err = cwist_http_send_response(client_fd, res);
3548 return (keep_alive && err.error.err_i16 == 0) ? CWIST_ASYNC_SEND_KEEPALIVE
3550 }
3551
3553 size_t header_len = serialize_headers(res, header_buf, sizeof(header_buf));
3554
3555 const void *body_ptr = NULL;
3556 size_t body_len = 0;
3557 if (!head_only) {
3558 if (res->is_ptr_body) {
3559 body_ptr = res->ptr_body;
3560 body_len = res->ptr_body_len;
3561 } else if (res->body && res->body->data) {
3562 body_ptr = res->body->data;
3563 body_len = res->body->size;
3564 }
3565 }
3566
3567 struct iovec iov[2];
3568 int iov_cnt = 1;
3569 iov[0].iov_base = header_buf;
3570 iov[0].iov_len = header_len;
3571 if (body_len > 0 && body_ptr) {
3572 iov[1].iov_base = (void *)body_ptr;
3573 iov[1].iov_len = body_len;
3574 iov_cnt = 2;
3575 }
3576
3577 int flags = 0;
3578#if defined(MSG_NOSIGNAL)
3579 flags |= MSG_NOSIGNAL;
3580#endif
3581#if defined(MSG_DONTWAIT)
3582 flags |= MSG_DONTWAIT;
3583#endif
3584
3585 size_t sent = 0;
3587
3588 if (st == CWIST_WRITE_PENDING) {
3589 /* Pointer bodies park by reference (ownership moves to the park
3590 * state); everything else keeps the deep-copy remainder path. */
3591 if (res->is_ptr_body && body_ptr && body_len > 0 &&
3592 http_park_streamed_body(client_fd, conn->reactor, conn, res, header_buf, header_len,
3593 sent, body_ptr, body_len, keep_alive, false)) {
3594 cwist_http_response_release_ptr_body(res); /* no-op: ownership moved */
3595 cwist_http_response_release_file_stream(res);
3597 }
3598 size_t total = header_len + body_len;
3599 size_t left = total - sent;
3600 http_parked_write_t w = {
3601 .reactor = conn->reactor,
3602 .conn = conn,
3603 .buf = cwist_alloc(left),
3604 .off = 0,
3605 .len = left,
3606 .deadline_sec = http_parked_write_deadline(),
3607 .keep_alive = keep_alive,
3608 };
3609 if (w.buf) {
3610 size_t hd_off = sent < header_len ? sent : header_len;
3611 size_t hd_left = header_len - hd_off;
3612 memcpy(w.buf, header_buf + hd_off, hd_left);
3613 if (left > hd_left) {
3614 size_t body_off = sent > header_len ? sent - header_len : 0;
3615 memcpy(w.buf + hd_left, (const char *)body_ptr + body_off, left - hd_left);
3616 }
3617 if (cwist_reactor_add_out(conn->reactor, client_fd, http_parked_write_cb, &w,
3618 sizeof(w))) {
3619 cwist_http_response_release_ptr_body(res);
3620 cwist_http_response_release_file_stream(res);
3622 }
3623 cwist_free(w.buf);
3624 }
3625 cwist_http_response_release_ptr_body(res);
3626 cwist_http_response_release_file_stream(res);
3628 }
3629
3630 cwist_http_response_release_ptr_body(res);
3631 cwist_http_response_release_file_stream(res);
3632 if (st == CWIST_WRITE_DONE) {
3634 }
3636}
3637
3638/* --- Response write coalescing (cleartext C1M async batch path) ------------
3639 * Responses served within one reactor batch turn append to conn->obuf and
3640 * flush with a single speculative write when the turn exits. A partial
3641 * flush deep-copies the remainder into the parked-write mechanism (the
3642 * batch loop must stop there; the parked drain re-arms the connection and
3643 * its continuation resumes the pipeline). Cap pressure, oversized bodies,
3644 * file streams, deferred completions, and 100 Continue drain the stash
3645 * through the bounded poll wait of cwist_http_sendmsg_all instead, keeping
3646 * byte order without unbounded buffering. */
3647
3653int cwist_http_coalesce_append(cwist_http_async_conn_t *conn, const void *data, size_t len) {
3654 if (len == 0) return 0;
3655 if (conn->olen + len > CWIST_HTTP_COALESCE_MAX) return -1;
3656 if (conn->olen + len > conn->ocap) {
3657 size_t ncap = conn->ocap ? conn->ocap : 16384;
3658 while (ncap < conn->olen + len) ncap *= 2;
3660 char *nb = cwist_alloc(ncap);
3661 if (!nb) return -1;
3662 if (conn->olen > 0) memcpy(nb, conn->obuf, conn->olen);
3663 cwist_free(conn->obuf);
3664 conn->obuf = nb;
3665 conn->ocap = ncap;
3666 /* Hysteresis anchor for the idle shrink: capacity was just needed. */
3668 }
3669 memcpy(conn->obuf + conn->olen, data, len);
3670 conn->olen += len;
3671 return 0;
3672}
3673
3682cwist_http_coalesce_flush(int client_fd, cwist_http_async_conn_t *conn, bool keep_alive) {
3683 if (!conn || conn->olen == 0) return CWIST_COALESCE_FLUSH_DONE;
3684
3685 struct iovec iov = {.iov_base = conn->obuf, .iov_len = conn->olen};
3686 int flags = 0;
3687#if defined(MSG_NOSIGNAL)
3688 flags |= MSG_NOSIGNAL;
3689#endif
3690#if defined(MSG_DONTWAIT)
3691 flags |= MSG_DONTWAIT;
3692#endif
3693
3695 /* Shutdown stops the reactor loop: finish this already-built batch
3696 * here instead of parking bytes on a callback that cannot run. */
3697 int rc = cwist_http_sendmsg_all(client_fd, &iov, 1, flags);
3698 conn->olen = 0;
3700 }
3701
3702 size_t sent = 0;
3704 if (st == CWIST_WRITE_DONE) {
3705 conn->olen = 0;
3707 }
3708 if (st == CWIST_WRITE_PENDING) {
3709 size_t left = conn->olen - sent;
3710 http_parked_write_t w = {
3711 .reactor = conn->reactor,
3712 .conn = conn,
3713 .buf = cwist_alloc(left),
3714 .off = 0,
3715 .len = left,
3716 .deadline_sec = http_parked_write_deadline(),
3717 .keep_alive = keep_alive,
3718 };
3719 if (w.buf) {
3720 memcpy(w.buf, conn->obuf + sent, left);
3721 if (cwist_reactor_add_out(conn->reactor, client_fd, http_parked_write_cb, &w,
3722 sizeof(w))) {
3723 conn->olen = 0;
3725 }
3726 cwist_free(w.buf);
3727 }
3728 }
3729 conn->olen = 0;
3731}
3732
3738 if (!conn || conn->olen == 0) return 0;
3739 struct iovec iov = {.iov_base = conn->obuf, .iov_len = conn->olen};
3740 int flags = 0;
3741#if defined(MSG_NOSIGNAL)
3742 flags |= MSG_NOSIGNAL;
3743#endif
3744#if defined(MSG_DONTWAIT)
3745 flags |= MSG_DONTWAIT;
3746#endif
3747 int rc = cwist_http_sendmsg_all(client_fd, &iov, 1, flags);
3748 conn->olen = 0;
3749 return rc;
3750}
3751
3757 const char *reason = cwist_http_status_reason(status);
3758 if (!reason) reason = "Error";
3759
3760 char buf[512];
3761 int n = snprintf(
3762 buf, sizeof(buf),
3763 "HTTP/1.1 %d %s\r\nContent-Type: text/plain\r\nContent-Length: %zu\r\nConnection: close\r\n\r\n%s",
3764 status, reason, strlen(reason), reason);
3765 if (n <= 0) return -1;
3766 size_t len = (size_t)n < sizeof(buf) ? (size_t)n : sizeof(buf) - 1;
3767 return cwist_http_coalesce_append(conn, buf, len);
3768}
3769
3782 bool keep_alive, bool head_only) {
3783 if (client_fd < 0 || !res || !conn) return CWIST_ASYNC_SEND_CLOSE;
3784
3785 /* File streams: drain any buffered responses ahead of the file without
3786 * blocking the reactor (the obuf remainder parks as the file park's
3787 * prefix bytes), then stream the file through the non-blocking path;
3788 * EAGAIN parks {fd, offset} state. */
3789 if (!head_only && res->use_file_stream) {
3790#if defined(__linux__)
3791 int flags = 0;
3792#if defined(MSG_NOSIGNAL)
3793 flags |= MSG_NOSIGNAL;
3794#endif
3795#if defined(MSG_DONTWAIT)
3796 flags |= MSG_DONTWAIT;
3797#endif
3798 size_t sent = 0;
3800 if (conn->olen > 0) {
3801 struct iovec iov = {.iov_base = conn->obuf, .iov_len = conn->olen};
3802 fst = cwist_http_sendmsg_speculative(client_fd, &iov, 1, flags, &sent);
3803 }
3804 if (fst == CWIST_WRITE_PENDING) {
3805 /* The obuf remainder becomes the park's prefix; the file body
3806 * resumes behind it, preserving byte order. */
3807 if (http_park_streamed_body(client_fd, conn->reactor, conn, res, conn->obuf, conn->olen,
3808 sent, NULL, 0, keep_alive, true)) {
3809 conn->olen = 0;
3810 cwist_http_response_release_file_stream(res);
3812 }
3813 conn->olen = 0;
3815 }
3816 conn->olen = 0;
3819 if (cwist_http_file_begin(client_fd, res, conn->reactor, conn, keep_alive, true,
3821 return fstatus;
3822 }
3823#endif
3824 if (cwist_http_coalesce_flush_blocking(client_fd, conn) != 0) return CWIST_ASYNC_SEND_CLOSE;
3825 cwist_error_t err = cwist_http_send_response(client_fd, res);
3826 return (keep_alive && err.error.err_i16 == 0) ? CWIST_ASYNC_SEND_KEEPALIVE
3828 }
3829
3830 const void *body_ptr = NULL;
3831 size_t body_len = 0;
3832 if (!head_only) {
3833 if (res->is_ptr_body) {
3834 body_ptr = res->ptr_body;
3835 body_len = res->ptr_body_len;
3836 } else if (res->body && res->body->data) {
3837 body_ptr = res->body->data;
3838 body_len = res->body->size;
3839 }
3840 }
3841
3842 /* Cap pressure is judged against the worst-case header size so the
3843 * header can be serialized directly into the coalesce buffer below,
3844 * skipping the stack-to-obuf copy on the common path. */
3845 size_t upper = CWIST_HTTP_MAX_HEADER_SIZE + body_len;
3847 if (cwist_http_coalesce_flush_blocking(client_fd, conn) != 0) {
3848 cwist_http_response_release_ptr_body(res);
3849 cwist_http_response_release_file_stream(res);
3851 }
3852 }
3853
3854 /* Oversized single response: serialize into a stack buffer and keep
3855 * the legacy speculative send with a parked remainder rather than
3856 * bouncing through the capped stash. */
3859 size_t header_len = serialize_headers(res, header_buf, sizeof(header_buf));
3860 size_t total = header_len + body_len;
3861 struct iovec iov[2];
3862 int iov_cnt = 1;
3863 iov[0].iov_base = header_buf;
3864 iov[0].iov_len = header_len;
3865 if (body_len > 0 && body_ptr) {
3866 iov[1].iov_base = (void *)body_ptr;
3867 iov[1].iov_len = body_len;
3868 iov_cnt = 2;
3869 }
3870
3871 int flags = 0;
3872#if defined(MSG_NOSIGNAL)
3873 flags |= MSG_NOSIGNAL;
3874#endif
3875#if defined(MSG_DONTWAIT)
3876 flags |= MSG_DONTWAIT;
3877#endif
3878
3879 size_t sent = 0;
3881 cwist_http_sendmsg_speculative(client_fd, iov, iov_cnt, flags, &sent);
3882 if (st == CWIST_WRITE_PENDING) {
3883 /* Pointer bodies park by reference (ownership moves to the park
3884 * state); everything else keeps the deep-copy remainder path. */
3885 if (res->is_ptr_body && body_ptr && body_len > 0 &&
3886 http_park_streamed_body(client_fd, conn->reactor, conn, res, header_buf, header_len,
3887 sent, body_ptr, body_len, keep_alive, false)) {
3888 cwist_http_response_release_ptr_body(res); /* no-op: ownership moved */
3889 cwist_http_response_release_file_stream(res);
3891 }
3892 size_t left = total - sent;
3893 http_parked_write_t w = {
3894 .reactor = conn->reactor,
3895 .conn = conn,
3896 .buf = cwist_alloc(left),
3897 .off = 0,
3898 .len = left,
3899 .deadline_sec = http_parked_write_deadline(),
3900 .keep_alive = keep_alive,
3901 };
3902 if (w.buf) {
3903 size_t hd_off = sent < header_len ? sent : header_len;
3904 size_t hd_left = header_len - hd_off;
3905 memcpy(w.buf, header_buf + hd_off, hd_left);
3906 if (left > hd_left) {
3907 size_t body_off = sent > header_len ? sent - header_len : 0;
3908 memcpy(w.buf + hd_left, (const char *)body_ptr + body_off, left - hd_left);
3909 }
3910 if (cwist_reactor_add_out(conn->reactor, client_fd, http_parked_write_cb, &w,
3911 sizeof(w))) {
3912 cwist_http_response_release_ptr_body(res);
3913 cwist_http_response_release_file_stream(res);
3915 }
3916 cwist_free(w.buf);
3917 }
3918 cwist_http_response_release_ptr_body(res);
3919 cwist_http_response_release_file_stream(res);
3921 }
3922
3923 cwist_http_response_release_ptr_body(res);
3924 cwist_http_response_release_file_stream(res);
3925 if (st == CWIST_WRITE_DONE) {
3927 }
3929 }
3930
3931 /* Common path: serialize the header straight into the stash buffer and
3932 * copy the body behind it. The cap check above guarantees
3933 * olen + MAX_HEADER_SIZE + body_len fits CWIST_HTTP_COALESCE_MAX, so the
3934 * single growth step below replaces the old serialize-to-stack then
3935 * memcpy-into-obuf sequence with one direct serialization. */
3936 if (conn->olen + upper > conn->ocap) {
3937 size_t ncap = conn->ocap ? conn->ocap : 16384;
3938 while (ncap < conn->olen + upper) ncap *= 2;
3939 char *nb = cwist_alloc(ncap);
3940 if (!nb) {
3941 cwist_http_response_release_ptr_body(res);
3942 cwist_http_response_release_file_stream(res);
3944 }
3945 if (conn->olen > 0) memcpy(nb, conn->obuf, conn->olen);
3946 cwist_free(conn->obuf);
3947 conn->obuf = nb;
3948 conn->ocap = ncap;
3949 /* Hysteresis anchor for the idle shrink: capacity was just needed. */
3951 }
3952 size_t header_len = serialize_headers(res, conn->obuf + conn->olen, CWIST_HTTP_MAX_HEADER_SIZE);
3953 conn->olen += header_len;
3954 if (body_len > 0 && body_ptr) {
3955 memcpy(conn->obuf + conn->olen, body_ptr, body_len);
3956 conn->olen += body_len;
3957 }
3958
3959 cwist_http_response_release_ptr_body(res);
3960 cwist_http_response_release_file_stream(res);
3962}
3963
3964#endif /* !CWIST_PLATFORM_NO_SOCKETS (socket send paths) */
3965
3970const char *cwist_http_status_reason(int status) {
3971 switch (status) {
3972 case 100: return "Continue";
3973 case 101: return "Switching Protocols";
3974 case 102: return "Processing";
3975 case 103: return "Early Hints";
3976 case 200: return "OK";
3977 case 201: return "Created";
3978 case 202: return "Accepted";
3979 case 203: return "Non-Authoritative Information";
3980 case 204: return "No Content";
3981 case 205: return "Reset Content";
3982 case 206: return "Partial Content";
3983 case 207: return "Multi-Status";
3984 case 208: return "Already Reported";
3985 case 226: return "IM Used";
3986 case 300: return "Multiple Choices";
3987 case 301: return "Moved Permanently";
3988 case 302: return "Found";
3989 case 303: return "See Other";
3990 case 304: return "Not Modified";
3991 case 305: return "Use Proxy";
3992 case 307: return "Temporary Redirect";
3993 case 308: return "Permanent Redirect";
3994 case 400: return "Bad Request";
3995 case 401: return "Unauthorized";
3996 case 402: return "Payment Required";
3997 case 403: return "Forbidden";
3998 case 404: return "Not Found";
3999 case 405: return "Method Not Allowed";
4000 case 406: return "Not Acceptable";
4001 case 407: return "Proxy Authentication Required";
4002 case 408: return "Request Timeout";
4003 case 409: return "Conflict";
4004 case 410: return "Gone";
4005 case 411: return "Length Required";
4006 case 412: return "Precondition Failed";
4007 case 413: return "Content Too Large";
4008 case 414: return "URI Too Long";
4009 case 415: return "Unsupported Media Type";
4010 case 416: return "Range Not Satisfiable";
4011 case 417: return "Expectation Failed";
4012 case 418: return "I'm a Teapot";
4013 case 421: return "Misdirected Request";
4014 case 422: return "Unprocessable Content";
4015 case 423: return "Locked";
4016 case 424: return "Failed Dependency";
4017 case 425: return "Too Early";
4018 case 426: return "Upgrade Required";
4019 case 428: return "Precondition Required";
4020 case 429: return "Too Many Requests";
4021 case 431: return "Request Header Fields Too Large";
4022 case 451: return "Unavailable For Legal Reasons";
4023 case 500: return "Internal Server Error";
4024 case 501: return "Not Implemented";
4025 case 502: return "Bad Gateway";
4026 case 503: return "Service Unavailable";
4027 case 504: return "Gateway Timeout";
4028 case 505: return "HTTP Version Not Supported";
4029 case 506: return "Variant Also Negotiates";
4030 case 507: return "Insufficient Storage";
4031 case 508: return "Loop Detected";
4032 case 510: return "Not Extended";
4033 case 511: return "Network Authentication Required";
4034 default: return NULL;
4035 }
4036}
4037
4038#ifndef CWIST_PLATFORM_NO_SOCKETS /* direct socket writers */
4046void cwist_http_send_error_response(int fd, int status, const char *msg) {
4047 if (fd < 0) return;
4048
4049 const char *reason = cwist_http_status_reason(status);
4050 if (!reason) reason = "Error";
4051 if (!msg) msg = reason;
4052
4053 /* The body goes out as its own iovec: formatting it into the fixed header
4054 * buffer truncated long messages while Content-Length still announced
4055 * strlen(msg), leaving the peer waiting for bytes that never came. */
4056 size_t msg_len = strlen(msg);
4057 char buf[256];
4058 int n = snprintf(
4059 buf, sizeof(buf),
4060 "HTTP/1.1 %d %s\r\nContent-Type: text/plain\r\nContent-Length: %zu\r\nConnection: close\r\n\r\n",
4061 status, reason, msg_len);
4062 if (n <= 0 || (size_t)n >= sizeof(buf)) return;
4063
4064 struct iovec iov[2] = {
4065 {.iov_base = buf, .iov_len = (size_t)n},
4066 {.iov_base = (void *)msg, .iov_len = msg_len},
4067 };
4068 int flags = 0;
4069#if defined(MSG_NOSIGNAL)
4070 flags |= MSG_NOSIGNAL;
4071#endif
4072#if defined(MSG_DONTWAIT)
4073 flags |= MSG_DONTWAIT;
4074#endif
4075 (void)cwist_http_sendmsg_all(fd, iov, msg_len ? 2 : 1, flags);
4076}
4077
4082static void http_send_100_continue(int fd) {
4083 static const char k_continue[] = "HTTP/1.1 100 Continue\r\n\r\n";
4084 struct iovec iov = {.iov_base = (void *)k_continue, .iov_len = sizeof(k_continue) - 1};
4085 int flags = 0;
4086#if defined(MSG_NOSIGNAL)
4087 flags |= MSG_NOSIGNAL;
4088#endif
4089 (void)cwist_http_sendmsg_all(fd, &iov, 1, flags);
4090}
4091
4092#endif /* !CWIST_PLATFORM_NO_SOCKETS (direct socket writers) */
4093
4100 // Deprecated / Debug only
4101 if (!res) return NULL;
4103 if (!s) return NULL;
4105 /* serialize_headers returns a byte span, not a NUL-terminated string. */
4106 size_t header_len = serialize_headers(res, header_buf, sizeof(header_buf));
4107 cwist_sstring_assign_len(s, header_buf, header_len);
4108 if (res->is_ptr_body && res->ptr_body) {
4109 cwist_sstring_append_len(s, (char *)res->ptr_body, res->ptr_body_len);
4110 } else if (res->body) {
4111 cwist_sstring_append_len(s, res->body->data, res->body->size);
4112 }
4113 return s;
4114}
4115
4128static int http_validate_transfer_encoding(cwist_http_header_node *headers) {
4129 bool seen = false;
4130 bool final_is_chunked = false;
4131 bool bad_coding = false;
4132 for (cwist_http_header_node *n = headers; n; n = n->next) {
4133 if (!n->key || !n->key->data || strcasecmp(n->key->data, "Transfer-Encoding") != 0)
4134 continue;
4135 const char *p = (n->value && n->value->data) ? n->value->data : "";
4136 bool any_token = false;
4137 bool last_chunked = false;
4138 while (*p) {
4139 while (*p == ' ' || *p == '\t' || *p == ',') p++;
4140 const char *tok = p;
4141 while (*p && *p != ',') p++;
4142 const char *end = p;
4143 while (end > tok && (end[-1] == ' ' || end[-1] == '\t')) end--;
4144 if (*p) p++;
4145 if (end == tok) continue;
4146 any_token = true;
4147 /* A token with parameters (e.g. "chunked;x=1") is not chunked. */
4148 last_chunked = ((size_t)(end - tok) == 7 && strncasecmp(tok, "chunked", 7) == 0);
4149 if (!last_chunked) bad_coding = true;
4150 }
4151 if (!seen) {
4153 seen = true;
4154 }
4155 }
4156 if (!seen) return 0;
4157 if (!final_is_chunked) return 1;
4158 if (bad_coding) return 2;
4159 return 0;
4160}
4161
4167static cwist_http_request *
4168cwist_http_parse_request_with_header_end(const char *raw_request, size_t raw_len,
4169 const char *header_end,
4171 if (!raw_request || !header_end) return NULL;
4172
4173 const char *line_start = raw_request;
4174 const char *line_end = cwist_simd_find_crlf(line_start, (size_t)(header_end - line_start + 2));
4175 if (!line_end || line_end > header_end) {
4177 return NULL;
4178 }
4179
4181 if (!req) {
4183 return NULL;
4184 }
4185
4186 // Fast path for root GET / HTTP/1.1\r\n
4187 if (line_start[0] == 'G' && line_start[1] == 'E' && line_start[2] == 'T' &&
4188 line_start[3] == ' ' && line_start[4] == '/' && line_start[5] == ' ' &&
4189 line_start[6] == 'H' && line_start[7] == 'T' && line_start[8] == 'T' &&
4190 line_start[9] == 'P' && line_start[10] == '/' && line_start[11] == '1' &&
4191 line_start[12] == '.' && line_start[13] == '1' && line_end == line_start + 14) {
4192 req->method = CWIST_HTTP_GET;
4193 cwist_sstring_borrow(req->path, "/", 1);
4194 cwist_sstring_borrow(req->query, "", 0);
4195 cwist_sstring_borrow(req->version, "HTTP/1.1", 8);
4196 req->keep_alive = true;
4197 } else {
4198 // 1. Request Line (Optimized: SIMD space search)
4199 const char *sp1 = cwist_simd_find_char(line_start, (size_t)(line_end - line_start), ' ');
4200 if (!sp1 || sp1 > line_end) {
4203 return NULL;
4204 }
4205 const char *sp2 = cwist_simd_find_char(sp1 + 1, (size_t)(line_end - (sp1 + 1)), ' ');
4206 if (!sp2 || sp2 > line_end) {
4209 return NULL;
4210 }
4211
4213
4214 const char *path_start = sp1 + 1;
4215 const char *path_end = sp2;
4216 const char *query_sep =
4217 (const char *)memchr(path_start, '?', (size_t)(path_end - path_start));
4218
4219 if (query_sep) {
4220 cwist_http_sstring_assign_arena(req->path, (cwist_arena_t *)req->arena, path_start,
4221 (size_t)(query_sep - path_start));
4222 cwist_http_sstring_assign_arena(req->query, (cwist_arena_t *)req->arena, query_sep + 1,
4223 (size_t)(path_end - (query_sep + 1)));
4225 if (req->query_params) {
4227 }
4228 } else {
4229 cwist_http_sstring_assign_arena(req->path, (cwist_arena_t *)req->arena, path_start,
4230 (size_t)(path_end - path_start));
4231 cwist_http_sstring_assign_arena(req->query, (cwist_arena_t *)req->arena, "", 0);
4232 }
4233
4234 cwist_http_sstring_assign_arena(req->version, (cwist_arena_t *)req->arena, sp2 + 1,
4235 (size_t)(line_end - (sp2 + 1)));
4236 if (strncmp(sp2 + 1, "HTTP/1.1", 8) == 0) {
4237 req->keep_alive = true;
4238 } else {
4239 req->keep_alive = false;
4240 }
4241 }
4242
4243 // 2. Headers (SIMD colon and CRLF scanning + SWAR)
4244 /* RFC 9110/9112 validation state tracked during the single header pass. */
4245 bool has_host = false, host_dup = false, host_empty = false;
4246 bool cl_seen = false, cl_bad = false;
4247 size_t cl_value = 0;
4248 bool te_seen = false;
4249 bool expect_100 = false, expect_bad = false;
4250 const bool is_http11 = req->version->data && strncmp(req->version->data, "HTTP/1.1", 8) == 0;
4251
4252 line_start = line_end + 2;
4253 while (line_start < header_end) {
4254 line_end = cwist_simd_find_crlf(line_start, (size_t)(header_end + 2 - line_start));
4255 if (!line_end || line_end == line_start) break;
4256
4257 const char *colon = cwist_simd_find_char(line_start, (size_t)(line_end - line_start), ':');
4258 if (colon) {
4259 size_t key_len = colon - line_start;
4260 const char *val_start = colon + 1;
4261 while (val_start < line_end && *val_start == ' ') val_start++;
4262 size_t val_len = line_end - val_start;
4263
4265 cwist_http_header_add_ex_len(&req->headers, (cwist_arena_t *)req->arena, line_start,
4266 key_len, val_start, val_len);
4267 bool header_ok = cwist_error_is_ok(&herr);
4269 if (!header_ok) {
4270 /* Out of memory: dropping the header could hide Host or
4271 * Content-Length from the checks below, so give up quietly as
4272 * cwist_http_request_create() failure does. */
4275 return NULL;
4276 }
4277
4278 char k0 = line_start[0];
4279 if (key_len == 10 && (k0 == 'C' || k0 == 'c')) {
4280 uint64_t w0;
4281 memcpy(&w0, line_start, 8);
4282 if ((w0 | 0x2020202020202020ULL) == 0x697463656e6e6f63ULL) { /* "connecti" */
4283 uint16_t w1;
4284 memcpy(&w1, line_start + 8, 2);
4285 if ((w1 | 0x2020) == 0x6e6f) { /* "on" */
4286 if (val_len == 5 && (val_start[0] == 'c' || val_start[0] == 'C')) {
4287 if (strncasecmp(val_start, "close", 5) == 0) req->keep_alive = false;
4288 } else if (val_len == 10 && (val_start[0] == 'k' || val_start[0] == 'K')) {
4289 if (strncasecmp(val_start, "keep-alive", 10) == 0)
4290 req->keep_alive = true;
4291 }
4292 }
4293 }
4294 } else if (key_len == 14 && (k0 == 'C' || k0 == 'c')) {
4295 if (strncasecmp(line_start, "Content-Length", 14) == 0) {
4296 /* RFC 9112 section 6.3: strict digits-only parse; duplicate CL is
4297 * idempotent only when every value matches. */
4298 size_t vlen = val_len;
4299 while (vlen > 0 && (val_start[vlen - 1] == ' ' || val_start[vlen - 1] == '\t'))
4300 vlen--;
4301 if (vlen == 0) {
4302 cl_bad = true;
4303 } else {
4304 size_t len = 0;
4305 bool valid = true;
4306 for (size_t i = 0; i < vlen; i++) {
4307 if (val_start[i] >= '0' && val_start[i] <= '9') {
4308 if (len > (SIZE_MAX - 9) / 10) {
4309 valid = false;
4310 break;
4311 }
4312 len = len * 10 + (size_t)(val_start[i] - '0');
4313 } else {
4314 valid = false;
4315 break;
4316 }
4317 }
4318 if (!valid) {
4319 cl_bad = true;
4320 } else {
4321 if (cl_seen && cl_value != len) cl_bad = true;
4322 cl_seen = true;
4323 cl_value = len;
4324 req->content_length = len;
4325 }
4326 }
4327 }
4328 } else if (key_len == 4 && (k0 == 'H' || k0 == 'h')) {
4329 if (strncasecmp(line_start, "Host", 4) == 0) {
4330 if (has_host) host_dup = true;
4331 has_host = true;
4332 size_t vlen = val_len;
4333 while (vlen > 0 && (val_start[vlen - 1] == ' ' || val_start[vlen - 1] == '\t'))
4334 vlen--;
4335 if (vlen == 0) host_empty = true;
4336 }
4337 } else if (key_len == 17 && (k0 == 'T' || k0 == 't')) {
4338 if (strncasecmp(line_start, "Transfer-Encoding", 17) == 0) te_seen = true;
4339 } else if (key_len == 6 && (k0 == 'E' || k0 == 'e')) {
4340 if (strncasecmp(line_start, "Expect", 6) == 0) {
4341 /* RFC 9110 section 10.1.1: only the 100-continue expectation is
4342 * supported; record it here so the receive paths need no
4343 * second header walk. */
4344 size_t vlen = val_len;
4345 while (vlen > 0 && (val_start[vlen - 1] == ' ' || val_start[vlen - 1] == '\t'))
4346 vlen--;
4347 if (vlen == 12 && strncasecmp(val_start, "100-continue", 12) == 0) {
4348 expect_100 = true;
4349 } else {
4350 expect_bad = true;
4351 }
4352 }
4353 }
4354 }
4355 line_start = line_end + 2;
4356 }
4357
4358 /* RFC 9112 section 3.2: HTTP/1.1 requests must carry exactly one non-empty Host. */
4359 if (is_http11 && (!has_host || host_dup || host_empty)) {
4362 return NULL;
4363 }
4364 if (cl_bad) {
4367 return NULL;
4368 }
4369 if (te_seen) {
4370 /* RFC 9112 section 6.3: TE and CL together are a request-smuggling vector. */
4371 if (cl_seen) {
4374 return NULL;
4375 }
4376 int tev = http_validate_transfer_encoding(req->headers);
4377 if (tev != 0) {
4379 if (err_out)
4380 *err_out =
4382 return NULL;
4383 }
4384 }
4385 /* RFC 9110 section 10.1.1: only the 100-continue expectation is supported. */
4386 if (expect_bad) {
4389 return NULL;
4390 }
4391 req->te_chunked_seen = te_seen;
4393
4394 const char *body_start = header_end + 4;
4395 size_t available = raw_len - (size_t)(body_start - raw_request);
4396 if (available > 0) {
4397 size_t to_take = (req->content_length > 0 && req->content_length < available)
4398 ? req->content_length
4399 : available;
4400 cwist_http_sstring_assign_arena(req->body, (cwist_arena_t *)req->arena, body_start,
4401 to_take);
4402 }
4403
4405 return req;
4406}
4407
4413 if (!raw_request) return NULL;
4414 size_t raw_len = strlen(raw_request);
4415 const char *header_end = cwist_simd_find_crlfcrlf(raw_request, raw_len);
4416 if (!header_end) return NULL;
4417 return cwist_http_parse_request_with_header_end(raw_request, raw_len, header_end, NULL);
4418}
4419
4425 if (!buf || len == 0) return NULL;
4426 const char *header_end = cwist_simd_find_crlfcrlf(buf, len);
4427 if (!header_end) return NULL;
4428 return cwist_http_parse_request_with_header_end(buf, len, header_end, NULL);
4429}
4430
4439int cwist_http_response_serialize(cwist_http_response *res, char **out, size_t *out_len) {
4440 if (!res || !out || !out_len) return -1;
4441 if (res->use_file_stream) return -1; /* streaming bodies need a socket */
4442
4444 size_t header_len = serialize_headers(res, header_buf, sizeof(header_buf));
4445
4446 const void *body = NULL;
4447 size_t body_len = 0;
4448 if (res->stream_mode) {
4449 /* Chunked framing was appended at write time; auto-finalize so the
4450 * terminator is present even when the handler skipped end(). */
4452 body = res->stream_buf ? res->stream_buf->data : NULL;
4453 body_len = res->stream_buf ? res->stream_buf->size : 0;
4454 } else if (res->is_ptr_body) {
4455 body = res->ptr_body;
4456 body_len = res->ptr_body_len;
4457 } else if (res->body && res->body->data) {
4458 body = res->body->data;
4459 body_len = res->body->size;
4460 }
4461
4462 char *buf = (char *)cwist_alloc(header_len + body_len + 1);
4463 if (!buf) return -1;
4464 memcpy(buf, header_buf, header_len);
4465 if (body_len) memcpy(buf + header_len, body, body_len);
4466 buf[header_len + body_len] = '\0';
4467 *out = buf;
4468 *out_len = header_len + body_len;
4469 return 0;
4470}
4471
4472/* --- Streaming producer (issue #201 Phase 1) ------------------------------ */
4473
4477static int cwist_response_stream_flush(cwist_http_response *res) {
4478 if (!res->stream_sink) return 0;
4479 if (!res->stream_head_sent) {
4481 size_t head_len = serialize_headers(res, head_buf, sizeof(head_buf));
4482 if (head_len == 0 || head_len >= sizeof(head_buf)) return -1;
4483 if (res->stream_sink(res->stream_sink_ctx, head_buf, head_len) != 0) return -1;
4484 res->stream_head_sent = true;
4485 }
4486 size_t pending = res->stream_buf ? res->stream_buf->size - res->stream_flushed : 0;
4487 if (pending > 0) {
4488 if (res->stream_sink(res->stream_sink_ctx, res->stream_buf->data + res->stream_flushed,
4489 pending) != 0)
4490 return -1;
4491 res->stream_flushed = res->stream_buf->size;
4492 }
4493 return 0;
4494}
4495
4504 if (!res || res->stream_mode || res->is_ptr_body || res->use_file_stream) return -1;
4505 res->stream_mode = true;
4506 /* A body assigned before begin is discarded: chunked framing owns the
4507 * payload from here on. */
4508 if (res->body) res->body->size = 0; /* discard any body assigned before begin */
4509 return 0;
4510}
4511
4519int cwist_http_response_stream_write(cwist_http_response *res, const char *data, size_t len) {
4520 if (!res || !res->stream_mode || res->stream_ended) return -1;
4521 if (!data) return -1;
4522 if (len == 0) return 0; /* an empty chunk is the terminator; never emit one mid-stream */
4523 if (!res->stream_buf) {
4524 res->stream_buf = cwist_http_sstring_create(res->arena);
4525 if (!res->stream_buf) return -1;
4526 }
4527 char frame_hdr[24];
4528 int frame_hdr_len = snprintf(frame_hdr, sizeof(frame_hdr), "%zx\r\n", len);
4529 if (frame_hdr_len <= 0) return -1;
4532 if (cwist_error_is_ok(&append_err))
4534 if (cwist_error_is_ok(&append_err))
4536 if (!cwist_error_is_ok(&append_err)) return -1;
4537 if (cwist_response_stream_flush(res) != 0) {
4538 res->stream_failed = true;
4539 return -1;
4540 }
4541 return 0;
4542}
4543
4550 if (!res || !res->stream_mode || res->stream_ended) return -1;
4551 if (!res->stream_buf) {
4552 res->stream_buf = cwist_http_sstring_create(res->arena);
4553 if (!res->stream_buf) return -1;
4554 }
4556 if (!cwist_error_is_ok(&end_err)) return -1;
4557 if (cwist_response_stream_flush(res) != 0) {
4558 res->stream_failed = true;
4559 return -1;
4560 }
4561 res->stream_ended = true;
4562 return 0;
4563}
4564
4565#ifndef CWIST_PLATFORM_NO_SOCKETS /* socket receive (blocking and reactor) */
4570static int http_parse_chunk_size(const char *line, size_t len, size_t *out_size) {
4571 size_t value = 0, digits = 0;
4572 for (size_t i = 0; i < len && line[i] != ';'; ++i) {
4573 unsigned char c = (unsigned char)line[i];
4574 unsigned int digit;
4575 if (c >= '0' && c <= '9')
4576 digit = c - '0';
4577 else if (c >= 'a' && c <= 'f')
4578 digit = c - 'a' + 10;
4579 else if (c >= 'A' && c <= 'F')
4580 digit = c - 'A' + 10;
4581 else
4582 return -1;
4583 if (value > (CWIST_HTTP_MAX_BODY_SIZE - digit) / 16) return -1;
4584 value = value * 16 + digit;
4585 ++digits;
4586 }
4587 if (!digits) return -1;
4588 *out_size = value;
4589 return 0;
4590}
4591
4599static bool http_chunk_append(cwist_sstring *out, const char *data, size_t len) {
4600 cwist_error_t err = cwist_sstring_append_len(out, data, len);
4601 bool ok = cwist_error_is_ok(&err);
4603 return ok;
4604}
4605
4613static int http_read_chunked_body(int client_fd, char *buf, size_t *avail, size_t buf_cap,
4614 cwist_sstring *out) {
4615 size_t offset = 0;
4616
4617 while (1) {
4618 char *crlf = memmem(buf + offset, *avail - offset, "\r\n", 2);
4619 while (!crlf) {
4620 if (*avail >= buf_cap - 1) return -1;
4621 ssize_t bytes = recv(client_fd, buf + *avail, buf_cap - 1 - *avail, 0);
4622 if (bytes < 0) {
4623 if (errno == EAGAIN || errno == EWOULDBLOCK) {
4624 struct pollfd pfd = {.fd = client_fd, .events = POLLIN};
4625 int ret = poll(&pfd, 1, CWIST_HTTP_TIMEOUT_MS);
4626 if (ret <= 0) return -1;
4627 continue;
4628 }
4629 if (errno == EINTR) continue;
4630 return -1;
4631 }
4632 if (bytes == 0) return -1;
4633 *avail += (size_t)bytes;
4634 buf[*avail] = '\0';
4635 crlf = memmem(buf + offset, *avail - offset, "\r\n", 2);
4636 }
4637
4638 size_t line_len = (size_t)(crlf - (buf + offset)) + 2;
4639 size_t chunk_size = 0;
4640 /* Strict hexadecimal parsing prevents accepting contaminated framing
4641 * such as `4junk` or signed/overflowed chunk lengths. */
4642 if (http_parse_chunk_size(buf + offset, line_len - 2, &chunk_size) != 0) return -1;
4643 offset += line_len;
4644
4645 if (chunk_size == 0) {
4646 /* Consume optional trailers until empty line */
4647 while (offset + 1 < *avail && !(buf[offset] == '\r' && buf[offset + 1] == '\n')) {
4648 char *trailer_crlf = memmem(buf + offset, *avail - offset, "\r\n", 2);
4649 if (!trailer_crlf) {
4650 if (*avail >= buf_cap - 1) return -1;
4651 ssize_t bytes = recv(client_fd, buf + *avail, buf_cap - 1 - *avail, 0);
4652 if (bytes < 0) {
4653 if (errno == EAGAIN || errno == EWOULDBLOCK) {
4654 struct pollfd pfd = {.fd = client_fd, .events = POLLIN};
4655 int ret = poll(&pfd, 1, CWIST_HTTP_TIMEOUT_MS);
4656 if (ret <= 0) return -1;
4657 continue;
4658 }
4659 if (errno == EINTR) continue;
4660 return -1;
4661 }
4662 if (bytes == 0) return -1;
4663 *avail += (size_t)bytes;
4664 buf[*avail] = '\0';
4665 trailer_crlf = memmem(buf + offset, *avail - offset, "\r\n", 2);
4666 }
4667 if (trailer_crlf) {
4668 offset = (size_t)(trailer_crlf - buf) + 2;
4669 }
4670 }
4671 if (offset + 1 < *avail && buf[offset] == '\r' && buf[offset + 1] == '\n') {
4672 offset += 2;
4673 }
4674 break;
4675 }
4676
4677 if (chunk_size > CWIST_HTTP_MAX_BODY_SIZE ||
4678 out->size + chunk_size > CWIST_HTTP_MAX_BODY_SIZE)
4679 return -1;
4680
4681 while (*avail - offset < chunk_size + 2) {
4682 if (*avail >= buf_cap - 1) return -1;
4683 ssize_t bytes = recv(client_fd, buf + *avail, buf_cap - 1 - *avail, 0);
4684 if (bytes < 0) {
4685 if (errno == EAGAIN || errno == EWOULDBLOCK) {
4686 struct pollfd pfd = {.fd = client_fd, .events = POLLIN};
4687 int ret = poll(&pfd, 1, CWIST_HTTP_TIMEOUT_MS);
4688 if (ret <= 0) return -1;
4689 continue;
4690 }
4691 if (errno == EINTR) continue;
4692 return -1;
4693 }
4694 if (bytes == 0) return -1;
4695 *avail += (size_t)bytes;
4696 buf[*avail] = '\0';
4697 }
4698
4699 if (buf[offset + chunk_size] != '\r' || buf[offset + chunk_size + 1] != '\n') return -1;
4700 if (!http_chunk_append(out, buf + offset, chunk_size)) return -1;
4701 offset += chunk_size + 2;
4702 }
4703
4704 size_t leftover = *avail - offset;
4705 if (leftover > 0) {
4706 memmove(buf, buf + offset, leftover);
4707 }
4708 *avail = leftover;
4709 return 0;
4710}
4711
4722cwist_http_request *cwist_http_receive_request(int client_fd, char *read_buf, size_t buf_size,
4723 size_t *buf_len, cwist_http_parse_error_t *err_out) {
4725 size_t total_received = *buf_len;
4726 char *header_end = NULL;
4727
4728 // 1. Read until headers are complete. The classic pool serves this
4729 // socket blocking, so a client dribbling the header byte-by-byte would
4730 // hold the thread forever; bound the whole header read by
4731 // CWIST_HTTP_HEADERS_TIMEOUT_MS (same budget the TLS path enforces).
4732 struct timespec header_start;
4734 while (!(header_end = (char *)cwist_simd_find_crlfcrlf(read_buf, total_received))) {
4735 if (total_received >= buf_size - 1) {
4736 /* Fat Cookie/Authorization combinations can legitimately push a
4737 * header block past the read buffer; without this trail the drop
4738 * is indistinguishable from a client vanish. */
4740 CWIST_LOG_WARN("[http] dropping connection: headers exceed %zu-byte read buffer",
4741 buf_size);
4743 return NULL;
4744 }
4745
4746 struct timespec now;
4748 long elapsed_ms = (now.tv_sec - header_start.tv_sec) * 1000L +
4749 (now.tv_nsec - header_start.tv_nsec) / 1000000L;
4751 if (remaining_ms <= 0) {
4752 CWIST_LOG_WARN("[http] dropping connection: header read exceeded %d ms",
4755 return NULL;
4756 }
4757 /* Nothing of the next request yet: the connection is idle, and a
4758 * shutting-down pool closes it instead of waiting. */
4759 const bool idle = total_received == 0;
4760 if (idle && !cwist_http_live_idle_enter()) return NULL;
4761 struct pollfd pfd = {.fd = client_fd, .events = POLLIN};
4762 int pr = poll(&pfd, 1, (int)(remaining_ms > 1000 ? 1000 : remaining_ms));
4763 if (pr <= 0) {
4764 if (pr < 0 && errno == EINTR) continue;
4765 if (pr < 0) continue; /* transient errors retry within the budget */
4766 continue; /* timeout slice expired; re-check the budget */
4767 }
4768
4769 ssize_t bytes =
4770 recv(client_fd, read_buf + total_received, buf_size - 1 - total_received, 0);
4771 if (idle) cwist_http_live_idle_leave();
4772 if (bytes <= 0) {
4773 if (bytes < 0 && errno == EINTR) continue;
4774 if (bytes < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) continue;
4775 return NULL;
4776 }
4777 total_received += (size_t)bytes;
4778 read_buf[total_received] = '\0';
4779 }
4780
4781 /* Like the nonblocking receiver, parse only this header block. Framing
4782 * below owns body assembly; subsequent pipelined requests are not a body. */
4783 size_t header_len = (size_t)(header_end + 4 - read_buf);
4784 cwist_http_request *req =
4785 cwist_http_parse_request_with_header_end(read_buf, header_len, header_end, err_out);
4786 if (!req) return NULL;
4787
4788 size_t body_received = total_received - header_len;
4789
4790 /* RFC 9110 section 10.1.1: the parser already validated that any Expect value is
4791 * exactly "100-continue"; answer it before waiting on the body. Flags were
4792 * recorded during the header pass — no second walk needed. */
4793 const bool te = req->te_chunked_seen;
4794 const bool expect = req->expect_100_seen;
4795 if (expect &&
4796 ((size_t)req->content_length > body_received || (te && req->content_length == 0))) {
4797 http_send_100_continue(client_fd);
4798 }
4799
4800 // 2. Read body based on Content-Length or Transfer-Encoding
4801 if (req->content_length > 0) {
4805 return NULL;
4806 }
4807
4808 // Allocate body
4809 char *body = cwist_alloc(req->content_length + 1);
4810 if (!body) {
4813 return NULL;
4814 }
4815
4816 size_t to_copy = (body_received < (size_t)req->content_length)
4818 : (size_t)req->content_length;
4819 memcpy(body, header_end + 4, to_copy);
4820 size_t current_body_len = to_copy;
4821
4822 while (current_body_len < (size_t)req->content_length) {
4823 ssize_t bytes = recv(client_fd, body + current_body_len,
4824 (size_t)req->content_length - current_body_len, 0);
4825 if (bytes < 0) {
4826 if (errno == EAGAIN || errno == EWOULDBLOCK) {
4827 struct pollfd pfd = {.fd = client_fd, .events = POLLIN};
4828 int ret = poll(&pfd, 1, CWIST_HTTP_TIMEOUT_MS);
4829 if (ret <= 0) {
4830 cwist_free(body);
4832 return NULL;
4833 }
4834 continue;
4835 }
4836 if (errno == EINTR) continue;
4837 cwist_free(body);
4839 return NULL;
4840 }
4841 if (bytes == 0) {
4842 cwist_free(body);
4844 return NULL;
4845 }
4846 current_body_len += (size_t)bytes;
4847 }
4848 body[req->content_length] = '\0';
4849 /* Adopt the filled buffer: one allocation, zero copies. The partial
4850 * body the parser staged in the arena is simply superseded. */
4851 cwist_sstring_adopt_len(req->body, body, (size_t)req->content_length);
4852
4853 // Calculate leftovers
4854 if (body_received > (size_t)req->content_length) {
4856 memmove(read_buf, header_end + 4 + req->content_length, leftover_len);
4857 *buf_len = leftover_len;
4858 } else {
4859 *buf_len = 0;
4860 }
4861 } else {
4862 /* A surviving Transfer-Encoding header was validated by the parser as
4863 * chunked-only with chunked final, so presence alone selects framing. */
4864 if (te) {
4865 if (body_received > 0) {
4866 memmove(read_buf, header_end + 4, body_received);
4867 }
4868 *buf_len = body_received;
4869 read_buf[*buf_len] = '\0';
4870
4872 if (!chunked) {
4874 return NULL;
4875 }
4876 if (http_read_chunked_body(client_fd, read_buf, buf_len, buf_size, chunked) != 0) {
4880 return NULL;
4881 }
4882 /* Move the assembled buffer into the request body instead of
4883 * copying it a second time. */
4884 char *chunked_data = chunked->data;
4885 size_t chunked_len = chunked->size;
4886 chunked->data = NULL;
4887 chunked->size = 0;
4890 } else {
4891 // No body, leftovers are everything after headers
4892 if (body_received > 0) {
4893 memmove(read_buf, header_end + 4, body_received);
4894 *buf_len = body_received;
4895 } else {
4896 *buf_len = 0;
4897 }
4898 }
4899 }
4900 read_buf[*buf_len] = '\0';
4901
4903 return req;
4904}
4905
4906/* --- Non-blocking request assembly for the async (C1M) path --------------- */
4907
4908#define CWIST_ASYNC_STASH_MAX (CWIST_HTTP_READ_BUFFER_SIZE + CWIST_HTTP_MAX_BODY_SIZE)
4909
4910/* Bodies at least this large transfer stash-buffer ownership to the request
4911 * (zero-copy region view) instead of being copied out. Below the threshold
4912 * the copy is cheaper than allocating the connection a fresh stash. */
4913#define CWIST_HTTP_BODY_ZC_MIN (16 * 1024)
4914
4916static bool http_async_stash_grow(cwist_http_async_conn_t *conn, size_t need) {
4917 /* RX-uring invariant: the stash buffer must not move while a RECV SQE
4918 * references it. Growth only happens while serving (in-flight RECV
4919 * cleared), so this is defensive: fail closed rather than relocate the
4920 * buffer under the kernel's DMA. */
4921 if (conn->rx_recv_inflight) return false;
4922 if (need > CWIST_ASYNC_STASH_MAX) return false;
4923 if (conn->cap >= need) return true;
4924 /* Doubling base: a virgin stash starts at the 4 KiB idle floor (the
4925 * first-arm site grows it to exactly that), so an idle C1M connection
4926 * pays floor memory, not CWIST_HTTP_READ_BUFFER_SIZE. Above the floor
4927 * the doubling ladder is unchanged: need 16 KiB on a virgin stash ends
4928 * at the same 16 KiB cap as before. */
4929 size_t cap = conn->cap ? conn->cap : CWIST_ASYNC_STASH_IDLE;
4930 while (cap < need) {
4931 if (cap > CWIST_ASYNC_STASH_MAX / 2) {
4933 break;
4934 }
4935 cap *= 2;
4936 }
4938 if (cap == conn->cap) return false;
4939 char *nb = cwist_realloc(conn->rbuf, cap);
4940 if (!nb) return false;
4941 conn->rbuf = nb;
4942 conn->cap = cap;
4943 /* Hysteresis anchor for the idle shrink: capacity was just needed. */
4945 return true;
4946}
4947
4957 if (conn->peer_eof) return 0;
4958 if (conn->rx_recv_inflight || conn->rx_data_ready) {
4959 /* RX-uring path: the in-flight RECV SQE owns the socket read (never
4960 * recv() on top of it), and a completed RECV already staged its bytes
4961 * (or recorded EOF) into the stash. Nothing to drain here. */
4962 conn->rx_data_ready = false;
4963 return 0;
4964 }
4965 for (;;) {
4966 if (conn->len + 1 >= conn->cap && !http_async_stash_grow(conn, conn->len + 4096)) {
4967 return -1;
4968 }
4969 size_t avail = conn->cap - 1 - conn->len;
4970 ssize_t n = recv(conn->fd, conn->rbuf + conn->len, avail, 0);
4971 if (n > 0) {
4972 conn->len += (size_t)n;
4973 conn->rbuf[conn->len] = '\0';
4974 conn->virgin = false;
4975 if ((size_t)n < avail) return 0; /* short read: drained for now */
4976 continue;
4977 }
4978 if (n == 0) {
4979 /* A half-close does not discard already buffered requests. */
4980 conn->peer_eof = true;
4981 return 0;
4982 }
4983 if (errno == EINTR) continue;
4984 if (errno == EAGAIN || errno == EWOULDBLOCK) return 0;
4985 return -1;
4986 }
4987}
4988
4994static int http_chunked_scan(const char *buf, size_t avail, size_t *consumed,
4996 size_t pos = 0;
4997 for (;;) {
4998 char *crlf = memmem(buf + pos, avail - pos, "\r\n", 2);
4999 if (!crlf) return avail > CWIST_ASYNC_STASH_MAX ? -1 : 0;
5000 size_t line_len = (size_t)(crlf - (buf + pos));
5001 size_t chunk_size = 0;
5002 if (http_parse_chunk_size(buf + pos, line_len, &chunk_size) != 0) return -1;
5003 pos += line_len + 2;
5004
5005 if (chunk_size == 0) {
5006 /* Trailers until an empty line. */
5007 for (;;) {
5008 if (avail - pos < 2) return 0;
5009 if (buf[pos] == '\r' && buf[pos + 1] == '\n') {
5010 *consumed = pos + 2;
5011 return 1;
5012 }
5013 char *tcrlf = memmem(buf + pos, avail - pos, "\r\n", 2);
5014 if (!tcrlf) return 0;
5015 pos = (size_t)(tcrlf - buf) + 2;
5016 }
5017 }
5018
5019 if (chunk_size > CWIST_HTTP_MAX_BODY_SIZE) return -1;
5020 if (avail - pos < chunk_size + 2) return 0;
5021 if (buf[pos + chunk_size] != '\r' || buf[pos + chunk_size + 1] != '\n') return -1;
5022 if (assemble) {
5023 if (assemble->size + chunk_size > CWIST_HTTP_MAX_BODY_SIZE) return -1;
5024 if (!http_chunk_append(assemble, (const char *)buf + pos, chunk_size)) return -1;
5025 }
5026 pos += chunk_size + 2;
5027 }
5028}
5029
5036 cwist_http_request **out,
5038 *out = NULL;
5040 if (!conn->rbuf || conn->len == 0) return CWIST_RECV_NEED_MORE;
5041
5042 char *header_end = (char *)cwist_simd_find_crlfcrlf(conn->rbuf, conn->len);
5043 if (!header_end) {
5044 if (conn->len >= CWIST_HTTP_READ_BUFFER_SIZE - 1) {
5046 CWIST_LOG_WARN("[http] dropping async connection: headers exceed %d-byte read buffer",
5049 return CWIST_RECV_FATAL;
5050 }
5051 return CWIST_RECV_NEED_MORE;
5052 }
5053
5054 /* Framing below assembles this request's body. Passing the whole stash
5055 * here copies later pipelined messages into body-less requests too. */
5056 size_t header_len = (size_t)(header_end + 4 - conn->rbuf);
5057 cwist_http_request *req =
5058 cwist_http_parse_request_with_header_end(conn->rbuf, header_len, header_end, err_out);
5059 if (!req) return CWIST_RECV_FATAL;
5060
5061 size_t body_received = conn->len - header_len;
5062 size_t consumed = header_len;
5063
5064 /* RFC 9110 section 10.1.1: answer a validated Expect: 100-continue once, before
5065 * the stash accumulates the full body. Flags come from the header pass. */
5066 const bool te = req->te_chunked_seen;
5067 const bool expect = req->expect_100_seen;
5068 if (expect && !conn->expect_continue_sent &&
5069 ((size_t)req->content_length > body_received || (te && req->content_length == 0))) {
5070 /* Buffered coalesced responses must drain first: the client holds
5071 * the body until this interim reply arrives. */
5073 http_send_100_continue(conn->fd);
5074 conn->expect_continue_sent = true;
5075 }
5076
5077 if (req->content_length > 0) {
5081 return CWIST_RECV_FATAL;
5082 }
5083 if (body_received < (size_t)req->content_length) {
5084 /* Not all here yet; make sure the stash can hold the full message. */
5085 if (!http_async_stash_grow(conn, header_len + (size_t)req->content_length + 1)) {
5088 return CWIST_RECV_FATAL;
5089 }
5091 return CWIST_RECV_NEED_MORE;
5092 }
5093 /* Zero-copy handover: a large body ending exactly at the stash end
5094 * moves buffer ownership to the request as a region view over the
5095 * payload, and the connection starts on a fresh stash. Both fill
5096 * paths keep rbuf[len] NUL-terminated, which is the adopted region's
5097 * terminating slot. Pipelined leftovers and small bodies keep the
5098 * copy path below. */
5099 if (header_len + (size_t)req->content_length == conn->len &&
5100 (size_t)req->content_length >= CWIST_HTTP_BODY_ZC_MIN) {
5102 if (fresh) {
5103 fresh[0] = '\0';
5104 conn->rbuf[conn->len] = '\0'; /* defensive: region NUL slot */
5105 cwist_sstring_adopt_region(req->body, conn->rbuf, header_len,
5106 (size_t)req->content_length);
5107 conn->rbuf = fresh;
5109 conn->len = 0;
5110 consumed = 0; /* nothing left to shift */
5111 /* Capacity was just provisioned for a large-body turn. */
5113 }
5114 }
5115 if (consumed > 0) {
5116 char *body = cwist_alloc((size_t)req->content_length + 1);
5117 if (!body) {
5120 return CWIST_RECV_FATAL;
5121 }
5122 memcpy(body, conn->rbuf + header_len, (size_t)req->content_length);
5123 body[req->content_length] = '\0';
5124 cwist_sstring_adopt_len(req->body, body, (size_t)req->content_length);
5126 }
5127 } else {
5128 /* Presence of TE implies parser-validated chunked framing. */
5129 if (te) {
5131 if (!assembled) {
5134 return CWIST_RECV_FATAL;
5135 }
5136 size_t chunk_bytes = 0;
5137 int scan =
5138 http_chunked_scan(conn->rbuf + header_len, body_received, &chunk_bytes, assembled);
5139 if (scan <= 0) {
5142 if (scan == 0) {
5143 if (!http_async_stash_grow(conn, conn->len + 4096)) {
5145 return CWIST_RECV_FATAL;
5146 }
5147 return CWIST_RECV_NEED_MORE;
5148 }
5150 return CWIST_RECV_FATAL;
5151 }
5152 char *data = assembled->data;
5153 size_t dlen = assembled->size;
5154 assembled->data = NULL;
5155 assembled->size = 0;
5157 cwist_sstring_adopt_len(req->body, data, dlen);
5159 }
5160 }
5161
5162 /* Shift the leftover (pipelined bytes) to the stash head. */
5163 size_t leftover = conn->len - consumed;
5164 if (leftover > 0) memmove(conn->rbuf, conn->rbuf + consumed, leftover);
5165 conn->len = leftover;
5166 conn->rbuf[leftover] = '\0';
5167 conn->expect_continue_sent = false;
5168
5169 req->client_fd = conn->fd;
5170 *out = req;
5171 return CWIST_RECV_OK;
5172}
5173
5174/* --- End Non-blocking Request Assembly --- */
5175#endif /* !CWIST_PLATFORM_NO_SOCKETS (socket receive) */
5176
5177typedef struct {
5178 const char *ext;
5179 const char *mime;
5180} cwist_mime_entry;
5181
5182static const cwist_mime_entry CWIST_MIME_TABLE[] = {{".html", "text/html; charset=utf-8"},
5183 {".htm", "text/html; charset=utf-8"},
5184 {".css", "text/css; charset=utf-8"},
5185 {".js", "application/javascript"},
5186 {".mjs", "application/javascript"},
5187 {".json", "application/json"},
5188 {".wasm", "application/wasm"},
5189 {".png", "image/png"},
5190 {".jpg", "image/jpeg"},
5191 {".jpeg", "image/jpeg"},
5192 {".gif", "image/gif"},
5193 {".svg", "image/svg+xml"},
5194 {".txt", "text/plain; charset=utf-8"},
5195 {".ico", "image/x-icon"}};
5196
5201static const char *cwist_guess_mime(const char *file_path) {
5202 if (!file_path) return "application/octet-stream";
5203 const char *dot = strrchr(file_path, '.');
5204 if (!dot) {
5205 return "application/octet-stream";
5206 }
5207 for (size_t i = 0; i < sizeof(CWIST_MIME_TABLE) / sizeof(CWIST_MIME_TABLE[0]); i++) {
5208 if (strcasecmp(dot, CWIST_MIME_TABLE[i].ext) == 0) {
5209 return CWIST_MIME_TABLE[i].mime;
5210 }
5211 }
5212 return "application/octet-stream";
5213}
5214
5224 const char *content_type_hint, size_t *out_size) {
5226 if (!res || !file_path) {
5227 err.error.err_i16 = -EINVAL;
5228 return err;
5229 }
5230
5231 cwist_http_response_release_file_stream(res);
5232 cwist_http_response_release_ptr_body(res);
5233
5234 int fd = open(file_path, O_RDONLY);
5235 if (fd < 0) {
5236 err.error.err_i16 = -errno;
5237 return err;
5238 }
5239
5240 struct stat st;
5241 if (fstat(fd, &st) < 0) {
5242 err.error.err_i16 = -errno;
5243 close(fd);
5244 return err;
5245 }
5246
5247 if (!S_ISREG(st.st_mode)) {
5248 close(fd);
5249 err.error.err_i16 = -EISDIR;
5250 return err;
5251 }
5252
5253 bool endpoint_file = cwist_endpoint_has(res->endpoint_opts, CWIST_ENDPOINT_FILE);
5254
5255 if (!endpoint_file && (size_t)st.st_size > CWIST_HTTP_MAX_BODY_SIZE) {
5256 close(fd);
5257 err.error.err_i16 = -EFBIG;
5258 return err;
5259 }
5260
5261 size_t file_size = (size_t)st.st_size;
5262 bool use_fast_stream = false;
5263
5264 if (file_size > 0 && endpoint_file) {
5265#if defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
5266 res->use_file_stream = true;
5267 res->file_stream_fd = fd;
5269 res->file_stream_offset = 0;
5270 res->file_stream_auto_close = true;
5271 use_fast_stream = true;
5272#endif
5273 }
5274
5275 char *buffer = NULL;
5276
5277 if (!use_fast_stream && file_size > 0) {
5278 buffer = (char *)cwist_alloc(file_size + 1);
5279 if (!buffer) {
5280 close(fd);
5281 err.error.err_i16 = -ENOMEM;
5282 return err;
5283 }
5284 }
5285
5286 if (!use_fast_stream) {
5287 size_t total_read = 0;
5288 while (total_read < file_size) {
5289 ssize_t bytes = read(fd, buffer + total_read, file_size - total_read);
5290 if (bytes < 0) {
5291 if (errno == EINTR) continue;
5292 err.error.err_i16 = -errno;
5293 cwist_free(buffer);
5294 close(fd);
5295 return err;
5296 }
5297 if (bytes == 0) {
5298 err.error.err_i16 = -EIO;
5299 cwist_free(buffer);
5300 close(fd);
5301 return err;
5302 }
5303 total_read += (size_t)bytes;
5304 }
5305 close(fd);
5306
5307 if (file_size > 0) {
5308 /* Adopt the read buffer as the body: no second file-size copy. */
5309 buffer[file_size] = '\0';
5310 cwist_sstring_adopt_len(res->body, buffer, file_size);
5311 } else {
5312 cwist_sstring_borrow(res->body, "", 0);
5313 }
5314 } else {
5315 cwist_sstring_borrow(res->body, "", 0);
5316 }
5317
5318 const char *mime = content_type_hint ? content_type_hint : cwist_guess_mime(file_path);
5319 if (mime && !cwist_http_header_get(res->headers, "Content-Type")) {
5320 cwist_http_header_add(&res->headers, "Content-Type", mime);
5321 }
5322
5323 if (out_size) {
5325 }
5326
5328 err.error.err_i16 = 0;
5329 return err;
5330}
5331
5332/* --- Predefined Static Blobs --- */
5333
5334const char CWIST_BLOB_200_OK[] =
5335 "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n";
5336const char CWIST_BLOB_404[] =
5337 "HTTP/1.1 404 Not Found\r\nContent-Length: 13\r\nConnection: keep-alive\r\n\r\n404 Not Found";
5338const char CWIST_BLOB_500[] =
5339 "HTTP/1.1 500 Internal Server Error\r\nContent-Length: 21\r\nConnection: close\r\n\r\nInternal Server Error";
5340
5341/* --- Socket Manipulation --- */
5342/* Everything from here on is the socket server runtime: WASI preview1 has
5343 * no sockets, so the whole section compiles out there. WASM hosts drive
5344 * requests through cwist_app_dispatch_memory() instead. */
5345#ifndef CWIST_PLATFORM_NO_SOCKETS
5346
5348 const char *env = getenv("CWIST_REUSEPORT");
5349 if (!env) return true;
5350 return !(env[0] == '0' || strcmp(env, "false") == 0);
5351}
5352
5362 uint16_t backlog) {
5363 int server_fd = -1;
5364 int opt = 1;
5365
5366 if (!address || inet_pton(AF_INET, address, &sockv4->sin_addr) != 1) {
5368 }
5369
5370 if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
5371 cJSON *err_json = cJSON_CreateObject();
5372 cJSON_AddStringToObject(err_json, "err", "Failed to create IPv4 socket");
5373 char *cjson_error_log = cJSON_Print(err_json);
5376 cJSON_Delete(err_json);
5377
5379 }
5380
5381 if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt))) {
5382 cJSON *err_json = cJSON_CreateObject();
5383 cJSON_AddStringToObject(err_json, "err", "Failed to set up IPv4 socket options");
5384 char *cjson_error_log = cJSON_Print(err_json);
5387 cJSON_Delete(err_json);
5388
5390 }
5391
5392#if defined(SO_REUSEPORT)
5395 }
5396#endif
5397
5398#if defined(__APPLE__) || defined(__FreeBSD__)
5399#ifdef SO_NOSIGPIPE
5400 int no_sig_pipe = 1;
5402#endif
5403#endif
5404
5405 sockv4->sin_family = AF_INET;
5406 sockv4->sin_port = htons(port);
5407
5408 if (bind(server_fd, (struct sockaddr *)sockv4, sizeof(struct sockaddr_in)) < 0) {
5409 cJSON *err_json = cJSON_CreateObject();
5410 cJSON_AddStringToObject(err_json, "err", "Failed to bind IPv4 socket");
5411 char *cjson_error_log = cJSON_Print(err_json);
5414 cJSON_Delete(err_json);
5415
5417 }
5418
5419 if (listen(server_fd, backlog) < 0) {
5420 cJSON *err_json = cJSON_CreateObject();
5421 char err_msg[128];
5422 char err_format[128] = "Failed to listen at %s:%d";
5423 snprintf(err_msg, 127, err_format, address, port);
5424
5425 cJSON_AddStringToObject(err_json, "err", err_msg);
5426 char *cjson_error_log = cJSON_Print(err_json);
5429 cJSON_Delete(err_json);
5430
5432 }
5433
5434 return server_fd;
5435}
5436
5442static bool cwist_accept_error_should_retry(int err) {
5443 switch (err) {
5444 case EINTR:
5445 case EAGAIN:
5446 case ECONNABORTED:
5447#ifdef ECONNRESET
5448 case ECONNRESET:
5449#endif
5450#ifdef EPROTO
5451 case EPROTO:
5452#endif
5453 return true;
5454 case EMFILE:
5455 case ENFILE:
5456 case ENOBUFS:
5457 case ENOMEM: return true;
5458 default: return false;
5459 }
5460}
5461
5466static void cwist_accept_error_backoff(int err) {
5467 switch (err) {
5468 case EMFILE:
5469 case ENFILE:
5470 case ENOBUFS:
5471 case ENOMEM: {
5472 fprintf(stderr, "[CWIST] accept() backoff triggered: %s\n", strerror(err));
5473 struct timespec ts;
5474 ts.tv_sec = 0;
5475 ts.tv_nsec = 50 * 1000 * 1000; // 50ms
5476 nanosleep(&ts, NULL);
5477 break;
5478 }
5479 default: break;
5480 }
5481}
5482
5489#if !defined(CWIST_PLATFORM_NO_PROC)
5490static void handle_client_forking(int client_fd, void (*handler_func)(int, void *), void *ctx) {
5491 pid_t pid = fork();
5492 if (pid == 0) {
5493 handler_func(client_fd, ctx);
5494 close(client_fd);
5495 _exit(0);
5496 } else if (pid > 0) {
5497 close(client_fd);
5498 }
5499}
5500#endif /* __wasi__ (no fork) */
5501
5502#if defined(__SANITIZE_THREAD__)
5503#define HTTP_ACCEPT_TSAN 1
5504#elif defined(__has_feature)
5505#if __has_feature(thread_sanitizer)
5506#define HTTP_ACCEPT_TSAN 1
5507#endif
5508#endif
5509
5510#ifdef HTTP_ACCEPT_TSAN
5511#define HTTP_ACCEPT_WAIT_MS 100
5512#else
5513#define HTTP_ACCEPT_WAIT_MS (-1)
5514#endif
5515
5527static int http_accept_blocking(int server_fd) {
5528#ifdef HTTP_ACCEPT_TSAN
5529 struct pollfd pfd = {.fd = server_fd, .events = POLLIN};
5530 while (atomic_load(&g_cwist_running)) {
5531 int r = poll(&pfd, 1, HTTP_ACCEPT_WAIT_MS);
5532 if (r > 0 || (r < 0 && errno != EINTR)) break;
5533 }
5535 errno = EINTR;
5536 return -1;
5537 }
5538#endif
5539 return accept(server_fd, NULL, NULL);
5540}
5541
5551 void (*handler_func)(int client_fd, void *), void *ctx) {
5552 int client_fd = -1;
5553 struct sockaddr_in peer_addr;
5554 socklen_t addrlen = sizeof(peer_addr);
5555
5556 while (true) {
5557 if ((client_fd = accept(server_fd, (struct sockaddr *)&peer_addr, &addrlen)) < 0) {
5558 if (errno == EINTR) continue;
5559 // ... (error handling)
5560 if (errno == EBADF || errno == EINVAL || errno == ENOTSOCK) {
5561 fprintf(stderr, "Fatal socket error %d. Exiting accept loop.\n", errno);
5562 break;
5563 }
5564 continue;
5565 }
5566
5567 if (sockv4) {
5568 memcpy(sockv4, &peer_addr, sizeof(peer_addr));
5569 }
5570
5571 int nodelay = 1;
5572 setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
5573
5574 handler_func(client_fd, ctx);
5575 }
5576
5578 err.error.err_i16 = -1;
5579 return err;
5580}
5581
5591 void (*handler)(int, void *), void *ctx) {
5593 if (!config || server_fd < 0 || !handler) {
5594 err.error.err_i16 = -1;
5595 return err;
5596 }
5597
5598#if !defined(CWIST_PLATFORM_NO_PROC)
5599 if (config->use_forking) {
5600 while (atomic_load(&g_cwist_running)) {
5601 int client_fd = http_accept_blocking(server_fd);
5602 if (client_fd < 0) {
5603 int accept_err = errno;
5604 if (accept_err == EBADF || accept_err == EINVAL) break;
5605 if (cwist_accept_error_should_retry(accept_err)) {
5606 cwist_accept_error_backoff(accept_err);
5607 continue;
5608 }
5609 err.error.err_i16 = -1;
5610 return err;
5611 }
5612 int nodelay = 1;
5613 setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
5614 handle_client_forking(client_fd, handler, ctx);
5615 }
5616 }
5617#endif /* __wasi__ (no fork) */
5618
5619 if (config->use_threading) {
5620 /* This loop submits to the classic dynamic pool, so initialise that
5621 * pool whatever CWIST_C1M_MODE says (cwist_app_listen_ex() may have
5622 * overridden it). */
5623 if (cwist_http_pool_init_mode(false) != 0) {
5624 err.error.err_i16 = -1;
5625 return err;
5626 }
5627 while (atomic_load(&g_cwist_running)) {
5628 int client_fd = http_accept_blocking(server_fd);
5629 if (client_fd < 0) {
5630 int accept_err = errno;
5631 if (accept_err == EINTR) continue;
5632 if (accept_err == EBADF || accept_err == EINVAL || accept_err == ENOTSOCK) break;
5633 if (cwist_accept_error_should_retry(accept_err)) {
5634 cwist_accept_error_backoff(accept_err);
5635 continue;
5636 }
5637 continue;
5638 }
5639 int nodelay = 1;
5640 setsockopt(client_fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
5641 cwist_http_pool_submit(client_fd, handler, ctx);
5642 }
5644 err.error.err_i16 = 0;
5645 return err;
5646 }
5647
5648#ifdef __linux__
5649 if (config->use_epoll) {
5650 int flags = fcntl(server_fd, F_GETFL, 0);
5651 if (flags >= 0) fcntl(server_fd, F_SETFL, flags | O_NONBLOCK);
5652
5653 int epoll_fd = epoll_create1(0);
5654 if (epoll_fd < 0) {
5655 err.error.err_i16 = -1;
5656 return err;
5657 }
5658 struct epoll_event event;
5659 event.events = EPOLLIN | EPOLLET;
5660 event.data.fd = server_fd;
5661 if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) < 0) {
5662 close(epoll_fd);
5663 err.error.err_i16 = -1;
5664 return err;
5665 }
5666
5667 while (atomic_load(&g_cwist_running)) {
5668 struct epoll_event events[1024];
5669 /* Bounded under TSan so a deferred SIGTERM gets to run; see
5670 * http_accept_blocking(). */
5672 if (count < 0) {
5673 if (errno == EINTR) continue;
5674 if (errno == EBADF) break;
5675 break;
5676 }
5677 for (int i = 0; i < count; i++) {
5678 if (events[i].data.fd == server_fd) {
5679 while (1) {
5680 int client_fd = accept(server_fd, NULL, NULL);
5681 if (client_fd >= 0) {
5682 int nodelay = 1;
5684 sizeof(nodelay));
5685 handler(client_fd, ctx);
5686 } else {
5687 int accept_err = errno;
5688 if (accept_err == EAGAIN || accept_err == EWOULDBLOCK) break;
5689 if (accept_err == EBADF || accept_err == EINVAL) goto epoll_exit;
5690 if (accept_err == EINTR) continue;
5691 if (cwist_accept_error_should_retry(accept_err)) {
5692 cwist_accept_error_backoff(accept_err);
5693 continue;
5694 }
5695 err.error.err_i16 = -1;
5696 close(epoll_fd);
5697 return err;
5698 }
5699 }
5700 }
5701 }
5702 }
5703 epoll_exit:
5704 close(epoll_fd);
5705 }
5706#endif
5707
5708#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
5709 if (config->use_epoll) {
5710 int flags = fcntl(server_fd, F_GETFL, 0);
5711 if (flags >= 0) fcntl(server_fd, F_SETFL, flags | O_NONBLOCK);
5712
5713 int kqueue_fd = kqueue();
5714 if (kqueue_fd < 0) {
5715 err.error.err_i16 = -1;
5716 return err;
5717 }
5718 struct kevent change;
5720 if (kevent(kqueue_fd, &change, 1, NULL, 0, NULL) < 0) {
5722 err.error.err_i16 = -1;
5723 return err;
5724 }
5725
5726 while (atomic_load(&g_cwist_running)) {
5727 struct kevent events[16];
5728 int count = kevent(kqueue_fd, NULL, 0, events, 16, NULL);
5729 if (count < 0) {
5730 if (errno == EINTR) continue;
5731 if (errno == EBADF) break;
5732 break;
5733 }
5734 for (int i = 0; i < count; i++) {
5735 if ((int)events[i].ident == server_fd) {
5736 while (1) {
5737 int client_fd = accept(server_fd, NULL, NULL);
5738 if (client_fd >= 0) {
5739 int nodelay = 1;
5741 sizeof(nodelay));
5742 handler(client_fd, ctx);
5743 } else {
5744 int accept_err = errno;
5745 if (accept_err == EAGAIN || accept_err == EWOULDBLOCK) break;
5746 if (accept_err == EBADF || accept_err == EINVAL) goto kq_exit;
5747 if (accept_err == EINTR) continue;
5748 if (cwist_accept_error_should_retry(accept_err)) {
5749 cwist_accept_error_backoff(accept_err);
5750 continue;
5751 }
5752 err.error.err_i16 = -1;
5754 return err;
5755 }
5756 }
5757 }
5758 }
5759 }
5760 kq_exit:
5762 }
5763#endif
5764
5765 return cwist_accept_socket(server_fd, NULL, handler, ctx);
5766}
5767#endif /* __wasi__ (socket server runtime) */
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
void * cwist_realloc(void *ptr, size_t new_size)
Resize an existing allocation (libttak tracked).
Definition alloc.c:27
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
Definition alloc.c:32
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
Request-scoped arena allocator backed by libttak arena generations.
struct cwist_arena cwist_arena_t
Definition arena.h:22
void * cwist_arena_alloc(cwist_arena_t *arena, size_t size)
Bump-allocate size bytes from the active generation.
Definition arena.c:44
bool cwist_arena_owns(const cwist_arena_t *arena, const void *ptr)
Check whether ptr lies inside the active generation buffer.
Definition arena.c:54
void cwist_arena_destroy(cwist_arena_t *arena)
Retire the generation and destroy the arena, releasing every allocation made from it at once.
Definition arena.c:60
cwist_arena_t * cwist_arena_create(size_t generation_bytes)
Create an arena backed by a recycled thread-local generation buffer.
Definition arena.c:32
Private wiring between the deferred-response path (async.c) and the C1M connection engine (http....
void cwist_async_forget_response(const cwist_http_response *res)
Definition async_tls.c:50
Safe string type with helper methods.
@ ERR_SSTRING_OKAY
Definition sstring.h:186
cwist_error_t cwist_sstring_append_len(cwist_sstring *str, const char *data, size_t len)
Append raw bytes with length.
Definition sstring.c:212
cwist_error_t cwist_sstring_adopt_region(cwist_sstring *str, char *base, size_t offset, size_t len)
Adopt a heap buffer as a region view without copying.
Definition sstring.c:183
size_t cwist_sstring_get_size(cwist_sstring *str)
Get the current size.
Definition sstring.c:306
cwist_sstring * cwist_sstring_create(void)
Create a new sstring instance.
Definition sstring.c:802
cwist_error_t cwist_sstring_assign_len(cwist_sstring *str, const char *data, size_t len)
Assign raw bytes with length.
Definition sstring.c:88
cwist_error_t cwist_sstring_assign(cwist_sstring *str, const char *data)
Assign a C string.
Definition sstring.c:504
cwist_error_t cwist_sstring_append_sstring(cwist_sstring *str, const cwist_sstring *from)
Append another sstring.
Definition sstring.c:700
void cwist_sstring_destroy(cwist_sstring *str)
Destroy an sstring instance.
Definition sstring.c:823
cwist_error_t cwist_sstring_borrow(cwist_sstring *str, const char *data, size_t len)
Borrow an external buffer without copying.
Definition sstring.c:122
cwist_error_t cwist_sstring_copy_sstring(cwist_sstring *origin, const cwist_sstring *from)
Copy from another sstring.
Definition sstring.c:786
int cwist_sstring_compare_sstring(cwist_sstring *left, const cwist_sstring *right)
Compare two sstrings.
Definition sstring.c:320
cwist_error_t cwist_sstring_adopt_len(cwist_sstring *str, char *buf, size_t len)
Adopt a heap buffer, taking ownership without copying.
Definition sstring.c:151
#define CWIST_ENDPOINT_FILE
#define CWIST_ENDPOINT_DEFAULT
Default option for new endpoints.
@ CWIST_ERR_INT16
used when checking common errcodes in Unix/Linux
Definition cwist_err.h:16
@ CWIST_ERR_INT8
mostly used to check a char
Definition cwist_err.h:15
@ CWIST_ERR_JSON
Definition cwist_err.h:37
cwist_error_t make_error(cwist_errtype_t type)
Creates an error object initialized with the specified type.
Definition error.c:14
#define MSG_DONTWAIT
Definition http3.c:82
struct http_pool_task http_pool_task_t
const char CWIST_BLOB_200_OK[]
Definition http.c:5334
bool cwist_http_pool_rearm_current(int client_fd, void(*handler)(int, void *), void *ctx)
Requeue the current connection's next handler turn on the pool.
Definition http.c:824
const char CWIST_BLOB_404[]
Definition http.c:5336
#define CWIST_RX_REARM_MIN_AVAIL
Definition http.c:1006
cwist_file_begin_result_t
Definition http.c:3261
@ CWIST_FILE_BEGIN_HANDLED
Definition http.c:3263
@ CWIST_FILE_BEGIN_FALLBACK
Definition http.c:3262
cwist_http_request * cwist_http_receive_request(int client_fd, char *read_buf, size_t buf_size, size_t *buf_len, cwist_http_parse_error_t *err_out)
Blocking read of one full request from a classic-pool connection.
Definition http.c:4722
int headers_have_content_length(cwist_http_header_node *headers)
Detect whether a header list already defines Content-Length.
Definition http.c:2394
cwist_async_send_status_t cwist_http_send_response_coalesced(int client_fd, cwist_http_response *res, cwist_http_async_conn_t *conn, bool keep_alive, bool head_only)
Coalescing variant of the async send: serialize small responses directly into the connection's output...
Definition http.c:3779
#define CWIST_ASYNC_STASH_IDLE
Definition http.c:1012
int cwist_http_async_conn_fill(cwist_http_async_conn_t *conn)
Drain the socket into the connection stash. Stops at EAGAIN, and also after a short read: poll is lev...
Definition http.c:4956
bool cwist_http_async_rearm(int client_fd, cwist_reactor_t *reactor, cwist_http_async_conn_t *conn)
Re-arm an async connection for its next event.
Definition http.c:1357
cwist_async_send_status_t cwist_http_send_response_async(int client_fd, cwist_http_response *res, cwist_http_async_conn_t *conn, bool keep_alive, bool head_only)
Async send variant that reports the outcome to the caller instead of re-arming or closing the connect...
Definition http.c:3534
long get_cpu_cores(void)
Return the number of CPUs available to this process.
Definition http.c:99
long cwist_http_inflight_count(void)
Definition http.c:301
void cwist_http_live_idle_leave(void)
Definition http.c:434
void cwist_http_send_error_response(int fd, int status, const char *msg)
Send a minimal error response with Connection: close, used to answer malformed requests (400/413/417/...
Definition http.c:4046
time_t cwist_http_parse_date(const char *str)
Parse an HTTP-date string into a time_t.
Definition http.c:2119
void cwist_http_async_send_response(int client_fd, cwist_http_response *res, cwist_reactor_t *reactor, cwist_http_async_conn_t *conn, bool keep_alive, bool head_only)
Send a response on the async path, parking the unsent remainder on EAGAIN instead of blocking the rea...
Definition http.c:3413
uint64_t cwist_http_reactor_generation(void)
Return the current reactor generation counter.
Definition http.c:268
#define MAGIC_POST
Definition http.c:1572
cwist_http_method_t cwist_http_string_to_method_len(const char *str, size_t len)
Parse a method token (with explicit length) into a method enum.
Definition http.c:1584
const int CWIST_HTTP_LISTEN_FAILED
Definition http.c:1545
size_t cwist_http_serialize_headers(cwist_http_response *res, char *buf, size_t buf_size)
Serialize the status line and headers into a caller-provided buffer.
Definition http.c:2609
cwist_error_t cwist_http_server_loop(int server_fd, cwist_server_config *config, void(*handler)(int, void *), void *ctx)
Run the main HTTP accept loop using the configured concurrency strategy.
Definition http.c:5590
bool cwist_reuseport_enabled(void)
Definition http.c:5347
cwist_error_t cwist_accept_socket(int server_fd, struct sockaddr *sockv4, void(*handler_func)(int client_fd, void *), void *ctx)
Accept one client connection and dispatch it according to the current server strategy.
Definition http.c:5550
int cwist_http_response_stream_end(cwist_http_response *res)
Terminate a streaming response with the final zero-length chunk and flush.
Definition http.c:4549
void cwist_http_response_set_body_ptr(cwist_http_response *res, const void *ptr, size_t len)
Attach an unmanaged zero-copy body pointer to a response.
Definition http.c:2346
#define CWIST_HTTP_INFLIGHT_PER_THREAD
Definition http.c:304
const int CWIST_CREATE_SOCKET_FAILED
Definition http.c:1541
int cwist_http_pool_init(void)
Initialize the HTTP connection pool using CWIST_C1M_MODE.
Definition http.c:678
size_t cwist_http_header_remove(cwist_http_header_node **head, const char *key)
Unlink and release every header node matching a name.
Definition http.c:1907
cwist_sstring * cwist_get_client_ip_from_fd(int fd)
Resolve the peer IP address for a connected client socket.
Definition http.c:2141
bool cwist_tcp_cork_enabled(void)
TCP_CORK coalescing stub for platforms without TCP_CORK.
Definition http.c:2684
void cwist_http_pool_limit_core(unsigned int limit)
Fix the HTTP pool worker count to a specific value.
Definition http.c:145
const char CWIST_BLOB_500[]
Definition http.c:5338
cwist_error_t cwist_http_send_response(int client_fd, cwist_http_response *res)
Serialize and send an HTTP response to a connected client socket.
Definition http.c:2878
void cwist_http_format_date(time_t t, char *buf, size_t len)
Resolve the peer IP address for a connected client socket.
Definition http.c:2108
cwist_error_t cwist_http_response_send_file(cwist_http_response *res, const char *file_path, const char *content_type_hint, size_t *out_size)
Prepare a response to serve a file either by buffering or direct streaming.
Definition http.c:5223
int cwist_make_socket_ipv4(struct sockaddr_in *sockv4, const char *address, uint16_t port, uint16_t backlog)
Create, configure, bind, and listen on an IPv4 TCP socket.
Definition http.c:5361
#define CWIST_HTTP_BODY_ZC_MIN
Definition http.c:4913
char * cwist_http_header_get(cwist_http_header_node *head, const char *key)
Find a header value using case-insensitive header-name comparison.
Definition http.c:1880
long get_optimal_thread_count(void)
Compute the HTTP worker/event-loop thread count.
Definition http.c:159
#define CWIST_HTTP_INFLIGHT_FD_RESERVE
Definition http.c:305
int cwist_http_coalesce_flush_blocking(int client_fd, cwist_http_async_conn_t *conn)
Flush the coalesced-output stash, blocking in poll() until drained.
Definition http.c:3737
cwist_error_t cwist_http_send_response_head(int client_fd, cwist_http_response *res)
Send only the status line and headers of a response (RFC 9110 section 9.3.2 HEAD semantics): Content-...
Definition http.c:2961
cwist_recv_status_t cwist_http_receive_request_nb(cwist_http_async_conn_t *conn, cwist_http_request **out, cwist_http_parse_error_t *err_out)
Try to assemble one complete request from the stash without any blocking IO. On CWIST_RECV_OK the con...
Definition http.c:5035
@ HTTP_PARK_INLINE
Definition http.c:3010
@ HTTP_PARK_FILE
Definition http.c:3011
@ HTTP_PARK_BODYREF
Definition http.c:3012
void cwist_http_response_add_hsts(cwist_http_response *res)
Add Strict-Transport-Security to a TLS response (HTTPS only).
Definition http.c:1993
int cwist_http_coalesce_error_response(cwist_http_async_conn_t *conn, int status)
Build a minimal text error response into the coalesce stash.
Definition http.c:3756
void cwist_http_response_set_body_ptr_managed(cwist_http_response *res, const void *ptr, size_t len, cwist_http_body_cleanup_fn cleanup, void *ctx)
Attach a managed zero-copy body pointer and optional cleanup hook to a response.
Definition http.c:2358
cwist_coalesce_flush_status_t cwist_http_coalesce_flush(int client_fd, cwist_http_async_conn_t *conn, bool keep_alive)
Flush the coalesced-output stash with one speculative write.
Definition http.c:3682
void cwist_http_request_destroy(cwist_http_request *req)
Destroy a parsed HTTP request and all nested allocations it owns.
Definition http.c:2180
cwist_http_request * cwist_http_parse_request(const char *raw_request)
Parse a NUL-terminated raw HTTP request buffer.
Definition http.c:4412
void cwist_http_response_add_security_headers(cwist_http_response *res)
Add default security headers to an HTTP response if not already present.
Definition http.c:1935
cwist_http_request * cwist_http_parse_request_len(const char *buf, size_t len)
Parse a raw HTTP request buffer with an explicit length.
Definition http.c:4424
void cwist_http_pool_submit(int client_fd, void(*handler)(int, void *), void *ctx)
Queue a connection for handling on the classic dynamic pool.
Definition http.c:772
bool cwist_http_live_idle_enter(void)
Definition http.c:423
cwist_sstring * cwist_http_stringify_response(cwist_http_response *res)
Materialize an HTTP response into a contiguous string for debugging or TLS writes.
Definition http.c:4099
#define CWIST_ASYNC_STASH_MAX
Definition http.c:4908
void cwist_http_async_close_orphan(int client_fd, cwist_http_async_conn_t *conn)
Close an async connection whose owning worker pool is gone.
Definition http.c:1453
long cwist_http_continuation_shed_count(void)
Definition http.c:293
cwist_error_t cwist_http_header_add(cwist_http_header_node **head, const char *key, const char *value)
Prepend one header node to the linked-list header collection.
Definition http.c:1861
void cwist_http_response_destroy(cwist_http_response *res)
Destroy an HTTP response and release any attached body resources.
Definition http.c:2315
struct http_live_conn http_live_conn_t
const int CWIST_HTTP_BIND_FAILED
Definition http.c:1543
#define MAGIC_DELE
Definition http.c:1574
void cwist_http_header_free_all(cwist_http_header_node *head)
Destroy every node in a request or response header list.
Definition http.c:2006
bool cwist_http_pool_submit_async(int client_fd, cwist_async_handler_t handler, void *ctx)
Submit a connection to the C1M async reactor pool.
Definition http.c:1468
cwist_http_response * cwist_http_response_create(void)
Allocate a response backed by its own arena.
Definition http.c:2291
int cwist_http_response_stream_write(cwist_http_response *res, const char *data, size_t len)
Append one data chunk to a streaming response.
Definition http.c:4519
#define MAGIC_HEAD
Definition http.c:1575
#define MAGIC_GET
Definition http.c:1571
#define HTTP_ACCEPT_WAIT_MS
Definition http.c:5513
void cwist_http_pool_destroy(void)
Tear down both pool variants.
Definition http.c:836
cwist_http_response * cwist_http_response_create_in_arena(void *arena)
Allocate a response carved from a caller-owned arena.
Definition http.c:2306
const int CWIST_HTTP_UNAVAILABLE_ADDRESS
Definition http.c:1542
const char * cwist_http_method_to_string(cwist_http_method_t method)
Convert a HTTP method enum into its wire-format token.
Definition http.c:1554
int cwist_http_response_serialize(cwist_http_response *res, char **out, size_t *out_len)
Serialize a full response (headers + body) into a heap buffer.
Definition http.c:4439
#define CWIST_POOL_STACK_SIZE
Definition http.c:353
cwist_http_method_t cwist_http_string_to_method(const char *method_str)
Parse a NUL-terminated method string into a method enum.
Definition http.c:1629
const int CWIST_HTTP_SETSOCKOPT_FAILED
Definition http.c:1544
#define CWIST_ASYNC_STASH_IDLE_SEC_DEFAULT
Definition http.c:960
int cwist_http_response_stream_begin(cwist_http_response *res)
Switch a response to chunked streaming mode.
Definition http.c:4503
int cwist_http_pool_init_mode(bool use_c1m)
Initialize the HTTP connection pool in the requested mode.
Definition http.c:696
bool cwist_http_reactor_post_live(cwist_reactor_t *reactor, uint64_t gen, cwist_reactor_post_t *node)
Post a reactor node only while the captured generation is current.
Definition http.c:283
#define MAGIC_PUT
Definition http.c:1573
void cwist_http_response_set_alt_svc(cwist_http_response *res, const char *alt_svc)
Set the Alt-Svc header value for HTTP/3 upgrade advertisement.
Definition http.c:2376
const char * cwist_http_status_reason(int status)
Map an HTTP status code to its RFC reason phrase.
Definition http.c:3970
int cwist_http_coalesce_append(cwist_http_async_conn_t *conn, const void *data, size_t len)
Append bytes to a connection's coalesced-output stash.
Definition http.c:3653
cwist_http_request * cwist_http_request_create(void)
Allocate and initialize a default HTTP request object.
Definition http.c:2056
#define CWIST_HTTP_READ_BUFFER_SIZE
Definition http.h:42
#define CWIST_HTTP_MAX_BODY_SIZE
Definition http.h:41
#define CWIST_HTTP_MAX_HEADER_SIZE
Definition http.h:40
cwist_http_parse_error_t
Failure reason reported by the request receive APIs.
Definition http.h:46
@ CWIST_HTTP_PARSE_EOF
Definition http.h:48
@ CWIST_HTTP_PARSE_OK
Definition http.h:47
@ CWIST_HTTP_PARSE_BODY_TOO_LARGE
Definition http.h:51
@ CWIST_HTTP_PARSE_HEADER_OVERFLOW
Definition http.h:50
@ CWIST_HTTP_PARSE_MALFORMED
Definition http.h:49
@ CWIST_HTTP_PARSE_TE_UNSUPPORTED
Definition http.h:52
@ CWIST_HTTP_PARSE_EXPECT_FAILED
Definition http.h:53
@ CWIST_HTTP_OK
Definition http.h:28
cwist_http_method_t
Definition http.h:16
@ CWIST_HTTP_OPTIONS
Definition http.h:23
@ CWIST_HTTP_HEAD
Definition http.h:22
@ CWIST_HTTP_PUT
Definition http.h:19
@ CWIST_HTTP_PATCH
Definition http.h:21
@ CWIST_HTTP_UNKNOWN
Definition http.h:24
@ CWIST_HTTP_POST
Definition http.h:18
@ CWIST_HTTP_GET
Definition http.h:17
@ CWIST_HTTP_DELETE
Definition http.h:20
#define CWIST_HTTP_TIMEOUT_MS
Definition http.h:43
Classic-pool connection registry hooks for shutdown (private).
Lightweight macro-based internal logging for CWIST.
#define CWIST_LOG_WARN(fmt,...)
Definition log.h:56
Production-grade metrics engine for CWIST.
cwist_metrics_registry_t * cwist_metrics_registry(void)
Obtain the global registry, creating it on first call.
Definition metrics.c:112
@ CWIST_METRIC_HTTP_HEADER_OVERFLOW
Definition metrics.h:60
@ CWIST_METRIC_HTTP_CONTINUATION_SHED
Definition metrics.h:62
void cwist_metric_inc(cwist_metrics_registry_t *reg, cwist_metric_id_t id)
Increment a counter metric by 1.
Definition metrics.c:179
HTTP Protocol Definitions and Helpers.
#define CWIST_HTTP_HEADERS_TIMEOUT_MS
Definition http.h:147
cwist_async_action_t(* cwist_async_handler_t)(int fd, struct cwist_http_async_conn *conn)
Definition http.h:454
#define CWIST_HTTP_KEEP_ALIVE_TIMEOUT_SEC
Definition http.h:150
cwist_async_send_status_t
Definition http.h:528
@ CWIST_ASYNC_SEND_DEFERRED
Definition http.h:531
@ CWIST_ASYNC_SEND_CLOSE
Definition http.h:530
@ CWIST_ASYNC_SEND_KEEPALIVE
Definition http.h:529
#define CWIST_HTTP_COALESCE_MAX
Definition http.h:547
cwist_coalesce_flush_status_t
Definition http.h:549
@ CWIST_COALESCE_FLUSH_DONE
Definition http.h:550
@ CWIST_COALESCE_FLUSH_ERROR
Definition http.h:552
@ CWIST_COALESCE_FLUSH_PARKED
Definition http.h:551
@ CWIST_HTTP_CONNECT
Definition http.h:45
cwist_async_action_t
Definition http.h:447
@ CWIST_ASYNC_CLOSE
Definition http.h:448
@ CWIST_ASYNC_DEFER
Definition http.h:451
@ CWIST_ASYNC_DETACH
Definition http.h:450
void(* cwist_http_body_cleanup_fn)(const void *ptr, size_t len, void *ctx)
Definition http.h:194
cwist_recv_status_t
Definition http.h:565
@ CWIST_RECV_FATAL
Definition http.h:568
@ CWIST_RECV_NEED_MORE
Definition http.h:567
@ CWIST_RECV_OK
Definition http.h:566
cwist_query_map * cwist_query_map_create_in_arena(void *arena)
Allocate a query map within a memory arena to avoid GC/heap allocations.
Definition query.c:61
void cwist_query_map_destroy(cwist_query_map *map)
Destroy a query map.
Definition query.c:81
void cwist_query_map_parse(cwist_query_map *map, const char *raw_query)
Parse raw query strings (e.g., "a=1&b=2") into the map.
Definition query.c:319
size_t head
Definition otel.c:57
size_t count
Definition otel.c:58
size_t cap
Definition otel.c:56
Target capability macros shared by all CWIST sources.
#define CWIST_REACTOR_PAYLOAD_SIZE
Definition reactor.h:19
struct cwist_reactor cwist_reactor_t
Definition reactor.h:14
bool cwist_reactor_add(cwist_reactor_t *reactor, int fd, cwist_reactor_cb_t cb, const void *payload, size_t payload_size)
Definition reactor.c:1357
struct cwist_reactor_post cwist_reactor_post_t
void cwist_reactor_destroy(cwist_reactor_t *reactor)
Definition reactor.c:1116
void cwist_reactor_stop(cwist_reactor_t *reactor)
Definition reactor.c:1904
cwist_reactor_t * cwist_reactor_create(void)
Definition reactor.c:1006
void cwist_reactor_run(cwist_reactor_t *reactor)
Definition reactor.c:1579
bool cwist_reactor_add_out(cwist_reactor_t *reactor, int fd, cwist_reactor_cb_t cb, const void *payload, size_t payload_size)
Definition reactor.c:1362
Signed client-side session cookie API.
void cwist_session_destroy(cwist_session_t *session)
Destroy a session handle and its data.
Definition session.c:464
Unified Graceful Shutdown support for CWIST.
int g_cwist_drain_timeout_sec
Drain timeout in seconds before process exits.
Definition shutdown.c:24
atomic_int g_cwist_running
Global running flag. Checked by all server event loops.
Definition shutdown.c:17
Listen-socket option knobs shared by the bind sites (private).
Integrated error structure used throughout the system.
Definition cwist_err.h:72
uint32_t last_active_sec
Definition http.h:467
bool expect_continue_sent
Definition http.h:466
uint32_t stash_grown_sec
Definition http.h:502
uint64_t rx_armed_ns
Definition http.h:488
cwist_reactor_t * reactor
Definition http.h:469
cwist_async_handler_t handler
Definition http.h:470
uint32_t worker_id
Definition http.h:468
Linked list for headers to handle multiple headers easily.
Definition http.h:59
bool arena_owned
True when the node lives in a request arena (freed with the arena).
Definition http.h:160
cwist_sstring * value
Definition http.h:61
cwist_sstring * key
Definition http.h:60
struct cwist_http_header_node * next
Definition http.h:62
cwist_query_map * query_params
Parsed query parameters.
Definition http.h:69
bool te_chunked_seen
Parser saw and validated Transfer-Encoding: chunked (HTTP/1.1).
Definition http.h:173
char * csrf_token
CSRF token populated by csrf middleware.
Definition http.h:186
size_t content_length
Definition http.h:79
cwist_query_map * flash
Flash messages for this request (one-time read).
Definition http.h:184
cwist_sstring * path
e.g., "/users/1"
Definition http.h:67
void * async_conn
C1M connection shell when on the reactor path (internal).
Definition http.h:188
cwist_sstring * query
e.g., "active=true" (raw)
Definition http.h:68
bool expect_100_seen
Parser saw and validated Expect: 100-continue (HTTP/1.1).
Definition http.h:174
void * h2_queue
HTTP/2 async queue when on H2 path (internal).
Definition http.h:191
struct cwist_app * app
Owning app context (if any).
Definition http.h:176
uint32_t stream_id
HTTP/2 or HTTP/3 stream ID (0 for HTTP/1.1).
Definition http.h:77
cwist_sstring * body
Definition http.h:73
void * arena
Per-request arena for bump-allocated structs (internal).
Definition http.h:187
void * session
cwist_session_t pointer set by session middleware.
Definition http.h:185
cwist_sstring * version
e.g., "HTTP/1.1"
Definition http.h:71
void * private_data
Internal framework use (protocol-specific context).
Definition http.h:78
cwist_query_map * path_params
Parsed path parameters (e.g. :id)
Definition http.h:70
void * https_conn
HTTPS connection wrapper when on TLS path (internal).
Definition http.h:190
cwist_http_header_node * headers
Definition http.h:72
bool keep_alive
Definition http.h:74
cwist_endpoint_opt_t endpoint_opts
Behavior hints for the active endpoint.
Definition http.h:183
cwist_db * db
Shared database handle from cwist_app.
Definition http.h:177
cwist_http_method_t method
Definition http.h:66
HTTP Response Object. Supports standard string body or Zero-Copy pointer body.
Definition http.h:82
cwist_endpoint_opt_t endpoint_opts
Mirrors req->endpoint_opts.
Definition http.h:206
cwist_sstring * version
e.g., "HTTP/1.1"
Definition http.h:83
cwist_sstring * status_text
e.g., "OK"
Definition http.h:85
void * ptr_body_cleanup_ctx
User data for release hook.
Definition http.h:213
int file_stream_fd
Open descriptor for sendfile.
Definition http.h:217
bool use_file_stream
Fast File Streaming.
Definition http.h:216
cwist_sstring * body
Definition http.h:87
bool arena_borrowed
Definition http.h:240
const void * ptr_body
Pointer to external data (e.g., mmap region)
Definition http.h:210
void * stream_sink_ctx
Definition http.h:231
cwist_http_status_t status_code
Definition http.h:84
bool file_stream_auto_close
Close fd after streaming.
Definition http.h:220
off_t file_stream_offset
Current offset for sendfile loop.
Definition http.h:219
char * alt_svc
Alt-Svc header for HTTP/3 upgrade advertisement.
Definition http.h:237
cwist_http_header_node * headers
Definition http.h:86
bool is_ptr_body
Zero-Copy Pointer Body.
Definition http.h:209
cwist_http_body_cleanup_fn ptr_body_cleanup
Optional release hook.
Definition http.h:212
size_t stream_flushed
stream_buf prefix already pushed to stream_sink.
Definition http.h:229
bool stream_head_sent
Head serialized and pushed to the sink.
Definition http.h:232
int(* stream_sink)(void *ctx, const char *data, size_t len)
Live sink (streaming dispatch only).
Definition http.h:230
void * arena
Per-response arena for bump-allocated structs (internal).
Definition http.h:239
bool stream_mode
cwist_http_response_stream_begin() ran.
Definition http.h:225
cwist_sstring * stream_buf
Framed chunk payload (hex\r\ndata\r ...).
Definition http.h:228
size_t ptr_body_len
Length of external data.
Definition http.h:211
bool stream_failed
A live sink rejected a chunk/head.
Definition http.h:227
size_t file_stream_len
Total bytes to stream.
Definition http.h:218
bool stream_ended
cwist_http_response_stream_end() ran.
Definition http.h:226
Definition reactor.h:46
bool use_forking
Process per request.
Definition http.h:220
bool use_threading
Thread per request.
Definition http.h:221
bool use_epoll
Use epoll for accepting.
Definition http.h:222
int(* compare)(struct cwist_sstring *left, const struct cwist_sstring *right)
Should mimic strcmp, internally use strncmp.
Definition sstring.h:27
size_t size
Definition sstring.h:20
char * base
Definition sstring.h:24
char * data
Access directly only when raw handling is necessary.
Definition sstring.h:15
size_t capacity
Definition sstring.h:21
cwist_error_t(* append)(struct cwist_sstring *str, const struct cwist_sstring *from)
Append another sstring.
Definition sstring.h:35
cwist_error_t(* copy)(struct cwist_sstring *str, const struct cwist_sstring *from)
Definition sstring.h:30
bool owns_storage
Definition sstring.h:17
size_t(* get_size)(struct cwist_sstring *str)
Definition sstring.h:26
bool is_fixed
Definition sstring.h:16
bool borrows_buffer
data is borrowed (static/arena); never freed, detached on mutation
Definition sstring.h:18
Unified error code and type definitions for the CWIST system layer.
void cwist_error_dispose(cwist_error_t *err)
Release resources owned by an error value. Currently only CWIST_ERR_JSON owns memory (the cJSON paylo...
Definition error.c:43
WASI target feature detection shared by all CWIST sources.
Zero-latency speculative fast-path writer and P2C worker scheduler.
cwist_write_status_t
Definition writer_fast.h:24
@ CWIST_WRITE_DONE
Definition writer_fast.h:25
@ CWIST_WRITE_PENDING
Definition writer_fast.h:27
@ CWIST_WRITE_ERR
Definition writer_fast.h:29
cwist_write_status_t cwist_http_sendmsg_speculative(int fd, struct iovec *iov, int iovcnt, int flags, size_t *total_sent)
Speculative scatter/gather sendmsg without blocking poll().
Definition writer_fast.c:83
uint32_t cwist_sched_p2c_select_worker(const _Atomic uint32_t *worker_loads, uint32_t num_workers)
Select worker using Power of Two Random Choices (P2C).
uint32_t cwist_fast_monotonic_sec(void)
Fast monotonic clock in seconds (vDSO coarse where available).