mirror of
https://github.com/zap-proto/http.git
synced 2026-10-04 03:07:19 +00:00
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:
@@ -6,6 +6,3 @@
|
||||
# Editor
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Schema codegen — checked in, but ignore stray files
|
||||
schema/go.capnp
|
||||
|
||||
@@ -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 ./...
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user