Skip to content

Wolverine.HTTP: opt-in 409 ProblemDetails mapping for ConcurrencyException and StreamLockedException #4512

Closed

Description

@jeremydmiller

Ask

Ship an opt-in, out-of-the-box mapping in Wolverine.HTTP from the Critter Stack concurrency failures to an HTTP 409 ProblemDetails response, so users do not have to hand-write the middleware in docs/guide/http/exception-handling.md for every service.

Today

Nothing in Wolverine.Http maps these exceptions to a status code. An unhandled ConcurrencyException from a Marten, Polecat or Fisher session, or a StreamLockedException from FetchForExclusiveWriting, escapes as a 500. The documented workaround is a user-authored middleware class with two OnException overloads plus opts.AddMiddleware(typeof(MartenConcurrencyExceptionMiddleware), chain => chain.IsTransactional), and an IHttpPolicy to advertise 409 in OpenAPI. That is four pieces the user has to know exist and get right, and the doc itself has to warn that catching only ConcurrencyException misses StreamLockedException (which does not derive from it in Marten or Polecat).

Proposed shape

Something like:

app.MapWolverineEndpoints(opts =>
{
    opts.MapConcurrencyFailuresTo409(); // name TBD
});

that:

  • catches JasperFx.ConcurrencyException (covers EventStreamUnexpectedMaxEventIdException, DcbConcurrencyException, document revision/version failures, and SagaConcurrencyException) and returns ProblemDetails { Status = 409, Title = "Conflict", Detail = ex.Message };
  • also catches the store-specific lock types that are not ConcurrencyExceptions: Marten.Exceptions.StreamLockedException and Polecat.Exceptions.StreamLockedException (Fisher has none, its exclusive methods are optimistic and throw EventStreamUnexpectedMaxEventIdException);
  • applies only to transactional chains by default, with the same predicate hook AddMiddleware takes;
  • stamps ProducesProblem(409) metadata on those chains so the OpenAPI document advertises it.

Whether ExistingStreamIdCollisionException belongs in the same mapping is a judgement call (it is a conflict, but usually a client bug rather than a race); it is reasonable to leave it out of the default and let the predicate/overloads add it.

Why now

A client has asked for this. It is also the kind of thing an AI-agent skill can only recommend as a copy-paste recipe today, and a one-line opt-in is a much better answer.

Activity

  1. added a commit that references this issue on Sep 23, 2026
  2. added a commit that references this issue on Sep 23, 2026
  3. added a commit that references this issue on Sep 24, 2026
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