The wire format¶
Binary mode, as the CloudEvents specification defines it and as the CloudEvents SDK's own bindings implement it. Attributes ride in headers; the payload is the body, untouched.
Required attributes¶
All four must be present and non-empty, and the absence of any of them — specversion included —
is ErrMissingAttribute.
ErrUnsupportedSpecVersion is for a specversion that is
present and is not 1.0. The distinction is load bearing: "this is not a CloudEvent" and "this
is a CloudEvent from a version we do not implement" are different problems, and an operator alerting
on version skew wants only the second.
| Header | Field | Notes |
|---|---|---|
ce-id |
Event.ID |
With source, uniquely identifies the occurrence. Do not reuse the pair |
ce-source |
Event.Source |
A URI-reference, so it may be relative — /phpbotscout/ingest is valid |
ce-specversion |
Event.SpecVersion |
Always 1.0. Marshal fills it in when empty |
ce-type |
Event.Type |
What consumers filter on. Reverse-DNS by convention: uk.phpboyscout.orders.created |
Optional attributes¶
Omitted entirely when empty — an absent optional attribute is an absent key, never an empty one.
| Header | Field | Notes |
|---|---|---|
content-type |
Event.DataContentType |
Not prefixed on write. ce-datacontenttype is also accepted on read — see below |
ce-dataschema |
Event.DataSchema |
An absolute URI. See URN grammar |
ce-subject |
Event.Subject |
Names the thing the event is about, within source |
ce-time |
Event.Time |
RFC 3339. Written in UTC, with fractional seconds when they are non-zero |
Percent-encoding¶
Both bindings make this a MUST, and this codec does it in both directions.
A header value has space (U+0020), double-quote (U+0022), percent (U+0025) and everything outside
printable ASCII U+0021–U+007E written as UTF-8 bytes in %XY form. A receiver performs exactly one
round of decoding — one, so a value that legitimately contains %25 is not decoded twice into %.
Upper-case hex is written and either case is read, as the specification asks. A value that decodes
to invalid UTF-8, or carries a truncated or non-hexadecimal escape, is
ErrInvalidEncoding.
content-type is exempt. It is a media type the transport interprets, it is never encoded on
write, and a media type containing a literal % is not valid anyway.
Control characters¶
Refused, not encoded. The specification's String type excludes U+0000–U+001F and U+007F–U+009F,
and while percent-encoding would carry them, transportable and permitted are different questions —
net/http rewrites a newline in a header rather than refusing it, so without the check a receiver
would get a different attribute value than the sender set, with no error anywhere.
Extensions¶
Any other ce--prefixed header. The prefix is stripped on read and added on write, so
Extensions["scope"] is ce-scope on the wire.
Names must be one or more lower-case ASCII letters or digits, must not be data, and must not be
one of the eight specification attribute names. Without that last rule Extensions["id"] would
overwrite ce-id on the way out, producing an event with a hijacked identity, no error, and a round
trip that agrees with itself. See
Add an extension attribute.
A complete event¶
ce-id: 01JQ8Z3F7K2M4N6P8R0T2V4W6X
ce-source: /phpbotscout/ingest
ce-specversion: 1.0
ce-type: uk.phpboyscout.orders.created
ce-subject: order-4172
ce-time: 2026-08-28T11:22:33Z
ce-dataschema: urn:phpboyscout:schema:orders.created:3
ce-scope: urn:phpboyscout:scope:continuity:01JQ8Z3F7K2M4N6P8R0T2V4W6X
content-type: application/json
{"order":"4172"}
That is testdata/full.txt, and it is read by the test suite rather than described by it.
Payload ownership¶
Marshal returns a body that aliases Event.Data, and Unmarshal returns an Event whose
Data aliases the body it was given. Neither copies.
That is deliberate — a codec on a message hot path should not double every payload — but it makes
the ownership yours. A producer reusing an encode buffer will mutate a message it has already handed
to a transport, and an Event retained past an HTTP handler that read into a pooled buffer will
change underneath you. Copy at the boundary if you need to hold either.
The two spellings of datacontenttype¶
The bindings genuinely disagree. The HTTP binding maps datacontenttype onto Content-Type and
says a ce-datacontenttype header MUST NOT also be present; the NATS binding prefixes it like every
other attribute.
This codec writes the HTTP form — which is what the SDK's NATS binding emits in practice — and accepts either on read, because silently losing a conformant NATS peer's content type is worse than tolerating a spelling. Where both arrive, the unprefixed one wins.
Header case¶
Marshal writes lower-case keys. Unmarshal lower-cases every key it reads, so an event that
crossed http.Header — which canonicalises to Ce-Id — is read back identically.
http.Header.Get will not find them
Get canonicalises the key it is given, so it cannot see a map keyed ce-id. Read Marshal's
output by iteration or by exact key.
Two spellings of one key carrying different values is
ErrAmbiguousHeader, not a choice. So are two values under one key.
Headers this codec does not own are ignored entirely — X-Forwarded-For with two hops is the
ordinary shape behind a proxy, and folding it would refuse the event over something that is none of
the codec's business.
Structured mode¶
Not implemented. It puts the whole envelope in the body as JSON, which hides the attributes from
anything routing on them. Additive later if a consumer needs it — the two modes are distinguished by
content-type, so adding it would not change what this module writes today.