Skip to content

[DeduplicatedWithResponse] replays ASP.NET Core IResult responses - #5352

Merged
jeremydmiller merged 8 commits into
JasperFx:mainfrom
esond:dedup-response-iresult
Nov 5, 2026
Merged

jeremydmiller merged 8 commits into
JasperFx:mainfrom
esond:dedup-response-iresult

Conversation

@esond

@esond esond commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #4742. [DeduplicatedWithResponse] refuses endpoints that return an ASP.NET Core IResult, so anything written with TypedResults can't use it. With this change, Ok, Created, Accepted (with or without a value), NoContent, StatusCodeHttpResult and Results.Empty are stored and replayed with their status, JSON body and Location, in both claim-and-release and ClaimInTransaction. A Results<...> union replays by the arm it answered with.

Start with CompleteDeduplicatedRequest in HttpHandler.cs, then the startup check in HttpChain.DeduplicatedWithResponse.cs.

Behavior

  • A 4xx/5xx arm, a challenge or a forbid releases the key, as failures do today.
  • Other results (text, files, redirects, JsonHttpResult, CreatedAtRoute) aren't stored. The claim is kept, repeats get 409, and an error is logged.
  • A declared return type that can never be replayed is refused at startup. A bare IResult or a union with a replayable arm is decided per request.

Design

Following the #4742 review and what shipped: the stored record stays JSON-only (DeduplicatedResponse(StatusCode, Body, Location)), declaration defects fail at startup, and no public API changes.

  • The response is read off the result before it executes, not captured from ExecuteAsync, because that runs after the commit and both claim modes record before it.
  • Replayable results are an allow-list. Interfaces alone get it wrong: ServerSentEventsResult<T> only exposes a status, and CreatedAtRoute computes Location while executing.
  • The body is serialized with ASP.NET Core's JsonOptions and the same type rule as HttpResultsHelper.WriteResultAsJsonAsync, so a replay matches the original bytes.

Known difference: Ok<ProblemDetails> replays as application/json rather than application/problem+json. I left it, since problem details in a success response is not a path endorsed by yours truly.

Tests

New cases in deduplicated_with_response, deduplicated_with_response_declaration and the Marten suite cover each replayed shape, union success and failure arms, a non-replayable text result, startup refusals, and ClaimInTransaction. All 129 *deduplicat* tests pass.

esond added 6 commits November 4, 2026 21:43
An endpoint that returns an IResult was refused at startup, because its writer is IResult.ExecuteAsync
rather than WriteJsonFrame. CompleteDeduplicatedRequest runs before that call, so the response is now
described from the result itself: Results<...> unions are unwrapped, the status comes from
IStatusCodeHttpResult, the body is the IValueHttpResult value serialized as ASP.NET Core's
HttpResultsHelper writes it (its own JsonOptions, the declared TValue unless the runtime type differs and
TValue has no known polymorphism), and the Location comes from Created and Accepted.

Only Ok, Created, Accepted (with or without a value), NoContent, StatusCodeHttpResult and EmptyHttpResult
are stored. A status of 400 or above, a challenge or a forbid gives the claim back as any failure does. Any
other result is done work whose response cannot be stored: the claim is kept unanswered, so repeats get
409, and an error names the result type. An endpoint declared to return only results that can never be
stored is refused at startup, from the same classification.
Created<T>, Ok<T> with a derived value, NoContent and both arms of a Results<Created<T>, NotFound> union on
claim-and-release; a text result that is kept unanswered; the same union claimed inside the Marten
transaction; and the startup refusal of a file result next to an accepted union.
- Replay a Location the endpoint set on the response when the result has none of its own
- Refuse at startup an IResult interface that no replayable result implements
- Recognize the IResult writer by the resource type rather than the writer frame's shape
- Resolve the logger only on the error paths
- Name every replayable result in the startup refusal
- Test Accepted with a Location, and keep the union's failure arm from doing work in the Marten test

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a995dc2da

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Http/Wolverine.Http/HttpHandler.cs Resolved
Comment thread src/Http/Wolverine.Http/HttpHandler.cs Resolved
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 5, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-05T04:54:16.123373Z c2904da New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

…nly Results unions

- An endpoint declared as object that returns a typed result is written as JSON by the resource writer, so its
  response is stored the same way rather than as the result's status and value
- Only ASP.NET Core's Results<...> unions are unwrapped. A custom INestedHttpResult may write more than the result it
  holds, so it is refused at startup when declared and kept unanswered when returned

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 080d5e09ab

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

A custom IResourceWriterPolicy runs ahead of ResultWriterPolicy and may write an IResult some other way, so the
JSON-only check looks for the IResult.ExecuteAsync writer again rather than the resource type
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.

2 participants