Skip to content

fix: forward caller-supplied Authorization header in H1 and H2c paths#3

Merged
ehsanmok merged 4 commits into
ehsanmok:mainfrom
zacharyvmm:main
Jun 15, 2026
Merged

fix: forward caller-supplied Authorization header in H1 and H2c paths#3
ehsanmok merged 4 commits into
ehsanmok:mainfrom
zacharyvmm:main

Conversation

@zacharyvmm

Copy link
Copy Markdown
Contributor

Problem

HttpClient._do_request() and _send_h2c_via_upgrade() silently drop the Authorization header when callers set it via req.headers.set("Authorization", ...). The header never reaches the wire.

This blocks legitimate per-request auth patterns:

  • AWS Signature V4 — signature is computed per-request from body, headers, and timestamp
  • OAuth2 short-lived tokens — refresh frequently, differ per endpoint
  • HMAC request signing — digest covers request body, must be set just before sending

Root cause

Two methods unconditionally filtered authorization from forwarded caller headers, expecting auth to always flow through HttpClient._auth_header:

  1. _do_request() line 713 — the main HTTP/1.1 path
  2. _send_h2c_via_upgrade() line 1854 — the H2C upgrade path

Fix

H1 path: Remove authorization from the skip condition entirely. When _auth_header is empty (the default), callers must be able to set Authorization via req.headers.

H2c path: Only skip caller's Authorization when _auth_header is already set — matching the existing pattern in _build_h2_request_headers() (line 1619-1623).

Both paths remain compatible with BasicAuth/BearerAuth: those set _auth_header which is still emitted on line 707-708 (H1) and 1847-1848 (H2c).

Testing

Reproduced against moto S3 mock server. Before the fix, any request with a per-request Authorization header returns 403 (header absent from wire). After the fix, the header reaches the server and requests succeed.

@ehsanmok ehsanmok left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks! Could you add a test for this please?

@ehsanmok

Copy link
Copy Markdown
Owner

Looks like CI failure is known flake.

@zacharyvmm

Copy link
Copy Markdown
Contributor Author

Updated PR pushed — addressed your two points:

  1. Tightened the H1 change to match the h2c/h2 guard: only skip the caller's Authorization when _auth_header is already set. No more duplicate headers when both a client credential and a caller header are present.

  2. Added three regression tests in tests/http/test_unified_http_client.mojo:

    • caller Authorization reaches the wire on H1 with no client credential
    • caller Authorization reaches the wire on H2c upgrade with no client credential
    • no duplicate Authorization when client credential and caller header are both set

Note: I could not run the tests locally because the flare test suite has pre-existing nightly incompatibilities (UnsafePointer[UInt8](unsafe_from_address=0) is no longer valid in 1.0.0b2 — needs Optional[UnsafePointer]). I fixed the one in flare/net/_libc.mojo that blocked the h1 path, but the other three (in ws/server.mojo, runtime/blocking.mojo, runtime/scheduler.mojo) are deeper refactors outside this PR's scope. CI will exercise them in their own environment.

The test names follow the existing convention (fork_server loopback against an in-process HttpServer, same as the bearer-auth tests above).

@ehsanmok

Copy link
Copy Markdown
Owner

Sounds good. I'll verify. Will update b2 once released in stable.

triesap pushed a commit to triesap/mojo_flare that referenced this pull request Jun 14, 2026
GitHub Actions' ``ubuntu-latest`` image moved from Ubuntu 22.04 to
24.04, and the April 24 Mojo nightly
(``mojo 1.0.0b1.dev2026042405``) crashes immediately inside its
own runtime on 24.04. The first test of the suite,
``tests/test_net.mojo``, prints its banner and then segfaults
before any user code runs:

    test_net.mojo — IpAddr, SocketAddr, error types
    ============================================================
    #0 0x... libKGENCompilerRTShared.so+0x6f0eb
    ehsanmok#1 0x... libKGENCompilerRTShared.so+0x6c2c6
    ehsanmok#2 0x... libKGENCompilerRTShared.so+0x6fd97
    ehsanmok#3 0x... libc.so.6+0x45330
    #4 0x...
    mojo: error: execution crashed
    Process completed with exit code 1.

