zeekayandHanzo Dev 05e4490213 perf: restore zero-alloc encode — the duplicated appender was load-bearing
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]>
2026-07-27 15:25:34 -07:00
2026-05-06 13:07:41 -07:00

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:

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 repo
  • zap-ws — multi-stream pubsub, per-stream FEC
  • zap-fix — FIX 4.4/5.0 trading channel
  • zap-rns — KEM-bound service naming
  • zap-mcp — Model Context Protocol over ZAP
  • zap-acp — Agent Communication Protocol
  • zap-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

S
Description
HTTP request/response over ZAP — drop-in net/http API
Readme MIT
191 KiB
Languages
Go 93.9%
ZAP 5.7%
Makefile 0.4%