Changelog
All notable changes to this project will be documented in this file.
Unreleased
This release changes how handlers are written: they take only the parameters they need, including typed path, query and header values. Middleware gets per-request context and hooks around sending the response. HTTP/1.1 handling is stricter and follows RFC 9110 and RFC 9112 more closely, after an audit with HTTP Garden. Dusty also has a documentation site now, at https://lalinsky.github.io/dusty/.
Highlights
- Handlers take only the parameters they need, in any order:
*App,*Request,*Response,*App.RequestContext, and typed values from the request.fn index(res: *http.Response) !voidis a valid handler.notFoundanduncaughtErrortake their parameters the same way. http.Param("id", u64)reads a path parameter as an integer, enum, bool, string, or your own type withparseParam. The name is checked against the route path at compile time. A value that doesn't parse doesn't match the route, and routing moves on to the next one.http.Queryandhttp.Headerread a query parameter or header the same way. A missing or malformed value is answered with a 400. Use?Tfor an optional one.App.RequestContextdeclares per-request data that middleware can fill in for handlers, such as the logged-in user. Middleware usesexecutor.ctx. Handlers take*App.RequestContextor callreq.ctx(App).- Middleware can declare
onHeaders, which runs just before the response headers are sent, andonComplete, which runs after the response is sent or fails. - Groups can be nested with
group.group(...), and a group can have no prefix. The outer group's middleware runs before the inner group's.
Routing
- Routes are matched against the decoded path, one segment at a time.
/a%2Fbis the single segmenta/b, and route paths are decoded the same way.req.pathholds the decoded path andreq.query_stringthe raw query. - A malformed percent escape in the path is answered with a 400. In the query string and form bodies, a
%that doesn't start an escape is now kept as a literal%. - Routes can name a parameter at the same position differently, such as
/users/:idand/users/:name/posts. The second route used to get its value under the first route's name. - A route on the same path as
router.statictakes precedence over the directory's index, regardless of registration order. StaticOptions.cache_controlsetsCache-Controlon files served withrouter.static.
HTTP conformance
- The server now sends an error response (400, 414, 431, 501 or 505) before closing the connection on a request it can't accept. Requests for a later HTTP/1.x version are served as HTTP/1.1.
- HTTP/1.1 requests must have one valid
Hostheader; a missing, duplicate or malformed one gets a 400. An absolute-form request target is routed by its path, and its authority is used as the host. Malformed request targets get a 400. - Client errors, such as a missing
Queryvalue, a body overmax_body_sizeor broken chunked framing, are now answered with a 400, 413 or 415 even when the app declaresuncaughtError. This happens whatever error the handler returns. A body over the limit always closes the connection. - Chunk extensions, trailers and chunk-size lines are limited to 16 KiB, in both the server and the client.
- Trailing whitespace is trimmed from header values.
- A connection is closed after an HTTP/1.0 message with
Transfer-Encoding, in both the server and the client. - Every response carries a
Dateheader, unless the handler set one. - llhttp is updated to 9.4.3.
HTTP client
- A CONNECT request now uses
host:portas its request target. - A response whose
Transfer-Encodingis anything other thanchunkednow fails witherror.UnsupportedTransferEncoding.
Other changes
- The
Sessionmiddleware now also sets the session cookie on streamed responses. - Added
ContentType.cbor(application/cbor). ServerConfig.request.max_header_countnow defaults to 64 instead of 32.zig build docsbuilds the API reference.
Upgrade notes
Server.ctxis renamed toServer.app. In middleware,executor.ctxis now the request context; the app isexecutor.app.- Declaring
App.dispatchis now a compile error. Use a middleware instead. - The
Sessionmiddleware keeps the session in the request context, so the app must declarepub const RequestContext = struct { session: http.SessionData = .{} };.req.sessionis removed; usectx.session. - The path and handler passed to
get,postand the other route methods must be known at compile time. Router.Groupis now a function of the prefix,Router.Group(prefix). Code that names the type should use@TypeOf(router.group(...))instead.- Registering two routes for the same method and path now panics, instead of the second one replacing the first.
- Values in
req.paramsare now percent-decoded. ServerConfig.request.max_param_countis removed. A request gets only the parameters its route declares, so there is nothing to limit.- An HTTP/1.1 request without
Hostis answered with a 400. uncaughtErrorno longer receives the 400, 413 and 415 client errors. To handle them yourself, for example as JSON, setServerConfig.request.answer_client_errors = false;req.clientErrorStatus()returns the status the server would have sent.res.stream(),res.startEventStream()andres.upgradeWebSocket()can returnerror.HeadersHookFailedwhen a middleware'sonHeadersfails. Nothing has been sent at that point. If the handler returns it,uncaughtErrorreceives the hook's original error.- A
Requestbuilt by hand, for example in tests, needsreq.splitTarget()before it is passed toRouter.findHandler.
0.5.0 - 2026-10-25
This release adds file and asset serving, more ways to produce a response body, and a more reliable HTTP client, while making TLS, compression and memory use cheaper per connection.
Highlights
- Added
router.static(prefix, dir, opts)andGroup.staticfor serving a directory,router.embedded(path, data, opts)andGroup.embeddedfor serving a file compiled into the binary, and theaddAssetsbuild helper, which embeds a directory of assets at build time and gives templates cache-busting URLs such as/assets/app.css?v=6ec8ea7d. - Added
Response.render(content_type, template, args)for templating libraries such as zt,Response.print(content_type, fmt, args)for formatting a body directly, andResponse.encode(content_type, encodeFn, value), which writes the body with a serializer's write-to-writer function. WebSocketgainsautoflushandflush(), so frames can be batched into fewer writes, andreceiveManyandtryReceivefor taking every message that has already arrived. The default behavior is unchanged.- Added
query()toRouterandGroupfor QUERY requests, andany()now matches every method, following Go'sServeMux: a request is matched against routes of its own method first, then HEAD against GET routes, thenany()routes. - TLS certificates and CA bundles can come from bytes in memory as well as files, for both listeners and the client's mutual-TLS certificate. Listeners offer protocols through ALPN with
ServerConfig.Tls.alpn_protocols, andConnection.negotiatedProtocol()reports the one agreed. - The
use_bundled_tlsbuild option lets an application keep TLS enabled while injecting its owntlsmodule in place of Dusty's pinned tls.zig. Connections calltls.Connection.deiniton close when the module declares one, so a module backed by a C library does not leak a connection object per connection. - Added
examples/todo, a TodoMVC-style app built with zt, pg.zig and htmx.
Performance and memory
- TLS responses are encrypted in place: the body is written straight into the record that gets encrypted, instead of being copied through the request write buffer first.
- Responses with
compressset are gzipped at zlib level 3 instead of zlib's default, 6. On JSON bodies of a few KB this sends about 43% more responses per second, for bodies about 12% larger. - The connection's write buffer moved into the pooled request buffers, so an idle connection no longer holds one, and
@sizeOf(Connection)drops from about 9K to under 1K. The newServerConfig.response.write_buffer_sizesets it and defaults to 16K. Listener.kernel_backlognow defaults to the system's limit,net.core.somaxconnon Linux, instead of 1024. A full accept queue makes Linux drop new connections, which then wait a second or more to retry.
HTTP client
- A response whose body was not read to the end no longer costs the connection: up to 256 KiB of it is discarded in the background, within 50 ms, and the connection goes back to the pool, with the same bounds as Go's net/http.
- An idempotent request (GET, HEAD, OPTIONS, TRACE, QUERY, or one with an
Idempotency-Keyheader) that fails on a pooled connection before any response arrives is sent once more on a new connection, within the fetch's deadline. This covers servers that close idle connections without aKeep-Alive: timeout=hint. - When sending a request fails because the server has already answered and closed, for example with a 413 for a body it rejects, the client returns that response instead of
ConnectionResetByPeer. - 301 and 302 redirects keep a QUERY request's method and body, as RFC 10008 requires; only 303 turns it into a GET.
- A redirect no longer drains its body when the connection will not be reused, which could block until 2 KB arrived or the deadline passed.
- Connections are no longer returned to the pool after a CONNECT request.
Fixes
- The server closes the connection after a CONNECT request. A body sent with one was left on the connection and served as the next request, which behind a proxy that frames CONNECT bodies by
Content-Lengthallowed request smuggling. - A handler that streamed part of its response before reading the request body no longer gets
100 Continuewritten into the middle of it. - An HTTP/1.0 request that asks to keep the connection now gets
Connection: keep-aliveon its response when the server keeps it. Without it an HTTP/1.0 client (ApacheBench with-k, for one) waits for the server to close, while the server waits for the next request. - A failed TLS handshake is now logged at info instead of err (only
OutOfMemorystays at err). Any peer that reaches the port can fail one at will, so err-level logging let anyone flood the error log.
Upgrade notes
ServerConfig.Tls.cert_path,key_pathanddirare replaced by.certificate = .{ .file = .{ .cert_path = ..., .key_path = ... } }or.certificate = .{ .bytes = .{ .cert = ..., .key = ... } }.TlsConfig.client_certificatetakes the same union. An injectedtlsmodule now needsCertKeyPair.fromSliceas well asfromFilePath.timeout.keepalivenow defaults to 30 seconds instead of 60, the same astimeout.request.Router.all_methodsis removed. Registering a method route and anany()route where both match some path and the method route is not at least as specific now panics, naming both routes; for exampleget("/users/:id")withany("/users/me"). This includesstatic("/")orget("/*")together with anany()route under a prefix.Listener.kernel_backlogis now?u31;null, the default, means the system's limit.
0.4.0 - 2026-10-06
This release expands how servers listen and send responses, while improving throughput and reducing per-connection memory use.
Highlights
- A server can now serve any number of listeners with one router and context, including plain HTTP, HTTPS with per-listener TLS settings, and Unix sockets. Handlers can distinguish them through
Request.listenerandRequest.secure, andServer.addressesexposes every bound address once the server is ready. - Added opt-in gzip response compression. Set
res.compress = trueto negotiate gzip fromAccept-Encodingfor buffered bodies, streamed bodies, and event streams. Dusty managesVary, weakens strongETagvalues, and removesAccept-Rangeswhen the representation is compressed; small or non-shrinking buffered bodies remain uncompressed. - The server now processes pipelined HTTP/1.1 requests in order instead of closing a connection when the next request has already arrived.
Performance and reliability
Response.writer()now writes directly into arena-backed segments and sends them with vectored writes instead of repeatedly growing and copying one buffer. Handlers that encode JSON bodies of a few KB are about twice as fast.- Each listener runs multiple accept loops to improve throughput when many connections arrive at once.
Listener.acceptorsdefaults to the base-2 logarithm of the CPUs available to the process, honoring cgroup CPU quotas and using at least two loops; set it explicitly to override the automatic value. - Request read buffers and arenas are pooled across connections and the pool is sharded to reduce contention. Connections allocate them only after receiving a request and normally retain them across keep-alive requests. Set
ServerConfig.request.keep_buffers_between_requeststofalseto reduce memory held by idle connections at some cost in throughput. - Accept loops now continue through client-specific failures and back off on persistent resource or network errors instead of spinning or stopping the server. Connections with unread input are closed gracefully so clients can still receive the response.
Compatibility and build options
- Dusty now builds with Zig 0.17 as well as 0.16. TLS uses a fork of tls.zig that supports both versions, and CI covers both Zig releases on Linux, macOS, and Windows.
- Request and client response bodies with
Content-Encoding: gzipordeflateare now decoded with zlib through zlib.zig, replacingstd.compress.flate. Corrupt encoded bodies reporterror.CorruptInput, and decoder memory is allocated according to the stream's window instead of reserving 64K up front. Build with-Duse_zlib=falseto omit zlib; encoded bodies then fail witherror.UnsupportedContentEncoding, and the client does not advertise gzip or deflate. - The HTTP client now rejects URLs with a missing, empty, or overlong host instead of allowing an invalid host through request setup.
Upgrade notes
- Listener configuration has moved to
ServerConfig.listeners, which defaults to127.0.0.1:8080when omitted or empty, and servers now start withServer.run().Server.listen()andServerConfig.tlshave been removed; move TLS, backlog, address-reuse, and accept-loop settings into eachListener. Response.writeHeaderis no longer public. UseResponse.stream()when headers must be sent before the body.
0.3.1 - 2026-09-21
- Added whole-request HTTP client deadlines.
ClientConfig.timeoutdefaults to 30 seconds, andFetchOptions.timeoutcan replace or disable it for an individual request. Buffered response bodies remain covered through completion; explicitly streamed responses are left to the caller. - Added trusted reverse-proxy hop handling to the HTTP server. Configure
ServerConfig.trusted_proxy_hopsto resolveRequest.remote_addresssafely from the right side ofX-Forwarded-For; direct peers remain the default and malformed or incomplete chains fall back to them. - Re-exported
BodyWriterandStreamingBodyWriterfrom the root module so callers can name the concrete types returned byResponse.writerandResponse.stream. - Added the Dusty httpbin container image and release-tagged publishing workflow.
- Added timeout and buffered-response controls to the HTTP client example.
0.3.0 - 2026-09-20
This is a substantial release focused on making Dusty runtime-independent, safer under load, and more predictable at HTTP protocol boundaries.
Highlights
- Dusty now targets Zig's
std.Iointerface directly and no longer has a hard dependency onzio. It works withstd.Io.Threaded, zio, and otherstd.Ioimplementations; zio remains the recommended backend for highly concurrent servers and gets native deadline cancellation when explicitly injected. - TLS is now configurable on both sides of a connection. The server can serve HTTPS, and the client and server both support mutual TLS, system or custom CA stores, and certificate verification controls. TLS can be compiled out with
-Duse_tls=false. - Server operation is bounded by default: at most 10,000 open connections, a 30-second request timeout, a 60-second keep-alive timeout, and a 30-second graceful-shutdown budget. Handlers can replace or disable their current deadline with
Request.setTimeout. - Request and response bodies can be processed as streams. Transparent gzip/deflate decoding now applies while streaming client responses and, by default, incoming server requests. Response streaming has explicit buffered and streaming writers with reliable framing and error reporting.
HTTP client
- Added a configurable default
User-Agentand first-class TLS configuration, including custom trust roots, client certificates, and an explicit insecure mode for testing. - Made the connection pool safe to share across concurrent tasks, and fixed stale, unread, failed, or redirecting connections being returned to the pool.
- Redirect handling now follows only redirect statuses, applies the expected method changes, strips credentials when crossing origins or downgrading to HTTP, sets
Referer, and reuses connections when the previous response can be drained safely. - Improved interoperability for IPv6 literal URLs, interim
1xxresponses,HEADresponses, bodylessPOST/PUT/PATCHrequests, and responses delimited by connection close. - Client response headers are bounded, duplicate header fields are preserved, and truncated or malformed bodies now return specific errors instead of generic I/O failures.
HTTP server and application API
- Added server-side TLS and optional/required client-certificate authentication, with TLS handshakes covered by the request timeout.
- Added transparent request-body decompression, caller-buffered body readers, typed path/query parameter lookups, request peer addresses, cookie iteration, and JSON serialization for headers, parameters, and cookies.
- Reworked response writing so buffered, fixed-length, chunked, HTTP/1.0,
HEAD, and bodyless-status responses are framed consistently. Server-Sent Events now support multi-line and incrementally produced event data. - Header names, values, cookies, multipart fields, body lengths, and message heads receive stricter validation. Oversized request heads receive
431, malformed query strings receive400, and handler failures produce error responses without leaking partial bodies. - Graceful shutdown now waits only for in-flight requests rather than idle keep-alive connections, and connection/request limits are enforced without busy-waiting.
WebSocket and reliability
- WebSocket sends are serialized so concurrent tasks cannot interleave frames. Incoming frames now enforce masking, fragmentation, close-code, UTF-8, opcode, and control-frame rules and reply with the appropriate protocol close.
- WebSocket message storage is reclaimed between receives, fixing growth over long-lived connections.
- Error propagation across plain sockets, TLS, response streams, SSE, and WebSockets now preserves the underlying transport error. Numerous cleanup paths were tightened to prevent leaks and double releases after failed connects, TLS handshakes, redirects, spawns, and disconnects.
Upgrade notes
- Server addresses and networking types now use
std.Io;Server.listenacceptsdusty.Address. Applications using zio should explicitly inject its module into Dusty to enable zio-native timeout cancellation. ServerConfig.timeout.requestand.keepalivenow takestd.Io.Durationinstead of millisecond integers and are finite by default. A new.shutdowntimeout andmax_connectionslimit are also enabled by default; set them tonullto remove those bounds.ClientConfig.use_system_ca_bundlewas replaced byClientConfig.tls; the equivalent setting is.tls = .{ .ca = .system }(also the new default).Request.readerandClientResponse.readernow take a caller-owned buffer and can fail during setup.Response.writer,Response.stream, and event writers return explicit writer objects whoseendmethod must be called;startEventStreamnow also takes a buffer.Headersis now a bounded, case-insensitive, multi-value collection. Useaddto preserve repeated fields and access iterator entries through.keyand.value. Request path and query values now useParams, which adds typed numeric lookups.
0.2.0 - 2026-04-26
- Support for Zig 0.16, still depends on zio, not using
std.Iodue to lack of timeout support. - Fix flushing in websocket client when using it over HTTPS.
- Added support for 100-continue in the HTTP client.
0.1.0 - 2026-03-02
Initial release.
Features
- HTTP/1.0 and HTTP/1.1 server with router, parameters, and wildcards.
- HTTP/HTTPS client with connection pooling and DNS resolution.
- Unix domain socket support for client connections.
- Chunked transfer encoding for request/response bodies.
- Server-Sent Events (SSE) support.
- WebSocket support (RFC 6455) for both client and server.
- Gzip/deflate decompression for HTTP client.
- Middleware system with CORS and session middleware.
- Cookie support.
- Request/keepalive timeouts via coroutine auto-cancellation.