fix: do not require an HTTP body on incoming binary event messages

This commit modifies the HTTP receivers/parsers to allow for the incoming body
of an HTTP request to be empty if the event message is sent using the binary
mode. In structured mode, a `ValidationError` will still be thrown, since the
entire event must be encoded in the HTTP body.

Signed-off-by: Lance Ball <lball@redhat.com>
This commit is contained in:
Lance Ball 2020-07-23 10:09:09 -04:00
parent d866691e29
commit a7c326b48c
6 changed files with 43 additions and 35 deletions

View File

@ -32,10 +32,11 @@ export class BinaryHTTPReceiver {
* @returns {CloudEvent} an instance of CloudEvent representing the incoming request * @returns {CloudEvent} an instance of CloudEvent representing the incoming request
* @throws {ValidationError} of the event does not conform to the spec * @throws {ValidationError} of the event does not conform to the spec
*/ */
parse(payload: string | Record<string, unknown>, headers: Headers): CloudEvent { parse(payload: string | Record<string, unknown> | undefined | null, headers: Headers): CloudEvent {
if (!payload) throw new ValidationError("payload is null or undefined");
if (!headers) throw new ValidationError("headers is null or undefined"); if (!headers) throw new ValidationError("headers is null or undefined");
isStringOrObjectOrThrow(payload, new ValidationError("payload must be an object or a string")); if (payload) {
isStringOrObjectOrThrow(payload, new ValidationError("payload must be an object or a string"));
}
if ( if (
headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] && headers[CONSTANTS.CE_HEADERS.SPEC_VERSION] &&
@ -61,11 +62,15 @@ export class BinaryHTTPReceiver {
} }
} }
const parser = parserByContentType[eventObj.datacontenttype as string]; let parsedPayload;
if (!parser) {
throw new ValidationError(`no parser found for content type ${eventObj.datacontenttype}`); if (payload) {
const parser = parserByContentType[eventObj.datacontenttype as string];
if (!parser) {
throw new ValidationError(`no parser found for content type ${eventObj.datacontenttype}`);
}
parsedPayload = parser.parse(payload);
} }
const parsedPayload = parser.parse(payload);
// Every unprocessed header can be an extension // Every unprocessed header can be an extension
for (const header in sanitizedHeaders) { for (const header in sanitizedHeaders) {

View File

@ -31,7 +31,7 @@ export class StructuredHTTPReceiver {
* @returns {CloudEvent} a new CloudEvent instance for the provided headers and payload * @returns {CloudEvent} a new CloudEvent instance for the provided headers and payload
* @throws {ValidationError} if the payload and header combination do not conform to the spec * @throws {ValidationError} if the payload and header combination do not conform to the spec
*/ */
parse(payload: Record<string, unknown> | string, headers: Headers): CloudEvent { parse(payload: Record<string, unknown> | string | undefined | null, headers: Headers): CloudEvent {
if (!payload) throw new ValidationError("payload is null or undefined"); if (!payload) throw new ValidationError("payload is null or undefined");
if (!headers) throw new ValidationError("headers is null or undefined"); if (!headers) throw new ValidationError("headers is null or undefined");
isStringOrObjectOrThrow(payload, new ValidationError("payload must be an object or a string")); isStringOrObjectOrThrow(payload, new ValidationError("payload must be an object or a string"));

View File

@ -59,7 +59,10 @@ export class Receiver {
* @param {Object|JSON} body The body of the HTTP request * @param {Object|JSON} body The body of the HTTP request
* @return {CloudEvent} A new {CloudEvent} instance * @return {CloudEvent} A new {CloudEvent} instance
*/ */
accept(headers: Headers, body: string | Record<string, unknown> | CloudEventV1 | CloudEventV03): CloudEvent { accept(
headers: Headers,
body: string | Record<string, unknown> | CloudEventV1 | CloudEventV03 | undefined | null,
): CloudEvent {
const cleanHeaders: Headers = sanitize(headers); const cleanHeaders: Headers = sanitize(headers);
const mode: Mode = getMode(cleanHeaders); const mode: Mode = getMode(cleanHeaders);
const version = getVersion(mode, cleanHeaders, body); const version = getVersion(mode, cleanHeaders, body);
@ -103,7 +106,7 @@ function getMode(headers: Headers): Mode {
function getVersion( function getVersion(
mode: Mode, mode: Mode,
headers: Headers, headers: Headers,
body: string | Record<string, unknown> | CloudEventV03 | CloudEventV1, body: string | Record<string, unknown> | CloudEventV03 | CloudEventV1 | undefined | null,
) { ) {
if (mode === Mode.BINARY) { if (mode === Mode.BINARY) {
// Check the headers for the version // Check the headers for the version
@ -113,7 +116,7 @@ function getVersion(
} }
} else { } else {
// structured mode - the version is in the body // structured mode - the version is in the body
return typeof body === "string" ? JSON.parse(body).specversion : body.specversion; return typeof body === "string" ? JSON.parse(body).specversion : (body as CloudEvent).specversion;
} }
return Version.V1; return Version.V1;
} }

View File

@ -39,6 +39,30 @@ describe("HTTP Transport Binding Receiver for CloudEvents", () => {
expect((event.data as Record<string, string>).lunch).to.equal("sushi"); expect((event.data as Record<string, string>).lunch).to.equal("sushi");
}); });
it("Accepts binary events when the data property is undefined", () => {
const binaryHeaders = {
"content-type": "application/json; charset=utf-8",
"ce-specversion": specversion,
"ce-id": id,
"ce-type": type,
"ce-source": source,
};
const event = receiver.accept(binaryHeaders, undefined);
expect(event.data).to.be.undefined;
});
it("Accepts binary events when the data property is null", () => {
const binaryHeaders = {
"content-type": "application/json; charset=utf-8",
"ce-specversion": specversion,
"ce-id": id,
"ce-type": type,
"ce-source": source,
};
const event = receiver.accept(binaryHeaders, null);
expect(event.data).to.be.undefined;
});
it("Converts the JSON body of a structured event to an Object", () => { it("Converts the JSON body of a structured event to an Object", () => {
const payload = { const payload = {
id, id,

View File

@ -9,18 +9,6 @@ const receiver = new BinaryHTTPReceiver(Version.V03);
describe("HTTP Transport Binding Binary Receiver for CloudEvents v0.3", () => { describe("HTTP Transport Binding Binary Receiver for CloudEvents v0.3", () => {
describe("Check", () => { describe("Check", () => {
it("Throw error when payload arg is null or undefined", () => {
// setup
const payload = undefined;
const attributes = {};
// act and assert
expect(receiver.parse.bind(receiver, (payload as unknown) as string, attributes)).to.throw(
ValidationError,
"payload is null or undefined",
);
});
it("Throw error when attributes arg is null or undefined", () => { it("Throw error when attributes arg is null or undefined", () => {
// setup // setup
const payload = {}; const payload = {};

View File

@ -10,18 +10,6 @@ const receiver = new BinaryHTTPReceiver(Version.V1);
describe("HTTP Transport Binding Binary Receiver for CloudEvents v1.0", () => { describe("HTTP Transport Binding Binary Receiver for CloudEvents v1.0", () => {
describe("Check", () => { describe("Check", () => {
it("Throw error when payload arg is null or undefined", () => {
// setup
const payload = null;
const attributes = {};
// act and assert
expect(receiver.parse.bind(receiver, (payload as unknown) as string, attributes)).to.throw(
ValidationError,
"payload is null or undefined",
);
});
it("Throw error when attributes arg is null or undefined", () => { it("Throw error when attributes arg is null or undefined", () => {
// setup // setup
const payload = {}; const payload = {};