Skip to content

Commit 1446898

Browse files
authored
fix: do not alter an event's data attribute (#344)
* fix: do not alter an event's data attribute When setting an event's data attribute we were trying to be really clever and this is problematic. Instead, keep the data attribute unchanged. Per the 1.0 specification, the data attribute is still inspected to determine if it is binary, and if so, a data_base64 attribute is added with the contents of the data property encoded as base64. Fixes: #343 Signed-off-by: Lance Ball <[email protected]>
1 parent 138de37 commit 1446898

File tree

10 files changed

+117
-78
lines changed

10 files changed

+117
-78
lines changed

src/event/cloudevent.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import {
1010
} from "./interfaces";
1111
import { validateCloudEvent } from "./spec";
1212
import { ValidationError, isBinary, asBase64, isValidType } from "./validation";
13-
import CONSTANTS from "../constants";
14-
import { isString } from "util";
1513

1614
/**
1715
* An enum representing the CloudEvent specification version
@@ -92,7 +90,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
9290
this.schemaurl = properties.schemaurl as string;
9391
delete properties.schemaurl;
9492

95-
this._setData(properties.data);
93+
this.data = properties.data;
9694
delete properties.data;
9795

9896
// sanity checking
@@ -125,25 +123,11 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
125123
}
126124

127125
get data(): unknown {
128-
if (
129-
this.datacontenttype === CONSTANTS.MIME_JSON &&
130-
!(this.datacontentencoding === CONSTANTS.ENCODING_BASE64) &&
131-
isString(this.#_data)
132-
) {
133-
return JSON.parse(this.#_data as string);
134-
} else if (isBinary(this.#_data)) {
135-
return asBase64(this.#_data as Uint32Array);
136-
}
137126
return this.#_data;
138127
}
139128

140129
set data(value: unknown) {
141-
this._setData(value);
142-
}
143-
144-
private _setData(value: unknown): void {
145130
if (isBinary(value)) {
146-
this.#_data = value;
147131
this.data_base64 = asBase64(value as Uint32Array);
148132
}
149133
this.#_data = value;
@@ -158,7 +142,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
158142
toJSON(): Record<string, unknown> {
159143
const event = { ...this };
160144
event.time = new Date(this.time as string).toISOString();
161-
event.data = this.data;
145+
event.data = !isBinary(this.data) ? this.data : undefined;
162146
return event;
163147
}
164148

@@ -175,7 +159,6 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
175159
try {
176160
return validateCloudEvent(this);
177161
} catch (e) {
178-
console.error(e.errors);
179162
if (e instanceof ValidationError) {
180163
throw e;
181164
} else {

src/message/http/index.ts

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@ import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } fro
22
import { Message, Headers } from "..";
33

44
import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers } from "./headers";
5-
import { asData, isBase64, isString, isStringOrObjectOrThrow, ValidationError } from "../../event/validation";
6-
import { Base64Parser, JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers";
5+
import { isStringOrObjectOrThrow, ValidationError } from "../../event/validation";
6+
import { JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers";
77

88
// implements Serializer
99
export function binary(event: CloudEvent): Message {
1010
const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CONTENT_TYPE };
1111
const headers: Headers = { ...contentType, ...headersFor(event) };
12-
let body = asData(event.data, event.datacontenttype as string);
13-
if (typeof body === "object") {
14-
body = JSON.stringify(body);
12+
let body = event.data;
13+
if (typeof event.data === "object" && !(event.data instanceof Uint32Array)) {
14+
// we'll stringify objects, but not binary data
15+
body = JSON.stringify(event.data);
1516
}
1617
return {
1718
headers,
@@ -21,6 +22,10 @@ export function binary(event: CloudEvent): Message {
2122

2223
// implements Serializer
2324
export function structured(event: CloudEvent): Message {
25+
if (event.data_base64) {
26+
// The event's data is binary - delete it
27+
event = event.cloneWith({ data: undefined });
28+
}
2429
return {
2530
headers: {
2631
[CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CE_CONTENT_TYPE,
@@ -89,7 +94,7 @@ function getMode(headers: Headers): Mode {
8994
* @param {Record<string, unknown>} body the HTTP request body
9095
* @returns {Version} the CloudEvent specification version
9196
*/
92-
function getVersion(mode: Mode, headers: Headers, body: string | Record<string, string>) {
97+
function getVersion(mode: Mode, headers: Headers, body: string | Record<string, string> | unknown) {
9398
if (mode === Mode.BINARY) {
9499
// Check the headers for the version
95100
const versionHeader = headers[CONSTANTS.CE_HEADERS.SPEC_VERSION];
@@ -129,8 +134,6 @@ function parseBinary(message: Message, version: Version): CloudEvent {
129134
throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`);
130135
}
131136

132-
body = isString(body) && isBase64(body) ? Buffer.from(body as string, "base64").toString() : body;
133-
134137
// Clone and low case all headers names
135138
const sanitizedHeaders = sanitize(headers);
136139

@@ -145,30 +148,26 @@ function parseBinary(message: Message, version: Version): CloudEvent {
145148
}
146149
}
147150

148-
let parsedPayload;
149-
150-
if (body) {
151-
const parser = parserByContentType[eventObj.datacontenttype as string];
152-
if (!parser) {
153-
throw new ValidationError(`no parser found for content type ${eventObj.datacontenttype}`);
154-
}
155-
parsedPayload = parser.parse(body);
156-
}
157-
158151
// Every unprocessed header can be an extension
159152
for (const header in sanitizedHeaders) {
160153
if (header.startsWith(CONSTANTS.EXTENSIONS_PREFIX)) {
161154
eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = headers[header];
162155
}
163156
}
157+
158+
const parser = parserByContentType[eventObj.datacontenttype as string];
159+
if (parser && body) {
160+
body = parser.parse(body as string);
161+
}
162+
164163
// At this point, if the datacontenttype is application/json and the datacontentencoding is base64
165164
// then the data has already been decoded as a string, then parsed as JSON. We don't need to have
166165
// the datacontentencoding property set - in fact, it's incorrect to do so.
167166
if (eventObj.datacontenttype === CONSTANTS.MIME_JSON && eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) {
168167
delete eventObj.datacontentencoding;
169168
}
170169

171-
return new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03, false);
170+
return new CloudEvent({ ...eventObj, data: body } as CloudEventV1 | CloudEventV03, false);
172171
}
173172

174173
/**
@@ -201,7 +200,7 @@ function parseStructured(message: Message, version: Version): CloudEvent {
201200
const contentType = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE];
202201
const parser: Parser = contentType ? parserByContentType[contentType] : new JSONParser();
203202
if (!parser) throw new ValidationError(`invalid content type ${sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]}`);
204-
const incoming = { ...(parser.parse(payload) as Record<string, unknown>) };
203+
const incoming = { ...(parser.parse(payload as string) as Record<string, unknown>) };
205204

206205
const eventObj: { [key: string]: unknown } = {};
207206
const parserMap: Record<string, MappedParser> = version === Version.V1 ? v1structuredParsers : v03structuredParsers;
@@ -220,10 +219,12 @@ function parseStructured(message: Message, version: Version): CloudEvent {
220219
eventObj[key] = incoming[key];
221220
}
222221

223-
// ensure data content is correctly decoded
224-
if (eventObj.data_base64) {
225-
const parser = new Base64Parser();
226-
eventObj.data = JSON.parse(parser.parse(eventObj.data_base64 as string));
222+
// data_base64 is a property that only exists on V1 events. For V03 events,
223+
// there will be a .datacontentencoding property, and the .data property
224+
// itself will be encoded as base64
225+
if (eventObj.data_base64 || eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) {
226+
const data = eventObj.data_base64 || eventObj.data;
227+
eventObj.data = new Uint32Array(Buffer.from(data as string, "base64"));
227228
delete eventObj.data_base64;
228229
delete eventObj.datacontentencoding;
229230
}

src/message/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export interface Headers {
2828
*/
2929
export interface Message {
3030
headers: Headers;
31-
body: string;
31+
body: string | unknown;
3232
}
3333

3434
/**

src/transport/receiver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export const Receiver = {
1717
*/
1818
accept(headers: Headers, body: string | Record<string, unknown> | undefined | null): CloudEvent {
1919
const cleanHeaders: Headers = sanitize(headers);
20-
const cleanBody = body ? (typeof body === "object" ? JSON.stringify(body) : body) : "";
20+
const cleanBody = body ? (typeof body === "object" ? JSON.stringify(body) : body) : undefined;
2121
const message: Message = {
2222
headers: cleanHeaders,
2323
body: cleanBody,

test/integration/ce.png

39.8 KB
Loading

test/integration/cloud_event_test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
import path from "path";
2+
import fs from "fs";
3+
14
import { expect } from "chai";
25
import { CloudEvent, ValidationError, Version } from "../../src";
36
import { CloudEventV03, CloudEventV1 } from "../../src/event/interfaces";
7+
import { asBase64 } from "../../src/event/validation";
48

59
const type = "org.cncf.cloudevents.example";
610
const source = "http://unit.test";
@@ -14,6 +18,9 @@ const fixture: CloudEventV1 = {
1418
data: `"some data"`,
1519
};
1620

21+
const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png")));
22+
const image_base64 = asBase64(imageData);
23+
1724
describe("A CloudEvent", () => {
1825
it("Can be constructed with a typed Message", () => {
1926
const ce = new CloudEvent(fixture);
@@ -151,6 +158,15 @@ describe("A 1.0 CloudEvent", () => {
151158
expect(ce.data).to.be.true;
152159
});
153160

161+
it("can be constructed with binary data", () => {
162+
const ce = new CloudEvent({
163+
...fixture,
164+
data: imageData,
165+
});
166+
expect(ce.data).to.equal(imageData);
167+
expect(ce.data_base64).to.equal(image_base64);
168+
});
169+
154170
it("can be constructed with extensions", () => {
155171
const extensions = {
156172
extensionkey: "extension-value",

test/integration/emitter_factory_test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,12 @@ function superagentEmitter(message: Message, options?: Options): Promise<unknown
5050
for (const key of Object.getOwnPropertyNames(message.headers)) {
5151
post.set(key, message.headers[key]);
5252
}
53-
return post.send(message.body);
53+
return post.send(message.body as string);
5454
}
5555

5656
function gotEmitter(message: Message, options?: Options): Promise<unknown> {
5757
return Promise.resolve(
58-
got.post(sink, { headers: message.headers, body: message.body, ...((options as unknown) as Options) }),
58+
got.post(sink, { headers: message.headers, body: message.body as string, ...((options as unknown) as Options) }),
5959
);
6060
}
6161

0 commit comments

Comments
 (0)