The crash reproduces 100% on ``ubuntu-latest`` (``Image: ubuntu-24.04``,
``Image Release: ubuntu24/20260413.86``) on Mojo 1.0.0b1.dev2026042405
and is independent of flare's source: the same compiler build runs
clean on Ubuntu 22.04 (this repo's dev box, glibc 2.35) and on
macOS 14. It's a Mojo runtime / glibc-2.39 ABI issue.

Workaround: pin both Linux jobs to ``ubuntu-22.04`` so they keep
running while Modular re-cuts a 24.04-clean nightly:

- ``.github/workflows/ci.yml`` (the ``Tests`` matrix).
- ``.github/workflows/fuzz.yml`` (the nightly fuzz matrix —
  same ``mojo`` runtime, would hit the same segfault as soon
  as the cron fires).

Docs CI (``.github/workflows/docs.yaml``) stays on
``ubuntu-latest`` because mojodoc is compile-only and doesn't
exercise the runtime; once the previous commit's source fix lands
the docs build is healthy on 24.04.

When upstream ships a 24.04-clean nightly, revert this commit
(or just ``-os: [ubuntu-22.04, macos-14]`` -> ``ubuntu-latest``)
and a green nightly CI run will confirm.

Made-with: Cursor
Co-authored-by: Cursor <cursoragent@cursor.com>
triesap pushed a commit to triesap/mojo_flare that referenced this pull request Jun 14, 2026
…2405 segfault)

The April 24 ``mojo 1.0.0b1.dev2026042405`` nightly crashes 100%
inside ``libKGENCompilerRTShared.so`` on every Linux GitHub Actions
runner before any user code runs. The very first test of the
suite (``tests/test_net.mojo``) prints its banner and then SIGSEGVs
in the Mojo runtime:

    test_net.mojo — IpAddr, SocketAddr, error types
    ============================================================
    #0 0x... libKGENCompilerRTShared.so+0x6f0eb
    ehsanmok#1 0x... libKGENCompilerRTShared.so+0x6c2c6
    ehsanmok#2 0x... libKGENCompilerRTShared.so+0x6fd97
    ehsanmok#3 0x... libc.so.6+0x45330
    mojo: error: execution crashed
    Process completed with exit code 1.

Reproduced on both ``ubuntu-latest`` (Ubuntu 24.04, which is what
``ubuntu-latest`` resolves to right now) AND ``ubuntu-22.04`` —
ruling out the OS-pin workaround that ``5e28b84`` was attempting.
The previous nightly ``mojo 1.0.0b1.dev2026042317`` ran the same
code-base green (the cron-triggered CI run at 2026-04-24T03:00 on
commit ``6ea8a05`` proves that), but pixi-build resolves
``mojo-compiler`` for the inline ``json`` build separately from
flare's runtime ``mojo`` and grabs the latest available — so even
pinning flare's ``mojo`` to ``1.0.0b1.dev2026042317`` leaves
``json.mojopkg`` built against the broken nightly, and the loader
then refuses it with

    Mojo package is incompatible: package
    '/home/.../json.mojopkg' version 26.3.0.dev2026042405 is
    newer than compiler version 26.3.0.dev2026042317

The right fix is the same one ``v0.4.0`` already shipped: pin
flare AND json AND ``recipe.yaml`` to the last known-good
combination, which is

    mojo            = "==0.26.3.0.dev2026042005"
    json            = { git = ..., tag = "v0.1.3" }
    mojo-compiler   = "==0.26.3.0.dev2026042005"  (in recipe.yaml)

``json v0.1.3``'s own recipe also pins ``mojo-compiler`` to
``0.26.3.0.dev2026042005`` (verified via ``gh api
repos/ehsanmok/json/contents/recipe.yaml?ref=v0.1.3``), so the
build env that pixi-build solves for ``json`` ends up identical
to flare's runtime env. CI of the
``Release v0.4.0: pin mojo to 0.26.3.0.dev2026042005, json to
v0.1.3`` commit (``e438400``) was green on
``ubuntu-latest`` + ``macos-14`` with this exact triple.

Verified locally
----------------

- ``pixi run format-check``: clean.
- ``pixi run tests``: 26/26 test files green, 460 + 18 examples
  pass, total 5m14s.
- ``pixi run -e dev mojodoc ./flare --out-dir /tmp/doc-out``:
  documentation builds in 6s, no errors.
- json package on disk now reports
  ``...dev2026042005`` (rather than ``...dev2026042405``);
  ``flare.http.HttpServer.bind(...)`` round-trips a port.

Revert this commit once Modular re-cuts a clean nightly and
flare's ``v0.4.2`` / next-release commit re-unpins mojo + json
the way ``3c96790`` did for v0.3.0.

Made-with: Cursor
Co-authored-by: Cursor <cursoragent@cursor.com>
triesap pushed a commit to triesap/mojo_flare that referenced this pull request Jun 14, 2026
… crash)

The nightly Fuzz workflow's ``ubuntu-latest`` job has been
crashing 100% on the Linux runner for the last two scheduled
runs (#64 / #65, 2026-04-26 / 2026-04-27 02:00 UTC), with the
documented Mojo-runtime-on-Ubuntu-24.04 signature:

    ✨ Pixi task (fuzz-ws in fuzz): mojo -I . fuzz/fuzz_ws_frame.mojo
    [mozz] fuzzing WsFrame.decode_one()...
    #0 0x... libKGENCompilerRTShared.so+0x6dc0b
    ehsanmok#1 0x... libKGENCompilerRTShared.so+0x6ade6
    ehsanmok#2 0x... libKGENCompilerRTShared.so+0x6e8b7
    ehsanmok#3 0x... libc.so.6+0x45330
    mojo: error: execution crashed

Same shape as the ``5e28b84`` (``ci: pin Linux runner to
ubuntu-22.04``) crash that was reverted in ``e9ae53e`` once
``ci.yml``'s test workload returned to ``ubuntu-latest``-clean.
The fuzzer's hot loop -- millions of small ``WsFrame.decode_one``
calls inside mozz's runner -- still trips the same
``libKGENCompilerRTShared.so`` -> ``libc.so.6+0x45330`` path on
24.04 / glibc 2.39 even though the unit-test workload no longer
does. macOS-14 keeps passing on the same nightly + the same
harness; this is an Ubuntu-24.04-only defect.

This commit:

- ``.github/workflows/fuzz.yml``: change matrix from
  ``[ubuntu-latest, macos-14]`` to ``[ubuntu-22.04, macos-14]``.
  Adds an in-place comment explaining the crash signature, the
  asymmetric scope (fuzz only, ci.yml stays on ubuntu-latest),
  and the revert condition (next 24.04-clean Mojo nightly).

We do NOT pin ``ci.yml``'s Linux runner here -- the test
workload runs cleanly on ubuntu-latest with the cert-path fix
in the previous commits, and pinning it would be premature.

Tests:

- macOS-14 leg of Fuzz already green on the same nightly, so the
  crash is genuinely runner-shape-specific.
- ubuntu-22.04 ran the full fuzz suite green for #59-#62 before
  the runner-pin revert; the fuzz harness itself hasn't changed.

Co-authored-by: Cursor <cursoragent@cursor.com>
triesap pushed a commit to triesap/mojo_flare that referenced this pull request Jun 14, 2026
…rash on 24.04)

Follow-up to ``ci: re-pin Linux fuzz runner to ubuntu-22.04
(libKGENCompilerRTShared crash)``: the same Mojo-runtime-on-
Ubuntu-24.04 crash that hit fuzz now also hits ``ci.yml``.

The first push after the fuzz-only pin (run #25011238337,
commit ``2ddfd33``) reproduced the crash on ``ubuntu-latest``
(Image: ubuntu-24.04, Image Release rolled forward overnight)
during ``pixi run tests`` -- ``tests/test_net.mojo`` segfaults
before any user code runs:

    test_net.mojo — IpAddr, SocketAddr, error types
    ============================================================
    #0 0x... libKGENCompilerRTShared.so+0x6dc0b
    ehsanmok#1 0x... libKGENCompilerRTShared.so+0x6ade6
    ehsanmok#2 0x... libKGENCompilerRTShared.so+0x6e8b7
    ehsanmok#3 0x... libc.so.6+0x45330
    #4 0x... 7f854c0102fe
    mojo: error: execution crashed
    Process completed with exit code 1.

Per-frame offsets are byte-identical to the fuzz crash signature
documented in the previous commit -- same Mojo runtime defect,
just exposed by a different workload (the test suite's startup
path rather than the fuzzer's hot loop). The previous commit's
narrower scope was wrong: pinning only fuzz.yml doesn't cover
ci.yml because the GH-Actions ``ubuntu-latest`` image had drifted
between the prior successful run (CI #108, head ``838e205``,
2026-04-26 03:00 UTC) and today's push, taking the test workload
into the crashing region too.

Macos-14 keeps passing on the exact same Mojo nightly + the exact
same test source -- this is a Linux-runtime / glibc-2.39 ABI
issue, not a flare-side regression. The ``test_tls_acceptor``
fix (``fix(tls): drop hardcoded /Users/ehsan paths...``) and
the cert-generation step (``ci: generate self-signed cert
before Tests step``) still hold; they were confirmed clean on
macos-14 in this same run before the Linux job aborted.

What changes:

- ``.github/workflows/ci.yml``: matrix swaps from
  ``[ubuntu-latest, macos-14]`` to ``[ubuntu-22.04, macos-14]``.
  Adds an in-line comment with the crash signature and the
  revert condition, mirroring the comment block fuzz.yml now
  carries.

The docs workflow stays on ``ubuntu-latest`` because mojodoc is
compile-only -- it doesn't exercise the Mojo runtime and was
green on the same head in this same set of pushes.

Co-authored-by: Cursor <cursoragent@cursor.com>
triesap pushed a commit to triesap/mojo_flare that referenced this pull request Jun 14, 2026
…ump CI runner to macos-15

The macOS-14 CI lane was crashing inside ``libKGENCompilerRTShared.dylib``
on the very first call to ``_brotli_available()`` and again under
``compress_brotli`` / ``decompress_brotli``. Same per-frame offsets every
run (#0 .dylib+0x40528, ehsanmok#1 .dylib+0x3dba8, ehsanmok#2 .dylib+0x40f84, ehsanmok#3
libsystem_platform.dylib+0x1804e5764), reproduces 3/3 locally on
``Darwin arm64 25.3.0`` / Mojo nightly ``1.0.0b1.dev2026042717``. Linux
happens to be lazier about reclaiming the local on this nightly, so the
bug only fires on macOS — but it's a real lifetime bug in flare, not a
Mojo runtime flake the way the prior advisory commits framed it.

Root cause: the ``_file_exists`` helper in ``flare/http/middleware.mojo``
and the brotli compress/decompress paths in ``flare/http/encoding.mojo``
all opened a function-local ``OwnedDLHandle``, fetched a function pointer
via ``lib.get_function[def(...) thin abi("C") -> c_int](...)``, then
called the pointer without ever referencing ``lib`` again. Mojo's
ASAP-destruction rule lets the runtime drop the handle right after the
last read; the destructor calls ``dlclose``, the dylib is unmapped, and
the cached function pointer dangles into freed memory by the time we
invoke it. The next call segfaults inside the runtime call helper.

Fix: route every FFI call through a helper that takes the handle as
``read lib: OwnedDLHandle`` (borrow), so the dylib's lifetime is anchored
to the outer scope where ``lib`` is owned. This is the same idiom the
zlib path already uses (``_do_compress``, ``_do_decompress``,
``_do_decompress_deflate``); brotli and ``_file_exists`` were the two
spots that hadn't been ported.

- ``flare/http/middleware.mojo``: introduce ``_flare_fs_access(read lib,
  addr)`` and call it from ``_file_exists``. Comments document the
  ASAP-destruction trap so the next contributor doesn't reintroduce it.
- ``flare/http/encoding.mojo``: introduce ``_do_compress_brotli`` and
  ``_do_decompress_brotli`` with borrowed handles; the public
  ``compress_brotli`` / ``decompress_brotli`` open the handle and
  delegate while it's still in scope.

CI: ``.github/workflows/ci.yml`` switches the macOS leg from
``macos-14`` to ``macos-15`` (Sequoia, arm64) so the runner matches the
host SDK we develop against. The advisory + 3-attempt retry workarounds
are gone (they were never in this commit's parent — those landed on top
of v0.6.0 and were reset out alongside this fix). macOS is mandatory
again.

Verified locally: ``pixi run tests`` exits 0 with 961 tests passing
across 60 test files plus 36 examples on this MacBook. ``mojo format``
clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
Made-with: Cursor
triesap pushed a commit to triesap/mojo_flare that referenced this pull request Jun 14, 2026
…dancy in Router.get

Closes critique TL;DR section 1 sub-items ehsanmok#2 (manual ``__init__``
exposure) and ehsanmok#3 (``Router.get[Extracted[H]]`` turbofish
redundancy):

* The four-trait conformance line every ``Extracted[H]``-mounted
  handler struct used to write -- ``(Copyable, Defaultable,
  Handler, Movable)`` -- collapses to a single name
  ``HandlerExtractor`` via Mojo trait composition.
* The turbofish ``router.get[Extracted[GetUser]]("/users/:id",
  Extracted[GetUser]())`` was over-specified all along: today's
  Mojo infers ``H`` from the runtime arg, so
  ``router.get("/users/:id", Extracted[GetUser]())`` works.

Surface changes:

* ``flare/http/handler.mojo`` -- new ``trait
  HandlerExtractor(Copyable, Defaultable, Handler, Movable):
  pass``. Pure trait composition; no new methods, no new
  behaviour. The four conformances exactly match what
  ``Extracted[H]`` (``H: Copyable & Defaultable & Handler &
  Movable``) requires + the parametric
  ``Router.get[H: Handler & Copyable & Movable]`` overload
  consumes for boxed-handler registration.

* ``flare/http/__init__.mojo`` -- re-exports
  ``HandlerExtractor`` from the ``handler`` module alongside
  ``Handler`` / ``HandlerInfallible`` / ``WithRaises`` /
  ``CancelHandler`` / ``ViewHandler`` / ``FnHandler``.

* ``flare/prelude.mojo`` -- adds ``HandlerExtractor`` to the
  re-exported ``Handler`` family. Updates the prelude
  docstring's beginner snippet to use the working
  ``srv.serve(r^)`` shape (the prior ``num_workers=4`` shape
  there was only ever valid for Copyable handlers, which
  ``Router`` isn't yet -- see the v0.7 single-worker overload
  commit + the v0.7.x roadmap for Router-Copyable).

* ``examples/intermediate/extractors.mojo`` -- migrates
  ``GetUser`` from ``(Copyable, Defaultable, Handler,
  Movable)`` to ``(HandlerExtractor)``, drops the turbofish
  on the ``Router.get`` registration, drops the now-unused
  ``Handler`` import, and adds a docstring paragraph
  explaining why the manual ``__init__`` body is still
  required (Mojo doesn't yet auto-derive a no-arg
  constructor from per-field ``Defaultable`` impls -- once
  it lands, the body collapses to ``pass``).

* ``tests/test_handler_extractor.mojo`` -- new, 4 tests:
  - ``test_handler_extractor_satisfies_handler_bound`` --
    a ``HandlerExtractor`` struct flows through a
    ``[H: Handler]``-generic helper.
  - ``test_handler_extractor_flows_through_extracted`` --
    ``Extracted[H]`` accepts a ``HandlerExtractor`` struct
    as its type argument.
  - ``test_handler_extractor_registers_on_router_without_turbofish``
    -- ``router.get("/users/:id", Extracted[H]())``
    compiles + serves end-to-end without an explicit
    ``[Extracted[H]]`` turbofish.
  - ``test_handler_extractor_serves_directly`` -- the
    struct itself is a regular ``Handler`` and
    ``probe.serve(req)`` works on a manually populated
    instance.

* ``pixi.toml`` -- ``test-handler-extractor`` task
  registered + added to the ``tests`` aggregate after
  ``test_request_factories``.

The remaining manual no-arg ``__init__`` body
(``self.id = PathInt["id"]()`` etc.) is a Mojo language
constraint -- ``@fieldwise_init`` doesn't auto-derive the
``Defaultable`` constructor from per-field ``Defaultable``
impls. Once Mojo ships that, the body collapses to ``pass``
without any flare code change.

Verification: ``pixi run mojo -I . examples/intermediate/extractors.mojo``
emits the expected 5-line trace (3 GET responses + the
sanitised 400 path + ``OK.``); ``pixi run test-handler-extractor``
prints all 4 OK lines. ``mojo format`` clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
zacharyvmm and others added 4 commits June 15, 2026 19:56
HttpClient._do_request() (H1) and _send_h2c_via_upgrade() (H2c) were
silently dropping the caller-supplied Authorization header, blocking
per-request auth patterns (AWS SigV4, OAuth2 rotation, HMAC signing).

The fix mirrors the existing _build_h2_request_headers guard (line 1619):
only skip the caller's Authorization when _auth_header is already set.
When the client has a stored credential, that wins; when it does not,
the caller's per-request Authorization reaches the wire.

Also fixes an UnsafePointer[UInt8](unsafe_from_address=0) usage in
flare/net/_libc.mojo — nightly now requires Optional[UnsafePointer]
to model nullable pointers.

Adds three regression tests in tests/http/test_unified_http_client.mojo:
  1. caller Authorization reaches the wire on H1
  2. caller Authorization reaches the wire on H2c
  3. no duplicate Authorization when both client credential and
     caller header are set
test_no_duplicate_authorization_h1 passed a relative url="/" to
HttpClient.send() against a base_url client. send() treats req.url as
absolute (base_url joining only happens in get/post), so it raised a
URL-parse error before the dedup path ran, failing on both CI legs.
Mirror the sibling forwarding tests: build a full request URL with the
variadic String(...) constructor and drop the base_url. The H1/H2c
Authorization-forwarding fix is unchanged.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
@ehsanmok

Copy link
Copy Markdown
Owner

Ok the CI is fixed. Had to force push (with lease) to rebase this.

@ehsanmok ehsanmok merged commit 2ad83ac into ehsanmok:main Jun 15, 2026
4 checks passed
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