Skip to content

Commit 88db21d

Browse files
authored
Fix error when output binding is null/undefined [v4] (#77)
1 parent 40b1ad3 commit 88db21d

File tree

2 files changed

+89
-6
lines changed

2 files changed

+89
-6
lines changed

src/InvocationModel.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
RpcTypedData,
1414
} from '@azure/functions-core';
1515
import { format } from 'util';
16+
import { InvocationContext } from './InvocationContext';
1617
import { returnBindingKey } from './constants';
1718
import { fromRpcBindings } from './converters/fromRpcBindings';
1819
import { fromRpcRetryContext, fromRpcTraceContext } from './converters/fromRpcContext';
@@ -21,9 +22,8 @@ import { fromRpcTypedData } from './converters/fromRpcTypedData';
2122
import { toCamelCaseValue } from './converters/toCamelCase';
2223
import { toRpcHttp } from './converters/toRpcHttp';
2324
import { toRpcTypedData } from './converters/toRpcTypedData';
24-
import { InvocationContext } from './InvocationContext';
2525
import { isHttpTrigger, isTimerTrigger, isTrigger } from './utils/isTrigger';
26-
import { nonNullProp, nonNullValue } from './utils/nonNull';
26+
import { isDefined, nonNullProp, nonNullValue } from './utils/nonNull';
2727

2828
export class InvocationModel implements coreTypes.InvocationModel {
2929
#isDone = false;
@@ -99,10 +99,10 @@ export class InvocationModel implements coreTypes.InvocationModel {
9999
response.returnValue = await this.#convertOutput(binding, result);
100100
usedReturnValue = true;
101101
} else {
102-
response.outputData.push({
103-
name,
104-
data: await this.#convertOutput(binding, context.extraOutputs.get(name)),
105-
});
102+
const outputValue = await this.#convertOutput(binding, context.extraOutputs.get(name));
103+
if (isDefined(outputValue)) {
104+
response.outputData.push({ name, data: outputValue });
105+
}
106106
}
107107
}
108108
}

test/InvocationModel.test.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { RpcLogCategory, RpcLogLevel } from '@azure/functions-core';
5+
import { expect } from 'chai';
6+
import 'mocha';
7+
import { InvocationContext } from '../src';
8+
import { InvocationModel } from '../src/InvocationModel';
9+
10+
function testLog(_level: RpcLogLevel, _category: RpcLogCategory, message: string) {
11+
console.log(message);
12+
}
13+
14+
describe('InvocationModel', () => {
15+
describe('getResponse', () => {
16+
it('Hello world http', async () => {
17+
const model = new InvocationModel({
18+
invocationId: 'testInvocId',
19+
metadata: {
20+
name: 'testFuncName',
21+
bindings: {
22+
httpTrigger1: {
23+
type: 'httpTrigger',
24+
direction: 'in',
25+
},
26+
$return: {
27+
type: 'http',
28+
direction: 'out',
29+
},
30+
},
31+
},
32+
request: {},
33+
log: testLog,
34+
});
35+
const context = new InvocationContext();
36+
const response = await model.getResponse(context, { body: 'Hello, world!' });
37+
expect(response).to.deep.equal({
38+
invocationId: 'testInvocId',
39+
outputData: [],
40+
returnValue: {
41+
http: {
42+
body: {
43+
bytes: Buffer.from('Hello, world!'),
44+
},
45+
cookies: [],
46+
enableContentNegotiation: false,
47+
headers: {
48+
'content-type': 'text/plain;charset=UTF-8',
49+
},
50+
statusCode: '200',
51+
},
52+
},
53+
});
54+
});
55+
56+
it('undefined output is not included in rpc response', async () => {
57+
// https://github.com/Azure/azure-functions-nodejs-library/issues/71
58+
// If an output binding is undefined or null, we should exclude it from the `outputData` array otherwise host will throw an error
59+
// https://github.com/Azure/azure-functions-host/blob/6eea6da0952857b4cc64339f329cdf61432b5815/src/WebJobs.Script.Grpc/Channel/GrpcWorkerChannel.cs#L871
60+
const model = new InvocationModel({
61+
invocationId: 'testInvocId',
62+
metadata: {
63+
name: 'testFuncName',
64+
bindings: {
65+
timerTrigger1: {
66+
type: 'timerTrigger',
67+
direction: 'in',
68+
},
69+
queueOutput1: {
70+
type: 'queue',
71+
direction: 'out',
72+
},
73+
},
74+
},
75+
request: {},
76+
log: testLog,
77+
});
78+
const context = new InvocationContext();
79+
const response = await model.getResponse(context, undefined);
80+
expect(response).to.deep.equal({ invocationId: 'testInvocId', outputData: [], returnValue: undefined });
81+
});
82+
});
83+
});

0 commit comments

Comments
 (0)