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

Open
#434 3 comments 0 reactions 0 assignees View on GitHub
breaking-change bug documentation
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.