Skip to content

Commit 81d47c1

Browse files
committed
feat: handle CloudEvent and Message responses from function invocation
We already **kind of** handled them, but these changes make it more intuitive for the user. For example, the function author can respond with `new CloudEvent(...)` or `HTTP.binary(event)` and the framework should handle the response correctly. Signed-off-by: Lance Ball <[email protected]>
1 parent f8e6513 commit 81d47c1

File tree

8 files changed

+108
-48
lines changed

8 files changed

+108
-48
lines changed

lib/context.js

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const Spec = require('../lib/ce-constants.js').Spec;
1+
const { CloudEvent } = require('cloudevents');
22

33
class Context {
44
constructor(request) {
@@ -24,34 +24,31 @@ class CloudEventResponse {
2424
#response;
2525

2626
constructor(response) {
27-
this.#response = response;
28-
if (!this.#response.headers) {
29-
this.#response.headers = [];
30-
}
27+
this.#response = { data: response };
3128
}
3229

3330
version(version) {
34-
this.#response.headers[Spec.version] = version;
31+
this.#response.specversion = version;
3532
return this;
3633
}
3734

3835
id(id) {
39-
this.#response.headers[Spec.id] = id;
36+
this.#response.id = id;
4037
return this;
4138
}
4239

4340
type(type) {
44-
this.#response.headers[Spec.type] = type;
41+
this.#response.type = type;
4542
return this;
4643
}
4744

4845
source(source) {
49-
this.#response.headers[Spec.source] = source;
46+
this.#response.source = source;
5047
return this;
5148
}
5249

5350
response() {
54-
return this.#response;
51+
return new CloudEvent(this.#response);
5552
}
5653
}
5754

lib/invoker.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
const { CloudEvent, HTTP } = require('cloudevents');
23

34
module.exports = function invoker(func) {
45
return async function invokeFunction(context, log) {
@@ -7,10 +8,10 @@ module.exports = function invoker(func) {
78
code: 200,
89
response: undefined,
910
headers: {
10-
'Content-Type': 'application/json; charset=utf8',
11-
'Access-Control-Allow-Methods':
11+
'content-type': 'application/json; charset=utf8',
12+
'access-control-allow-methods':
1213
'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH',
13-
'Access-Control-Allow-Origin': '*'
14+
'access-control-allow-origin': '*'
1415
}
1516
};
1617

@@ -47,9 +48,32 @@ module.exports = function invoker(func) {
4748

4849
// Check for user defined headers
4950
if (typeof payload.response.headers === 'object') {
50-
Object.assign(payload.headers, payload.response.headers);
51+
const headers = {};
52+
// normalize the headers as lowercase
53+
for (const header in payload.response.headers) {
54+
headers[header.toLocaleLowerCase()] = payload.response.headers[header];
55+
}
56+
payload.headers = { ...payload.headers, ...headers };
5157
delete payload.response.headers;
5258
}
59+
60+
// If the response is a CloudEvent, we need to convert it
61+
// to a Message first and respond with the headers/body
62+
if (payload.response instanceof CloudEvent) {
63+
try {
64+
const message = HTTP.binary(payload.response);
65+
payload.headers = {...payload.headers, ...message.headers};
66+
payload.response = message.body;
67+
} catch (e) {
68+
console.error(e);
69+
}
70+
}
71+
72+
// Check for user supplied body
73+
if (payload.response.body !== undefined) {
74+
payload.response = payload.response.body;
75+
delete payload.response.body;
76+
}
5377
return payload;
5478
};
5579
};

lib/request-handler.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ module.exports = function(fastify, opts, done) {
2222
};
2323

2424
function sendReply(reply, payload) {
25-
if (payload.headers['Content-Type'].startsWith('text/plain')) {
25+
const contentType = payload.headers['content-type'];
26+
if (contentType.startsWith('text/plain') && (typeof payload.response !== 'string')) {
2627
payload.response = JSON.stringify(payload.response);
2728
}
2829
return reply

package-lock.json

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"bin": "./bin/cli.js",
2222
"dependencies": {
2323
"chalk": "^4.1.0",
24-
"cloudevents": "^3.1.0",
24+
"cloudevents": "^3.2.0",
2525
"commander": "^6.1.0",
2626
"death": "^1.1.0",
2727
"fastify": "^3.3.0",
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module.exports = function testFunc(context) {
2-
return context;
2+
return { ...context.query };
33
};

test/test-context.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ test('Provides HTTP request query parameters with the context parameter', t => {
4343
.expect('Content-Type', /json/)
4444
.end((err, res) => {
4545
t.error(err, 'No error');
46-
t.equal(res.body.query.lunch, 'tacos');
47-
t.equal(res.body.query.supper, 'burgers');
46+
t.equal(res.body.lunch, 'tacos');
47+
t.equal(res.body.supper, 'burgers');
4848
t.end();
4949
server.close();
5050
});

test/test.js

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const Spec = require('../lib/ce-constants.js').Spec;
99
const { existsSync, readdirSync } = require('fs');
1010
const { execSync } = require('child_process');
1111
const path = require('path');
12+
const { CloudEvent, HTTP } = require('cloudevents');
1213

1314
// Ensure fixture dependencies are installed
1415
const fixtureDir = path.join(__dirname, 'fixtures');
@@ -172,6 +173,62 @@ test('Responds to 1.0 structured cloud events', t => {
172173
}, { log: false });
173174
});
174175

176+
test('Handles 1.0 CloudEvent responses', t => {
177+
framework(_ => {
178+
return new CloudEvent({
179+
source: 'test',
180+
type: 'test-type',
181+
data: 'some data',
182+
datacontenttype: 'text/plain'
183+
});
184+
}, server => {
185+
request(server)
186+
.post('/')
187+
.send({ message: 'hello' })
188+
.set(Spec.id, '1')
189+
.set(Spec.source, 'integration-test')
190+
.set(Spec.type, 'dev.knative.example')
191+
.set(Spec.version, '1.0')
192+
.expect(200)
193+
.expect('Content-Type', /text/)
194+
.end((err, res) => {
195+
t.error(err, 'No error');
196+
t.equal(res.text, 'some data');
197+
t.end();
198+
server.close();
199+
});
200+
},
201+
{ log: false });
202+
});
203+
204+
test('Handles 1.0 CloudEvent Message responses', t => {
205+
framework(_ => {
206+
return HTTP.binary(new CloudEvent({
207+
source: 'test',
208+
type: 'test-type',
209+
data: 'some data',
210+
datacontenttype: 'text/plain'
211+
}));
212+
}, server => {
213+
request(server)
214+
.post('/')
215+
.send({ message: 'hello' })
216+
.set(Spec.id, '1')
217+
.set(Spec.source, 'integration-test')
218+
.set(Spec.type, 'dev.knative.example')
219+
.set(Spec.version, '1.0')
220+
.expect(200)
221+
.expect('Content-Type', /text/)
222+
.end((err, res) => {
223+
t.error(err, 'No error');
224+
t.equal(res.text, 'some data');
225+
t.end();
226+
server.close();
227+
});
228+
},
229+
{ log: false });
230+
});
231+
175232
test('Extracts event data as the first parameter to a function', t => {
176233
const data = {
177234
lunch: "tacos"
@@ -202,25 +259,6 @@ test('Extracts event data as the first parameter to a function', t => {
202259
}, { log: false });
203260
});
204261

205-
test('Responds with error code (4xx or 5xx) to malformed cloud events', t => {
206-
const func = require(`${__dirname}/fixtures/cloud-event/`);
207-
framework(func, server => {
208-
request(server)
209-
.post('/')
210-
.send({ message: 'hello' })
211-
.set(Spec.id, '1')
212-
.set(Spec.source, 'integration-test')
213-
.set(Spec.version, '0.3')
214-
.set('ce-datacontenttype', 'application/json')
215-
.expect('Content-Type', /json/)
216-
.end((err, res) => {
217-
t.assert(res.statusCode >= 400 && res.statusCode <= 599, 'Error code 4xx or 5xx expected.');
218-
t.end();
219-
server.close();
220-
});
221-
}, { log: false });
222-
});
223-
224262
test('Responds with 406 Not Acceptable to unknown cloud event versions', t => {
225263
const func = require(`${__dirname}/fixtures/cloud-event/`);
226264
framework(func, server => {

0 commit comments

Comments
 (0)