Skip to content

F# codegen: AsyncMode-aware abort + terminal IResult endpoint (GH-2969)#2982

Merged
jeremydmiller merged 1 commit into
mainfrom
feat-2969-fsharp-http-results
May 29, 2026
Merged

F# codegen: AsyncMode-aware abort + terminal IResult endpoint (GH-2969)#2982
jeremydmiller merged 1 commit into
mainfrom
feat-2969-fsharp-http-results

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

A route-bound endpoint whose handler returns IResult compiles to AsyncMode.ReturnFromLastNode — the IResult's ExecuteAsync is the method's returned Task, with no task { } block. In that mode every branch of the body must itself be a Task, so the route-guard's abort can't be () — it must be Task.CompletedTask. This PR makes the abort expression AsyncMode-aware and adds the endpoint that exercises it.

Changes

  • FSharpEmitHelpers.AbortExpression(method)() inside a task { } body or a synchronous-Task method (None, where the machinery appends a trailing CompletedTask), but Task.CompletedTask for ReturnFromLastNode. WriteAbortGuard and ReadHttpFrame's route-value 404 guards now use it instead of a bare ().
  • Fixture: GET /fsharp/result/{id} returns IResult with a route value, exercising the ReturnFromLastNode abort (a missing id yields Task.CompletedTask). The terminal IResult is executed directly — matching the C# path (no MaybeEnd guard for a terminal result).

Generated F#

override this.Handle(httpContext: HttpContext) : Task =
    let id = (httpContext.GetRouteValue("id") :?> string)
    if isNull id then
        httpContext.Response.StatusCode <- 404
        System.Threading.Tasks.Task.CompletedTask   // <- was () before; ReturnFromLastNode needs Task
    else
        let result = thingEndpoints.GetResult(id)
        result.ExecuteAsync(httpContext)

Deferred

MaybeEndWithResultFrame F# emit (the WolverineContinue short-circuit for intermediate IResults) is intentionally not done here: that frame is added by ResultContinuationPolicy during a full MapWolverineEndpoints compile, which the no-host ChainFor fixture harness doesn't run — so it can't be exercised until the behavioural (host-based) harness lands. Documented inline.

Verification

  • Both F# gates pass; GET /fsharp/result/{id} renders + compiles.
  • Full wolverine.slnx Release regression — 0 warnings, 0 errors.

Part of #2969.

🤖 Generated with Claude Code

…H-2969)

A route-bound endpoint whose handler returns IResult is AsyncMode.ReturnFromLastNode
(the IResult's ExecuteAsync IS the method's returned Task, no task { } block). In
that mode every branch of the body must itself be a Task — so the route-guard's
abort can't be `()`, it must be Task.CompletedTask.

- FSharpEmitHelpers.AbortExpression(method): `()` in a task { } body or a
  synchronous-Task method (None, where the machinery appends a trailing
  CompletedTask), but Task.CompletedTask for ReturnFromLastNode. WriteAbortGuard
  and ReadHttpFrame's route-value 404 guards now use it instead of a bare `()`.
- Fixture: GET /fsharp/result/{id} returns IResult with a route value, exercising
  the ReturnFromLastNode abort (a missing id yields Task.CompletedTask). The
  terminal IResult is executed directly (matches the C# path; no MaybeEnd guard).

MaybeEndWithResultFrame F# emit is intentionally deferred: that frame is added by
ResultContinuationPolicy during a full MapWolverineEndpoints compile, which the
no-host ChainFor fixture harness doesn't run — so it can't be exercised until the
behavioural (host-based) harness lands. Documented inline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit c3102a3 into main May 29, 2026
24 checks passed
This was referenced Jun 1, 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