mirror of
https://github.com/zap-proto/http.git
synced 2026-10-04 03:07:19 +00:00
docs(schema): the schema described a wire we stopped sending
d05974f moved headers and trailers from a JSON map to length-prefixed
pairs and regenerated the golden vectors. schema/zap_http.zap — which
calls itself the source of truth — was never updated, so the one file a
reimplementer reads first described a JSON map<Text, List(Text)> and a
`List(Header)` struct list that no encoder has emitted since.
Three things were wrong, all of them the kind that only surface as a
byte mismatch in someone else's language:
- headers/trailer are Data slots holding `count` then
{keyLen,key,valLen,value}*, all little-endian. A repeated name is
repeated pairs; there is no values list. An empty block is a null
{0,0} slot, NOT a count=0 block.
- streaming was "reserved for a follow-up slice"; it shipped.
FrameResponseHead/FrameData/FrameEnd are 0x03/0x04/0x05, so the
closing note's "today there are exactly two" type IDs was off by
three.
- the preamble credited a zap-proto/go runtime dependency that
codec.go replaced by being its own encoder and decoder.
Also written down because a port gets them wrong once each: relOffset
is measured from the slot's own position, relOffset==0 is NULL whatever
the length word says, the frame is little-endian but the transport's
length prefix is big-endian, and Host/Content-Length are frame-owned so
they must never appear as pairs.
No code changes — the schema is documentation, not a codegen input
(Makefile: "no code-generation step"). wire.go's offsets remain the
binding; this makes the document agree with them.
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
+84
-29
@@ -3,33 +3,71 @@
|
||||
# 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 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.
|
||||
# ../wire.go and ../codec.go are the BINDING — there is no external
|
||||
# codec dependency, codec.go is both encoder and decoder — and this
|
||||
# file documents the logical shape they implement. Each struct below is
|
||||
# an object whose fixed payload packs every text/bytes field into an
|
||||
# 8-byte slot of {relOffset UInt32, length UInt32}, and scalars at
|
||||
# their natural-width offset. A slot's relOffset is measured from THE
|
||||
# SLOT'S OWN position; relOffset == 0 is NULL regardless of the length
|
||||
# word, and an empty field keeps a zeroed {0,0} slot contributing no
|
||||
# tail bytes. Slot offsets, from ../wire.go:
|
||||
#
|
||||
# Request method 0 target 8 proto 16 headers 24 body 32 trailer 40 (48 total)
|
||||
# Response status 0 reason 8 proto 16 headers 24 body 32 trailer 40 (48 total)
|
||||
# Chunk data 0 ( 8 total)
|
||||
#
|
||||
# Byte order is little-endian everywhere INSIDE the frame. The only
|
||||
# big-endian quantity on the connection is the transport's 4-byte
|
||||
# length prefix that precedes each frame.
|
||||
#
|
||||
# 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.
|
||||
# may carry trailers (RFC 9110 §6.5).
|
||||
#
|
||||
# Streaming (chunked-style transfer) is reserved for a follow-up
|
||||
# protocol slice and not represented here. v0.1 assumes the entire body
|
||||
# fits in one frame; the transport (zap-proto/go) negotiates the
|
||||
# maximum frame size on connection setup.
|
||||
# Headers and trailers are LENGTH-PREFIXED PAIRS in a single bytes
|
||||
# slot — not a JSON map, and not a nested struct list. A JSON encoder
|
||||
# inside a zero-copy binary protocol bought escaping, sorting and a
|
||||
# grammar nobody needed, and forced two implementations to be kept
|
||||
# byte-identical; see ../wire.go's "Wire" note. The golden-wire tests
|
||||
# pin the current bytes. Peers must agree on the version.
|
||||
#
|
||||
# Streaming is represented: a body too large for one frame is sent as
|
||||
# FrameResponseHead (status + headers, no body), then zero or more
|
||||
# FrameData chunks, then FrameEnd (optional trailers) — the shape
|
||||
# HTTP/2 gets from HEADERS + DATA + END_STREAM. A NON-streamed body
|
||||
# must still fit in one frame; the transport negotiates the maximum
|
||||
# frame size on connection setup.
|
||||
|
||||
# Header is one HTTP header name and its values. RFC 9110 allows the
|
||||
# same name to appear multiple times; we represent that as a list of
|
||||
# values rather than concatenating with commas, so callers see what
|
||||
# was actually sent.
|
||||
struct Header
|
||||
name Text
|
||||
values List(Text)
|
||||
# The header block encoding, written out because it is a byte layout
|
||||
# rather than a struct the schema language can express. `headers` and
|
||||
# `trailer` below are Data slots holding exactly this, all integers
|
||||
# little-endian:
|
||||
#
|
||||
# count UInt32 -- number of pairs that follow
|
||||
# pair* -- repeated `count` times:
|
||||
# keyLen UInt32
|
||||
# key keyLen bytes
|
||||
# valLen UInt32
|
||||
# value valLen bytes
|
||||
#
|
||||
# An EMPTY block writes no bytes at all and leaves a null {0,0} slot —
|
||||
# it is not a `count=0` block. Decoders must treat a zero-length slot
|
||||
# as "no headers" and stop; a block shorter than its own declared
|
||||
# pairs is a truncated-header error, not a silent short read.
|
||||
#
|
||||
# RFC 9110 allows a name to repeat. There is no values list: a repeated
|
||||
# name is simply REPEATED PAIRS, in the order sent, so callers still
|
||||
# see what was actually on the wire without comma-concatenation.
|
||||
#
|
||||
# Some fields are FRAME-OWNED and must not appear as pairs, because the
|
||||
# frame already carries the information and sending it would put two
|
||||
# sources of truth on the wire: Content-Length (the body slot's length
|
||||
# is authoritative) and, on requests, Host (reconstructed from the
|
||||
# target). Encoders skip them; decoders ignore Content-Length and let
|
||||
# the last Host value win.
|
||||
|
||||
# Request is one HTTP request. `target` is the request-target as it
|
||||
# would appear on an HTTP/1.1 request line — origin-form (`/path?q=v`),
|
||||
@@ -39,9 +77,9 @@ struct Request
|
||||
method Text
|
||||
target Text
|
||||
proto Text
|
||||
headers List(Header)
|
||||
headers Data
|
||||
body Data
|
||||
trailer List(Header)
|
||||
trailer Data
|
||||
|
||||
# Response is one HTTP response. `status` is the numeric status code
|
||||
# (200, 404, 503, …); `reason` is the optional reason phrase; the
|
||||
@@ -51,13 +89,30 @@ struct Response
|
||||
status UInt16
|
||||
reason Text
|
||||
proto Text
|
||||
headers List(Header)
|
||||
headers Data
|
||||
body Data
|
||||
trailer List(Header)
|
||||
trailer Data
|
||||
|
||||
# Chunk is the body of a FrameData or FrameEnd frame: one bytes slot
|
||||
# holding the chunk, or — for FrameEnd — the trailer pairs in the same
|
||||
# encoding as `headers`. FrameEnd with a null slot terminates a stream
|
||||
# that declared no trailers.
|
||||
struct Chunk
|
||||
data Data
|
||||
|
||||
# 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.
|
||||
# (flags>>8), not a union struct (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; a Chunk is the root for the two
|
||||
# streaming data frames. There are five type IDs:
|
||||
#
|
||||
# FrameRequest 0x01 -- Request is the root
|
||||
# FrameResponse 0x02 -- Response is the root, body in-frame
|
||||
# FrameResponseHead 0x03 -- Response is the root, body slot null,
|
||||
# FrameData/FrameEnd follow
|
||||
# FrameData 0x04 -- Chunk is the root: one body chunk
|
||||
# FrameEnd 0x05 -- Chunk is the root: trailer pairs, or null
|
||||
#
|
||||
# A decoder that reads only single-frame replies must REFUSE 0x03 by
|
||||
# name rather than return a body-less 200 — a truncated success is
|
||||
# worse than a named error.
|
||||
|
||||
Reference in New Issue
Block a user