Skip to content

GH-4680: emit F# for the MultipartReader endpoint parameter - #4688

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-4680-fsharp-multipart
Sep 29, 2026
Merged

jeremydmiller merged 1 commit into
mainfrom
gh-4680-fsharp-multipart

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #4680. Follow-up to #4674.

ReadMultipartBody overrode GenerateCode only, so an F# endpoint taking a MultipartReader fell through to Frame's default-throwing seam:

System.NotSupportedException : F# code generation is not supported for the frame 'Wolverine.Http.CodeGen.ReadMultipartBody'.

The contributor disclosed this in the PR and was right about the precedent — FromFileStrategy's frames have no emitter either, so this is not a regression that PR introduced. But JsonHandling does have one, and the new frame is otherwise a faithful copy of the C# JSON shape, so the asymmetry is worth closing.

The one non-obvious part

Qualification, and the issue was right to flag it.

Helper Kind C# emit F# emit
ReadJsonAsync inherited instance bare this.ReadJsonAsync — #393
ReadMultipartAsync static bare Wolverine.Http.HttpHandler.ReadMultipartAsync(...)

So it follows WriteStringFrame, which already qualifies the static WriteString by type, rather than JsonHandling. The C# emit can call either bare only because the generated handler derives from HttpHandler; F# resolves no such thing and a bare call is an FS0039.

Everything else follows JsonHandling: the return is a struct tuple, so F# needs let! struct (a, b) =, and since F# has no early return, the abort guard renders the rest of the chain inside its else via FSharpEmitHelpers.WriteAbortGuard.

Emitted output:

// Stream the multipart request body without buffering the form
let! struct (reader, multipartContinue) = Wolverine.Http.HttpHandler.ReadMultipartAsync(httpContext)
if multipartContinue = Wolverine.HandlerContinuation.Stop then
    ()
else
    ...

Proven by compilation, not string matching

A MultipartReader endpoint joins Wolverine.Http.FSharpContracts, and its chain joins HttpFSharpCodegenSample, so the existing compile gate regenerates Generated.fs and shells dotnet build over the F# fixture. That is the acceptance test; the string assertion in multipart_reader_qualifies_its_static_helper_by_type just pins why it compiles, since getting the qualification wrong is the failure mode that would otherwise be caught only by a confusing compiler error.

Reverting only the emitter fails 5 of 6 tests with the NotSupportedException above — which also proves the new endpoint genuinely reaches this frame rather than the test passing vacuously.

dotnet build wolverine.slnx -c Release -f net9.0: 0 warnings / 0 errors.

Note on the alternative

The issue floats a startup assertion — "this parameter is not supported in F# endpoints" — as possibly the better first step. That is still worth doing for the frames that have no emitter, and it belongs with #4681 where the remaining bucket is managed; it just isn't needed for this frame any more.

Merge order with #4681

#4681 commits a baseline of the frames still lacking an emitter, and ReadMultipartBody is currently in it. Whichever of the two merges second needs that one line updated — I'll handle it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj

Follow-up to #4674. ReadMultipartBody overrode GenerateCode only, so an F#
endpoint taking a MultipartReader fell through to Frame's default-throwing seam:
"F# code generation is not supported for the frame 'ReadMultipartBody'".

The contributor disclosed this and was right about the precedent -- FromFileStrategy's
frames have no emitter either, so it is not a regression from that PR. But the
frame is otherwise a faithful copy of the C# JSON shape, and JsonHandling DOES
have an emitter, so the asymmetry is worth closing.

The one non-obvious part is the qualification. ReadJsonAsync is an inherited
INSTANCE method, so the F# emitter writes `this.ReadJsonAsync` (#393).
ReadMultipartAsync is static, so it is qualified by TYPE instead --
`Wolverine.Http.HttpHandler.ReadMultipartAsync(...)` -- exactly as
WriteStringFrame already does for the static WriteString. The C# emit can call
either one bare only because the generated handler derives from HttpHandler;
F# resolves no such thing and a bare call is an FS0039.

Everything else follows JsonHandling: the return is a struct tuple, so F# needs
`let! struct (a, b) =`, and since F# has no early `return` the abort guard
renders the rest of the chain in its `else` via FSharpEmitHelpers.WriteAbortGuard.

Proven by compilation, not by string matching: a MultipartReader endpoint joins
Wolverine.Http.FSharpContracts and its chain joins the codegen sample, so the
existing compile gate regenerates Generated.fs and runs dotnet build over it.
Reverting only the emitter fails 5 of the 6 tests with the NotSupportedException
above, which also proves the new endpoint actually reaches this frame.

Does not change the fsharp-coverage total's story -- see #4681, where the
ratchet that would have caught this frame arriving unemitted lives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj
@jeremydmiller
jeremydmiller merged commit c8ec2d2 into main Sep 29, 2026
45 checks passed
jeremydmiller added a commit that referenced this pull request Sep 29, 2026
jeremydmiller added a commit that referenced this pull request Sep 29, 2026
ReadMultipartBody has an F# emitter as of #4680/#4688, so it leaves the
remaining bucket: 54 -> 53.

This is the ratchet doing exactly what it is for. Merging origin/main made the
gate fail on its own terms, naming the frame and saying to strike it from the
list rather than leaving a count mismatch to decipher.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VDUrBeB4tTnKj4AExCS1nj
@jeremydmiller

Copy link
Copy Markdown
Member Author

Released in Wolverine 6.42.0.

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.

F# code generation for the MultipartReader endpoint parameter

1 participant