Skip to content

Commit

Permalink
fix!: send correct response code on exception (#427)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewrobertson committed Feb 15, 2022
1 parent 6c08ebd commit 5d996fe
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 10 deletions.
6 changes: 1 addition & 5 deletions src/invoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
// functions with HTTP trigger).
import * as express from 'express';
import * as http from 'http';
import {FUNCTION_STATUS_HEADER_FIELD} from './types';
import {sendCrashResponse} from './logger';

/**
Expand All @@ -47,10 +46,7 @@ export function sendResponse(
res: express.Response
) {
if (err) {
res.set(FUNCTION_STATUS_HEADER_FIELD, 'error');
// Sending error message back is fine for Pub/Sub-based functions as they do
// not reach the caller anyway.
res.send(err.message);
sendCrashResponse({err, res, statusHeader: 'error'});
return;
}
if (typeof result === 'undefined' || result === null) {
Expand Down
12 changes: 10 additions & 2 deletions src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ export function sendCrashResponse({
res,
callback,
silent = false,
statusHeader = 'crash',
}: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
err: Error | any;
res: express.Response | null;
callback?: Function;
silent?: boolean;
statusHeader?: string;
}) {
if (!silent) {
console.error(err.stack || err);
Expand All @@ -43,8 +45,14 @@ export function sendCrashResponse({
// right before sending the response, to make sure that no concurrent
// execution sends the response between the check and 'send' call below.
if (res && !res.headersSent) {
res.set(FUNCTION_STATUS_HEADER_FIELD, 'crash');
res.send((err.message || err) + '');
res.set(FUNCTION_STATUS_HEADER_FIELD, statusHeader);

if (process.env.NODE_ENV !== 'production') {
res.status(500);
res.send((err.message || err) + '');
} else {
res.sendStatus(500);
}
}
if (callback) {
callback();
Expand Down
4 changes: 2 additions & 2 deletions test/conformance/function.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const fileName = 'function_output.json';

functions.http('writeHttpDeclarative', (req, res) => {
writeJson(req.body);
res.end(200);
res.sendStatus(200);
});

functions.cloudEvent('writeCloudEventDeclarative', cloudEvent => {
Expand All @@ -15,7 +15,7 @@ functions.cloudEvent('writeCloudEventDeclarative', cloudEvent => {

function writeHttp(req, res) {
writeJson(req.body);
res.end(200);
res.sendStatus(200);
}

function writeCloudEvent(cloudEvent) {
Expand Down
19 changes: 19 additions & 0 deletions test/integration/cloud_event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ describe('CloudEvent Function', () => {
clock = sinon.useFakeTimers();
// Prevent log spew from the PubSub emulator request.
sinon.stub(console, 'warn');
sinon.stub(console, 'error');
});

afterEach(() => {
clock.restore();
(console.warn as sinon.SinonSpy).restore();
(console.error as sinon.SinonSpy).restore();
});

const testData = [
Expand Down Expand Up @@ -302,4 +304,21 @@ describe('CloudEvent Function', () => {
})
.expect(204);
});

it('returns a 500 if the function throws an exception', async () => {
functions.cloudEvent('testTypedCloudEvent', () => {
throw 'I crashed';
});

// invoke the function with a CloudEvent with a string payload
const server = getTestServer('testTypedCloudEvent');
await supertest(server)
.post('/')
.send(TEST_CLOUD_EVENT)
.expect(res => {
assert.deepStrictEqual(res.headers['x-google-status'], 'error');
assert.deepStrictEqual(res.body, {});
})
.expect(500);
});
});
22 changes: 21 additions & 1 deletion test/integration/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// limitations under the License.

import * as assert from 'assert';
import * as sinon from 'sinon';
import * as supertest from 'supertest';

import * as functions from '../../src/index';
Expand All @@ -24,14 +25,25 @@ describe('HTTP Function', () => {
before(() => {
functions.http('testHttpFunction', (req, res) => {
++callCount;
if (req.query.crash) {
throw 'I crashed';
}
res.send({
result: req.body.text,
query: req.query.param,
});
});
});

beforeEach(() => (callCount = 0));
beforeEach(() => {
callCount = 0;
// Prevent log spew from the PubSub emulator request.
sinon.stub(console, 'error');
});

afterEach(() => {
(console.error as sinon.SinonSpy).restore();
});

const testData = [
{
Expand All @@ -58,6 +70,14 @@ describe('HTTP Function', () => {
expectedStatus: 200,
expectedCallCount: 1,
},
{
name: 'GET throws exception',
httpVerb: 'GET',
path: '/foo?crash=true',
expectedBody: {},
expectedStatus: 500,
expectedCallCount: 1,
},
{
name: 'GET favicon.ico',
httpVerb: 'GET',
Expand Down
31 changes: 31 additions & 0 deletions test/integration/legacy_event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import * as assert from 'assert';
import * as functions from '../../src/functions';
import * as sinon from 'sinon';
import {getServer} from '../../src/server';
import * as supertest from 'supertest';

Expand All @@ -31,6 +32,15 @@ const TEST_CLOUD_EVENT = {
};

describe('Event Function', () => {
beforeEach(() => {
// Prevent log spew from the PubSub emulator request.
sinon.stub(console, 'error');
});

afterEach(() => {
(console.error as sinon.SinonSpy).restore();
});

const testData = [
{
name: 'GCF event',
Expand Down Expand Up @@ -188,4 +198,25 @@ describe('Event Function', () => {
assert.deepStrictEqual(receivedContext, test.expectedContext);
});
});

it('returns a 500 if the function throws an exception', async () => {
const server = getServer(() => {
throw 'I crashed';
}, 'event');
await supertest(server)
.post('/')
.send({
eventId: 'testEventId',
timestamp: 'testTimestamp',
eventType: 'testEventType',
resource: 'testResource',
data: {some: 'payload'},
})
.set({'Content-Type': 'application/json'})
.expect(res => {
assert.deepStrictEqual(res.headers['x-google-status'], 'error');
assert.deepStrictEqual(res.body, {});
})
.expect(500);
});
});

0 comments on commit 5d996fe

Please sign in to comment.