Skip to content

Commit c5f8047

Browse files
fix!: send correct response code on exception
1 parent 6c08ebd commit c5f8047

File tree

6 files changed

+84
-10
lines changed

6 files changed

+84
-10
lines changed

src/invoker.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
// functions with HTTP trigger).
2222
import * as express from 'express';
2323
import * as http from 'http';
24-
import {FUNCTION_STATUS_HEADER_FIELD} from './types';
2524
import {sendCrashResponse} from './logger';
2625

2726
/**
@@ -47,10 +46,7 @@ export function sendResponse(
4746
res: express.Response
4847
) {
4948
if (err) {
50-
res.set(FUNCTION_STATUS_HEADER_FIELD, 'error');
51-
// Sending error message back is fine for Pub/Sub-based functions as they do
52-
// not reach the caller anyway.
53-
res.send(err.message);
49+
sendCrashResponse({err, res, statusHeader: 'error'});
5450
return;
5551
}
5652
if (typeof result === 'undefined' || result === null) {

src/logger.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,14 @@ export function sendCrashResponse({
2727
res,
2828
callback,
2929
silent = false,
30+
statusHeader = 'crash',
3031
}: {
3132
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3233
err: Error | any;
3334
res: express.Response | null;
3435
callback?: Function;
3536
silent?: boolean;
37+
statusHeader?: string;
3638
}) {
3739
if (!silent) {
3840
console.error(err.stack || err);
@@ -43,8 +45,14 @@ export function sendCrashResponse({
4345
// right before sending the response, to make sure that no concurrent
4446
// execution sends the response between the check and 'send' call below.
4547
if (res && !res.headersSent) {
46-
res.set(FUNCTION_STATUS_HEADER_FIELD, 'crash');
47-
res.send((err.message || err) + '');
48+
res.set(FUNCTION_STATUS_HEADER_FIELD, statusHeader);
49+
50+
if (process.env.NODE_ENV !== 'production') {
51+
res.status(500);
52+
res.send((err.message || err) + '');
53+
} else {
54+
res.sendStatus(500);
55+
}
4856
}
4957
if (callback) {
5058
callback();

test/conformance/function.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const fileName = 'function_output.json';
55

66
functions.http('writeHttpDeclarative', (req, res) => {
77
writeJson(req.body);
8-
res.end(200);
8+
res.sendStatus(200);
99
});
1010

1111
functions.cloudEvent('writeCloudEventDeclarative', cloudEvent => {
@@ -15,7 +15,7 @@ functions.cloudEvent('writeCloudEventDeclarative', cloudEvent => {
1515

1616
function writeHttp(req, res) {
1717
writeJson(req.body);
18-
res.end(200);
18+
res.sendStatus(200);
1919
}
2020

2121
function writeCloudEvent(cloudEvent) {

test/integration/cloud_event.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,13 @@ describe('CloudEvent Function', () => {
5050
clock = sinon.useFakeTimers();
5151
// Prevent log spew from the PubSub emulator request.
5252
sinon.stub(console, 'warn');
53+
sinon.stub(console, 'error');
5354
});
5455

5556
afterEach(() => {
5657
clock.restore();
5758
(console.warn as sinon.SinonSpy).restore();
59+
(console.error as sinon.SinonSpy).restore();
5860
});
5961

6062
const testData = [
@@ -302,4 +304,21 @@ describe('CloudEvent Function', () => {
302304
})
303305
.expect(204);
304306
});
307+
308+
it('returns a 500 if the function throws an exception', async () => {
309+
functions.cloudEvent('testTypedCloudEvent', () => {
310+
throw 'I crashed';
311+
});
312+
313+
// invoke the function with a CloudEvent with a string payload
314+
const server = getTestServer('testTypedCloudEvent');
315+
await supertest(server)
316+
.post('/')
317+
.send(TEST_CLOUD_EVENT)
318+
.expect(res => {
319+
assert.deepStrictEqual(res.headers['x-google-status'], 'error');
320+
assert.deepStrictEqual(res.body, {});
321+
})
322+
.expect(500);
323+
});
305324
});

test/integration/http.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
import * as assert from 'assert';
16+
import * as sinon from 'sinon';
1617
import * as supertest from 'supertest';
1718

1819
import * as functions from '../../src/index';
@@ -24,14 +25,25 @@ describe('HTTP Function', () => {
2425
before(() => {
2526
functions.http('testHttpFunction', (req, res) => {
2627
++callCount;
28+
if (req.query.crash) {
29+
throw 'I crashed';
30+
}
2731
res.send({
2832
result: req.body.text,
2933
query: req.query.param,
3034
});
3135
});
3236
});
3337

34-
beforeEach(() => (callCount = 0));
38+
beforeEach(() => {
39+
callCount = 0;
40+
// Prevent log spew from the PubSub emulator request.
41+
sinon.stub(console, 'error');
42+
});
43+
44+
afterEach(() => {
45+
(console.error as sinon.SinonSpy).restore();
46+
});
3547

3648
const testData = [
3749
{
@@ -58,6 +70,14 @@ describe('HTTP Function', () => {
5870
expectedStatus: 200,
5971
expectedCallCount: 1,
6072
},
73+
{
74+
name: 'GET throws exception',
75+
httpVerb: 'GET',
76+
path: '/foo?crash=true',
77+
expectedBody: {},
78+
expectedStatus: 500,
79+
expectedCallCount: 1,
80+
},
6181
{
6282
name: 'GET favicon.ico',
6383
httpVerb: 'GET',

test/integration/legacy_event.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import * as assert from 'assert';
1616
import * as functions from '../../src/functions';
17+
import * as sinon from 'sinon';
1718
import {getServer} from '../../src/server';
1819
import * as supertest from 'supertest';
1920

@@ -31,6 +32,15 @@ const TEST_CLOUD_EVENT = {
3132
};
3233

3334
describe('Event Function', () => {
35+
beforeEach(() => {
36+
// Prevent log spew from the PubSub emulator request.
37+
sinon.stub(console, 'error');
38+
});
39+
40+
afterEach(() => {
41+
(console.error as sinon.SinonSpy).restore();
42+
});
43+
3444
const testData = [
3545
{
3646
name: 'GCF event',
@@ -188,4 +198,25 @@ describe('Event Function', () => {
188198
assert.deepStrictEqual(receivedContext, test.expectedContext);
189199
});
190200
});
201+
202+
it('returns a 500 if the function throws an exception', async () => {
203+
const server = getServer(() => {
204+
throw 'I crashed';
205+
}, 'event');
206+
await supertest(server)
207+
.post('/')
208+
.send({
209+
eventId: 'testEventId',
210+
timestamp: 'testTimestamp',
211+
eventType: 'testEventType',
212+
resource: 'testResource',
213+
data: {some: 'payload'},
214+
})
215+
.set({'Content-Type': 'application/json'})
216+
.expect(res => {
217+
assert.deepStrictEqual(res.headers['x-google-status'], 'error');
218+
assert.deepStrictEqual(res.body, {});
219+
})
220+
.expect(500);
221+
});
191222
});

0 commit comments

Comments
 (0)