refactor: de-capnproto HTTP-over-ZAP codec onto zap-proto/go v1.1.1

Remove capnproto.org/go/capnp/v3; reimplement the wire codec on the pure-stdlib
github.com/zap-proto/go runtime. Public API (ListenAndServe/Server/Transport/
Marshal*) unchanged — consumers rebuild only. One wire across the ZAP stack.
This commit is contained in:
2026-06-18 13:26:00 -07:00
parent 777d2c0858
commit 48e4b83b40
8 changed files with 266 additions and 919 deletions
-3
View File
@@ -6,6 +6,3 @@
# Editor
.vscode/
.idea/
# Schema codegen — checked in, but ignore stray files
schema/go.capnp
+6 -19
View File
@@ -1,24 +1,11 @@
# zap-http Makefile.
#
# Re-runs Cap'n Proto codegen from schema/zap_http.zap into
# internal/wire/. Requires capnp + capnpc-go on PATH.
# The ZAP-HTTP wire codec is hand-written in wire.go against the
# github.com/zap-proto/go runtime — there is no code-generation step and
# no external schema toolchain. schema/zap_http.zap documents the
# logical wire shape; wire.go's field offsets are the binding.
GOPATH ?= $(shell go env GOPATH)
SCHEMA := schema/zap_http.zap
GENDIR := internal/wire
.PHONY: schema test build bench clean
schema:
@command -v capnp >/dev/null || { echo "install capnp: brew install capnp"; exit 1; }
@command -v $(GOPATH)/bin/capnpc-go >/dev/null || { echo "installing capnpc-go..."; \
go install capnproto.org/go/capnp/v3/capnpc-go@latest; }
@mkdir -p $(GENDIR)
@if [ ! -f schema/go.capnp ]; then \
gocapnp=$$(find $(GOPATH)/pkg/mod/capnproto.org -name go.capnp 2>/dev/null | head -1); \
[ -n "$$gocapnp" ] && cp "$$gocapnp" schema/go.capnp; \
fi
PATH=$(GOPATH)/bin:$$PATH capnp compile -I schema -ogo:$(GENDIR) $(SCHEMA)
.PHONY: build test bench clean
build:
go build ./...
@@ -30,4 +17,4 @@ bench:
go test ./... -run=^$$ -bench=. -benchmem
clean:
rm -rf $(GENDIR)
go clean ./...
+2 -7
View File
@@ -1,10 +1,5 @@
module github.com/zap-proto/http
go 1.23
go 1.26
require capnproto.org/go/capnp/v3 v3.0.1-alpha.2
require (
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381 // indirect
golang.org/x/sync v0.7.0 // indirect
)
require github.com/zap-proto/go v1.1.0
+2 -20
View File
@@ -1,20 +1,2 @@
capnproto.org/go/capnp/v3 v3.0.1-alpha.2 h1:W/cf+XEArUSwcBBE/9wS2NpWDkM5NLQOjmzEiHZpYi0=
capnproto.org/go/capnp/v3 v3.0.1-alpha.2/go.mod h1:2vT5D2dtG8sJGEoEKU17e+j7shdaYp1Myl8X03B3hmc=
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381 h1:d5EKgQfRQvO97jnISfR89AiCCCJMwMFoSxUiU0OGCRU=
github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381/go.mod h1:OU76gHeRo8xrzGJU3F3I1CqX1ekM8dfJw0+wPeMwnp0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU=
github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k=
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
github.com/zap-proto/go v1.1.0 h1:DDGhuTBNqkmNax7QV/VvkInJR7hiOcybwUjN3YBt8fg=
github.com/zap-proto/go v1.1.0/go.mod h1:914SNGTH6Rv3Yu1MweWJBPEN8FZlo5C39QyhaB0C7Q0=
-665
View File
@@ -1,665 +0,0 @@
// Code generated by capnpc-go. DO NOT EDIT.
package zaphttp_wire
import (
capnp "capnproto.org/go/capnp/v3"
text "capnproto.org/go/capnp/v3/encoding/text"
schemas "capnproto.org/go/capnp/v3/schemas"
strconv "strconv"
)
type Header capnp.Struct
// Header_TypeID is the unique identifier for the type Header.
const Header_TypeID = 0x82ef9c51fc9be8ce
func NewHeader(s *capnp.Segment) (Header, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
return Header(st), err
}
func NewRootHeader(s *capnp.Segment) (Header, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2})
return Header(st), err
}
func ReadRootHeader(msg *capnp.Message) (Header, error) {
root, err := msg.Root()
return Header(root.Struct()), err
}
func (s Header) String() string {
str, _ := text.Marshal(0x82ef9c51fc9be8ce, capnp.Struct(s))
return str
}
func (s Header) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr {
return capnp.Struct(s).EncodeAsPtr(seg)
}
func (Header) DecodeFromPtr(p capnp.Ptr) Header {
return Header(capnp.Struct{}.DecodeFromPtr(p))
}
func (s Header) ToPtr() capnp.Ptr {
return capnp.Struct(s).ToPtr()
}
func (s Header) IsValid() bool {
return capnp.Struct(s).IsValid()
}
func (s Header) Message() *capnp.Message {
return capnp.Struct(s).Message()
}
func (s Header) Segment() *capnp.Segment {
return capnp.Struct(s).Segment()
}
func (s Header) Name() (string, error) {
p, err := capnp.Struct(s).Ptr(0)
return p.Text(), err
}
func (s Header) HasName() bool {
return capnp.Struct(s).HasPtr(0)
}
func (s Header) NameBytes() ([]byte, error) {
p, err := capnp.Struct(s).Ptr(0)
return p.TextBytes(), err
}
func (s Header) SetName(v string) error {
return capnp.Struct(s).SetText(0, v)
}
func (s Header) Values() (capnp.TextList, error) {
p, err := capnp.Struct(s).Ptr(1)
return capnp.TextList(p.List()), err
}
func (s Header) HasValues() bool {
return capnp.Struct(s).HasPtr(1)
}
func (s Header) SetValues(v capnp.TextList) error {
return capnp.Struct(s).SetPtr(1, v.ToPtr())
}
// NewValues sets the values field to a newly
// allocated capnp.TextList, preferring placement in s's segment.
func (s Header) NewValues(n int32) (capnp.TextList, error) {
l, err := capnp.NewTextList(capnp.Struct(s).Segment(), n)
if err != nil {
return capnp.TextList{}, err
}
err = capnp.Struct(s).SetPtr(1, l.ToPtr())
return l, err
}
// Header_List is a list of Header.
type Header_List = capnp.StructList[Header]
// NewHeader creates a new list of Header.
func NewHeader_List(s *capnp.Segment, sz int32) (Header_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz)
return capnp.StructList[Header](l), err
}
// Header_Future is a wrapper for a Header promised by a client call.
type Header_Future struct{ *capnp.Future }
func (f Header_Future) Struct() (Header, error) {
p, err := f.Future.Ptr()
return Header(p.Struct()), err
}
type Request capnp.Struct
// Request_TypeID is the unique identifier for the type Request.
const Request_TypeID = 0xb2063b47a28c074a
func NewRequest(s *capnp.Segment) (Request, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 6})
return Request(st), err
}
func NewRootRequest(s *capnp.Segment) (Request, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 6})
return Request(st), err
}
func ReadRootRequest(msg *capnp.Message) (Request, error) {
root, err := msg.Root()
return Request(root.Struct()), err
}
func (s Request) String() string {
str, _ := text.Marshal(0xb2063b47a28c074a, capnp.Struct(s))
return str
}
func (s Request) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr {
return capnp.Struct(s).EncodeAsPtr(seg)
}
func (Request) DecodeFromPtr(p capnp.Ptr) Request {
return Request(capnp.Struct{}.DecodeFromPtr(p))
}
func (s Request) ToPtr() capnp.Ptr {
return capnp.Struct(s).ToPtr()
}
func (s Request) IsValid() bool {
return capnp.Struct(s).IsValid()
}
func (s Request) Message() *capnp.Message {
return capnp.Struct(s).Message()
}
func (s Request) Segment() *capnp.Segment {
return capnp.Struct(s).Segment()
}
func (s Request) Method() (string, error) {
p, err := capnp.Struct(s).Ptr(0)
return p.Text(), err
}
func (s Request) HasMethod() bool {
return capnp.Struct(s).HasPtr(0)
}
func (s Request) MethodBytes() ([]byte, error) {
p, err := capnp.Struct(s).Ptr(0)
return p.TextBytes(), err
}
func (s Request) SetMethod(v string) error {
return capnp.Struct(s).SetText(0, v)
}
func (s Request) Target() (string, error) {
p, err := capnp.Struct(s).Ptr(1)
return p.Text(), err
}
func (s Request) HasTarget() bool {
return capnp.Struct(s).HasPtr(1)
}
func (s Request) TargetBytes() ([]byte, error) {
p, err := capnp.Struct(s).Ptr(1)
return p.TextBytes(), err
}
func (s Request) SetTarget(v string) error {
return capnp.Struct(s).SetText(1, v)
}
func (s Request) Proto() (string, error) {
p, err := capnp.Struct(s).Ptr(2)
return p.Text(), err
}
func (s Request) HasProto() bool {
return capnp.Struct(s).HasPtr(2)
}
func (s Request) ProtoBytes() ([]byte, error) {
p, err := capnp.Struct(s).Ptr(2)
return p.TextBytes(), err
}
func (s Request) SetProto(v string) error {
return capnp.Struct(s).SetText(2, v)
}
func (s Request) Headers() (Header_List, error) {
p, err := capnp.Struct(s).Ptr(3)
return Header_List(p.List()), err
}
func (s Request) HasHeaders() bool {
return capnp.Struct(s).HasPtr(3)
}
func (s Request) SetHeaders(v Header_List) error {
return capnp.Struct(s).SetPtr(3, v.ToPtr())
}
// NewHeaders sets the headers field to a newly
// allocated Header_List, preferring placement in s's segment.
func (s Request) NewHeaders(n int32) (Header_List, error) {
l, err := NewHeader_List(capnp.Struct(s).Segment(), n)
if err != nil {
return Header_List{}, err
}
err = capnp.Struct(s).SetPtr(3, l.ToPtr())
return l, err
}
func (s Request) Body() ([]byte, error) {
p, err := capnp.Struct(s).Ptr(4)
return []byte(p.Data()), err
}
func (s Request) HasBody() bool {
return capnp.Struct(s).HasPtr(4)
}
func (s Request) SetBody(v []byte) error {
return capnp.Struct(s).SetData(4, v)
}
func (s Request) Trailer() (Header_List, error) {
p, err := capnp.Struct(s).Ptr(5)
return Header_List(p.List()), err
}
func (s Request) HasTrailer() bool {
return capnp.Struct(s).HasPtr(5)
}
func (s Request) SetTrailer(v Header_List) error {
return capnp.Struct(s).SetPtr(5, v.ToPtr())
}
// NewTrailer sets the trailer field to a newly
// allocated Header_List, preferring placement in s's segment.
func (s Request) NewTrailer(n int32) (Header_List, error) {
l, err := NewHeader_List(capnp.Struct(s).Segment(), n)
if err != nil {
return Header_List{}, err
}
err = capnp.Struct(s).SetPtr(5, l.ToPtr())
return l, err
}
// Request_List is a list of Request.
type Request_List = capnp.StructList[Request]
// NewRequest creates a new list of Request.
func NewRequest_List(s *capnp.Segment, sz int32) (Request_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 6}, sz)
return capnp.StructList[Request](l), err
}
// Request_Future is a wrapper for a Request promised by a client call.
type Request_Future struct{ *capnp.Future }
func (f Request_Future) Struct() (Request, error) {
p, err := f.Future.Ptr()
return Request(p.Struct()), err
}
type Response capnp.Struct
// Response_TypeID is the unique identifier for the type Response.
const Response_TypeID = 0xab7e8c4a7b2a9bff
func NewResponse(s *capnp.Segment) (Response, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 5})
return Response(st), err
}
func NewRootResponse(s *capnp.Segment) (Response, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 5})
return Response(st), err
}
func ReadRootResponse(msg *capnp.Message) (Response, error) {
root, err := msg.Root()
return Response(root.Struct()), err
}
func (s Response) String() string {
str, _ := text.Marshal(0xab7e8c4a7b2a9bff, capnp.Struct(s))
return str
}
func (s Response) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr {
return capnp.Struct(s).EncodeAsPtr(seg)
}
func (Response) DecodeFromPtr(p capnp.Ptr) Response {
return Response(capnp.Struct{}.DecodeFromPtr(p))
}
func (s Response) ToPtr() capnp.Ptr {
return capnp.Struct(s).ToPtr()
}
func (s Response) IsValid() bool {
return capnp.Struct(s).IsValid()
}
func (s Response) Message() *capnp.Message {
return capnp.Struct(s).Message()
}
func (s Response) Segment() *capnp.Segment {
return capnp.Struct(s).Segment()
}
func (s Response) Status() uint16 {
return capnp.Struct(s).Uint16(0)
}
func (s Response) SetStatus(v uint16) {
capnp.Struct(s).SetUint16(0, v)
}
func (s Response) Reason() (string, error) {
p, err := capnp.Struct(s).Ptr(0)
return p.Text(), err
}
func (s Response) HasReason() bool {
return capnp.Struct(s).HasPtr(0)
}
func (s Response) ReasonBytes() ([]byte, error) {
p, err := capnp.Struct(s).Ptr(0)
return p.TextBytes(), err
}
func (s Response) SetReason(v string) error {
return capnp.Struct(s).SetText(0, v)
}
func (s Response) Proto() (string, error) {
p, err := capnp.Struct(s).Ptr(1)
return p.Text(), err
}
func (s Response) HasProto() bool {
return capnp.Struct(s).HasPtr(1)
}
func (s Response) ProtoBytes() ([]byte, error) {
p, err := capnp.Struct(s).Ptr(1)
return p.TextBytes(), err
}
func (s Response) SetProto(v string) error {
return capnp.Struct(s).SetText(1, v)
}
func (s Response) Headers() (Header_List, error) {
p, err := capnp.Struct(s).Ptr(2)
return Header_List(p.List()), err
}
func (s Response) HasHeaders() bool {
return capnp.Struct(s).HasPtr(2)
}
func (s Response) SetHeaders(v Header_List) error {
return capnp.Struct(s).SetPtr(2, v.ToPtr())
}
// NewHeaders sets the headers field to a newly
// allocated Header_List, preferring placement in s's segment.
func (s Response) NewHeaders(n int32) (Header_List, error) {
l, err := NewHeader_List(capnp.Struct(s).Segment(), n)
if err != nil {
return Header_List{}, err
}
err = capnp.Struct(s).SetPtr(2, l.ToPtr())
return l, err
}
func (s Response) Body() ([]byte, error) {
p, err := capnp.Struct(s).Ptr(3)
return []byte(p.Data()), err
}
func (s Response) HasBody() bool {
return capnp.Struct(s).HasPtr(3)
}
func (s Response) SetBody(v []byte) error {
return capnp.Struct(s).SetData(3, v)
}
func (s Response) Trailer() (Header_List, error) {
p, err := capnp.Struct(s).Ptr(4)
return Header_List(p.List()), err
}
func (s Response) HasTrailer() bool {
return capnp.Struct(s).HasPtr(4)
}
func (s Response) SetTrailer(v Header_List) error {
return capnp.Struct(s).SetPtr(4, v.ToPtr())
}
// NewTrailer sets the trailer field to a newly
// allocated Header_List, preferring placement in s's segment.
func (s Response) NewTrailer(n int32) (Header_List, error) {
l, err := NewHeader_List(capnp.Struct(s).Segment(), n)
if err != nil {
return Header_List{}, err
}
err = capnp.Struct(s).SetPtr(4, l.ToPtr())
return l, err
}
// Response_List is a list of Response.
type Response_List = capnp.StructList[Response]
// NewResponse creates a new list of Response.
func NewResponse_List(s *capnp.Segment, sz int32) (Response_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 5}, sz)
return capnp.StructList[Response](l), err
}
// Response_Future is a wrapper for a Response promised by a client call.
type Response_Future struct{ *capnp.Future }
func (f Response_Future) Struct() (Response, error) {
p, err := f.Future.Ptr()
return Response(p.Struct()), err
}
type Frame capnp.Struct
type Frame_Which uint16
const (
Frame_Which_request Frame_Which = 0
Frame_Which_response Frame_Which = 1
)
func (w Frame_Which) String() string {
const s = "requestresponse"
switch w {
case Frame_Which_request:
return s[0:7]
case Frame_Which_response:
return s[7:15]
}
return "Frame_Which(" + strconv.FormatUint(uint64(w), 10) + ")"
}
// Frame_TypeID is the unique identifier for the type Frame.
const Frame_TypeID = 0xda62522904e66ee9
func NewFrame(s *capnp.Segment) (Frame, error) {
st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1})
return Frame(st), err
}
func NewRootFrame(s *capnp.Segment) (Frame, error) {
st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1})
return Frame(st), err
}
func ReadRootFrame(msg *capnp.Message) (Frame, error) {
root, err := msg.Root()
return Frame(root.Struct()), err
}
func (s Frame) String() string {
str, _ := text.Marshal(0xda62522904e66ee9, capnp.Struct(s))
return str
}
func (s Frame) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr {
return capnp.Struct(s).EncodeAsPtr(seg)
}
func (Frame) DecodeFromPtr(p capnp.Ptr) Frame {
return Frame(capnp.Struct{}.DecodeFromPtr(p))
}
func (s Frame) ToPtr() capnp.Ptr {
return capnp.Struct(s).ToPtr()
}
func (s Frame) Which() Frame_Which {
return Frame_Which(capnp.Struct(s).Uint16(0))
}
func (s Frame) IsValid() bool {
return capnp.Struct(s).IsValid()
}
func (s Frame) Message() *capnp.Message {
return capnp.Struct(s).Message()
}
func (s Frame) Segment() *capnp.Segment {
return capnp.Struct(s).Segment()
}
func (s Frame) Request() (Request, error) {
if capnp.Struct(s).Uint16(0) != 0 {
panic("Which() != request")
}
p, err := capnp.Struct(s).Ptr(0)
return Request(p.Struct()), err
}
func (s Frame) HasRequest() bool {
if capnp.Struct(s).Uint16(0) != 0 {
return false
}
return capnp.Struct(s).HasPtr(0)
}
func (s Frame) SetRequest(v Request) error {
capnp.Struct(s).SetUint16(0, 0)
return capnp.Struct(s).SetPtr(0, capnp.Struct(v).ToPtr())
}
// NewRequest sets the request field to a newly
// allocated Request struct, preferring placement in s's segment.
func (s Frame) NewRequest() (Request, error) {
capnp.Struct(s).SetUint16(0, 0)
ss, err := NewRequest(capnp.Struct(s).Segment())
if err != nil {
return Request{}, err
}
err = capnp.Struct(s).SetPtr(0, capnp.Struct(ss).ToPtr())
return ss, err
}
func (s Frame) Response() (Response, error) {
if capnp.Struct(s).Uint16(0) != 1 {
panic("Which() != response")
}
p, err := capnp.Struct(s).Ptr(0)
return Response(p.Struct()), err
}
func (s Frame) HasResponse() bool {
if capnp.Struct(s).Uint16(0) != 1 {
return false
}
return capnp.Struct(s).HasPtr(0)
}
func (s Frame) SetResponse(v Response) error {
capnp.Struct(s).SetUint16(0, 1)
return capnp.Struct(s).SetPtr(0, capnp.Struct(v).ToPtr())
}
// NewResponse sets the response field to a newly
// allocated Response struct, preferring placement in s's segment.
func (s Frame) NewResponse() (Response, error) {
capnp.Struct(s).SetUint16(0, 1)
ss, err := NewResponse(capnp.Struct(s).Segment())
if err != nil {
return Response{}, err
}
err = capnp.Struct(s).SetPtr(0, capnp.Struct(ss).ToPtr())
return ss, err
}
// Frame_List is a list of Frame.
type Frame_List = capnp.StructList[Frame]
// NewFrame creates a new list of Frame.
func NewFrame_List(s *capnp.Segment, sz int32) (Frame_List, error) {
l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}, sz)
return capnp.StructList[Frame](l), err
}
// Frame_Future is a wrapper for a Frame promised by a client call.
type Frame_Future struct{ *capnp.Future }
func (f Frame_Future) Struct() (Frame, error) {
p, err := f.Future.Ptr()
return Frame(p.Struct()), err
}
func (p Frame_Future) Request() Request_Future {
return Request_Future{Future: p.Future.Field(0, nil)}
}
func (p Frame_Future) Response() Response_Future {
return Response_Future{Future: p.Future.Field(0, nil)}
}
const schema_a3d4b2e80f1c5a7e = "x\xda\xc4\x93\xbdk\x14Q\x14\xc5\xcfyo&k\x91" +
"\xcd\xecc\xb6\xb0\x10baa\x82\x91D\x1b\x89\xc5J" +
"\xf03 \xe4\xa5\x11ld\xe2>\\!\xbb3\xce\xcc" +
"\xba\xa8\x90\x05\xeb\x94\x16\x86$\x90\xf8\x01\xa2\xddba" +
"\xc4\x80\xda\xd8Y\xa5\xf2\x0f\x90X\xf9\x0f\x88#o?" +
"%nj\xbb\x99;g\xeey\xefw\xcf\x9dn\xf0\x82" +
"3\x93\xff\" \xf4qw$\xfb\xba\xbf\xfeKo\xfc" +
"|\x0c\xe51[\xb9y\xcc\xdbo\xed=\x87+r\x80" +
"\x7f\x8d-_\xd3>]g\x03\xcc\xb2\xf5\xc9G\xf3\xab" +
"+o\xa0=\xfe-v\xadd\x87\x9f\xfdOV|v" +
"\x977\x08f\xf3\xb9\xd5gW\xce\x8f\xb4\x0et\x1e\xb1" +
"\xe2\x09\xf9\xde\x9f\x91V<%\xdb\xe2\x1f\xb5\xef\xce\xc4" +
"\xe2\xd2\xb7\x03\xad/1'\x00\x7f\xcdy\xedo;\xf6" +
"\xc7M\xa7\x81\xa9\xeca\x10\xdd\xaa\xa4i\xc4\xd3\xb7\x83" +
"\xa8\x16\xcd^\x1d7A\xd9\xc4\x0b\xa4>\"\x1d\xc0!" +
"\xa0&&\x01}BRO\x0b*\xb2H[\x9c\x9a\x05" +
"\xf4II}Q\xd0\xab\x05U\xc3Q\x08\x8e\x82\xa5\xfb" +
"\xc1r\xdd$\x1c\x03\x17$\xdb\xd51\xb0\xef$\xbaN" +
"\x8b&\x89\xc2Zb`\xbd\x8e\xf6\xbd\xd6l\xdb'\x92" +
"zK\xb0g\xb5ikO%\xf5\x0bA%X\xa4\x00" +
"\xd4\xf6\x19@oH\xeaW\x82J\x8a\"%\xa0^\xce" +
"\x01zKR\x7f\x10T\x8e,\xd2\x01\xd4\x8e=\xfe[" +
"I\xfdQP\xb9N\x91.\xa0v\xad\xf2\x9d\xa4\xde\x13" +
",%i\x90\xd6\x13\xe6 \x98\x03K\xb1\x09\x92\xb0\xd6" +
"\xbb\xcfx\x14\x87i\xd8{kV\xda\x80\xfa\xd7+\x0c" +
"\xe6\x0e\xda\xa2\xb7\x14\x96\x1f0\x0f\xc1<\xd8L\xe3\xe0" +
"\xee\xb2\x89\x0fU\xff3\x80\xc5\x92\xb9W7Iz8" +
"\x155\x1c\x8b\x18\x86E\x0e\xc3\xe2\x0c\xc3\xe2\x0e\xc1R" +
"5i%,\xf7\xe7\x9a\x06\xf1\x1d\x93\xfe\x1f,\x97\xbd" +
"8\xa8\x9an,G\xb3\xac\x93\xcb\xb9A.\xf3\xfc\x9d" +
"u\x839\x0f\xe8S\x92\xfa\x9c`3\xee\xd0da\xb0" +
"D \x0b`\x16\xf7\xf2\x07\xb00X\xc8\xce\xd7?\x01" +
"\x00\x00\xff\xff\xacC\xe6\x10"
func RegisterSchema(reg *schemas.Registry) {
reg.Register(&schemas.Schema{
String: schema_a3d4b2e80f1c5a7e,
Nodes: []uint64{
0x82ef9c51fc9be8ce,
0xab7e8c4a7b2a9bff,
0xb2063b47a28c074a,
0xda62522904e66ee9,
},
Compressed: true,
})
}
+19 -14
View File
@@ -3,15 +3,20 @@
# Authored in ZAP schema language (whitespace-significant, no curly braces,
# field ordinals inferred from declaration order).
#
# This is the source of truth for the ZAP-HTTP wire format. The
# generated Go bindings in ../internal/wire are produced via:
# This is the source of truth for the ZAP-HTTP wire format. The Go
# codec is hand-written against the github.com/zap-proto/go runtime in
# ../wire.go — each struct below maps to a zap-proto/go object whose
# fields are packed at fixed 8-byte-aligned slots (text/bytes) or
# natural-width scalar offsets. The field offsets in ../wire.go are the
# binding; this file documents the logical shape they implement.
#
# zapc compile zap_http.zap --out=zap_http.capnp
# capnp compile -ogo:../internal/wire zap_http.capnp
#
# The wire is one ZAP frame per HTTP message. A request frame carries
# the request line + headers + body; a response frame carries status +
# headers + body. Both sides may carry trailers (RFC 9110 §6.5).
# The wire is one ZAP frame per HTTP message. The frame type (request
# vs response) rides in the message header flags (flags>>8), not a
# union struct. A request frame carries the request line + headers +
# body; a response frame carries status + headers + body. Both sides
# may carry trailers (RFC 9110 §6.5). Headers and trailers are encoded
# as a JSON map<Text, List(Text)> rather than a nested struct list,
# matching http.Header's native shape losslessly.
#
# Streaming (chunked-style transfer) is reserved for a follow-up
# protocol slice and not represented here. v0.1 assumes the entire body
@@ -50,9 +55,9 @@ struct Response
body Data
trailer List(Header)
# Frame is the union the wire actually carries. Some sub-protocols may
# add Notify or Stream variants in future; today there are exactly two.
struct Frame
union
request Request
response Response
# Frame discrimination is carried by the ZAP message header flags
# (flags>>8), not a union struct: FrameRequest=0x01 tags a Request,
# FrameResponse=0x02 tags a Response (see ../wire.go). A Request object
# is the message root when the flag is FrameRequest; a Response object
# is the root when it is FrameResponse. Some sub-protocols may add
# Notify or Stream type IDs in future; today there are exactly two.
+141 -191
View File
@@ -4,9 +4,11 @@
// from ZAP frames.
//
// The wire format is documented in schema/zap_http.zap (authored in
// the ZAP schema language). Go bindings in internal/wire are derived
// from that schema; today via the zapc + capnp toolchain, replaceable
// with a hand-rolled encoder once zapc grows a Go backend.
// the ZAP schema language). The bytes on the wire are
// github.com/zap-proto/go Objects — the canonical pure-stdlib ZAP
// runtime — packed at explicit byte offsets within each object's fixed
// payload. There is no external codec dependency: this file is the
// encoder and decoder.
//
// What's covered today (v0.1):
// - Request method, target, headers, body
@@ -19,6 +21,30 @@
// - HTTP/2 server push semantics; HTTP/3 priorities.
// - WebSocket / SSE upgrade paths; those land in zap-proto/ws.
//
// # Wire layout
//
// A frame is one zap-proto/go message. The message type rides in the
// header flags (flags>>8): FrameRequest for a request, FrameResponse
// for a response — the same discriminator discipline the zap forward
// envelopes use. There is no union struct; the flag selects which
// object shape is the message root.
//
// # Offset discipline
//
// zap-proto/go's ObjectBuilder.SetBytes / SetText write an 8-byte slot
// at the field offset: {relOffset uint32, length uint32}. The
// variable-length tail is appended after the fixed section in Finish()
// and the relOffset is patched to point at it. Consequently EVERY text
// or bytes field consumes 8 bytes of fixed payload — adjacent text
// fields MUST be spaced 8 bytes apart or their slots overlap and
// corrupt each other. Fixed scalars use their natural width on a
// natural boundary. These offsets are the contract; changing one is a
// wire break.
//
// Headers (and trailers) ride as a JSON map[string][]string — the
// native shape of http.Header — preserving multi-value semantics
// (RFC 9110 §5.2) losslessly without a nested-object list.
//
// This file holds the marshal/unmarshal between *http.Request /
// http.Response and ZAP frames. The transport layer (TCP framing
// today, PQ-handshake-wrapped ZAP transport in v0.2) lives in
@@ -28,6 +54,7 @@ package zaphttp
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
@@ -35,9 +62,41 @@ import (
"strconv"
"strings"
"capnproto.org/go/capnp/v3"
zap "github.com/zap-proto/go"
)
zhwire "github.com/zap-proto/http/internal/wire"
// Frame type IDs. The ZAP message header carries the type in the upper
// byte of the 16-bit flags field; FinishWithFlags(t<<8) tags the
// message and Message.Flags()>>8 recovers it. A request frame and a
// response frame are the two shapes the wire carries today.
const (
FrameRequest uint16 = 0x01
FrameResponse uint16 = 0x02
)
// Request frame field offsets within the object's fixed payload. Every
// text/bytes slot is 8 bytes (relOffset+length); see "Offset
// discipline" in the package doc.
const (
reqMethod = 0 // text (8)
reqTarget = 8 // text (8)
reqProto = 16 // text (8)
reqHeaders = 24 // bytes (8) -- JSON map[string][]string
reqBody = 32 // bytes (8)
reqTrailer = 40 // bytes (8) -- JSON map[string][]string
reqSlotSize = 48
)
// Response frame field offsets. status@0 is a uint16 (2 bytes); the
// first text slot starts at the next 8-byte boundary.
const (
respStatus = 0 // uint16 (2)
respReason = 8 // text (8)
respProto = 16 // text (8)
respHeaders = 24 // bytes (8) -- JSON map[string][]string
respBody = 32 // bytes (8)
respTrailer = 40 // bytes (8) -- JSON map[string][]string
respSlotSize = 48
)
// MarshalRequest serializes an *http.Request into a ZAP frame. The
@@ -52,47 +111,30 @@ func MarshalRequest(req *http.Request) ([]byte, error) {
return nil, fmt.Errorf("zaphttp: read body: %w", err)
}
msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
if err != nil {
return nil, err
}
frame, err := zhwire.NewRootFrame(seg)
if err != nil {
return nil, err
}
r, err := frame.NewRequest()
if err != nil {
return nil, err
}
if err := r.SetMethod(orDefault(req.Method, http.MethodGet)); err != nil {
return nil, err
}
target := req.RequestURI
if target == "" && req.URL != nil {
target = req.URL.RequestURI()
}
if err := r.SetTarget(target); err != nil {
return nil, err
headers, err := encodeHeaders(req.Header)
if err != nil {
return nil, fmt.Errorf("zaphttp: encode headers: %w", err)
}
if err := r.SetProto(orDefault(req.Proto, "ZAP-HTTP/1.0")); err != nil {
return nil, err
}
if err := writeHeaders(seg, req.Header, r.SetHeaders); err != nil {
return nil, err
}
if len(body) > 0 {
if err := r.SetBody(body); err != nil {
return nil, err
}
}
if len(req.Trailer) > 0 {
if err := writeHeaders(seg, req.Trailer, r.SetTrailer); err != nil {
return nil, err
}
trailer, err := encodeHeaders(req.Trailer)
if err != nil {
return nil, fmt.Errorf("zaphttp: encode trailer: %w", err)
}
return msg.Marshal()
b := zap.NewBuilder(reqSlotSize + len(body) + len(headers) + len(trailer) +
len(req.Method) + len(target) + len(req.Proto) + 64)
ob := b.StartObject(reqSlotSize)
ob.SetText(reqMethod, orDefault(req.Method, http.MethodGet))
ob.SetText(reqTarget, target)
ob.SetText(reqProto, orDefault(req.Proto, "ZAP-HTTP/1.0"))
ob.SetBytes(reqHeaders, headers)
ob.SetBytes(reqBody, body)
ob.SetBytes(reqTrailer, trailer)
ob.FinishAsRoot()
return b.FinishWithFlags(FrameRequest << 8), nil
}
// UnmarshalRequest reconstructs an *http.Request from a ZAP frame. The
@@ -100,45 +142,26 @@ func MarshalRequest(req *http.Request) ([]byte, error) {
// bytes; the request URL is parsed from target. Host is taken from the
// request's Host header (RFC 9110 §7.2).
func UnmarshalRequest(b []byte) (*http.Request, error) {
msg, err := capnp.Unmarshal(b)
msg, err := zap.Parse(b)
if err != nil {
return nil, err
}
frame, err := zhwire.ReadRootFrame(msg)
if err != nil {
return nil, err
}
if frame.Which() != zhwire.Frame_Which_request {
return nil, fmt.Errorf("zaphttp: frame is %s, want request", frame.Which())
}
r, err := frame.Request()
if err != nil {
return nil, err
if t := msg.Flags() >> 8; t != FrameRequest {
return nil, fmt.Errorf("zaphttp: frame type %#x, want request (%#x)", t, FrameRequest)
}
r := msg.Root()
method, err := r.Method()
method := r.Text(reqMethod)
target := r.Text(reqTarget)
proto := r.Text(reqProto)
headers, err := decodeHeaders(r.Bytes(reqHeaders))
if err != nil {
return nil, err
return nil, fmt.Errorf("zaphttp: decode headers: %w", err)
}
target, err := r.Target()
body := r.Bytes(reqBody)
trailer, err := decodeHeaders(r.Bytes(reqTrailer))
if err != nil {
return nil, err
}
proto, err := r.Proto()
if err != nil {
return nil, err
}
headers, err := readHeaders(r.Headers)
if err != nil {
return nil, err
}
body, err := r.Body()
if err != nil {
return nil, err
}
trailer, err := readHeaders(r.Trailer)
if err != nil {
return nil, err
return nil, fmt.Errorf("zaphttp: decode trailer: %w", err)
}
u, err := url.ParseRequestURI(target)
@@ -177,89 +200,58 @@ func MarshalResponse(resp *http.Response) ([]byte, error) {
return nil, fmt.Errorf("zaphttp: read body: %w", err)
}
msg, seg, err := capnp.NewMessage(capnp.SingleSegment(nil))
if err != nil {
return nil, err
}
frame, err := zhwire.NewRootFrame(seg)
if err != nil {
return nil, err
}
r, err := frame.NewResponse()
if err != nil {
return nil, err
}
if resp.StatusCode == 0 {
r.SetStatus(http.StatusOK)
} else {
r.SetStatus(uint16(resp.StatusCode))
status := resp.StatusCode
if status == 0 {
status = http.StatusOK
}
reason := resp.Status
if i := strings.IndexByte(reason, ' '); i >= 0 {
reason = reason[i+1:]
}
if err := r.SetReason(reason); err != nil {
return nil, err
headers, err := encodeHeaders(resp.Header)
if err != nil {
return nil, fmt.Errorf("zaphttp: encode headers: %w", err)
}
if err := r.SetProto(orDefault(resp.Proto, "ZAP-HTTP/1.0")); err != nil {
return nil, err
}
if err := writeHeaders(seg, resp.Header, r.SetHeaders); err != nil {
return nil, err
}
if len(body) > 0 {
if err := r.SetBody(body); err != nil {
return nil, err
}
}
if len(resp.Trailer) > 0 {
if err := writeHeaders(seg, resp.Trailer, r.SetTrailer); err != nil {
return nil, err
}
trailer, err := encodeHeaders(resp.Trailer)
if err != nil {
return nil, fmt.Errorf("zaphttp: encode trailer: %w", err)
}
return msg.Marshal()
b := zap.NewBuilder(respSlotSize + len(body) + len(headers) + len(trailer) +
len(reason) + len(resp.Proto) + 64)
ob := b.StartObject(respSlotSize)
ob.SetUint16(respStatus, uint16(status))
ob.SetText(respReason, reason)
ob.SetText(respProto, orDefault(resp.Proto, "ZAP-HTTP/1.0"))
ob.SetBytes(respHeaders, headers)
ob.SetBytes(respBody, body)
ob.SetBytes(respTrailer, trailer)
ob.FinishAsRoot()
return b.FinishWithFlags(FrameResponse << 8), nil
}
// UnmarshalResponse reconstructs an *http.Response from a ZAP frame.
func UnmarshalResponse(b []byte) (*http.Response, error) {
msg, err := capnp.Unmarshal(b)
msg, err := zap.Parse(b)
if err != nil {
return nil, err
}
frame, err := zhwire.ReadRootFrame(msg)
if err != nil {
return nil, err
}
if frame.Which() != zhwire.Frame_Which_response {
return nil, fmt.Errorf("zaphttp: frame is %s, want response", frame.Which())
}
r, err := frame.Response()
if err != nil {
return nil, err
if t := msg.Flags() >> 8; t != FrameResponse {
return nil, fmt.Errorf("zaphttp: frame type %#x, want response (%#x)", t, FrameResponse)
}
r := msg.Root()
status := int(r.Status())
reason, err := r.Reason()
status := int(r.Uint16(respStatus))
reason := r.Text(respReason)
proto := r.Text(respProto)
headers, err := decodeHeaders(r.Bytes(respHeaders))
if err != nil {
return nil, err
return nil, fmt.Errorf("zaphttp: decode headers: %w", err)
}
proto, err := r.Proto()
body := r.Bytes(respBody)
trailer, err := decodeHeaders(r.Bytes(respTrailer))
if err != nil {
return nil, err
}
headers, err := readHeaders(r.Headers)
if err != nil {
return nil, err
}
body, err := r.Body()
if err != nil {
return nil, err
}
trailer, err := readHeaders(r.Trailer)
if err != nil {
return nil, err
return nil, fmt.Errorf("zaphttp: decode trailer: %w", err)
}
statusText := reason
@@ -293,71 +285,29 @@ func readAllBody(r io.Reader) ([]byte, error) {
return io.ReadAll(r)
}
func writeHeaders(seg *capnp.Segment, h http.Header, setter func(zhwire.Header_List) error) error {
// encodeHeaders serializes an http.Header as a JSON map[string][]string.
// An empty header set encodes as nil (the SetBytes null slot), which
// decodeHeaders maps back to an empty, non-nil http.Header. Header is
// already map[string][]string, so this is a direct, lossless encoding
// that preserves multi-value names (RFC 9110 §5.2).
func encodeHeaders(h http.Header) ([]byte, error) {
if len(h) == 0 {
// Allocate an empty list so consumers don't see a default-zero
// pointer that fails to decode.
empty, err := zhwire.NewHeader_List(seg, 0)
if err != nil {
return err
}
return setter(empty)
return nil, nil
}
list, err := zhwire.NewHeader_List(seg, int32(len(h)))
if err != nil {
return err
}
i := 0
for name, values := range h {
hdr := list.At(i)
if err := hdr.SetName(name); err != nil {
return err
}
vlist, err := capnp.NewTextList(seg, int32(len(values)))
if err != nil {
return err
}
for j, v := range values {
if err := vlist.Set(j, v); err != nil {
return err
}
}
if err := hdr.SetValues(vlist); err != nil {
return err
}
i++
}
return setter(list)
return json.Marshal(h)
}
func readHeaders(getter func() (zhwire.Header_List, error)) (http.Header, error) {
list, err := getter()
if err != nil {
return nil, err
}
if list.Len() == 0 {
// decodeHeaders is the inverse of encodeHeaders. A null/empty slot
// yields an empty, non-nil http.Header so callers never see a nil map.
func decodeHeaders(b []byte) (http.Header, error) {
if len(b) == 0 {
return http.Header{}, nil
}
out := make(http.Header, list.Len())
for i := 0; i < list.Len(); i++ {
h := list.At(i)
name, err := h.Name()
if err != nil {
return nil, err
}
vlist, err := h.Values()
if err != nil {
return nil, err
}
for j := 0; j < vlist.Len(); j++ {
v, err := vlist.At(j)
if err != nil {
return nil, err
}
out.Add(name, v)
}
h := http.Header{}
if err := json.Unmarshal(b, &h); err != nil {
return nil, err
}
return out, nil
return h, nil
}
func orDefault(s, fallback string) string {
+96
View File
@@ -308,3 +308,99 @@ func TestMarshalRequest_idempotent(t *testing.T) {
t.Errorf("body mismatch")
}
}
// TestMarshalResponse_idempotent — marshal/unmarshal round-trip
// preserves response fields exactly, including a reason phrase,
// multi-value headers, a body, and a trailer. Pure unit test, no
// network — the symmetric counterpart to TestMarshalRequest_idempotent
// and the explicit guard on the response frame's field offsets.
func TestMarshalResponse_idempotent(t *testing.T) {
body := []byte("payload-bytes-\x00\x01\x02-with-nuls")
orig := &http.Response{
StatusCode: http.StatusTeapot,
Status: "418 I'm a teapot",
Proto: "ZAP-HTTP/1.0",
Header: http.Header{
"Content-Type": {"application/octet-stream"},
"X-Multi": {"one", "two", "three"},
},
Body: io.NopCloser(bytes.NewReader(body)),
Trailer: http.Header{"X-Checksum": {"deadbeef"}},
}
frame, err := zaphttp.MarshalResponse(orig)
if err != nil {
t.Fatalf("marshal: %v", err)
}
got, err := zaphttp.UnmarshalResponse(frame)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if got.StatusCode != http.StatusTeapot {
t.Errorf("status = %d want 418", got.StatusCode)
}
if got.Status != "418 I'm a teapot" {
t.Errorf("status line = %q want %q", got.Status, "418 I'm a teapot")
}
if ct := got.Header.Get("Content-Type"); ct != "application/octet-stream" {
t.Errorf("Content-Type = %q want application/octet-stream", ct)
}
if multi := got.Header.Values("X-Multi"); len(multi) != 3 ||
multi[0] != "one" || multi[1] != "two" || multi[2] != "three" {
t.Errorf("X-Multi = %v want [one two three]", multi)
}
if cs := got.Trailer.Get("X-Checksum"); cs != "deadbeef" {
t.Errorf("trailer X-Checksum = %q want deadbeef", cs)
}
gotBody, _ := io.ReadAll(got.Body)
if !bytes.Equal(gotBody, body) {
t.Errorf("body mismatch: got %q want %q", gotBody, body)
}
}
// TestFrameType_crossGuard — a request frame must not decode as a
// response and vice versa. The frame type rides in the message header
// flags; the decoder rejects a mismatched type rather than silently
// misreading another shape's bytes.
func TestFrameType_crossGuard(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "http://example.com/x", nil)
reqFrame, err := zaphttp.MarshalRequest(req)
if err != nil {
t.Fatalf("marshal request: %v", err)
}
if _, err := zaphttp.UnmarshalResponse(reqFrame); err == nil {
t.Errorf("UnmarshalResponse accepted a request frame; want error")
}
resp := &http.Response{StatusCode: http.StatusOK, Body: http.NoBody}
respFrame, err := zaphttp.MarshalResponse(resp)
if err != nil {
t.Fatalf("marshal response: %v", err)
}
if _, err := zaphttp.UnmarshalRequest(respFrame); err == nil {
t.Errorf("UnmarshalRequest accepted a response frame; want error")
}
}
// TestEmptyHeaders_nonNil — a request/response with no headers must
// decode to an empty, non-nil http.Header (the null-slot path), so
// callers can Get/Add without a nil-map panic.
func TestEmptyHeaders_nonNil(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "http://example.com/", nil)
req.Header = nil // force the empty-headers encode path
frame, err := zaphttp.MarshalRequest(req)
if err != nil {
t.Fatalf("marshal: %v", err)
}
got, err := zaphttp.UnmarshalRequest(frame)
if err != nil {
t.Fatalf("unmarshal: %v", err)
}
if got.Header == nil {
t.Fatal("decoded request Header is nil; want empty non-nil")
}
got.Header.Add("X-Added", "ok") // must not panic on a nil map
if got.Header.Get("X-Added") != "ok" {
t.Errorf("Add to decoded empty Header did not take")
}
}