Skip to content

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 %.

Subject: "order 4172"   ->   ce-subject: order%204172
Subject: "Euro €"       ->   ce-subject: Euro%20%E2%82%AC

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 entirelyX-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.