From db40de9d3826a0be00a48eb08a308451c9972c87 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Mon, 28 Sep 2020 16:27:15 -0400 Subject: [PATCH 1/4] 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: https://github.com/cloudevents/sdk-javascript/issues/343 Signed-off-by: Lance Ball --- src/event/cloudevent.ts | 20 ++--------------- src/message/http/index.ts | 28 ++++++++++++++---------- src/message/index.ts | 2 +- test/integration/emitter_factory_test.ts | 4 ++-- test/integration/message_test.ts | 27 ++++++++++++++++------- test/integration/spec_03_tests.ts | 6 ----- test/integration/spec_1_tests.ts | 5 ----- 7 files changed, 41 insertions(+), 51 deletions(-) diff --git a/src/event/cloudevent.ts b/src/event/cloudevent.ts index c9ade6f1..c5da0ecf 100644 --- a/src/event/cloudevent.ts +++ b/src/event/cloudevent.ts @@ -10,8 +10,6 @@ import { } from "./interfaces"; import { validateCloudEvent } from "./spec"; import { ValidationError, isBinary, asBase64, isValidType } from "./validation"; -import CONSTANTS from "../constants"; -import { isString } from "util"; /** * An enum representing the CloudEvent specification version @@ -92,7 +90,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { this.schemaurl = properties.schemaurl as string; delete properties.schemaurl; - this._setData(properties.data); + this.data = properties.data; delete properties.data; // sanity checking @@ -125,25 +123,11 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { } get data(): unknown { - if ( - this.datacontenttype === CONSTANTS.MIME_JSON && - !(this.datacontentencoding === CONSTANTS.ENCODING_BASE64) && - isString(this.#_data) - ) { - return JSON.parse(this.#_data as string); - } else if (isBinary(this.#_data)) { - return asBase64(this.#_data as Uint32Array); - } return this.#_data; } set data(value: unknown) { - this._setData(value); - } - - private _setData(value: unknown): void { if (isBinary(value)) { - this.#_data = value; this.data_base64 = asBase64(value as Uint32Array); } this.#_data = value; @@ -158,7 +142,7 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { toJSON(): Record { const event = { ...this }; event.time = new Date(this.time as string).toISOString(); - event.data = this.data; + event.data = !isBinary(this.data) ? this.data : undefined; return event; } diff --git a/src/message/http/index.ts b/src/message/http/index.ts index 97c005b6..f10b628c 100644 --- a/src/message/http/index.ts +++ b/src/message/http/index.ts @@ -2,16 +2,17 @@ import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } fro import { Message, Headers } from ".."; import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers } from "./headers"; -import { asData, isBase64, isString, isStringOrObjectOrThrow, ValidationError } from "../../event/validation"; +import { isBase64, isString, isStringOrObjectOrThrow, ValidationError } from "../../event/validation"; import { Base64Parser, JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers"; // implements Serializer export function binary(event: CloudEvent): Message { const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CONTENT_TYPE }; const headers: Headers = { ...contentType, ...headersFor(event) }; - let body = asData(event.data, event.datacontenttype as string); - if (typeof body === "object") { - body = JSON.stringify(body); + let body = event.data; + if (typeof event.data === "object" && !(event.data instanceof Uint32Array)) { + // we'll stringify objects, but not binary data + body = JSON.stringify(event.data); } return { headers, @@ -89,7 +90,7 @@ function getMode(headers: Headers): Mode { * @param {Record} body the HTTP request body * @returns {Version} the CloudEvent specification version */ -function getVersion(mode: Mode, headers: Headers, body: string | Record) { +function getVersion(mode: Mode, headers: Headers, body: string | Record | unknown) { if (mode === Mode.BINARY) { // Check the headers for the version const versionHeader = headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]; @@ -149,10 +150,12 @@ function parseBinary(message: Message, version: Version): CloudEvent { if (body) { const parser = parserByContentType[eventObj.datacontenttype as string]; - if (!parser) { - throw new ValidationError(`no parser found for content type ${eventObj.datacontenttype}`); + if (parser) { + parsedPayload = parser.parse(body as string); + } else { + // Just use the raw body + parsedPayload = body; } - parsedPayload = parser.parse(body); } // Every unprocessed header can be an extension @@ -201,7 +204,7 @@ function parseStructured(message: Message, version: Version): CloudEvent { const contentType = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]; const parser: Parser = contentType ? parserByContentType[contentType] : new JSONParser(); if (!parser) throw new ValidationError(`invalid content type ${sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]}`); - const incoming = { ...(parser.parse(payload) as Record) }; + const incoming = { ...(parser.parse(payload as string) as Record) }; const eventObj: { [key: string]: unknown } = {}; const parserMap: Record = version === Version.V1 ? v1structuredParsers : v03structuredParsers; @@ -221,9 +224,12 @@ function parseStructured(message: Message, version: Version): CloudEvent { } // ensure data content is correctly decoded - if (eventObj.data_base64) { + if (eventObj.data_base64 || eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) { const parser = new Base64Parser(); - eventObj.data = JSON.parse(parser.parse(eventObj.data_base64 as string)); + // data_base64 is a property that only exists on V1 events. For V03 events, + // there will be a .datacontentencoding property, and the .data property + // itself will be encoded as base64 + eventObj.data = JSON.parse(parser.parse((eventObj.data_base64 as string) || (eventObj.data as string))); delete eventObj.data_base64; delete eventObj.datacontentencoding; } diff --git a/src/message/index.ts b/src/message/index.ts index 14ed270c..6a1343c1 100644 --- a/src/message/index.ts +++ b/src/message/index.ts @@ -28,7 +28,7 @@ export interface Headers { */ export interface Message { headers: Headers; - body: string; + body: string | unknown; } /** diff --git a/test/integration/emitter_factory_test.ts b/test/integration/emitter_factory_test.ts index 4f6cba35..5e2a103a 100644 --- a/test/integration/emitter_factory_test.ts +++ b/test/integration/emitter_factory_test.ts @@ -50,12 +50,12 @@ function superagentEmitter(message: Message, options?: Options): Promise { return Promise.resolve( - got.post(sink, { headers: message.headers, body: message.body, ...((options as unknown) as Options) }), + got.post(sink, { headers: message.headers, body: message.body as string, ...((options as unknown) as Options) }), ); } diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index 6495a801..fc35e11a 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -102,7 +102,7 @@ describe("HTTP transport", () => { it("Binary Messages can be created from a CloudEvent", () => { const message: Message = HTTP.binary(fixture); - expect(JSON.parse(message.body)).to.deep.equal(data); + expect(message.body).to.equal(JSON.stringify(data)); // validate all headers expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype); expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V1); @@ -120,7 +120,7 @@ describe("HTTP transport", () => { const message: Message = HTTP.structured(fixture); expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); // Parse the message body as JSON, then validate the attributes - const body = JSON.parse(message.body); + const body = JSON.parse(message.body as string); expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V1); expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); @@ -147,6 +147,7 @@ describe("HTTP transport", () => { it("Supports Base-64 encoded data in structured messages", () => { const event = fixture.cloneWith({ data: dataBinary }); expect(event.data_base64).to.equal(data_base64); + expect(event.data).to.equal(dataBinary); const message = HTTP.structured(event); const eventDeserialized = HTTP.toEvent(message); expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); @@ -155,9 +156,11 @@ describe("HTTP transport", () => { it("Supports Base-64 encoded data in binary messages", () => { const event = fixture.cloneWith({ data: dataBinary }); expect(event.data_base64).to.equal(data_base64); + expect(event.data).to.equal(dataBinary); const message = HTTP.binary(event); + expect(message.body).to.equal(dataBinary); const eventDeserialized = HTTP.toEvent(message); - expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + expect(eventDeserialized.data).to.equal(dataBinary); }); }); @@ -196,7 +199,7 @@ describe("HTTP transport", () => { const message: Message = HTTP.structured(fixture); expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); // Parse the message body as JSON, then validate the attributes - const body = JSON.parse(message.body); + const body = JSON.parse(message.body as string); expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V03); expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); @@ -221,19 +224,27 @@ describe("HTTP transport", () => { }); it("Supports Base-64 encoded data in structured messages", () => { - const event = fixture.cloneWith({ data: dataBinary, datacontentencoding }); - expect(event.data_base64).to.equal(data_base64); + const event = fixture.cloneWith({ data: data_base64, datacontentencoding }); const message = HTTP.structured(event); + expect(JSON.parse(message.body as string).data).to.equal(data_base64); + // An incoming event with datacontentencoding set to base64, + // and encoded data, should decode the data before setting + // the .data property on the event const eventDeserialized = HTTP.toEvent(message); expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + expect(eventDeserialized.datacontentencoding).to.be.undefined; }); it("Supports Base-64 encoded data in binary messages", () => { - const event = fixture.cloneWith({ data: dataBinary, datacontentencoding }); - expect(event.data_base64).to.equal(data_base64); + const event = fixture.cloneWith({ data: data_base64, datacontentencoding }); const message = HTTP.binary(event); + expect(message.body).to.equal(data_base64); + // An incoming event with datacontentencoding set to base64, + // and encoded data, should decode the data before setting + // the .data property on the event const eventDeserialized = HTTP.toEvent(message); expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + expect(eventDeserialized.datacontentencoding).to.be.undefined; }); }); }); diff --git a/test/integration/spec_03_tests.ts b/test/integration/spec_03_tests.ts index 84252a4a..27cb3d84 100644 --- a/test/integration/spec_03_tests.ts +++ b/test/integration/spec_03_tests.ts @@ -168,12 +168,6 @@ describe("CloudEvents Spec v0.3", () => { expect(typeof cloudevent.data).to.equal("string"); }); - - it("should convert data with stringified json to a json object", () => { - cloudevent = cloudevent.cloneWith({ datacontenttype: Constants.MIME_JSON }); - cloudevent.data = JSON.stringify(data); - expect(cloudevent.data).to.deep.equal(data); - }); }); describe("'subject'", () => { diff --git a/test/integration/spec_1_tests.ts b/test/integration/spec_1_tests.ts index 9329dbd0..6da9774a 100644 --- a/test/integration/spec_1_tests.ts +++ b/test/integration/spec_1_tests.ts @@ -168,11 +168,6 @@ describe("CloudEvents Spec v1.0", () => { cloudevent = cloudevent.cloneWith({ datacontenttype: dct }); }); - it("should convert data with stringified json to a json object", () => { - cloudevent = cloudevent.cloneWith({ datacontenttype: Constants.MIME_JSON, data: JSON.stringify(data) }); - expect(cloudevent.data).to.deep.equal(data); - }); - it("should be ok when type is 'Uint32Array' for 'Binary'", () => { const dataString = ")(*~^my data for ce#@#$%"; From 624fc79b66190c7b3e44ae54282de9234ebbb530 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Wed, 30 Sep 2020 17:38:26 -0400 Subject: [PATCH 2/4] fix: ensure that received binary data remains binary in an event Unless... its datacontenttype is application/json. In that case it should be converted into an object. Signed-off-by: Lance Ball --- src/event/cloudevent.ts | 1 - src/message/http/index.ts | 56 ++++++++++------- src/transport/receiver.ts | 2 +- test/integration/ce.png | Bin 0 -> 40750 bytes test/integration/cloud_event_test.ts | 16 +++++ test/integration/message_test.ts | 90 +++++++++++++++++++-------- 6 files changed, 114 insertions(+), 51 deletions(-) create mode 100644 test/integration/ce.png diff --git a/src/event/cloudevent.ts b/src/event/cloudevent.ts index c5da0ecf..e6322f83 100644 --- a/src/event/cloudevent.ts +++ b/src/event/cloudevent.ts @@ -159,7 +159,6 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 { try { return validateCloudEvent(this); } catch (e) { - console.error(e.errors); if (e instanceof ValidationError) { throw e; } else { diff --git a/src/message/http/index.ts b/src/message/http/index.ts index f10b628c..542c400c 100644 --- a/src/message/http/index.ts +++ b/src/message/http/index.ts @@ -2,8 +2,8 @@ import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } fro import { Message, Headers } from ".."; import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers } from "./headers"; -import { isBase64, isString, isStringOrObjectOrThrow, ValidationError } from "../../event/validation"; -import { Base64Parser, JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers"; +import { isBinary, isStringOrObjectOrThrow, ValidationError } from "../../event/validation"; +import { JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers"; // implements Serializer export function binary(event: CloudEvent): Message { @@ -22,6 +22,10 @@ export function binary(event: CloudEvent): Message { // implements Serializer export function structured(event: CloudEvent): Message { + if (event.data_base64) { + // The event's data is binary - delete it + event = event.cloneWith({ data: undefined }); + } return { headers: { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CE_CONTENT_TYPE, @@ -130,8 +134,6 @@ function parseBinary(message: Message, version: Version): CloudEvent { throw new ValidationError(`invalid spec version ${headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]}`); } - body = isString(body) && isBase64(body) ? Buffer.from(body as string, "base64").toString() : body; - // Clone and low case all headers names const sanitizedHeaders = sanitize(headers); @@ -146,24 +148,26 @@ function parseBinary(message: Message, version: Version): CloudEvent { } } - let parsedPayload; - - if (body) { - const parser = parserByContentType[eventObj.datacontenttype as string]; - if (parser) { - parsedPayload = parser.parse(body as string); - } else { - // Just use the raw body - parsedPayload = body; - } - } - // Every unprocessed header can be an extension for (const header in sanitizedHeaders) { if (header.startsWith(CONSTANTS.EXTENSIONS_PREFIX)) { eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = headers[header]; } } + + if (isBinary(body)) { + if (eventObj.datacontenttype === CONSTANTS.MIME_JSON) { + // even though the body is binary, it's just JSON - convert to string + body = Buffer.from(body as string, "base64").toString(); + } + } + + // If the body is base64 that means we are dealing with binary data + const parser = parserByContentType[eventObj.datacontenttype as string]; + if (parser && body) { + body = parser.parse(body as string); + } + // At this point, if the datacontenttype is application/json and the datacontentencoding is base64 // then the data has already been decoded as a string, then parsed as JSON. We don't need to have // the datacontentencoding property set - in fact, it's incorrect to do so. @@ -171,7 +175,7 @@ function parseBinary(message: Message, version: Version): CloudEvent { delete eventObj.datacontentencoding; } - return new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03, false); + return new CloudEvent({ ...eventObj, data: body } as CloudEventV1 | CloudEventV03, false); } /** @@ -223,13 +227,19 @@ function parseStructured(message: Message, version: Version): CloudEvent { eventObj[key] = incoming[key]; } - // ensure data content is correctly decoded + // data_base64 is a property that only exists on V1 events. For V03 events, + // there will be a .datacontentencoding property, and the .data property + // itself will be encoded as base64 if (eventObj.data_base64 || eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) { - const parser = new Base64Parser(); - // data_base64 is a property that only exists on V1 events. For V03 events, - // there will be a .datacontentencoding property, and the .data property - // itself will be encoded as base64 - eventObj.data = JSON.parse(parser.parse((eventObj.data_base64 as string) || (eventObj.data as string))); + const data = eventObj.data_base64 || eventObj.data; + + // we can choose to decode the binary data as JSON, if datacontentype is application/json + if (eventObj.datacontenttype === CONSTANTS.MIME_JSON) { + eventObj.data = JSON.parse(Buffer.from(data as string, "base64").toString()); + } else { + eventObj.data = new Uint32Array(Buffer.from(data as string, "base64")); + } + delete eventObj.data_base64; delete eventObj.datacontentencoding; } diff --git a/src/transport/receiver.ts b/src/transport/receiver.ts index f15e8f4c..3d7b5754 100644 --- a/src/transport/receiver.ts +++ b/src/transport/receiver.ts @@ -17,7 +17,7 @@ export const Receiver = { */ accept(headers: Headers, body: string | Record | undefined | null): CloudEvent { const cleanHeaders: Headers = sanitize(headers); - const cleanBody = body ? (typeof body === "object" ? JSON.stringify(body) : body) : ""; + const cleanBody = body ? (typeof body === "object" ? JSON.stringify(body) : body) : undefined; const message: Message = { headers: cleanHeaders, body: cleanBody, diff --git a/test/integration/ce.png b/test/integration/ce.png new file mode 100644 index 0000000000000000000000000000000000000000..67b50a84c576eef40ded5eb4eb46f0f98d574e83 GIT binary patch literal 40750 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 CqZX3@ literal 0 HcmV?d00001 diff --git a/test/integration/cloud_event_test.ts b/test/integration/cloud_event_test.ts index 2b948f22..86f0c4c0 100644 --- a/test/integration/cloud_event_test.ts +++ b/test/integration/cloud_event_test.ts @@ -1,6 +1,10 @@ +import path from "path"; +import fs from "fs"; + import { expect } from "chai"; import { CloudEvent, ValidationError, Version } from "../../src"; import { CloudEventV03, CloudEventV1 } from "../../src/event/interfaces"; +import { asBase64 } from "../../src/event/validation"; const type = "org.cncf.cloudevents.example"; const source = "http://unit.test"; @@ -14,6 +18,9 @@ const fixture: CloudEventV1 = { data: `"some data"`, }; +const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png"))); +const image_base64 = asBase64(imageData); + describe("A CloudEvent", () => { it("Can be constructed with a typed Message", () => { const ce = new CloudEvent(fixture); @@ -151,6 +158,15 @@ describe("A 1.0 CloudEvent", () => { expect(ce.data).to.be.true; }); + it("can be constructed with binary data", () => { + const ce = new CloudEvent({ + ...fixture, + data: imageData, + }); + expect(ce.data).to.equal(imageData); + expect(ce.data_base64).to.equal(image_base64); + }); + it("can be constructed with extensions", () => { const extensions = { extensionkey: "extension-value", diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index fc35e11a..9f6fb904 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -1,3 +1,6 @@ +import path from "path"; +import fs from "fs"; + import { expect } from "chai"; import { CloudEvent, CONSTANTS, Version } from "../../src"; import { asBase64 } from "../../src/event/validation"; @@ -16,7 +19,6 @@ const data = { // Attributes for v03 events const schemaurl = "https://cloudevents.io/schema.json"; -const datacontentencoding = "base64"; const ext1Name = "extension1"; const ext1Value = "foobar"; @@ -25,7 +27,11 @@ const ext2Value = "acme"; // Binary data as base64 const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number); -const data_base64 = asBase64(dataBinary); + +// Since the above is a special case (string as binary), let's test +// with a real binary file one is likely to encounter in the wild +const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png"))); +const image_base64 = asBase64(imageData); describe("HTTP transport", () => { it("Can detect invalid CloudEvent Messages", () => { @@ -45,6 +51,7 @@ describe("HTTP transport", () => { new CloudEvent({ source: "/message-test", type: "example", + data, }), ); expect(HTTP.isEvent(message)).to.be.true; @@ -144,23 +151,47 @@ describe("HTTP transport", () => { expect(event).to.deep.equal(fixture); }); - it("Supports Base-64 encoded data in structured messages", () => { - const event = fixture.cloneWith({ data: dataBinary }); - expect(event.data_base64).to.equal(data_base64); - expect(event.data).to.equal(dataBinary); + it("Converts binary data to base64 when serializing structured messages", () => { + const event = fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }); + expect(event.data).to.equal(imageData); const message = HTTP.structured(event); + const messageBody = JSON.parse(message.body as string); + expect(messageBody.data_base64).to.equal(image_base64); + }); + + it("Converts base64 encoded data to binary when deserializing structured messages", () => { + const message = HTTP.structured(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" })); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal(imageData); + expect(eventDeserialized.data_base64).to.equal(image_base64); + }); + + it("Parses binary data from structured messages with content type application/json", () => { + const message = HTTP.structured(fixture.cloneWith({ data: dataBinary })); const eventDeserialized = HTTP.toEvent(message); expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + expect(eventDeserialized.data_base64).to.be.undefined; + }); + + it("Converts base64 encoded data to binary when deserializing binary messages", () => { + const message = HTTP.binary(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" })); + const eventDeserialized = HTTP.toEvent(message); + expect(eventDeserialized.data).to.deep.equal(imageData); + expect(eventDeserialized.data_base64).to.equal(image_base64); }); - it("Supports Base-64 encoded data in binary messages", () => { + it("Keeps binary data binary when serializing binary messages", () => { const event = fixture.cloneWith({ data: dataBinary }); - expect(event.data_base64).to.equal(data_base64); expect(event.data).to.equal(dataBinary); const message = HTTP.binary(event); expect(message.body).to.equal(dataBinary); + }); + + it("Parses binary data from binary messages with content type application/json", () => { + const message = HTTP.binary(fixture.cloneWith({ data: dataBinary })); const eventDeserialized = HTTP.toEvent(message); - expect(eventDeserialized.data).to.equal(dataBinary); + expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); + expect(eventDeserialized.data_base64).to.be.undefined; }); }); @@ -223,28 +254,35 @@ describe("HTTP transport", () => { expect(event).to.deep.equal(fixture); }); - it("Supports Base-64 encoded data in structured messages", () => { - const event = fixture.cloneWith({ data: data_base64, datacontentencoding }); + it("Converts binary data to base64 when serializing structured messages", () => { + const event = fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }); + expect(event.data).to.equal(imageData); const message = HTTP.structured(event); - expect(JSON.parse(message.body as string).data).to.equal(data_base64); - // An incoming event with datacontentencoding set to base64, - // and encoded data, should decode the data before setting - // the .data property on the event + const messageBody = JSON.parse(message.body as string); + expect(messageBody.data_base64).to.equal(image_base64); + }); + + it("Converts base64 encoded data to binary when deserializing structured messages", () => { + // Creating an event with binary data automatically produces base64 encoded data + // which is then set as the 'data' attribute on the message body + const message = HTTP.structured(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" })); const eventDeserialized = HTTP.toEvent(message); - expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); - expect(eventDeserialized.datacontentencoding).to.be.undefined; + expect(eventDeserialized.data).to.deep.equal(imageData); + expect(eventDeserialized.data_base64).to.equal(image_base64); }); - it("Supports Base-64 encoded data in binary messages", () => { - const event = fixture.cloneWith({ data: data_base64, datacontentencoding }); - const message = HTTP.binary(event); - expect(message.body).to.equal(data_base64); - // An incoming event with datacontentencoding set to base64, - // and encoded data, should decode the data before setting - // the .data property on the event + it("Converts base64 encoded data to binary when deserializing binary messages", () => { + const message = HTTP.binary(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" })); const eventDeserialized = HTTP.toEvent(message); - expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); - expect(eventDeserialized.datacontentencoding).to.be.undefined; + expect(eventDeserialized.data).to.deep.equal(imageData); + expect(eventDeserialized.data_base64).to.equal(image_base64); + }); + + it("Keeps binary data binary when serializing binary messages", () => { + const event = fixture.cloneWith({ data: dataBinary }); + expect(event.data).to.equal(dataBinary); + const message = HTTP.binary(event); + expect(message.body).to.equal(dataBinary); }); }); }); From 5255a19ea458ab6bd9854a42fbc83fe59b2a783f Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Wed, 30 Sep 2020 17:42:41 -0400 Subject: [PATCH 3/4] fixup: remove stray old comment Signed-off-by: Lance Ball --- src/message/http/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/message/http/index.ts b/src/message/http/index.ts index 542c400c..a219a6d2 100644 --- a/src/message/http/index.ts +++ b/src/message/http/index.ts @@ -162,7 +162,6 @@ function parseBinary(message: Message, version: Version): CloudEvent { } } - // If the body is base64 that means we are dealing with binary data const parser = parserByContentType[eventObj.datacontenttype as string]; if (parser && body) { body = parser.parse(body as string); From 4407a0b79874adc7d6f1c56766268f7793211ca6 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Thu, 1 Oct 2020 15:10:20 -0400 Subject: [PATCH 4/4] fixup: do not parse binary data even as application/json Signed-off-by: Lance Ball --- src/message/http/index.ts | 18 ++---------------- test/integration/message_test.ts | 13 +++++++------ 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/src/message/http/index.ts b/src/message/http/index.ts index a219a6d2..bd90eb56 100644 --- a/src/message/http/index.ts +++ b/src/message/http/index.ts @@ -2,7 +2,7 @@ import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } fro import { Message, Headers } from ".."; import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers } from "./headers"; -import { isBinary, isStringOrObjectOrThrow, ValidationError } from "../../event/validation"; +import { isStringOrObjectOrThrow, ValidationError } from "../../event/validation"; import { JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers"; // implements Serializer @@ -155,13 +155,6 @@ function parseBinary(message: Message, version: Version): CloudEvent { } } - if (isBinary(body)) { - if (eventObj.datacontenttype === CONSTANTS.MIME_JSON) { - // even though the body is binary, it's just JSON - convert to string - body = Buffer.from(body as string, "base64").toString(); - } - } - const parser = parserByContentType[eventObj.datacontenttype as string]; if (parser && body) { body = parser.parse(body as string); @@ -231,14 +224,7 @@ function parseStructured(message: Message, version: Version): CloudEvent { // itself will be encoded as base64 if (eventObj.data_base64 || eventObj.datacontentencoding === CONSTANTS.ENCODING_BASE64) { const data = eventObj.data_base64 || eventObj.data; - - // we can choose to decode the binary data as JSON, if datacontentype is application/json - if (eventObj.datacontenttype === CONSTANTS.MIME_JSON) { - eventObj.data = JSON.parse(Buffer.from(data as string, "base64").toString()); - } else { - eventObj.data = new Uint32Array(Buffer.from(data as string, "base64")); - } - + eventObj.data = new Uint32Array(Buffer.from(data as string, "base64")); delete eventObj.data_base64; delete eventObj.datacontentencoding; } diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index 9f6fb904..7e1caf59 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -27,6 +27,7 @@ const ext2Value = "acme"; // Binary data as base64 const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number); +const data_base64 = asBase64(dataBinary); // Since the above is a special case (string as binary), let's test // with a real binary file one is likely to encounter in the wild @@ -166,11 +167,11 @@ describe("HTTP transport", () => { expect(eventDeserialized.data_base64).to.equal(image_base64); }); - it("Parses binary data from structured messages with content type application/json", () => { + it("Does not parse binary data from structured messages with content type application/json", () => { const message = HTTP.structured(fixture.cloneWith({ data: dataBinary })); const eventDeserialized = HTTP.toEvent(message); - expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); - expect(eventDeserialized.data_base64).to.be.undefined; + expect(eventDeserialized.data).to.deep.equal(dataBinary); + expect(eventDeserialized.data_base64).to.equal(data_base64); }); it("Converts base64 encoded data to binary when deserializing binary messages", () => { @@ -187,11 +188,11 @@ describe("HTTP transport", () => { expect(message.body).to.equal(dataBinary); }); - it("Parses binary data from binary messages with content type application/json", () => { + it("Does not parse binary data from binary messages with content type application/json", () => { const message = HTTP.binary(fixture.cloneWith({ data: dataBinary })); const eventDeserialized = HTTP.toEvent(message); - expect(eventDeserialized.data).to.deep.equal({ foo: "bar" }); - expect(eventDeserialized.data_base64).to.be.undefined; + expect(eventDeserialized.data).to.deep.equal(dataBinary); + expect(eventDeserialized.data_base64).to.equal(data_base64); }); });