Skip to content

Commit 744edfc

Browse files
fix!: send correct response code on exception
1 parent 1ce3472 commit 744edfc

File tree

5 files changed

+75
-7
lines changed

5 files changed

+75
-7
lines changed

src/invoker.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ export function sendResponse(
4747
res: express.Response
4848
) {
4949
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);
50+
sendCrashResponse({err, res, statusHeader: 'error'});
5451
return;
5552
}
5653
if (typeof result === 'undefined' || result === null) {

src/logger.ts

Lines changed: 7 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,11 @@ 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+
res.status(500);
50+
if (process.env.NODE_ENV !== 'production') {
51+
res.send((err.message || err) + '');
52+
}
4853
}
4954
if (callback) {
5055
callback();

test/integration/cloud_event.ts

Lines changed: 16 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,18 @@ 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', ce => {
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 => assert.deepStrictEqual(res.headers['x-google-status'], 'error'))
319+
.expect(500);
320+
});
305321
});

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: 30 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,24 @@ 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((data: {}, context: functions.Context) => {
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+
)
219+
.expect(500);
220+
});
191221
});

0 commit comments

Comments
 (0)