Skip to content

Cascade messages returned from OnException middleware (supersedes #3000)#3005

Merged
jeremydmiller merged 2 commits into
mainfrom
feat-3000-onexception-return-cascade
Jun 1, 2026
Merged

Cascade messages returned from OnException middleware (supersedes #3000)#3005
jeremydmiller merged 2 commits into
mainfrom
feat-3000-onexception-return-cascade

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Supersedes #3000 (credit retained: co-authored with @danielwinkler).

What #3000 set out to fix already works on main

Starting from #3000, I reproduced its scenarios as tests on a fresh branch. All of them pass without the PR's change:

  • non-static OnException middleware with constructor injection
  • ILogger constructor injection
  • static middleware with extra injected parameters
  • non-static Before + OnException sharing instance state (added a stronger test asserting the same instance's field set in Before is visible in OnException)

So a test in #3000 cannot fail without #3000's own code, and its BuildConstructorFrame() duplication (author-flagged "keep in sync") addresses a problem that isn't present. HTTP has no gap either — a probe with OnException(CustomHttpException, ILogger<…>) => ProblemDetails (the original ILogger example) passes.

The real, unaddressed gap (fixed here)

A value returned from an OnException method was silently dropped instead of being published as a cascading message — unlike a handler method's return value. Neither main nor #3000 handled this.

MiddlewarePolicy.ApplyExceptionHandling now cascades the OnException return value when no continuation strategy (IResult / HandlerContinuation / HTTP ProblemDetails) claims it. EnqueueCascadingAsync already unwraps OutgoingMessages/IEnumerable, so one path covers every return shape.

Codegen note

The cascade uses a catch-safe frame. CaptureCascadingMessages is a MethodCall whose FindVariables exposes its dependency on the OnException call's return variable, which makes the codegen arranger pre-link the catch frames' Next pointers and collide with TryCatchFinallyFrame's manual chaining ("Frame chain is being re-arranged"). The new CaptureCascadingMessagesInCatch mirrors the HTTP ProblemDetails catch frame: it takes the variable in its ctor but does not yield it from FindVariables. (The pre-existing OutgoingMessages-from-OnException path had the same latent bug, now also covered.)

Tests (kept as regression regardless)

Full wolverine.slnx Release build clean (0 warnings / 0 errors); broader middleware/exception suite green.

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits June 1, 2026 13:21
Ports PR #3000's four OnException tests onto main (WITHOUT its MiddlewarePolicy
implementation) plus a stronger test that sets instance state in Before and reads it
in OnException. All of them PASS on main as-is, so the PR's tests do not reproduce a
gap — the existing OnException convention (commit 85cd239) already supports:
  - non-static middleware OnException with constructor injection (incl. ILogger),
  - additional injected parameters on OnException,
  - shared middleware instance across Before + OnException (instance state preserved).

This branch is an investigation baseline: before adopting #3000 (which also duplicates
BuildBeforeCalls' constructor/disposal logic) we need a scenario that actually fails on
main; none of #3000's scenarios do.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Investigating PR #3000 showed its stated scenarios (non-static/ctor-injected
OnException, ILogger injection, extra injected params, shared Before+OnException
instance state) already work on main — the ported tests pass without the PR's
change. The real, unaddressed gap is that a *value returned* from an OnException
method was silently dropped instead of being published as a cascading message,
unlike a handler method's return value.

ApplyExceptionHandling now cascades the OnException return value (when no
continuation strategy — IResult/HandlerContinuation/HTTP ProblemDetails — claims
it). EnqueueCascadingAsync already unwraps OutgoingMessages/IEnumerable, so one
path covers every return shape.

The cascade must use a catch-safe frame: CaptureCascadingMessages is a MethodCall
whose FindVariables exposes its dependency on the OnException call's return
variable, which makes the codegen arranger pre-link the catch frames' Next
pointers and collide with TryCatchFinallyFrame's manual chaining ("Frame chain is
being re-arranged"). CaptureCascadingMessagesInCatch mirrors the HTTP ProblemDetails
catch frame: it takes the variable in its ctor but does not yield it from
FindVariables.

Regression tests (kept regardless of the PR outcome):
- CoreTests: ported PR #3000 scenarios + a stronger shared-instance-state test +
  static/instance OnException-return-value-cascaded tests.
- HTTP: probe confirming an endpoint OnException with an extra injected parameter
  (ILogger) returning ProblemDetails works.

Supersedes #3000.

Co-Authored-By: Daniel Winkler <dani.winkler@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 7e7d00c into main Jun 1, 2026
24 checks passed
This was referenced Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant