elsa-workflows / elsa-workflows/elsa-core

NaN in Output leads to Exception

Open
#6,639 0 comments 0 reactions 0 assignees View on GitHub
triaged
Dominant language
C#
Stars
7.9k
Forks
1.5k
Avg merge
15h 22m
Merged PRs (30d)
114

Description

## Description
When I use a NaN directly in a Output or in an Object what has variables of type double inside it leeds to excpetions in combination with Elsa Studio

## Steps to Reproduce

- Create a simple activity that set a double.NaN to a double Output

```
[Activity("Elsa.Tests", nameof(NanTestActivity), Category = "Test", DisplayName = "NaNTest", Version = 1)]
public class NanTestActivity : Activity
{

[Output(DisplayName = "Result")]
public Output Result { get; set; } = default!;

protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
var result = double.NaN;
context.Set(Result, result);
}
}
```

- Create a workflow in Elsa Studio that just uses this activity
- Start the workflow
- In the instance view of the workflow click on the activity
- you won't see a value in the output and the following exeption is logged:
```
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
An unhandled exception has occurred while executing the request.
System.ArgumentException: .NET number values such as positive and negative infinity cannot be written as valid JSON. To make it work when using 'JsonSerializer', consider specifying 'JsonNumberHandling.AllowNamedFloatingPointLiterals' (see https://docs.microsoft.com/dotnet/api/system.text.json.serialization.jsonnumberhandling).
at System.Text.Json.ThrowHelper.ThrowArgumentException_ValueNotSupported()
at System.Text.Json.Utf8JsonWriter.WriteNumberValue(Double value)
at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter`1.TryWriteAsObject(Utf8JsonWriter writer, Object value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.Converters.DictionaryDefaultConverter`3.OnWriteResume(Utf8JsonWriter writer, TDictionary value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonDictionaryConverter`3.OnTryWrite(Utf8JsonWriter writer, TDictionary dictionary, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.Metadata.JsonPropertyInfo`1.GetMemberAndWriteJson(Object obj, WriteStack& state, Utf8JsonWriter writer)
at System.Text.Json.Serialization.Converters.ObjectDefaultConverter`1.OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter`1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.SerializeAsync(Stream utf8Json, T rootValue, CancellationToken cancellationToken, Object rootValueBoxed)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.SerializeAsync(Stream utf8Json, T rootValue, CancellationToken cancellationToken, Object rootValueBoxed)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.SerializeAsync(Stream utf8Json, T rootValue, CancellationToken cancellationToken, Object rootValueBoxed)
at Elsa.Workflows.Api.Endpoints.ActivityExecutions.Get.Endpoint.HandleAsync(CancellationToken cancellationToken)
at FastEndpoints.Endpoint`2.ExecAsync(CancellationToken ct)
at FastEndpoints.Endpoint`2.ExecAsync(CancellationToken ct)
at Elsa.Http.Middleware.HttpWorkflowsMiddleware.InvokeAsync(HttpContext httpContext, IServiceProvider serviceProvider)
at Elsa.Workflows.Api.Middleware.JsonSerializationErrorHandlerMiddleware.InvokeAsync(HttpContext httpContext)
at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)

```

![Image](https://github.com/user-attachments/assets/76d7ff7b-2119-4185-add6-34ce17cc83ed)

- if a variable is linked to the output the following exception will happen before and no state will be visible in the instance view:
```
warn: Elsa.Workflows.Middleware.Workflows.EngineExceptionHandlingMiddleware[0]
An exception was caught from a downstream middleware component
System.ArgumentException: .NET number values such as positive and negative infinity cannot be written as valid JSON. To make it work when using 'JsonSerializer', consider specifying 'JsonNumberHandling.AllowNamedFloatingPointLiterals' (see https://docs.microsoft.com/dotnet/api/system.text.json.serialization.jsonnumberhandling).
at System.Text.Json.ThrowHelper.ThrowArgumentException_ValueNotSupported()
at System.Text.Json.Serialization.JsonConverter`1.TryWrite(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.JsonConverter`1.WriteCore(Utf8JsonWriter writer, T& value, JsonSerializerOptions options, WriteStack& state)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.Serialize(Utf8JsonWriter writer, T& rootValue, Object rootValueBoxed)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.SerializeAsObject(Utf8JsonWriter writer, Object rootValue)
at System.Text.Json.Serialization.Metadata.JsonTypeInfo`1.Serialize(Utf8JsonWriter writer, T& rootValue, Object rootValueBoxed)
at System.Text.Json.JsonSerializer.WriteNode[TValue](TValue& value, JsonTypeInfo`1 jsonTypeInfo)
at System.Text.Json.JsonSerializer.SerializeToNode[TValue](TValue value, JsonSerializerOptions options)
at Elsa.Workflows.WorkflowInstanceStorageDriver.<>c__DisplayClass7_0.b__0(VariablesDictionary dictionary)
at Elsa.Workflows.WorkflowInstanceStorageDriver.UpdateVariablesDictionary(StorageDriverContext context, Action`1 update)
at Elsa.Workflows.WorkflowInstanceStorageDriver.WriteAsync(String id, Object value, StorageDriverContext context)
at Elsa.Workflows.VariablePersistenceManager.SaveVariablesAsync(WorkflowExecutionContext workflowExecutionContext)
at Elsa.Workflows.Runtime.Middleware.Workflows.PersistentVariablesMiddleware.InvokeAsync(WorkflowExecutionContext context)
at Elsa.Workflows.Runtime.Middleware.Workflows.PersistWorkflowExecutionLogMiddleware.InvokeAsync(WorkflowExecutionContext context)
at Elsa.Workflows.Runtime.Middleware.Workflows.PersistActivityExecutionLogMiddleware.InvokeAsync(WorkflowExecutionContext context)
at Elsa.Workflows.Runtime.Middleware.Workflows.PersistBookmarkMiddleware.InvokeAsync(WorkflowExecutionContext context)
at Elsa.Workflows.Middleware.Workflows.EngineExceptionHandlingMiddleware.InvokeAsync(WorkflowExecutionContext context)
```

- I had one more exception following these two, but at the moment I am not able to reproduce and is maybe related to my investigation.

Workflow:
[nan-test.json](https://github.com/user-attachments/files/20127368/nan-test.json)

## Expected Behavior
Would be great, if I can use NaN.

## Environment
- **Elsa Package Version**: 3.3.5.
- **Operating System**: Windows 11
- **Browser and Version**: Microsoft Edge

## Troubleshooting Attempts
I tried to add a JsonSerializerOption to set the demanded Option
```
services.AddTransient(provider =>
{
var options = new JsonSerializerOptions
{
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
};

return options;
});
```

But I reckognized that this is not passed to Elsa.Workflows.WorkflowInstanceStorageDriver

So i tried the following:
```
public ValueTask WriteAsync(string id, object value, StorageDriverContext context)
{
var options = payloadSerializer.GetOptions();

UpdateVariablesDictionary(context, dictionary =>
{
var node = JsonSerializer.SerializeToNode(value, **options**);
dictionary[id] = node;
});
return ValueTask.CompletedTask;
}
```

This seems to solve the variable topic, but I don't know what influence this change can have.

Next investigation leads to Elsa.Common.Serialization.ConfigurableSerializer which I try to change to:
```
private JsonSerializerOptions CreateOptionsInternal()
{
var options = ServiceProvider.GetService() ?? new();

options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.PropertyNameCaseInsensitive = true;
options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);

options.Converters.Add(new JsonStringEnumConverter());
options.Converters.Add(JsonMetadataServices.TimeSpanConverter);
options.Converters.Add(new IntegerJsonConverter());
options.Converters.Add(new BigIntegerJsonConverter());
options.Converters.Add(new DecimalJsonConverter());

return options;
}
```
So that the external Options are used. But I am aware that this might not be intended. This would be the next test on monday.

Hope that helps.

Thank you a lot!

Best regards
Maui

Contributor guide

Open the contributing guide

Research direction

Start with Elsa.Workflows.WorkflowInstanceStorageDriver.WriteAsync and Elsa.Common.Serialization.ConfigurableSerializer, then trace the ActivityExecutions.Get endpoint involved in the output failure. Reproduce the workflow with a double.NaN output and with a variable linked to it. Done means both persistence and instance-view serialization handle the value without exceptions, with regression coverage added where the relevant tests reside.

Written by the indexing model from the issue text.

Assessment

Tech stack
csharp
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.