Skip to content

Commit

Permalink
feat: Enable OPTIONS preflight and a method to enable cros origins (#359
Browse files Browse the repository at this point in the history
)

### Reasoning

At the moment, no support for OPTIONS request is implemented. This is the preflight request for CORS checks, thus API calls to a serverless function from a web browser is failing.

### Changes

Enable preflight response [OPTIONS] with generic cors allow.
Export a function "cors" where you can specify the allowed origins.
  • Loading branch information
giurgiur99 authored Jun 20, 2024
1 parent 4756c01 commit b3fa2b9
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 8 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export interface Function {
// This function is optional and should be synchronous.
shutdown?: () => any;

// Function that returns an array of CORS origins
// This function is optional.
cors?: () => string[];

// The liveness function, called to check if the server is alive
// This function is optional and should return 200/OK if the server is alive.
liveness?: HealthCheck;
Expand Down
5 changes: 4 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const INCLUDE_RAW = false;

/**
* Starts the provided Function. If the function is a module, it will be
* inspected for init, shutdown, liveness, and readiness functions and those
* inspected for init, shutdown, cors, liveness, and readiness functions and those
* will be used to configure the server. If it's a function, it will be used
* directly.
*
Expand Down Expand Up @@ -56,6 +56,9 @@ async function start(func, options) {
if (typeof func.readiness === 'function') {
options.readiness = func.readiness;
}
if (typeof func.cors === 'function') {
options.cors = func.cors;
}
return __start(func.handle, options);
}

Expand Down
12 changes: 11 additions & 1 deletion lib/invocation-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ const invoker = require('./invoker');
module.exports = function use(fastify, opts, done) {
fastify.get('/', doGet);
fastify.post('/', doPost);
const invokeFunction = invoker(opts.func);
fastify.options('/', doOptions);
const invokeFunction = invoker(opts);

// TODO: if we know this is a CloudEvent function, should
// we allow GET requests?
Expand All @@ -15,6 +16,15 @@ module.exports = function use(fastify, opts, done) {
async function doPost(request, reply) {
sendReply(reply, await invokeFunction(request.fcontext, reply.log));
}

async function doOptions(_request, reply) {
reply.code(204).headers({
'content-type': 'application/json; charset=utf-8',
'access-control-allow-methods': 'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH',
'access-control-allow-origin': '*',
'access-control-allow-headers': '*'
}).send();
}
done();
};

Expand Down
18 changes: 13 additions & 5 deletions lib/invoker.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,28 @@
*/
const { CloudEvent, HTTP } = require('cloudevents');

module.exports = function invoker(func) {
module.exports = function invoker(opts) {
const func = opts.func;
return async function invokeFunction(context, log) {
// Default payload values
const payload = {
code: 200,
response: undefined,
headers: {
'content-type': 'application/json; charset=utf-8',
'access-control-allow-methods':
'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH',
'access-control-allow-origin': '*'
'access-control-allow-methods': 'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH',
}
};

if (opts.funcConfig.cors) {
const allowedOrigins = opts.funcConfig.cors();
const requestOrigin = context.headers.origin;
const originIncluded = allowedOrigins.includes(requestOrigin) ? requestOrigin : null;
payload.headers['access-control-allow-origin'] = originIncluded;
} else {
payload.headers['access-control-allow-origin'] = '*';
}

let fnReturn;
const scope = Object.freeze({});
try {
Expand All @@ -29,7 +37,7 @@ module.exports = function invoker(func) {
if (fnReturn instanceof CloudEvent || fnReturn.constructor?.name === 'CloudEvent') {
try {
const message = HTTP.binary(fnReturn);
payload.headers = {...payload.headers, ...message.headers};
payload.headers = { ...payload.headers, ...message.headers };
payload.response = message.body;
// In this case, where the function is invoked with a CloudEvent
// and returns a CloudEvent we don't need to continue processing the
Expand Down
6 changes: 5 additions & 1 deletion lib/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable no-unused-vars */
import { IncomingHttpHeaders, IncomingMessage } from 'http';
import { CloudEvent } from 'cloudevents';
import { IncomingHttpHeaders, IncomingMessage } from 'http';
import { Http2ServerRequest, Http2ServerResponse } from 'http2';

/**
Expand All @@ -16,6 +16,10 @@ export interface Function {
// This function is optional.
shutdown?: () => (any | Promise<any>);

// Function that returns an array of CORS origins
// This function is optional.
cors?: () => string[];

// The liveness function, called to check if the server is alive
// This function is optional and should return 200/OK if the server is alive.
liveness?: HealthCheck;
Expand Down
39 changes: 39 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,45 @@ test('Sends CORS headers in HTTP response', t => {
}, errHandler(t));
});

test('Responds to OPTIONS with CORS headers in HTTP response', t => {
start(_ => '')
.then(server => {
t.plan(2);
request(server)
.options('/')
.expect(204)
.expect('Access-Control-Allow-Origin', '*')
.expect('Access-Control-Allow-Methods',
'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH')
.end((err, res) => {
t.error(err, 'No error');
t.equal(res.text, '');
t.end();
server.close();
});
}, errHandler(t));
});

test('Responds to GET with CORS headers in HTTP response', t => {
start(_ => '', { cors: () => ['http://example.com', 'http://example2.com'] } )
.then(server => {
t.plan(2);
request(server)
.get('/')
.expect(204)
.set('Origin', 'http://example.com')
.expect('Access-Control-Allow-Origin', 'http://example.com')
.expect('Access-Control-Allow-Methods',
'OPTIONS, GET, DELETE, POST, PUT, HEAD, PATCH')
.end((err, res) => {
t.error(err, 'No error');
t.equal(res.text, '');
t.end();
server.close();
});
}, errHandler(t));
});

test('Respects headers set by the function', t => {
const func = require(`${__dirname}/fixtures/response-header/`);
start(func)
Expand Down

0 comments on commit b3fa2b9

Please sign in to comment.