Conversation
…hy not segments Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
…ds through the reader io/pipe.h: the reader hands out received bytes as one contiguous span at a time - in place in the kernel's buffer when they lie within one, gathered into the consumer's buffer when they span or when kept bytes must stay contiguous - with read/examine/ drop/keep/run_begin/release/copy; it holds at most two kernel buffers, one with kept bytes of earlier runs, one with the live bytes. The writer is a slab with reserve/ advance/write/flush/send. Both suspend the coroutine when they must wait. The engine now reads through the reader: read_head parses the live span in place and keeps the head (zero copy when it arrived in one receive), the chunk parser walks spans, ioma_body_all keeps its bytes, streamed reads copy straight from the kernel's buffer, and release at the end of a request replaces the pipelining carry-over. The 16 KB request buffer is the reader's gathering buffer. Public: ioma_pipe with ioma_run_pipes for raw TCP handlers, on the same runner as ioma_run. tests/pipe-server.c is a line echo on it, tests/pipes.py drives it (split lines, two lines in a packet, a line spanning kernel buffers, one too long); smoke gains a head split across three sends and a body arriving after its head. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
conn.h is what a user of a connection needs: RX_QUEUE, rx_item, the recv state, struct conn, and the awaits (await_recv, await_send, ioma__await_item). proactor.h keeps the worker: the ring tunables, struct proactor, run and spawn. The pipe and the HTTP plane include conn.h; run.c takes proactor.h as well. The loop-side plumbing conn.c and proactor.c share stays in io/internal.h. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
Its tunables, BUF_COUNT and BUF_SIZE with the power-of-two assert, its struct - ring, slab, local tail, the dirty and returned flags that used to be loose fields of the proactor - and its operations with the module prefix: init, return, publish, unregister, unmap, and ioma__bufring_at for a buffer's bytes. The proactor embeds one; conn.c, pipe.c and proactor.c address it through it. internal.h keeps only what the plane's files share. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
…'s header RX_MASK and CONN_POOL_MAX join RX_QUEUE in conn.h, with conn.c's loop-facing exports (conn_new, conn_main, arm_recv, on_recv, pool_drain) in a section of their own, the way bufring.h declares its loop-facing init and publish. ioma__sqe, the worker's SQE claim, is in proactor.h. What is left in internal.h is what the loop and the operations share and neither owns: the user_data tags, UD, op_t, and the trace switch. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
A header per module that exports something, as in the I/O plane: engine.h declares ioma__serve for the runner, router.h declares ioma__router_build and ioma__dispatch for the runner and the engine. internal.h is down to ioma__hexval, shared by the percent decoder and the chunk-size parser. api.c and run.c export nothing private and get no header. Each file includes the standard headers it uses. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
…tion, await_recv gone The pipewriter has a head and a tail now: front(n) reserves bytes just before the pending data, back(n) just after it, and one flush sends the span. The HTTP reply's lead and slack are the writer's - the head and a chunk's size line go in front, a chunk's CRLF behind - and the chunked terminator rides the final send instead of a send of its own. The engine no longer owns a slab or calls await_send; ioma_response loses buf/cap/len, and ioma_reserve / ioma_advance on the context are the way to format straight into the reply. The connection's coroutine builds the pipe, so every handler receives one: ioma__serve takes a pipe, ioma_run_pipes passes the user's function through, and run.c's wrapper is gone. await_recv had no callers left and is deleted; the reader's primitive, ioma__await_item, is the one way bytes leave the queue. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
include/ for the public header, lib/ for the library, tests/ and playground/ for what links it. Build files, the tidy config and the docs follow; the sources' includes are relative to the include path and did not change. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
A forward-only writer, the shape of .NET's Utf8JsonWriter: object, array, end, key, string, cstr, int, uint, double, bool, null and raw, each putting its bytes straight into a sink - the reply (content-type set for you), a raw pipe, or a memory buffer - with no tree and no allocation. Strings are escaped as they are copied, a safe run at a time; doubles take the shortest of 15, 16 or 17 significant digits that reads back the same, decimal point forced to '.'; nan and inf become null. Nesting and the commas each level owes are two bits per level; too deep, a full buffer or a gone peer fail the writer and every later call. Unit tests cover escaping, every number edge, empty containers, a top-level scalar, the depth limit and a full buffer. The fixture serves GET /json/:id and GET /json/big, whose 3000 objects stream out chunked while the writer keeps going. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
The C sibling of ioxide, the .NET library this design started from, gets a name that says so and stays four letters: ioxd_ and IOXD_ prefixes, <ioxd.h>, libioxd, ioxd.pc, the ioxd CMake package, IOXD_WORKERS and IOXD_PORT, the ioxd: log prefix, the server header the fixture sends. Mechanical throughout; nothing else changed. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
IOXD_JSON_STRUCT(name, FIELDS) expands a field list twice - into the struct's members and into a name_to_json function - so the two cannot drift. A line is the field's kind, its type (or the nested struct's name) and its name: VALUE picks the writer by C type through _Generic; OBJECT calls the nested struct's function; OPTIONAL is a pointer that comes out as null when NULL; ARRAY and OBJECTS loop over a count field. Plain text substitution: the generated function is the code one would write by hand, with no table and nothing at runtime. IOXD_JSON_WRITER makes only the function, for a struct declared elsewhere; IOXD_JSON_FIELD and IOXD_JSON_VALUE serve hand-written documents. A null char pointer serializes as null. The hello example's GET /users/:id fills a struct - nested address, optional billing, an array of tags, an array of order objects - and serializes it with one call; unit tests cover every kind, empty and null cases, and the field macro; a strict-C11 consumer compiles the macros without a warning. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
slice.h (slices, conversions, key/value parsing), http.h (request, response, context, body, reply, ioxd_run), router.h (groups, endpoints, middleware, the script macros), json.h (the writer, structs described once) and pipe.h (a connection as a pipe). Each stands on its own under strict C11 and includes what it needs; ioxd.h includes them all. Installed as <prefix>/include/ioxd.h plus <prefix>/include/ioxd/*.h, so a consumer still writes #include <ioxd.h> with -I<prefix>/include; pkg-config, the Makefile and CMake follow. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
…ion; static files over the ring with a watched snapshot Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
Every worker opens its own SO_REUSEPORT socket on each port; an accept CQE carries its listener, and a connection remembers the port it came in on and the port's TLS store - the hook the TLS prologue will use. ioxd_listen(port, tls) before the run adds a listener, at most eight; ioxd_run's own port joins as a plain one, and 0 there means only the added ones. The fixture serves a second plain port and the smoke suite hits it. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
…ificate store
A TLS listener's connection runs a prologue before its handler: OpenSSL's TLS 1.3
handshake through memory BIOs, reading ciphertext from the pipe and flushing its
flights through it; the keylog callback catches the traffic secrets; HKDF-Expand-Label
makes key and IV; TCP_ULP "tls" and SOL_TLS TLS_TX / TLS_RX go in through the ring
(SOCKET_URING_OP_SETSOCKOPT, so the registered file table needs no fd). From then on
the kernel does every record and nothing above the pipe knows. The receive handoff is
exact: the multishot recv is paused, whatever it already delivered is decrypted in
userspace record by record - the tail of a split record fetched straight from the
socket - the plaintext injected into the reader, TLS_RX installed at the sequence
consumed, the recv resumed. One suite (TLS_AES_128_GCM_SHA256), no tickets, no 0-RTT;
a client that cannot do that fails the handshake.
The store loads <dir>/<host>/{cert,key}.pem per hostname, `default` for no SNI or no
match, `_.example.com` for a wildcard; the ClientHello callback picks the context;
ioxd_tls_reload swaps a reference-counted table under handshakes in flight. make TLS=0
builds without OpenSSL.
Also: await_send no longer sets MSG_WAITALL - kernel TLS refuses it and the loop
finishes short sends anyway. Tests: self-signed certificates from tests/mkcerts.sh,
the fixture listening on a third port, and smoke checks through Python's ssl for the
default certificate, SNI, an unknown name, a POST body over kernel RX, keep-alive, a
3000-object streamed reply over kernel TX, and a refused TLS 1.2 client.
Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
…, a 1 MB upload over TLS tests/tls_early.py (tlslite-ng, skipped when it is not installed) holds the client's writes back to send the request in the same TCP write as its Finished - whole, and with the record cut in two with a pause, so the server must decrypt what the kernel already delivered and fetch the tail of a split record before kernel RX takes over - then two early requests with the second split, a close_notify, and a corrupted record. make check runs it with TLS_PYTHON. The limits, at their boundaries, as known and to be grown later: a 15 KB request head served and one over 16 KB a 431; a 16000-byte body read whole and 16400 a 413; the 16 reply headers (15 beside the middleware's) with the next refused and the reply still served; a 3 KB reply header served and a 5 KB one closing the connection; 32 of 40 query parameters kept; 70 request headers a 400. Over TLS: a 1 MB upload streamed through kernel RX in 256 reads. The raw exchange helper decodes chunked replies. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
…uzzer as a make target A TLS connection now ends with a close_notify alert sent as a control record through the kernel (sendmsg with TLS_SET_RECORD_TYPE, over the ring), so clients see a clean close; the wire test checks it. The fixture's default certificate is RSA - what most clients and test suites assume - with sni.test on ECDSA so both key types serve. make check-tlsfuzzer runs the six tlsfuzzer TLS 1.3 scripts that apply to a one-suite, TLS 1.3-only server; TLS.md records what the rest probe and why they fail by design, and the one real limit: a control record from the peer ends the connection without an alert of ours. The prologue's split-record scratch moved off the coroutine stack. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
…client's Finished is not lost The handshake loop wrote every delivered byte into the read BIO. When the client's Finished and its first application record arrived in one segment, the record vanished into OpenSSL: the drain found nothing, kernel RX was installed one sequence number behind, and the connection hung. Records are now assembled one at a time in the prologue's own scratch, during the handshake and during the drain alike, so what follows the Finished stays in the reader; a record cut by the pause is completed from the socket. The drain stops at a close_notify and refuses a post-handshake message it cannot follow; the early plaintext is bounded by what the reader can take. The early-data test held the client's Finished through send() while tlslite sends it through sendall(), so its three coalescing cases never coalesced; the wrapper holds both now and the cases fail on the old code. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
… cannot serve is refused A reload carries a host that failed to load forward by sharing its old SSL_CTX, and the loop after it then called SSL_CTX_set_client_hello_cb(ctx, on_client_hello, new_table) on every context of the new table - re-pointing, with no lock, a context worker threads are reading and that a handshake already in flight was created from. A client sitting on its ClientHello was answered out of a table it never started on, and out of freed memory once that table was dropped. The callback goes on in context_for now, once, with a NULL argument, and finds the table through the SSL's ex_data instead: the prologue calls ioxd__tls_bind right after SSL_new and holds a reference to that table for as long as the SSL lives. Nothing writes to a context after it is published. A loaded certificate is checked against the clock - X509_cmp_time on notBefore and notAfter - and one that is not valid yet or has run out is a load failure like any other, so the host keeps what was serving. `default` is required: without it ioxd_tls_new fails instead of promoting whatever readdir returned first, and a reload that loses it keeps the previous fallback by host name or is refused whole. ioxd_tls_reload holds a reload mutex over all of it and a reference to the table it reads, so it no longer walks a table it does not own and no longer races another reload. Smaller things: host_eq folds only A-Z, so CR no longer matches `-` and DEL `_`; one trailing root dot is dropped, so `sni.test.` finds sni.test; the SNI extension must be exactly the one host_name entry we read (name_len + 3 == list_len, list_len + 2 == ext_len); ssl_error reports ERR_peek_last_error, the outermost reason, and clears the queue behind it; calloc and strdup are checked; snprintf truncation, S_ISREG on both files, and a warning for a key.pem readable past its owner; errno.h and limits.h come in directly. ioxd_tls_free joins the public header, with the note that it is not for a store a listener still uses; the TLS=0 stubs keep up. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
… it; a reply's head follows the request and the status; copied headers Request side (RFC 9112): a Content-Length is digits and nothing else, two of them must agree, Transfer-Encoding must end in chunked and name no other coding (501 otherwise), the two framings never come together, a folded header line is refused, an HTTP/1.1 request carries exactly one Host, every Connection line counts with close winning, an absolute-form target is routed by its path, a chunk size line keeps to the grammar, trailers are bounded, and a query that does not fit its arrays is a 400 or 414 rather than a partial view. Each was a request-smuggling vector behind a proxy. Reply side: no body after HEAD or on a 1xx, 204 or 304, no framing header on a 1xx or 204, a status outside three digits goes out as 500, a declared Content-Length is held to (corrected when buffered whole, cut and closed when streamed), a body left unread past the drain limit closes, and an Expect: 100-continue gets its interim reply on the first body read - or a close when the handler never reads. ioxd_header and ioxd_content_type copy into a per-reply arena (the head was serialized after the handler's frame was gone) and refuse control bytes, non-token names and the engine's own headers. ioxd_content_type and ioxd_content_length return whether they applied. ioxd_run checks that the application's ioxd_ctx is the library's, since the limits that size it are compile-time constants. The pipe reader pins a kernel buffer whose kept run it moves, so a kept pointer stays valid until release; drop and keep are clamped to the live bytes, the writer's advance to the slab; a raw handler's unsent slab is flushed at close. ioxd_cstr refuses an embedded NUL, %00 stays literal. The public headers under include/ioxd/ were never committed: a bare "ioxd" in .gitignore matched the directory. tests/conformance.py drives all of it at the wire level; the fixture routes it needs follow with the test-suite merge. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
# Conflicts: # include/ioxd/tls.h
…inst its level A double was formatted with whatever decimal point LC_NUMERIC named and mended one byte afterwards, so a locale whose point is several bytes - fa_IR, ps_AF - turned every double into invalid JSON, quietly: "0.1" came out as 0x30 0x2e 0xab 0x31. The per-double localeconv() handed back a process-wide static besides, read from every worker. The numbers now go through a locale_t made once under pthread_once, worn with uselocale() around the snprintf/strtod pair and handed straight back, and the writer fails if that locale cannot be made. A float has a path of its own - 6 to 9 significant digits, round-tripped with strtof - so 0.1f prints as 0.1 and not as the double it was promoted to; _Generic routes float: to it. What a call may do is checked now: a key belongs in an object, one per value; a value inside an object has to follow a key; an end needs something open and the pair it closes complete. Each refusal marks the writer failed, where an unbalanced end used to return false silently and the writer kept accepting. ioxd_json_done() answers the other half - nothing failed, nothing left open - so a missing end is no longer invisible. Also: the depth is checked before the separator is written, so a document one level too deep leaves no comma behind; IOXD_JSON_DEPTH is 63 with a static_assert under it, since level 64 aliased the root's bit through depth & 63; an empty ioxd_json_raw fails rather than emitting "[,]"; and a run the sink refuses whole is asked for again halved, down to 64 bytes, since the reply and the pipe both refuse a reserve larger than their slab. In the header: IOXD_JSON_FIELD's answer goes through a static inline, so a field written for its effect is a statement and not a value the compiler sees discarded; the USER_FIELDS example no longer shows a // note before a line continuation, which would swallow the lines after it, and points at playground/hello/main.c for the block-comment form; and it says that strings go out byte for byte, invalid UTF-8 included, so untrusted input has to be validated first. The unit tests cover each of these. The locale one tries ps_AF, fa_IR, de_DE and fr_FR and prints a note instead when the box has none of them. include/ioxd/json.h enters the tree here: .gitignore's `ioxd` line, meant for an old binary at the root, also matched include/ioxd/, so none of the split headers were ever committed and this one no longer matches the json.c beside it. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
# Conflicts: # include/ioxd/json.h
…e machine's stranded corners Stopping used to abandon whatever was open: parked coroutines, their stacks, the conn_t and the socket, and then it unmapped the recv slab while multishot recvs could still be in flight. A worker now drains instead - it cancels the accepts by name, asks the kernel to cancel everything else with one ASYNC_CANCEL_ANY, ends by hand the recvs parked on -ENOBUFS that hold no operation to cancel, and keeps running the loop, ignoring the stop flag, until the last connection has closed itself or two seconds are up. Parked coroutines wake with errors, handlers unwind, stacks go back to the pool; if any are still open the slab stays mapped and the log says so. Twenty idle keep-alive connections now go from SIGINT to "0 still open" in under 100 ms. conn.c's pause/resume had corners that stranded a coroutine for good. The -ENOBUFS branch ignored c->pausing, so a TLS prologue parked in recv_pause was never woken under buffer starvation, and ignored c->eof, so a connection the queue-full policy had already finished was parked on the starved list instead of ending. recv_pause reported success with the input already ended and recv_resume armed a recv on a dead connection; both say so now, and resume during a drain ends the input rather than arming an operation nothing will cancel. A positive result without IORING_CQE_F_BUFFER used a buffer id that was never set. The queue-full policy staged a cancel per arrival while the queue stayed full: one flag now bounds it to one in flight. A terminal -EIO on a TLS listener is the peer's control record, which TLS.md calls the end of input, so it reads as a clean close rather than an error. Closing a socket goes through an IORING_OP_CLOSE on both paths - the plain close(2) raced SQEs already staged for that same descriptor. A persistent accept error (-ENFILE when the file table is full, -EMFILE, -ENOMEM, -ENOBUFS) was an unbounded re-arm spin with one log line per turn. The listener now stalls and is re-armed once p->live has dropped, or once a second in case the shortage was someone else's, and it does not accept past the registered-file ceiling in the first place; the error log is rate-limited and counts what it stands for. With an eight-slot table, forty clients: eight served, two log lines, and everything works again when they let go. ioxd__sqe discarded uring_submit's return, so on -EBUSY - the CQ overflowing, the kernel refusing submissions - all sixteen retries were identical and the worker aborted. The CQ head is now published per entry, so a handler resuming inline always has room for its completions, and the retry enters with GETEVENTS to flush the backlog; the abort, if it comes, names the errno. CQ overflow was invisible: sq_off.flags is mapped, an overflow forces an enter that flushes it, and the count is in the stop line. uring_init left a half-built struct behind on an mmap failure - sqe_mem was MAP_FAILED, which passes `if (ring->sqe_mem)` - so uring_exit double-unmapped and closed a stale fd; the struct is emptied on every failure path and only owns an fd once both mappings are up. Also: fd 0 is a legal descriptor, the -EINVAL retry no longer claims every -EINVAL was NO_SQARRAY, RING_FDS returns a count that must be 1 before up.offset means anything, sq_array is null under NO_SQARRAY instead of aliasing sq_head, and the compat block covers the flags and the getevents arg that older headers lack. The guard below a coroutine stack was one 4 KB page under frames of 25 KB and 10 KB - a deep frame would have stepped clean over it into the neighbour. It is 64 KB of PROT_NONE now (address space only), and STACK_SIZE is 128 KB against a measured 44 KB of use. swap_ctx and the coro_* API are hidden, so nothing outside the library can interpose the scheduler; the switch carries CFI, so gdb and perf unwind through it; and coro_resume's loop-only rule is checked in every build rather than compiled out with -DNDEBUG. run.c never cleared g_stop and never removed the port it added, so a second ioxd_run was a silent no-op with doubled listeners; both are undone before it returns, and it returns non-zero when a worker's ring died. A failed pthread_create now stops and joins the workers already running instead of leaking them and their allocations. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
…be re-armed ioxd__recv_resume now says whether it re-armed; a false return means the peer is gone or the worker is draining, so the handler is not run. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
…aptures
A 405's allow header now names every method the path has. The walk latched only the
first end-of-path node, so a path that ends a static route and a capture route both -
"/users/new" beside "/users/:id" - answered with one node's methods and hid the other's:
DELETE /users/new said "allow: POST" while GET /users/new was served. Every such node is
remembered, and the union of their methods, each once and in registration order, is built
into a small arena in the request (route_arena), which outlives the handler whether or not
ioxd_header copies. The per-node allow strings are gone with it. HEAD is served by the GET
of a path that has none of its own (RFC 9110 9.3.2) and is listed behind that GET in the
allow header, so DELETE /health answers "GET, HEAD".
A group's prefix and what follows it are joined by a '/' when neither side brings one:
ioxd_get(api, "users", h) under "/api" registered "/apiusers" and now registers
"/api/users"; a repeated slash still counts once. Captured values arrive percent-decoded
into that same arena ("/users/a%2Fb" gives "a/b"), while the matching itself stays on the
raw bytes, so an encoded '/' cannot split a segment - and a value that does not fit the
arena is handed over raw. Method, path and prefix are copied at registration, so a caller
may pass temporaries and the method is no longer compared against the caller's memory for
the life of the process.
ioxd_group_use, ioxd_use, ioxd_endpoint_use and ioxd_default now carry ioxd_route's
"registered after ioxd_run started" guard: middleware added once the workers are running
raced them, and an endpoint's was dropped in silence. ioxd_next_run advances its cursor in
place, so a middleware that calls it twice no longer replays the rest of the chain - the
second call runs nothing. An IOXD_GROUP block left early by break, return or goto used to
leave the group open and nest every later registration inside it; the block's variable now
carries __attribute__((cleanup)) where the compiler has it, and ioxd_run reports any group
still open. Seventeen middleware on an IOXD_GROUP compiled with a warning and dropped the
last one - the slot is an undefined sentinel now, a compile error like the endpoint form's.
tests/router_test.c drives ioxd__dispatch with a context built by hand: the allow union,
the prefix joining, %2F and %65 in a capture, HEAD on GET, the group-depth diagnostic, the
guards, and a second ioxd_next_run. make check runs it after the unit test. The smoke
suite's allow check follows HEAD onto GET.
Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
# Conflicts: # include/ioxd/http.h # include/ioxd/router.h
…ainst, assertions that can fail The sequence make check ran inline is now tests/run-suites.sh, which CMake's check target and its ctest entries run as well, so it lives in one place. It keeps the fixture's output in a log instead of /dev/null, takes its exit status after the kill -INT, and fails - printing the log - when a worker reported an error or stopped with connections still open. The TLS suites are gated on the TLS port answering a socket probe rather than on a certificate file existing, so make check TLS=0 skips them with a line instead of a traceback. The fixture serves a copy of tests/certs, so a suite may rewrite what it serves. make check-tiny builds the fixture and the objects under it with BUF_COUNT=8, BUF_SIZE=64 and RX_QUEUE=4 into obj-tiny/ and runs the stress suite against it on 8410, where every request empties the buffer group: the -ENOBUFS parking, the re-arm and the queue-full path are exercised for real. check-all is check plus check-tiny. make tidy runs clang-tidy over the library with the flags the objects are built with, and .clang-tidy enables concurrency-*. Assertions that a hang would have passed now say what they mean: the read helpers return whether the peer really closed, and the oversized-header, 5 KB reply header, quit and long-line cases assert a close rather than a swallowed timeout. The flood without reading asserts the server stopped taking it, and within a time bound, instead of check(..., True). New coverage: HEAD of a buffered, a declared-length and a chunked route, each asking for the GET reply's head and no body (all three fail until the engine routes HEAD); ioxd_content_length and ioxd_flush through GET /declared, checked byte-exact and followed by another request on the same connection; ioxd_reserve and ioxd_advance through GET /raw, and a reserve past the slab; ioxd_pipe_copy, keep, release, write and send through the pipe fixture's copy and hold commands; the allow header of a path with two methods; //health and /api//admin/stats; a group registered with prefix ""; a wildcard _.example.com certificate by SNI; and a TLS reload - new files for a host, POST /tls/reload, the new certificate served and the connection in flight unharmed. mkcerts.sh makes the wildcard host and remakes any certificate within three days of expiry. The build: TLS=0/1 is a dependency at last - the effective flags are written to obj/flags whenever they differ and every object depends on it, so make TLS=0 lib after a normal build rebuilds instead of linking -DIOXD_TLS=1 objects without -lssl. The library is compiled with -fstack-clash-protection (the coroutine guard page needs the probes) and libioxd.so is linked through cmake/ioxd.map, so nm -D shows ioxd_* and nothing else. cmake/ioxdConfig.cmake.in carries IOXD_TLS and find_dependency(OpenSSL 3) when it is on, so find_package(ioxd) works downstream: verified with a consumer built against an installed prefix, and with pkg-config --libs --static. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
… any enter made inside one Publishing per entry cost measurable throughput; what the mid-batch flush needs is only that the head be current before it enters, so the loop counts the entries it has taken and ioxd__sqe publishes them first. The conformance suite reads a HEAD reply as ending at its blank line, and files a Transfer-Encoding that names identity with the unimplemented codings (501). Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
…ild, a tidy target, the CMake package fixed Resolved against the router branch (its test binary joins the check target). The fixture answers HEAD, 204/304, reflected headers and a declared length for tests/conformance.py, which runs against the same fixture as the smoke suite; expectations that changed by design follow the engine: a 5 KB header is refused by ioxd_header rather than breaking the reply, forty query parameters are a 400, HEAD rides on GET in the allow list, and a request without Host is not HTTP/1.1. strerror goes through one helper so the thread-safety note lives in one place. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
…est runs the router test and the conformance suite Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
README lists the real prerequisites (gcc 14, OpenSSL 3, the TLS opt-out) and every check target; ARCHITECTURE follows the code again - the file map, the 128 KB stacks and their guard, the tag table, the loop and its drain, the pipe's pinning, the engine's framing and reply rules, the router's HEAD and allow union, the JSON writer's float path and level rules; PERF names the knobs' files and records the hardening's cost; TLS.md separates what is built from the watcher that is planned and describes the record-at-a-time handshake; STREAMS lists the shipped verbs; DESIGN.md is marked as the historical v1 record; FILES.md says none of it exists yet. Claude-Session: https://claude.ai/code/session_013wYnJvEFUjKEpGLkyTLt9P
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.