http

package module
v0.3.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: Apache-2.0, MIT Imports: 14 Imported by: 0

README

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/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  |   (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/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

Documentation

Index

Constants

View Source
const (
	FrameRequest  uint16 = 0x01
	FrameResponse uint16 = 0x02
	// Streaming response frames. A streamed response is a FrameResponseHead
	// (status + headers, no body) followed by zero or more FrameData chunks and
	// a terminating FrameEnd (optional trailers). This is how server→client
	// push (SSE, MCP notifications, chunked bodies) rides ZAP — the analogue of
	// HTTP/2 HEADERS + DATA + END_STREAM. The non-streaming FrameResponse path
	// is unchanged, so existing peers interoperate.
	FrameResponseHead uint16 = 0x03
	FrameData         uint16 = 0x04
	FrameEnd          uint16 = 0x05
)

Frame type IDs. The ZAP message header carries the type in the upper byte of the 16-bit flags field; the encoder tags the message with type<<8 and flags>>8 recovers it. A request frame and a response frame are the two shapes the non-streaming wire carries.

View Source
const MaxFrameSize = 64 << 20 // 64 MiB

MaxFrameSize bounds an inbound frame. The wire format leaves room for larger frames; the limit here defends a server against a malicious peer announcing a multi-gigabyte length prefix.

Variables

This section is empty.

Functions

func AppendRequest added in v0.2.1

func AppendRequest(dst []byte, req *fasthttp.Request) ([]byte, error)

AppendRequest appends a request frame to dst and returns the extended slice. Passing a reused (len-0) buffer makes the steady-state marshal zero-alloc; MarshalRequest is the dst==nil convenience.

func AppendResponse added in v0.2.1

func AppendResponse(dst []byte, resp *fasthttp.Response) ([]byte, error)

AppendResponse appends a response frame to dst and returns the extended slice. See AppendRequest.

func DataChunkOf added in v0.2.0

func DataChunkOf(frame []byte) ([]byte, error)

DataChunkOf extracts the chunk bytes from a FrameData frame. The returned slice aliases the frame buffer; copy it to retain past the frame's lifetime.

func FrameTypeOf added in v0.2.0

func FrameTypeOf(frame []byte) (uint16, error)

FrameTypeOf peeks a frame's type from its ZAP header without decoding the body, so a reader can dispatch (response vs streamed head vs data vs end).

func Get

func Get(addr, path string) (*fasthttp.Response, error)

Get is a convenience for one-shot service-to-service calls. The caller owns resp and must fasthttp.ReleaseResponse it when done.

func ListenAndServe

func ListenAndServe(addr string, handler fasthttp.RequestHandler) error

ListenAndServe is the convenience equivalent of fasthttp.ListenAndServe.

func MarshalData added in v0.2.0

func MarshalData(chunk []byte) []byte

MarshalData wraps one body chunk as a FrameData frame.

func MarshalEnd added in v0.2.0

func MarshalEnd(trailer []byte) []byte

MarshalEnd terminates a stream, carrying optional encoded trailers.

func MarshalRequest

func MarshalRequest(req *fasthttp.Request) ([]byte, error)

MarshalRequest serializes a *fasthttp.Request into a ZAP frame. The returned bytes are the ZAP message; the transport layer prepends the length prefix (see transport.go). It is the dst==nil convenience for AppendRequest — hot callers append into a reused buffer to stay allocation-free.

func MarshalResponse

func MarshalResponse(resp *fasthttp.Response) ([]byte, error)

MarshalResponse serializes a *fasthttp.Response into a ZAP frame. See MarshalRequest; AppendResponse is the reused-buffer form.

func MarshalResponseHead added in v0.2.0

func MarshalResponseHead(resp *fasthttp.Response) ([]byte, error)

MarshalResponseHead serializes a response's status + headers (NO body) as the opening frame of a stream. The body is delivered by subsequent FrameData frames.

func UnmarshalRequest

func UnmarshalRequest(frame []byte, dst *fasthttp.Request) error

UnmarshalRequest reconstructs a request into dst from a ZAP frame. dst is reset first, then populated: method, request-URI, protocol, headers, trailers, and body. Content-Length is derived from the body length by SetBody; Host comes from a Host header if the frame carried one.

func UnmarshalResponse

func UnmarshalResponse(frame []byte, dst *fasthttp.Response) error

UnmarshalResponse reconstructs a response into dst from a ZAP frame.

func UnmarshalResponseHead added in v0.2.0

func UnmarshalResponseHead(frame []byte, dst *fasthttp.Response) error

UnmarshalResponseHead applies a streamed response's status + headers to dst. The body is left empty; the caller attaches a streaming body that reads the following FrameData frames.

Types

type Server

type Server struct {
	Addr string // ":9999" if empty
	// Network mirrors net.Listen's first argument: "tcp" when empty, or
	// "unix" to serve the same ZAP frames on a socket path given in Addr.
	// The wire is identical either way; only the plumbing differs.
	Network      string
	Handler      fasthttp.RequestHandler // required
	ReadTimeout  time.Duration           // 0 means no timeout
	WriteTimeout time.Duration
	IdleTimeout  time.Duration
	Logger       fasthttp.Logger // passed to each RequestCtx; nil is fine
	// contains filtered or unexported fields
}

Server is a ZAP-HTTP server. Zero-value is usable; common knobs (Addr, Handler, ReadTimeout, …) mirror fasthttp.Server.

func (*Server) Close

func (s *Server) Close() error

Close stops the listener; in-flight handlers are not interrupted.

func (*Server) ListenAndServe

func (s *Server) ListenAndServe() error

ListenAndServe binds Addr and serves until Close is called or a fatal accept error occurs.

func (*Server) Serve

func (s *Server) Serve(ln net.Listener) error

Serve accepts connections on ln and serves each one in its own goroutine. Returns when the listener is closed.

type Transport

type Transport struct {
	// contains filtered or unexported fields
}

Transport speaks ZAP over a pooled connection. Field-zero is invalid; use Dial.

func Dial added in v0.2.2

func Dial(network, addr string) *Transport

Dial returns a Transport that speaks ZAP to addr over network, mirroring net.Dial: the network is a value ("tcp", "unix", …), not a family of functions. A unix address is a socket path and carries the same ZAP frames as tcp — the wire does not change with the network.

http.Dial("tcp", "billing.hanzo.svc:9653")
http.Dial("unix", "/run/hanzo/billing.sock")

Dialing is lazy; the first request opens the connection.

func (*Transport) CloseIdleConnections

func (t *Transport) CloseIdleConnections()

CloseIdleConnections closes every idle conn in the pool. Active requests are unaffected. Useful in tests and on shutdown.

func (*Transport) Do

func (t *Transport) Do(req *fasthttp.Request, resp *fasthttp.Response) error

Do executes a single request/response exchange, filling resp. It is safe for concurrent use: each call takes its own connection from the pool.

func (*Transport) SetDialTimeout

func (t *Transport) SetDialTimeout(d time.Duration)

SetDialTimeout overrides the default 10s dial timeout.

func (*Transport) SetMaxIdleConns

func (t *Transport) SetMaxIdleConns(n int)

SetMaxIdleConns caps the number of idle conns held in the pool. Surplus conns close on return.

func (*Transport) SetReadTimeout

func (t *Transport) SetReadTimeout(d time.Duration)

SetReadTimeout overrides the default 30s response-read timeout.

Directories

Path Synopsis
examples
hello command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL