aws / aws/aws-dotnet-messaging

HandlerInvoker.LogError does not include the exception when handler resolution fails

Open
#326 2 comments 0 reactions 1 assignee Claimed by @normj View on GitHub
bug p2 queued xs
Dominant language
C#
Stars
143
Forks
27
Avg merge
1d 18h
Merged PRs (30d)
4

Description

### Description

When `HandlerInvoker` catches a DI resolution failure, the actual exception (with root cause and stack trace) is **never logged** in the SQS poller path. This makes production debugging extremely difficult — we spent significant time tracking down a constructor exception that was completely invisible in Datadog/CloudWatch.

### The full chain

**Step 1 — `HandlerInvoker.InvokeAsync`**: Catches the DI exception, logs a generic message **without the exception object**, then wraps it in `InvalidMessageHandlerSignatureException`:

```csharp
catch (Exception e)
{
_logger.LogError(
"Unable to resolve a handler for {HandlerType} while handling message ID {MessageEnvelopeId}.",
subscriberMapping.HandlerType, messageEnvelope.Id);
// ^^^ no `e` parameter — exception not in the log
throw new InvalidMessageHandlerSignatureException("...", e);
}
```

**Step 2 — `DefaultMessageManager.InvokeHandler`**: Catches `InvalidMessageHandlerSignatureException` and **re-throws without logging**:

```csharp
catch (InvalidMessageHandlerSignatureException)
{
throw; // no logging
}
```

**Step 3 — `DefaultMessageManager.ProcessMessageAsync`**: Has **no catch block**. Exception propagates.

**Step 4 — `SQSMessagePoller.ProcessInStandardMode`**: **Fire-and-forgets the task**:

```csharp
_ = _messageManager.ProcessMessageAsync(result.Envelope, result.Mapping, token);
```

The exception becomes an **unobserved task exception** that .NET silently ignores. The inner exception (the actual root cause) is lost forever.

### What you see in logs

Only this — for every failed message, forever:

> Unable to resolve a handler for {HandlerType} while handling message ID {MessageEnvelopeId}.

No stack trace, no inner exception, no indication of whether it's a missing DI registration, a constructor throwing, a scoping issue, or anything else.

### Expected behavior

At minimum, the `LogError` call in `HandlerInvoker` should include the exception:

```csharp
_logger.LogError(e,
"Unable to resolve a handler for {HandlerType} while handling message ID {MessageEnvelopeId}.",
subscriberMapping.HandlerType, messageEnvelope.Id);
```

Ideally, the fire-and-forget in `SQSMessagePoller` should also handle exceptions from the discarded task (e.g., `ContinueWith` to log).

### Note on Lambda vs SQS paths

In the **Lambda path** (`DefaultLambdaMessageProcessor`), tasks are collected and awaited, and the catch block logs `Exception ex` — so the inner exception IS visible there. This inconsistency means the same handler failure is debuggable in Lambda but invisible in ECS/SQS poller deployments.

### Impact

- There is no stack trace, no inner exception, no indication of what actually failed
- The error message is identical whether the failure is a missing DI registration, a constructor exception, a scoping issue, or anything else
- Operators have to guess or reproduce locally to find the root cause
- This specifically affects the SQS poller path (the most common deployment pattern for long-running workers)

### Additional context

- PR #169 (fixing #166) added the catch + re-throw logic but missed passing the exception to the logger
- `DefaultMessageManager` inconsistently handles this — generic `Exception` is logged with the exception object, but `InvalidMessageHandlerSignatureException` is specifically re-thrown without logging

### Environment

- `AWS.Messaging` version: 1.2.0
- .NET 10.0

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.