Repository: elsa-workflows/elsa-core (2026-06-09T19:43:22Z)

Issue:
Guard HTTP workflow fault handling when workflow instance reload returns null
## Summary
`HandleWorkflowFaultAsync` assumes `IWorkflowInstanceManager.FindByIdAsync` always returns an instance and null-forgives the result before dereferencing it.

## Evidence
File: `src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs`

The method does:

```csharp
var workflowState = (await workflowInstanceManager.FindByIdAsync(workflowExecutionResult.WorkflowState.Id, cancellationToken))!;
await httpEndpointFaultHandler.HandleAsync(new(httpContext, workflowState.WorkflowState, cancellationToken));
```

If the instance was deleted or cannot be reloaded, the fault path throws a second exception and masks the original incident.

## Risk
Runtime diagnostics/correctness risk in failure handling.

## Constraints
- Safe code-only fix only.
- No persisted-data changes.
- Preserve public APIs.

## Suggested fix
Handle the `null` case explicitly and fall back to the in-memory `workflowExecutionResult.WorkflowState` or another guarded path.

## Acceptance criteria
- Fault handling no longer crashes when the reload returns `null`.
- Original workflow incident remains reportable.
- Targeted tests cover the missing-instance path.


Discussion:
Confirmed against `main` in `HandleWorkflowFaultAsync`: the workflow instance reload is still null-forgiven before dereferencing.

A safe fix here is to treat the reload as optional and fall back to `workflowExecutionResult.WorkflowState` when `FindByIdAsync(...)` returns `null`. That preserves the original incident instead of fault handling throwing a second exception and masking it. A focused test for the missing-instance path should cover it.



Patch:
diff --git a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs
index 577a062ff1..019a169c9d 100644
--- a/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs
+++ b/src/modules/Elsa.Http/Middleware/HttpWorkflowsMiddleware.cs
@@ -361,8 +361,8 @@ private async Task<bool> HandleWorkflowFaultAsync(IServiceProvider serviceProvid
 
         var httpEndpointFaultHandler = serviceProvider.GetRequiredService<IHttpEndpointFaultHandler>();
         var workflowInstanceManager = serviceProvider.GetRequiredService<IWorkflowInstanceManager>();
-        var workflowState = (await workflowInstanceManager.FindByIdAsync(workflowExecutionResult.WorkflowState.Id, cancellationToken))!;
-        await httpEndpointFaultHandler.HandleAsync(new(httpContext, workflowState.WorkflowState, cancellationToken));
+        var workflowState = (await workflowInstanceManager.FindByIdAsync(workflowExecutionResult.WorkflowState.Id, cancellationToken))?.WorkflowState ?? workflowExecutionResult.WorkflowState;
+        await httpEndpointFaultHandler.HandleAsync(new(httpContext, workflowState, cancellationToken));
         return true;
     }
 


Test patch:
diff --git a/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs b/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs
index 90eac8bedc..69ec35e6dc 100644
--- a/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs
+++ b/test/unit/Elsa.Http.UnitTests/Middleware/HttpWorkflowsMiddlewareTests.cs
@@ -4,18 +4,24 @@
 using Elsa.Http.Middleware;
 using Elsa.Http.Options;
 using Elsa.Workflows;
+using Elsa.Workflows.Management;
+using Elsa.Workflows.Management.Entities;
+using Elsa.Workflows.Models;
 using Elsa.Workflows.Runtime;
 using Elsa.Workflows.Runtime.Entities;
 using Elsa.Workflows.Runtime.Filters;
+using Elsa.Workflows.State;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Routing;
 using Microsoft.Extensions.DependencyInjection;
