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);