v0.3.0 merged appendRequestHeaders and appendResponseHeaders into one generic
over an interface constraint, in the name of not repeating twelve lines. The
comment it deleted had explained exactly why they were separate: escape analysis
must be able to prove the VisitAll closure does not leave the stack.
A generic there compiles to a shared GC-shape instantiation —
appendHeaders[go.shape.*uint8] in the -gcflags=-m output — so the call goes
through a dictionary, the closure escapes, and every encode picked up a heap
allocation on a path this package documents and benchmarks as allocation-free.
before 82.39 ns/op 32 B/op 1 allocs/op
after 25.83 ns/op 0 B/op 0 allocs/op
Restoring the two concrete functions is 3.2x faster and allocation-free again.
Twelve duplicated lines were buying that; deleting them cost it.
Also fixes the benchmarks, which were measuring the wrong thing. benchRequest
sets only Host, which isFrameOwnedHeaderBytes strips, so its headers slot is
NULL and forEachHeader returns on its first line — the header codec these
numbers advertise was not being exercised at all. Added Real fixtures carrying
a realistic header set (auth, trace, ratelimit, and a Location with an "&" in
it, which under the old JSON codec forced the whole message onto the allocating
fallback):
AppendRequestReal 113.5 ns/op 0 B/op 0 allocs/op
AppendResponseReal 89.28 ns/op 0 B/op 0 allocs/op
UnmarshalRequestReal 406.7 ns/op 0 B/op 0 allocs/op
UnmarshalResponseReal 301.8 ns/op 0 B/op 0 allocs/op
Co-authored-by: Hanzo Dev <[email protected]>
zap-http
Docs: HTTP over ZAP · part of the ZAP Protocol
HTTP request/response semantics over the ZAP transport.
zap-proto.io · Spec · Paper · Discord
Drop-in replacement for net/http server and client when both peers live in a trusted boundary (in-cluster service mesh, agent-to-tool, edge-to-edge). Existing handlers and http.Client code work unchanged — only the wire underneath changes.
Why
| Property | net/http over TCP+TLS |
zap-http |
|---|---|---|
| Confidentiality | TLS (classical curves) | X-Wing hybrid PQ (X25519 + ML-KEM-768) |
| Authentication | bearer / JWT at app layer | KEM keypair at transport layer |
| Wire encoding | text headers + chunked body | ZAP wire, zero-copy |
| Field access | parse → allocate → copy | pointer offset, O(1) |
| JWT mint per call | typical | not in the path |
Install
go get github.com/zap-proto/http
Requires Go 1.23+.
Server
package main
import (
"io"
"net/http"
zaphttp "github.com/zap-proto/http"
)
func main() {
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "ok")
})
if err := zaphttp.ListenAndServe(":9999", nil); err != nil {
panic(err)
}
}
Same http.Handler, same http.HandleFunc, same default mux. The wire is ZAP-HTTP.
Client
package main
import (
"io"
"net/http"
"os"
zaphttp "github.com/zap-proto/http"
)
func main() {
client := &http.Client{Transport: zaphttp.NewTransport("server:9999")}
resp, err := client.Get("http://server/healthz")
if err != nil { panic(err) }
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
}
http.Client machinery — timeouts, redirects, cookies — keeps working unchanged.
Wire format
Each HTTP message is one ZAP frame, defined in schema/zap_http.zap using the ZAP schema language. Today the wire layer is length-prefixed framing over TCP; the paper and the v0.2 release will swap that for the full ZAP transport with X-Wing PQ KEM handshake on connect.
zap-http v0.1 wire (transitional):
+---------+-------------------+
| u32 BE | ZAP Frame |
| length | (zap_http.zap) |
+---------+-------------------+
zap-http v0.2 wire:
+-----------------------------+--------+-----------+
| ZAP transport (X-Wing KEM, | AEAD | ZAP Frame |
| mutual auth, multi-stream) | header | |
+-----------------------------+--------+-----------+
The .zap schema is the source of truth for what's on the wire. Marshal/unmarshal in any language follows from the schema; bindings:
- Go (this repo)
- Rust —
zap-proto/rust(consumer; sub-protocol bindings TBD) - TypeScript —
zap-proto/ts(planned)
What's in v0.1
| Feature | Status |
|---|---|
| Request method, target, headers, body | ✓ |
| Response status, reason, headers, body | ✓ |
| Multi-value headers preserved | ✓ |
| Trailers (read & write) | ✓ |
http.Client / http.Handler compatibility |
✓ |
| Length-prefixed framing over TCP | ✓ |
| 64 MiB max frame size (configurable) | ✓ |
| Streaming bodies (chunked-style) | v0.2 |
| Connection pool / keep-alive on client | v0.2 |
| Real ZAP transport (PQ KEM handshake) | v0.2 |
| WebSocket-style upgrade | see zap-proto/ws |
Sub-protocol family
zap-http— this repozap-ws— multi-stream pubsub, per-stream FECzap-fix— FIX 4.4/5.0 trading channelzap-rns— KEM-bound service namingzap-mcp— Model Context Protocol over ZAPzap-acp— Agent Communication Protocolzap-a2a— Google Agent2Agent over ZAP
By the composability theorem, every sub-protocol that embeds onto ZAP-base inherits its post-quantum confidentiality and mutual authentication automatically.
Schema regeneration
make schema # regenerates internal/wire/zap_http.go from schema/zap_http.zap
Requires zapc (the ZAP schema compiler) on PATH. Build it once from
zap-proto/spec:
cargo install --path . # from a clone of zap-proto/spec
License
MIT OR Apache-2.0