Skip to content

fix: reuse cached form in consumeForm (multipart + antiforgery)#154

Merged
pimbrouwers merged 2 commits intofalcoframework:masterfrom
michaelglass:fix-multipart-form-after-antiforgery
Apr 24, 2026
Merged

fix: reuse cached form in consumeForm (multipart + antiforgery)#154
pimbrouwers merged 2 commits intofalcoframework:masterfrom
michaelglass:fix-multipart-form-after-antiforgery

Conversation

@michaelglass
Copy link
Copy Markdown
Contributor

When antiforgery validation runs before Request.getForm (the flow getFormSecure uses internally), IAntiforgery.ValidateRequestAsync reads and buffers the form body via ReadFormAsync. For multipart/form-data, consumeForm would then take the StreamFormAsync branch and re-read the now-drained stream, failing with IOException: Unexpected end of Stream. Non-multipart was fine because ReadFormAsync returns the cached HttpRequest.Form.

Fix: detect the already-parsed form via IFormFeature.Form and reuse it before deciding between streaming and non-streaming read.

Regression test: AntiforgeryMultipartTests.POST multipart/form-data with valid CSRF token succeeds via getForm — asserted to fail on upstream/master with the IOException, passes with the fix. A second guard test covers the urlencoded path so it can't regress in the other direction.

…-read bug

When antiforgery validation runs before Request.getForm (e.g. via
getFormOptions / mapForm / validateCsrfToken), IAntiforgery.ValidateRequestAsync
reads and buffers the form body via ReadFormAsync. For multipart/form-data
POSTs, consumeForm would then branch into StreamFormAsync, which re-reads
the (now-drained) body and fails with 'Unexpected end of Stream'.

Detect that the form has already been parsed by checking the request's
IFormFeature and reuse the cached ctx.Request.Form in that case, instead
of attempting to re-read the stream.
@pimbrouwers pimbrouwers added the bug Something isn't working label Apr 24, 2026
@pimbrouwers
Copy link
Copy Markdown
Member

Amazing work. Thank you.

@pimbrouwers
Copy link
Copy Markdown
Member

It looks like something in the integration tests is causing the build action to stall.

Have a look and let me know what you think.

@michaelglass
Copy link
Copy Markdown
Contributor Author

taking another look myself 🤦

Top-level module statements (`let bldr = CreateBuilder()` / `let wapp =
bldr.Build()` / `wapp.Run()`) compile into one-shot static initialization.
When a second WebApplicationFactory (e.g. the derived factory in
AntiforgeryMultipartTests via WithWebHostBuilder) re-invokes the entry
point to spin up its own test host, DeferredHostBuilder.Build() runs
against the already-built builder and throws
"InvalidOperationException: Build can only be called once" — reliably on
CI's parallel collections, intermittently elsewhere.

Move construction into an explicit [<EntryPoint>] main so each factory
invocation gets a fresh builder and WebApplication.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@pimbrouwers pimbrouwers added the v6 label Apr 24, 2026
@pimbrouwers
Copy link
Copy Markdown
Member

Honestly, don't sweat it. Thanks for looking so quickly. I truly appreciate your effort. It's been a while since I had any technical discussions with anyone about this project, and I really enjoyed doing that with you. I hope we can continue working together when time permits!

@pimbrouwers pimbrouwers merged commit f234447 into falcoframework:master Apr 24, 2026
1 check passed
@michaelglass michaelglass deleted the fix-multipart-form-after-antiforgery branch April 24, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working v6

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants