Azure / Azure/azure-functions-durable-js
Activity return value is passed back as null if it's an object that shares a key with the name of some output binding
- Dominant language
- TypeScript
- Stars
- 142
- Forks
- 66
- Avg merge
- 3d 19h
- Merged PRs (30d)
- 4
Description
**Describe the bug**
If the return value of an Activity function is an object with at least one key that is the same as the `name` property of an output binding in the `function.json` file, the orchestration will always receive `null` as the result of the activity.
This is due to the way currently the node `@azure/functions` framework allows durable activities to pass results back to the orchestration, in this line [here](https://github.com/Azure/azure-functions-nodejs-library/blob/6740c156a47f71bf72588fdc66fc79072b68cb82/src/InvocationModel.ts#L114). This line is in place because Activity functions do not set an explicit output binding, but their output should still be set as the `returnValue` of the rpc invocation response.
In the check above, `response.outputData.length == 0` will be false if the return value of the activity function is an object with at least one key that has the same name as the `name` property of one output binding, owing to this line [here](https://github.com/Azure/azure-functions-nodejs-library/blob/6740c156a47f71bf72588fdc66fc79072b68cb82/src/InvocationModel.ts#L102). The upshot of it is the framework considers this to be an object of output bindings rather than the output value of the function itself. This is a consequence of the many ways that the current model supports for returning output bindings.
Ideally, here's what I would propose:
* `context.bindings.bindingName = value` should be the only supported way to set output bindings in Durable Activities
* The return value of the Activity function should always be passed back as-is to the orchestrator
I understand that this may not be possible to implement because it could be considered a breaking change, or it could also not be worth our time investment to fix. In that case, I would propose that at the very least we should document this limitation somewhere, or log a warning message if we notice a durable activity doing this.
**Investigative information**
- Durable Functions extension version: 2.9.0
- durable-functions npm module version: 2.1.1
- Language (JavaScript/TypeScript) and version: TypeScript
- Node.js version: 16.13.0
**To Reproduce**
Steps to reproduce the behavior:
1. Generate a simple hello sequence durable functions app:
`DurableFunctionsOrchestratorJS/index.ts`:
```TS
import * as df from "durable-functions";
const orchestrator = df.orchestrator(function* (context) {
const outputs = [];
// Replace "Hello" with the name of your Durable Activity Function.
outputs.push(yield context.df.callActivity("Hello", "Tokyo"));
outputs.push(yield context.df.callActivity("Hello", "Seattle"));
outputs.push(yield context.df.callActivity("Hello", "Cairo"));
return outputs;
});
export default orchestrator;
```
`Hello/index.ts`:
```TS
import { AzureFunction, Context } from "@azure/functions";
const activityFunction: AzureFunction = async function (
context: Context
): Promise {
const msg = `Hello ${context.bindings.name}!`;
// output binding value should be set here
context.bindings.blobOutput = msg;
// this object as-is should be returned to the orchestration
return {
blobOutput: msg,
};
};
export default activityFunction;
```
`Hello/function.json`:
```json
{
"bindings": [
{
"name": "name",
"type": "activityTrigger",
"direction": "in"
},
{
"name": "blobOutput",
"type": "blob",
"direction": "out",
"path": "somePath/someFile.txt",
"connection": "someConnString"
}
],
"scriptFile": "../dist/Hello/index.js"
}
```
**Note here that the name property of the blob output binding (`blobOuput`) matches the name of the key in the return value of the activity.**
2. Try calling your orchestrator.
**Expected behavior**
The output of the orchestration should be:
`[{ "blobOutput" : "Hello Tokyo!" }, { "blobOutput" : "Hello Seattle!" }, { "blobOutput" : "Hello Cairo!" }]`
**Actual behavior**
The output of the orchestration is instead:
`[null, null, null]`
**Known workarounds**
1. Change the name of the output binding
2. Change the property name in the return value of the activity to not be the same as the `name` property of any output binding.
Contributor guide
Research direction
Start with the referenced src/InvocationModel.ts logic around lines 102 and 114, then reproduce the behavior using Hello/index.ts and Hello/function.json from the issue. Trace how the matching blobOutput key is interpreted, and add a regression test showing the activity's returned object reaches the orchestration instead of null while output bindings remain handled as intended.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100