From 434daffb217fe372689e984943aab3dff033095e Mon Sep 17 00:00:00 2001 From: Shiva Date: Thu, 5 Nov 2026 10:38:28 -0700 Subject: [PATCH] fix: serialize object-valued extension attributes in binary HTTP headers The current implementation copies binary object and values into the header map as-is. This would produce incorrectly encoded HTTP headers, and the current behaviour relies on object coercion in axios. Instead we are explicitly serializing incompatible types to String. The CloudEvents type system has no object type, so accepting objects as extension values is an SDK convenience rather than spec behavior. In the long run, we should pivot to a spec compliant option, and throw an error for this case, and instead as the client to correctly set headers. Split from #646. Signed-off-by: Shiva Co-authored-by: Kevin Mathew Sunny --- README.md | 23 +++++++++++++++++++++++ src/event/interfaces.ts | 13 ++++++++++++- src/message/http/headers.ts | 7 ++++++- test/integration/message_test.ts | 17 +++++++++++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f0256adb..120e46bf 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,29 @@ const event3 = new CloudEvent({ }); ``` +### Extension Attributes + +Any property passed to the `CloudEvent` constructor that is not a core attribute becomes an +[extension attribute](https://github.com/cloudevents/spec/blob/v1.0/spec.md#extension-context-attributes). +In binary mode each extension is sent as its own header, named after the attribute with the +transport's prefix: an extension `traceid` becomes the `ce-traceid` HTTP header. Extension names +must be lowercase letters and digits only. + +Extension values should be one of the CloudEvents types: string, boolean, integer, binary, URI or +timestamp. The spec has no object type, so encode structured data as a string yourself: + +```js +const ce = new CloudEvent({ + source: "/orders", + type: "order.placed", + traceinfo: JSON.stringify({ span: "abc", sampled: true }), +}); +``` + +As a convenience the SDK also accepts an object as an extension value and serializes it as JSON +when the event is sent in binary mode. This is not spec-conformant: a receiver gets a plain string +and has to know to parse it, and other SDKs may reject the event. Prefer the explicit form above. + ### A Note About Big Integers When parsing JSON data, if a JSON field value is a number, and that number diff --git a/src/event/interfaces.ts b/src/event/interfaces.ts index f73c6d8f..bde4fceb 100644 --- a/src/event/interfaces.ts +++ b/src/event/interfaces.ts @@ -136,7 +136,18 @@ export interface CloudEventV1OptionalAttributes { data_base64?: string; /** - * [OPTIONAL] CloudEvents extension attributes. + * [OPTIONAL] CloudEvents extension attributes. Any property that is not a + * core attribute is treated as an extension. Values should be one of the + * CloudEvents types: string, boolean, integer, binary, URI or timestamp. + * + * Object values are accepted as a convenience and for compatibility reasons. + * They are serialized as JSON in binary mode, but this is not spec-conformant; + * prefer encoding the value as a string yourself. + * + * @deprecated automatic serialization of object values is not spec-conformant + * and may be removed in future versions of the SDK. + * + * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md#extension-context-attributes */ [key: string]: unknown; } diff --git a/src/message/http/headers.ts b/src/message/http/headers.ts index b0cc519b..ab908c6b 100644 --- a/src/message/http/headers.ts +++ b/src/message/http/headers.ts @@ -41,7 +41,12 @@ export function headersFor(event: CloudEventV1): Headers { if (map) { headers[map.name] = map.parser.parse(value as string) as string; } else if (property !== CONSTANTS.DATA_ATTRIBUTE && property !== `${CONSTANTS.DATA_ATTRIBUTE}_base64`) { - headers[`${CONSTANTS.EXTENSIONS_PREFIX}${property}`] = value as string; + // The CloudEvents type system has no object type: extension values are strings, + // booleans, integers, binary, URIs or timestamps. The SDK accepts objects as a + // convenience, so serialize them as JSON rather than let the HTTP client coerce + // them to "[object Object]". Prefer encoding the value as a string yourself. + headers[`${CONSTANTS.EXTENSIONS_PREFIX}${property}`] = + typeof value === "object" ? JSON.stringify(value) : (value as string); } } }); diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index 9ddb292d..b161f7a9 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -65,6 +65,23 @@ describe("HTTP transport", () => { expect(message.headers["ce-extboolean"]).to.equal(false); }); + it("Serializes object-valued extensions as JSON in binary mode", () => { + const extobject = { some: "object", nested: { answer: 42 } }; + const evt = new CloudEvent({ source: "test", type: "test", extobject }); + const message = HTTP.binary(evt); + // HTTP headers can only carry strings; a raw object reaches the wire as "[object Object]" + expect(message.headers["ce-extobject"]).to.equal(JSON.stringify(extobject)); + }); + + it("Round-trips an object-valued extension through binary mode as a JSON string", () => { + const extobject = { some: "object", nested: { answer: 42 } }; + const evt = new CloudEvent({ source: "test", type: "test", extobject }); + const received = HTTP.toEvent(HTTP.binary(evt)) as CloudEvent; + // the receiver sees the serialized form and is responsible for parsing it + expect(received.extobject).to.equal(JSON.stringify(extobject)); + expect(JSON.parse(received.extobject as string)).to.deep.equal(extobject); + }); + it("Includes extensions in structured when type is 'boolean' with a false value", () => { const evt = new CloudEvent({ source: "test", type: "test", extboolean: false }); expect(evt.hasOwnProperty("extboolean")).to.equal(true);