Commits
fix: close listener on startup errors
Bump websocket.zig dependency to latest master (b70e733)
Dead websocket connections never returned their reactor Conn node: the
thread pool's cleanup paths freed the ws side but conn_mem_pool and len
are reactor-owned, so every websocket permanently consumed a conn slot
and the worker stopped accepting once len reached max_conn.
Park dead nodes on a ws_graveyard list and drain it in processSignal,
mirroring the handover flow. The death paths deliberately skip
releaseProcessing: the socket is already closed so no further events can
arrive, and touching the node after parking it would race the reactor's
destroy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
processSignal iterates a detached snapshot of handover_list, but released
nodes via disown(), whose List.remove ran against the node's stale
snapshot links. When the removed node was the snapshot head, the live
list's head was pointed back at the rest of the snapshot chain, so those
connections were processed a second time on the next signal: reading the
websocket union as http, double-releasing pooled HTTPConns, and letting
two live sockets share one HTTPConn (responses written to the wrong
connection).
Release snapshot nodes with releaseDetached instead, clear the stale
links on websocket conversion, and stop the switchToOneShot failure path
from disowning a conn whose HTTPConn was already released.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
worker: remove fd from epoll on the loop thread before recycling Conn
worker: join thread pool before websocket.deinit frees connection buffers
worker: stop thread pool before deinit to fix shutdown use-after-free
readme was upstream's karlseguin/http.zig content with no fork notice (the
MIT LICENSE retains Karl's copyright, but the readme read as original). Add a
short header crediting Karl, pointing at LICENSE, and noting this fork targets
0.16 and pairs with our websocket.zig fork.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move off the GitHub fork URL onto the canonical tangled.org websocket so
consumers (zds, etc.) resolve a single websocket version. Our httpz
already targets the v0.1.8 WorkerState.init(allocator, io) signature.
zig build && zig build test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
worker: fix recv/disown use-after-free by deferring signal handling
epoll can return a .signal and a .recv for the same connection in one batch.
Processing the signal first disowns/frees the connection (http_conn_pool.release
+ conn_mem_pool.destroy), and the later .recv then dereferences freed memory in
getState() -> segfault. Defer processSignal until the whole event batch is
drained: every .recv runs while its connection is alive, sees the .handover
state set before the signal, and skips it; the free happens safely afterward.
Bump websocket.zig to include the TLS read-timeout fix
Fix lost websocket read by releasing processing before re-arm
With EPOLLONESHOT, re-arming the fd while still holding processing loses a read
that arrives in the window between the re-arm and the release: the event loop
sees the connection as busy and the one-shot arming is consumed, so the read is
dropped and the connection hangs / leaks (CLOSE_WAIT). Release processing before
re-arming instead; no extra state needed.
zat main moved to websocket v0.1.7 (Address now wraps sockaddr.storage
so unix/in6 addresses fit); httpz follows per the one-websocket rule.
adapts the upgrade path to widen our bare sockaddr into storage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zat and httpz pinning different websocket versions makes their generated
'build' options modules collide when both land in one test compilation
(seen again in prefect-server's atproto-storage branch; same confusion
previously hit in zlay). httpz capitulates: pin zat's exact websocket.
adapts to the v0.1.5 API: WorkerState.init takes (allocator, io, config),
Worker.init pulls io from state, createConn takes the sockaddr-shim
Address (new posix.Address.fromIOAddress provides the conversion).
also fixes a pre-existing compile error in the subprotocol negotiation
patch: upgradeWebsocket dereferenced ctx unconditionally, which broke
'zig build test' on master for non-pointer ctx (e.g. the test suite's
integer ctx). now guarded behind a comptime pointer check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
https://github.com/karlseguin/http.zig/issues/207
This reverts commit cf7b8b28d0b43cf12772ed4ff6747afe22c731d7.
Replace two TODOs in setSockFlags:
- CLOEXEC: Windows socket handles created by accept() did not have
close-on-exec semantics. Fixed by calling SetHandleInformation with
HANDLE_FLAG_INHERIT=0 — the Windows equivalent of FD_CLOEXEC.
(socket() already handled this via WSA_FLAG_NO_HANDLE_INHERIT.)
- NONBLOCK: replace the generic WSA error catch-all with specific
handling for WSAEINVAL (returns error), WSAEFAULT / WSAEOPNOTSUPP /
WSAEINPROGRESS (all unreachable for FIONBIO).
Also add constants HANDLE_FLAG_INHERIT, HANDLE_FLAG_PROTECT_FROM_CLOSE
and a setNoInherit() helper to windows.zig.
createReply's second parameter is ?*KeyValue (a generic header-map written
verbatim into the 101 reply), not a subprotocol string. Build a stack-allocated
single-entry KeyValue with the 'Sec-WebSocket-Protocol' header and pass that
through. Comptime @hasField gate is unchanged so callers without the field
still pass null.
If the caller's ctx has a non-null `subprotocol` field, echo it in the 101
response header instead of hardcoding null. Per RFC 6455 §1.3, a server that
intends to speak a subprotocol the client offered MUST include it in the
handshake reply; libraries like python's `websockets` close the connection
otherwise. Detected via comptime @hasField so existing callers without a
subprotocol field are unaffected.
The ctx is anytype so this is structural — no API change at the public
function signature.
https://github.com/karlseguin/http.zig/pull/197
tsan: add buffer pool race test reproduction
Reject ambiguous Content-Length and Host headers
https://github.com/karlseguin/http.zig/issues/170
Fixes: https://github.com/karlseguin/http.zig/issues/193
Zig 0.16's networking is incomplete. Io.Threaded treats AGAIN as a "programmer
bug". Fair because using only std.Io.net you can't put a socket in nonblocking
mode, but unfair because there's nothing inherently wrong about mixing
nonblocking sockets with threads - zig's Io.Threaded just doesn't support it
[and they decide to (a) panic and (b) blame the user).
As with most things with Zig 0.16, the solution is to avoid std.Io.net as much
as possible. Most of httpz was already using posix.zig, but I left this one
case in
Cannot resize from fixed allocator to arena/fallback. Fixes
https://github.com/karlseguin/http.zig/issues/192
(Also, add format to Config.Address)
Websocket is currently disabled. It'll come very soon.
Dead websocket connections never returned their reactor Conn node: the
thread pool's cleanup paths freed the ws side but conn_mem_pool and len
are reactor-owned, so every websocket permanently consumed a conn slot
and the worker stopped accepting once len reached max_conn.
Park dead nodes on a ws_graveyard list and drain it in processSignal,
mirroring the handover flow. The death paths deliberately skip
releaseProcessing: the socket is already closed so no further events can
arrive, and touching the node after parking it would race the reactor's
destroy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
processSignal iterates a detached snapshot of handover_list, but released
nodes via disown(), whose List.remove ran against the node's stale
snapshot links. When the removed node was the snapshot head, the live
list's head was pointed back at the rest of the snapshot chain, so those
connections were processed a second time on the next signal: reading the
websocket union as http, double-releasing pooled HTTPConns, and letting
two live sockets share one HTTPConn (responses written to the wrong
connection).
Release snapshot nodes with releaseDetached instead, clear the stale
links on websocket conversion, and stop the switchToOneShot failure path
from disowning a conn whose HTTPConn was already released.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
readme was upstream's karlseguin/http.zig content with no fork notice (the
MIT LICENSE retains Karl's copyright, but the readme read as original). Add a
short header crediting Karl, pointing at LICENSE, and noting this fork targets
0.16 and pairs with our websocket.zig fork.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move off the GitHub fork URL onto the canonical tangled.org websocket so
consumers (zds, etc.) resolve a single websocket version. Our httpz
already targets the v0.1.8 WorkerState.init(allocator, io) signature.
zig build && zig build test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
epoll can return a .signal and a .recv for the same connection in one batch.
Processing the signal first disowns/frees the connection (http_conn_pool.release
+ conn_mem_pool.destroy), and the later .recv then dereferences freed memory in
getState() -> segfault. Defer processSignal until the whole event batch is
drained: every .recv runs while its connection is alive, sees the .handover
state set before the signal, and skips it; the free happens safely afterward.
With EPOLLONESHOT, re-arming the fd while still holding processing loses a read
that arrives in the window between the re-arm and the release: the event loop
sees the connection as busy and the one-shot arming is consumed, so the read is
dropped and the connection hangs / leaks (CLOSE_WAIT). Release processing before
re-arming instead; no extra state needed.
zat main moved to websocket v0.1.7 (Address now wraps sockaddr.storage
so unix/in6 addresses fit); httpz follows per the one-websocket rule.
adapts the upgrade path to widen our bare sockaddr into storage.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zat and httpz pinning different websocket versions makes their generated
'build' options modules collide when both land in one test compilation
(seen again in prefect-server's atproto-storage branch; same confusion
previously hit in zlay). httpz capitulates: pin zat's exact websocket.
adapts to the v0.1.5 API: WorkerState.init takes (allocator, io, config),
Worker.init pulls io from state, createConn takes the sockaddr-shim
Address (new posix.Address.fromIOAddress provides the conversion).
also fixes a pre-existing compile error in the subprotocol negotiation
patch: upgradeWebsocket dereferenced ctx unconditionally, which broke
'zig build test' on master for non-pointer ctx (e.g. the test suite's
integer ctx). now guarded behind a comptime pointer check.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace two TODOs in setSockFlags:
- CLOEXEC: Windows socket handles created by accept() did not have
close-on-exec semantics. Fixed by calling SetHandleInformation with
HANDLE_FLAG_INHERIT=0 — the Windows equivalent of FD_CLOEXEC.
(socket() already handled this via WSA_FLAG_NO_HANDLE_INHERIT.)
- NONBLOCK: replace the generic WSA error catch-all with specific
handling for WSAEINVAL (returns error), WSAEFAULT / WSAEOPNOTSUPP /
WSAEINPROGRESS (all unreachable for FIONBIO).
Also add constants HANDLE_FLAG_INHERIT, HANDLE_FLAG_PROTECT_FROM_CLOSE
and a setNoInherit() helper to windows.zig.
createReply's second parameter is ?*KeyValue (a generic header-map written
verbatim into the 101 reply), not a subprotocol string. Build a stack-allocated
single-entry KeyValue with the 'Sec-WebSocket-Protocol' header and pass that
through. Comptime @hasField gate is unchanged so callers without the field
still pass null.
If the caller's ctx has a non-null `subprotocol` field, echo it in the 101
response header instead of hardcoding null. Per RFC 6455 §1.3, a server that
intends to speak a subprotocol the client offered MUST include it in the
handshake reply; libraries like python's `websockets` close the connection
otherwise. Detected via comptime @hasField so existing callers without a
subprotocol field are unaffected.
The ctx is anytype so this is structural — no API change at the public
function signature.
Fixes: https://github.com/karlseguin/http.zig/issues/193
Zig 0.16's networking is incomplete. Io.Threaded treats AGAIN as a "programmer
bug". Fair because using only std.Io.net you can't put a socket in nonblocking
mode, but unfair because there's nothing inherently wrong about mixing
nonblocking sockets with threads - zig's Io.Threaded just doesn't support it
[and they decide to (a) panic and (b) blame the user).
As with most things with Zig 0.16, the solution is to avoid std.Io.net as much
as possible. Most of httpz was already using posix.zig, but I left this one
case in