Skip to content

Commit 60b22a7

Browse files
committed
src(message): make HTTP.toEvent loosely validated & improve error msgs
This commit modifies the ValidationError class so that the error message string includes the JSON.stringified version of any schema validation errors. It also makes the HTTP.toEvent() function create CloudEvent objects with loose/no validation. Incorporates comments from #328 Signed-off-by: Lance Ball <[email protected]>
1 parent b3dd850 commit 60b22a7

File tree

5 files changed

+69
-53
lines changed

5 files changed

+69
-53
lines changed

src/event/validation.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,18 @@ export class ValidationError extends TypeError {
88
errors?: string[] | ErrorObject[] | null;
99

1010
constructor(message: string, errors?: string[] | ErrorObject[] | null) {
11-
super(message);
11+
const messageString =
12+
errors instanceof Array
13+
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
14+
// @ts-ignore
15+
errors?.reduce(
16+
(accum: string, err: Record<string, string>) =>
17+
(accum as string).concat(`
18+
${err instanceof Object ? JSON.stringify(err) : err}`),
19+
message,
20+
)
21+
: message;
22+
super(messageString);
1223
this.errors = errors ? errors : [];
1324
}
1425
}

src/message/http/headers.ts

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { PassThroughParser, DateParser, MappedParser } from "../../parsers";
2-
import { ValidationError, CloudEvent } from "../..";
2+
import { CloudEvent } from "../..";
33
import { Headers } from "../";
44
import { Version } from "../../event/cloudevent";
55
import CONSTANTS from "../../constants";
@@ -12,35 +12,6 @@ export const requiredHeaders = [
1212
CONSTANTS.CE_HEADERS.SPEC_VERSION,
1313
];
1414

