Commits
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>
This reverts commit cf7b8b28d0b43cf12772ed4ff6747afe22c731d7.
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.
The heading `Configuration` was used twice, the TOC misleadingly linked
to the first one which was the subheading for Configuration of the
Router.
https://github.com/karlseguin/http.zig/pull/188
https://github.com/karlseguin/http.zig/issues/187
https://github.com/karlseguin/http.zig/issues/186
Also add .any as an alternative to the first example in the readme
See: https://github.com/karlseguin/http.zig/pull/185
he reason I didn't think it was possible is that, once we upgrade to websockets, we switch the to ONESHOT / EV_DISPATCH. And I _thought_ this would be enough to ensure that we only ever process 1 message at a time. The re-arming only happens as the last step the worker thread does..so while 2 threads could technically be running on the same connection, one of those threads would be in the process of shutting down and 100% NOT be doing anything with the connection. And that all works.
EXCEPT for a tiny window during our initial switch to EV_DISPATCH. Say a connection is upgraded and also sends 2 websocket messages. While a single poll (`kevent`) won't return 2 distinct events for that 1 connection, they could get split into 2 separate polls. We end up with:
```
wait#1
[signal, fd#1.recv#1]
wait#2
[fd#1.recv#2]
```
and this happens because as we're processing the wait#1 and before we switch to EV_DISPATCH, `fd#1.recv#2` has already been received and queued. Even if you delete and re-add, it seems like that only deals with future events, it doesn't clear our any already/pending events. So we need to mimic what we alreayd do for HTTP connections, which is as a guard clause for a connection that's already being processed. But we still keep the ONESHOT / DISPATCH because it does work beyond the initial window, and that just helps prevent wakeups that we'll just have to reject via the guard.
Fix shutdown on 32-bit Linux: eventfd write must be 8 bytes
* Introduce Config.AddressConfig.
* Let callers provide a pre-parsed Address for the listener
* Express the default listener address in a simpler way
* update readme to show the new way of setting listener address
Add errdefer to unlock mutex when listen() fails before reaching
the success paths. Without this, errors during socket setup, binding,
or worker initialization leave the mutex locked, causing subsequent
calls to listen() or listenInNewThread() to deadlock.
This bug is particularly severe with listenInNewThread() because the
main thread waits on a condition variable, and when the spawned thread
exits with the mutex locked, the condition wakes the main thread which
then hangs forever trying to reacquire the mutex.
Adds test that verifies mutex is properly released by calling listen()
twice when bind fails. Without the fix, the second call deadlocks.
https://github.com/karlseguin/http.zig/issues/178
https://github.com/karlseguin/http.zig/pull/173
* Enhance CORS to include credentials handling and allow multi cors origin values
Added support for credentials in CORS configuration.
Allow multi cors origin values
* Enhance CORS middleware: support wildcard & multiple origins, credentials; add tests
- Add Origin union and parseOrigin() to middleware/Cors.zig; init now takes MiddlewareConfig and uses arena for origin parsing
- Support wildcard ("*") and comma-separated origin lists; set Access-Control-Allow-Origin to "*" for wildcard, or echo allowed origin for lists
- Preserve and emit Access-Control-Allow-Credentials, Allow-Methods, Allow-Headers, and Max-Age for preflight requests
- Update tests: increase test thread array size, spawn dedicated CORS servers (wildcard, single, multiple), add comprehensive tests for GET and OPTIONS preflight flows and negative cases
- Ensure Origin header is sent in relevant test requests
* Fix parsing of multiple CORS origins by initializing count to 0
* tests: stop and deinit CORS test servers in tests:afterAll
* docs(readme): cleanup formatting, fix typos, and expand CORS documentation
- Normalize whitespace, punctuation and list formatting across README
- Fix typos and improve wording for clarity
- Expand CORS middleware section with examples and behavior:
- document wildcard, single and multiple origins
- add credentials option and note about wildcard+credentials restriction
- show preflight headers and 204 behavior
- Update middleware path reference to httpz.middleware.Cors
- Minor formatting tweaks in many sections (headers, code blocks, comments)
https://github.com/karlseguin/http.zig/issues/156
* fix worker KQueue signal() and stop() implementation
* fix response writer double buffering
Based on: https://github.com/karlseguin/http.zig/pull/150
https://github.com/karlseguin/http.zig/issues/108#issuecomment-3123612037
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>
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
See: https://github.com/karlseguin/http.zig/pull/185
he reason I didn't think it was possible is that, once we upgrade to websockets, we switch the to ONESHOT / EV_DISPATCH. And I _thought_ this would be enough to ensure that we only ever process 1 message at a time. The re-arming only happens as the last step the worker thread does..so while 2 threads could technically be running on the same connection, one of those threads would be in the process of shutting down and 100% NOT be doing anything with the connection. And that all works.
EXCEPT for a tiny window during our initial switch to EV_DISPATCH. Say a connection is upgraded and also sends 2 websocket messages. While a single poll (`kevent`) won't return 2 distinct events for that 1 connection, they could get split into 2 separate polls. We end up with:
```
wait#1
[signal, fd#1.recv#1]
wait#2
[fd#1.recv#2]
```
and this happens because as we're processing the wait#1 and before we switch to EV_DISPATCH, `fd#1.recv#2` has already been received and queued. Even if you delete and re-add, it seems like that only deals with future events, it doesn't clear our any already/pending events. So we need to mimic what we alreayd do for HTTP connections, which is as a guard clause for a connection that's already being processed. But we still keep the ONESHOT / DISPATCH because it does work beyond the initial window, and that just helps prevent wakeups that we'll just have to reject via the guard.
Add errdefer to unlock mutex when listen() fails before reaching
the success paths. Without this, errors during socket setup, binding,
or worker initialization leave the mutex locked, causing subsequent
calls to listen() or listenInNewThread() to deadlock.
This bug is particularly severe with listenInNewThread() because the
main thread waits on a condition variable, and when the spawned thread
exits with the mutex locked, the condition wakes the main thread which
then hangs forever trying to reacquire the mutex.
Adds test that verifies mutex is properly released by calling listen()
twice when bind fails. Without the fix, the second call deadlocks.
* Enhance CORS to include credentials handling and allow multi cors origin values
Added support for credentials in CORS configuration.
Allow multi cors origin values
* Enhance CORS middleware: support wildcard & multiple origins, credentials; add tests
- Add Origin union and parseOrigin() to middleware/Cors.zig; init now takes MiddlewareConfig and uses arena for origin parsing
- Support wildcard ("*") and comma-separated origin lists; set Access-Control-Allow-Origin to "*" for wildcard, or echo allowed origin for lists
- Preserve and emit Access-Control-Allow-Credentials, Allow-Methods, Allow-Headers, and Max-Age for preflight requests
- Update tests: increase test thread array size, spawn dedicated CORS servers (wildcard, single, multiple), add comprehensive tests for GET and OPTIONS preflight flows and negative cases
- Ensure Origin header is sent in relevant test requests
* Fix parsing of multiple CORS origins by initializing count to 0
* tests: stop and deinit CORS test servers in tests:afterAll
* docs(readme): cleanup formatting, fix typos, and expand CORS documentation
- Normalize whitespace, punctuation and list formatting across README
- Fix typos and improve wording for clarity
- Expand CORS middleware section with examples and behavior:
- document wildcard, single and multiple origins
- add credentials option and note about wildcard+credentials restriction
- show preflight headers and 204 behavior
- Update middleware path reference to httpz.middleware.Cors
- Minor formatting tweaks in many sections (headers, code blocks, comments)