Skip to content

An OnException middleware that returns a message type the application also handles generates uncompilable code (CS0136), silently killing that handler #4139

Description

@jeremydmiller

Found while working #4125 — this was hidden behind a green test.

Repro

An OnException middleware whose return value is cascaded, where the returned type is also handled by the application:

public record CollidingCascade(string Text);

public static class CollidingCascadeHandler
{
    public static void Handle(CollidingCascade message, CollisionRecorder recorder) { ... }
}

public static class CollidingCascadeMiddleware
{
    public static CollidingCascade OnException(CollisionException ex) => new($"cascaded:{ex.Message}");
}

The generated handler for CollidingCascade fails to compile:

CS0136: A local or parameter named 'collidingCascade' cannot be declared in this scope
        because that name is used in an enclosing local scope to define a local or parameter
var collidingCascade = (CollidingCascade)context.Envelope.Message;   // message body
try { ... }
catch (CollisionException e)
{
    var collidingCascade = middleware.OnException(e);                // <-- collision
    await context.EnqueueCascadingAsync(collidingCascade);
}

Cause

The OnException MethodCall's ReturnVariable takes its default name from its type, and so does the chain's message body variable. The catch block is emitted inside the scope that declares the body variable, so when a chain's input type equals an OnException return type the two locals nest and collide.

Both construction sites are affected: MiddlewarePolicy.Application.BuildOnExceptionCalls (middleware types) and the handler-type OnException loop in Chain.cs.

Why it went unnoticed

The compile failure happens at runtime, inside the handler pipeline, so it surfaces as an exception on the cascaded envelope — the message is still sent, only never executed.

CoreTests.Acceptance.on_exception_convention covers exactly this feature and was green, because both of its tests assert on session.Sent.SingleEnvelope<CascadedFromOnException>() and recorder.Actions from the first handler. Neither requires CascadedFromOnExceptionHandler to run. On top of that, both use DoNotAssertOnExceptionsDetected(), which until #4125 also suppressed the session timeout — so the failure was invisible from two directions at once.

A probe asserting the cascaded message is actually handled fails on main.

Fix

Give the OnException return variable a name that cannot collide with the enclosing scope (onException + the default name), applied at both construction sites.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions