Skip to content

fix(beam): read request headers from Cowboy instead of returning empty - #80

Merged
dbrattli merged 1 commit into
mainfrom
fix/beam-request-headers
Aug 6, 2026
Merged

fix(beam): read request headers from Cowboy instead of returning empty#80
dbrattli merged 1 commit into
mainfrom
fix/beam-request-headers

Conversation

@dbrattli

@dbrattli dbrattli commented Aug 6, 2026

Copy link
Copy Markdown
Owner

The gap

On the BEAM target both request-header accessors were placeholders:

member x.GetTypedHeaders() : RequestHeaders = RequestHeaders(ResizeArray())  // always empty
member x.Headers = HeaderDictionary()                                        // always empty

So mustAccept and content negotiation could never resolve, and no handler could read a request header at all. An endpoint gating on Authorization had to reach past Giraffe to cowboy_req:header/3 itself. Tracked in FOLLOWUPS.md; the JS and Python backends both read their real header source.

The binding was never missing — Fable.Beam.Cowboy.CowboyReq.headers has been there all along. Giraffe just wasn't calling it.

The fix

src/beam/HttpContext.fs — both accessors read cowboy_req:headers/1 through a shared private HeaderPairs. Cowboy is already the normalising layer (lowercase names, duplicates folded into one comma-joined value), so this is a map read rather than a reimplementation of header semantics. The two-element rows it yields match the shape the Python/ASGI backend builds from scope["headers"], so mustAccept behaves identically across targets. Explicit for loops rather than Seq.map, per the Ref-vs-list caveat the file already documents.

test/beam/TestContext.fs — the other half of the gap, and the reason the suite couldn't have caught this: it accepted a ?headers argument and silently dropped it, while the JS and Python factories both wired theirs in. It now seeds them into the fake Req map under headers, lowercased on the way in to match what Cowboy hands a real handler.

test/shared/HandlerTests.fs — un-skips the three Accept-header cases on BEAM, and adds two tests covering reading an arbitrary header (Authorization, X-Request-Id) and case-insensitive lookup. Those two are the case Accept-only coverage wouldn't have pinned.

Test results

Target Before After
BEAM 94 passed / 7 skipped 99 passed / 4 skipped
JS 100 passed / 1 skipped 102 passed / 1 skipped
Python 101 passed 103 passed

The 4 remaining BEAM skips are the unrelated routef typed-capture divergences (%i / %O / %u), still tracked in FOLLOWUPS.md. Fantomas clean.

🤖 Generated with Claude Code

`HttpRequest.GetTypedHeaders()` returned an empty `RequestHeaders` and
`HttpRequest.Headers` an empty `HeaderDictionary`, so on the BEAM target
`mustAccept` and content negotiation could never resolve, and no handler
could read a request header at all -- an endpoint gating on `Authorization`
had to reach past Giraffe to `cowboy_req:header/3` itself.

The binding was never missing: `Fable.Beam.Cowboy.CowboyReq.headers` has
been there all along. Both accessors now read it through a shared
`HeaderPairs`. Cowboy is already the normalising layer -- lowercase names,
duplicates folded into one comma-joined value -- so this is a map read
rather than a reimplementation of header semantics, and the two-element
rows it yields match the shape the Python/ASGI backend builds from
`scope["headers"]`, keeping `mustAccept` identical across targets.

The BEAM `TestContext` was the other half of the gap, and the reason the
suite could not have caught this: it accepted a `?headers` argument and
silently dropped it, while the JS and Python factories both wired theirs
in. It now seeds them into the fake Req map, lowercased on the way in to
match what Cowboy hands a real handler.

The three Accept-header cases are un-skipped on BEAM, and two tests cover
reading an arbitrary header and case-insensitive lookup -- the case
Accept-only coverage would not have pinned. BEAM 99 passed / 4 skipped
(the remaining skips are the unrelated `routef` typed-capture
divergences), JS 102/1, Python 103/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dbrattli
dbrattli merged commit 30c29ca into main Aug 6, 2026
3 checks passed
@dbrattli
dbrattli deleted the fix/beam-request-headers branch August 6, 2026 06:12
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