15-
/**
16-
* Validates cloud event headers and their values
17-
* @param {Headers} headers event transport headers for validation
18-
* @throws {ValidationError} if the headers are invalid
19-
* @return {boolean} true if headers are valid
20-
*/
21-
export function validate(headers: Headers): Headers {
22-
const sanitizedHeaders = sanitize(headers);
23-
24-
// if content-type exists, be sure it's an allowed type
25-
const contentTypeHeader = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE];
26-
const noContentType = !allowedContentTypes.includes(contentTypeHeader);
27-
if (contentTypeHeader && noContentType) {
28-
throw new ValidationError("invalid content type", [sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]]);
29-
}
30-
31-
requiredHeaders
32-
.filter((required: string) => !sanitizedHeaders[required])
33-
.forEach((required: string) => {
34-
throw new ValidationError(`header '${required}' not found`);
35-
});
36-
37-
if (!sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]) {
38-
sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON;
39-
}
40-
41-
return sanitizedHeaders;
42-
}
43-
4415
/**
4516
* Returns the HTTP headers that will be sent for this event when the HTTP transmission
4617
* mode is "binary". Events sent over HTTP in structured mode only have a single CE header
@@ -89,6 +60,11 @@ export function sanitize(headers: Headers): Headers {
8960
.filter((header) => Object.hasOwnProperty.call(headers, header))
9061
.forEach((header) => (sanitized[header.toLowerCase()] = headers[header]));
9162

63+
// If no content-type header is sent, assume application/json
64+
if (!sanitized[CONSTANTS.HEADER_CONTENT_TYPE]) {
65+
sanitized[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON;
66+
}
67+
9268
return sanitized;
9369
}
9470

src/message/http/index.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } from "../..";
22
import { Message, Headers } from "..";
33

4-
import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers, validate } from "./headers";
4+
import { headersFor, sanitize, v03structuredParsers, v1binaryParsers, v1structuredParsers } from "./headers";
55
import { asData, isBase64, isString, isStringOrObjectOrThrow, ValidationError } from "../../event/validation";
6-
import { validateCloudEvent } from "../../event/spec";
76
import { Base64Parser, JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers";
87

98
// implements Serializer
@@ -129,7 +128,7 @@ function parseBinary(message: Message, version: Version): CloudEvent {
129128
body = isString(body) && isBase64(body) ? Buffer.from(body as string, "base64").toString() : body;
130129

131130
// Clone and low case all headers names
132-
const sanitizedHeaders = validate(headers);
131+
const sanitizedHeaders = sanitize(headers);
133132

134133
const eventObj: { [key: string]: unknown | string | Record<string, unknown> } = {};
135134
const parserMap: Record<string, MappedParser> = version === Version.V1 ? v1binaryParsers : v1binaryParsers;
@@ -165,9 +164,7 @@ function parseBinary(message: Message, version: Version): CloudEvent {
165164
delete eventObj.datacontentencoding;
166165
}
167166

168-
const cloudevent = new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03);
169-
validateCloudEvent(cloudevent);
170-
return cloudevent;
167+
return new CloudEvent({ ...eventObj, data: parsedPayload } as CloudEventV1 | CloudEventV03, false);
171168
}
172169

173170
/**
@@ -226,9 +223,5 @@ function parseStructured(message: Message, version: Version): CloudEvent {
226223
delete eventObj.data_base64;
227224
delete eventObj.datacontentencoding;
228225
}
229-
const cloudevent = new CloudEvent(eventObj as CloudEventV1 | CloudEventV03);
230-
231-
// Validates the event
232-
validateCloudEvent(cloudevent);
233-
return cloudevent;
226+
return new CloudEvent(eventObj as CloudEventV1 | CloudEventV03, false);
234227
}

test/integration/cloud_event_test.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { ValidationError } from "ajv";
21
import { expect } from "chai";
3-
import { CloudEvent, Version } from "../../src";
2+
import { CloudEvent, ValidationError, Version } from "../../src";
43
import { CloudEventV03, CloudEventV1 } from "../../src/event/interfaces";
54

65
const type = "org.cncf.cloudevents.example";
@@ -12,6 +11,7 @@ const fixture: CloudEventV1 = {
1211
specversion: Version.V1,
1312
source,
1413
type,
14+
data: `"some data"`,
1515
};
1616

1717
describe("A CloudEvent", () => {
@@ -29,12 +29,11 @@ describe("A CloudEvent", () => {
2929
it("Loosely validated events can be cloned", () => {
3030
const ce = new CloudEvent({} as CloudEventV1, false);
3131
expect(ce.cloneWith({}, false)).to.be.instanceOf(CloudEvent);
32-
console.error(ce);
3332
});
3433

3534
it("Loosely validated events throw when validated", () => {
3635
const ce = new CloudEvent({} as CloudEventV1, false);
37-
expect(ce.validate).to.throw(TypeError, "invalid payload");
36+
expect(ce.validate).to.throw(ValidationError, "invalid payload");
3837
});
3938

4039
it("serializes as JSON with toString()", () => {
@@ -169,7 +168,7 @@ describe("A 1.0 CloudEvent", () => {
169168
});
170169
} catch (err) {
171170
expect(err).to.be.instanceOf(TypeError);
172-
expect(err.message).to.equal("invalid payload");
171+
expect(err.message).to.include("invalid payload");
173172
}
174173
});
175174

@@ -252,8 +251,8 @@ describe("A 0.3 CloudEvent", () => {
252251
source: (null as unknown) as string,
253252
});
254253
} catch (err) {
255-
expect(err).to.be.instanceOf(TypeError);
256-
expect(err.message).to.equal("invalid payload");
254+
expect(err).to.be.instanceOf(ValidationError);
255+
expect(err.message).to.include("invalid payload");
257256
}
258257
});
259258

test/integration/message_test.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,21 @@ const ext2Value = "acme";
2727
const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number);
2828
const data_base64 = asBase64(dataBinary);
2929

30-
describe("HTTP transport messages", () => {
31-
it("can detect CloudEvent Messages", () => {
30+
describe("HTTP transport", () => {
31+
it("Can detect invalid CloudEvent Messages", () => {
3232
// Create a message that is not an actual event
33-
let message: Message = {
33+
const message: Message = {
3434
body: "Hello world!",
3535
headers: {
3636
"Content-type": "text/plain",
3737
},
3838
};
3939
expect(HTTP.isEvent(message)).to.be.false;
40+
});
4041

42+
it("Can detect valid CloudEvent Messages", () => {
4143
// Now create a message that is an event
42-
message = HTTP.binary(
44+
const message = HTTP.binary(
4345
new CloudEvent({
4446
source: "/message-test",
4547
type: "example",
@@ -48,6 +50,41 @@ describe("HTTP transport messages", () => {
4850
expect(HTTP.isEvent(message)).to.be.true;
4951
});
5052

53+
// Allow for external systems to send bad events - do what we can
54+
// to accept them
55+
it("Does not throw an exception when converting an invalid Message to a CloudEvent", () => {
56+
const message: Message = {
57+
body: `"hello world"`,
58+
headers: {
59+
"content-type": "application/json",
60+
"ce-id": "1234",
61+
"ce-type": "example.bad.event",
62+
"ce-specversion": "1.0",
63+
// no required ce-source header, thus an invalid event
64+
},
65+
};
66+
const event = HTTP.toEvent(message);
67+
expect(event).to.be.instanceOf(CloudEvent);
68+
// ensure that we actually now have an invalid event
69+
expect(event.validate).to.throw;
70+
});
71+
72+
it("Does not allow an invalid CloudEvent to be converted to a Message", () => {
73+
const badEvent = new CloudEvent(
74+
{
75+
source: "/example.source",
76+
type: "", // type is required, empty string will throw with strict validation
77+
},
78+
false, // turn off strict validation
79+
);
80+
expect(() => {
81+
HTTP.binary(badEvent);
82+
}).to.throw;
83+
expect(() => {
84+
HTTP.structured(badEvent);
85+
}).to.throw;
86+
});
87+
5188
describe("Specification version V1", () => {
5289
const fixture: CloudEvent = new CloudEvent({
5390
specversion: Version.V1,

0 commit comments

Comments
 (0)