+using NSubstitute;
 
 namespace Elsa.Http.UnitTests.Middleware;
 
 public class HttpWorkflowsMiddlewareTests
 {
-    private static readonly MethodInfo ExecuteWithinTimeoutAsyncMethod = typeof(HttpWorkflowsMiddleware).GetMethod("ExecuteWithinTimeoutAsync", BindingFlags.Instance | BindingFlags.NonPublic)!;
+    private static readonly MethodInfo HandleWorkflowFaultAsyncMethod = GetRequiredPrivateMethod("HandleWorkflowFaultAsync");
+    private static readonly MethodInfo ExecuteWithinTimeoutAsyncMethod = GetRequiredPrivateMethod("ExecuteWithinTimeoutAsync");
     private const string CurrentTenantId = "tenant-a";
     private const string OtherTenantId = "tenant-b";
     private const string BookmarkHash = "http-endpoint:/colliding:get";
@@ -56,6 +62,73 @@ await _middleware.InvokeAsync(
         Assert.False(filter.TenantAgnostic);
     }
 
+    [Fact]
+    public async Task HandleWorkflowFaultAsync_UsesReloadedWorkflowState_WhenAvailable()
+    {
+        var workflowState = CreateFaultedWorkflowState("workflow-1");
+        var reloadedWorkflowState = CreateFaultedWorkflowState(workflowState.Id);
+        var workflowInstanceManager = Substitute.For<IWorkflowInstanceManager>();
+        var httpEndpointFaultHandler = Substitute.For<IHttpEndpointFaultHandler>();
+        var serviceProvider = new ServiceCollection()
+            .AddSingleton(workflowInstanceManager)
+            .AddSingleton(httpEndpointFaultHandler)
+            .BuildServiceProvider();
+        var httpContext = new DefaultHttpContext();
+        var workflowInstance = new WorkflowInstance
+        {
+            Id = workflowState.Id,
+            DefinitionId = workflowState.DefinitionId,
+            DefinitionVersionId = workflowState.DefinitionVersionId,
+            WorkflowState = reloadedWorkflowState
+        };
+
+        workflowInstanceManager.FindByIdAsync(workflowState.Id, Arg.Any<CancellationToken>()).Returns(Task.FromResult<WorkflowInstance?>(workflowInstance));
+
+        var handled = await HandleWorkflowFaultAsync(serviceProvider, httpContext, CreateRunWorkflowResult(workflowState), CancellationToken.None);
+
+        Assert.True(handled);
+        await httpEndpointFaultHandler.Received(1).HandleAsync(Arg.Is<HttpEndpointFaultContext>(context => ReferenceEquals(context.WorkflowState, reloadedWorkflowState)));
+    }
+
+    [Fact]
+    public async Task HandleWorkflowFaultAsync_FallsBackToExecutionResultState_WhenReloadReturnsNull()
+    {
+        var workflowState = CreateFaultedWorkflowState("workflow-2");
+        var workflowInstanceManager = Substitute.For<IWorkflowInstanceManager>();
+        var httpEndpointFaultHandler = Substitute.For<IHttpEndpointFaultHandler>();
+        var serviceProvider = new ServiceCollection()
+            .AddSingleton(workflowInstanceManager)
+            .AddSingleton(httpEndpointFaultHandler)
+            .BuildServiceProvider();
+        var httpContext = new DefaultHttpContext();
+
+        workflowInstanceManager.FindByIdAsync(workflowState.Id, Arg.Any<CancellationToken>()).Returns(Task.FromResult<WorkflowInstance?>(null));
+
+        var handled = await HandleWorkflowFaultAsync(serviceProvider, httpContext, CreateRunWorkflowResult(workflowState), CancellationToken.None);
+
+        Assert.True(handled);
+        await httpEndpointFaultHandler.Received(1).HandleAsync(Arg.Is<HttpEndpointFaultContext>(context => ReferenceEquals(context.WorkflowState, workflowState)));
+    }
+
+    private async Task<bool> HandleWorkflowFaultAsync(IServiceProvider serviceProvider, HttpContext httpContext, RunWorkflowResult workflowExecutionResult, CancellationToken cancellationToken)
+    {
+        var task = (Task<bool>)HandleWorkflowFaultAsyncMethod.Invoke(_middleware, [serviceProvider, httpContext, workflowExecutionResult, cancellationToken])!;
+        return await task;
+    }
+
+    private static RunWorkflowResult CreateRunWorkflowResult(WorkflowState workflowState) => new(default!, workflowState, default!, null, Journal.Empty);
+
+    private static WorkflowState CreateFaultedWorkflowState(string id) => new()
+    {
+        Id = id,
+        DefinitionId = "definition",
+        DefinitionVersionId = "definition-version",
+        Incidents = new List<ActivityIncident>
+        {
+            new("activity", "activity-node", "TestActivity", "Boom", null, DateTimeOffset.UtcNow)
+        }
+    };
+
     [Fact]
     public async Task ExecuteWithinTimeoutAsync_RestoresRequestAbortedAfterSuccess()
     {
@@ -113,6 +186,9 @@ private async Task<T> ExecuteWithinTimeoutAsync<T>(Func<CancellationToken, Task<
         return await task;
     }
 
+    private static MethodInfo GetRequiredPrivateMethod(string name) =>
+        typeof(HttpWorkflowsMiddleware).GetMethod(name, BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new MissingMethodException(typeof(HttpWorkflowsMiddleware).FullName, name);
+
     private static IEnumerable<StoredBookmark> CreateCollidingHttpEndpointBookmarks()
     {
         yield return CreateBookmark("current-tenant-bookmark", CurrentTenantId);