Send a CloudEvent over HTTP¶
http.Header is also map[string][]string, so nothing about the codec changes:
headers, body, err := cloudevents.Marshal(e)
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, bytes.NewReader(body))
if err != nil {
return err
}
req.Header = http.Header(headers)
The case-folding wrinkle¶
http.Header canonicalises keys when you use its Set and Add methods: ce-id becomes Ce-Id,
content-type becomes Content-Type. Assigning the map directly, as above, skips that — Go's HTTP
server and client both handle lower-case keys correctly on the wire.
Either way round, Unmarshal
lower-cases every key it reads, so it accepts both spellings.
Header.Get cannot see Marshal's output
Get canonicalises the key it is given, so req.Header.Get("Ce-Id") returns "" against the
map Marshal produced. Assign the map as above and read it back with Unmarshal, or iterate
it — do not reach for Get in a test or a middleware.
func handler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "unreadable body", http.StatusBadRequest)
return
}
e, err := cloudevents.Unmarshal(r.Header, body)
if err != nil {
http.Error(w, "not a CloudEvent this service accepts", http.StatusBadRequest)
return
}
handle(e)
}
The fixture at testdata/sdk-binary-1.0.txt was produced by the CloudEvents SDK's own HTTP binding
and carries Ce-Id rather than ce-id. It is checked in precisely so that the folding cannot be
removed without a test noticing.
What if two spellings disagree¶
Unmarshal returns
ErrAmbiguousHeader.
It does not pick one. An event whose ce-id and CE-ID differ means two different things depending
on which transport carried it, and choosing either would make that difference invisible.
Only headers the codec owns are considered, so an ordinary proxy's repeated X-Forwarded-For,
Via or Accept is ignored rather than treated as an ambiguous event.