Skip to content

fronted/kindling: add one-shot HTTP request (separate from meek tunnel)#9

Merged
myleshorton merged 2 commits into
mainfrom
fisk/kindling-oneshot-request
Jun 25, 2026
Merged

fronted/kindling: add one-shot HTTP request (separate from meek tunnel)#9
myleshorton merged 2 commits into
mainfrom
fisk/kindling-oneshot-request

Conversation

@myleshorton

@myleshorton myleshorton commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Bootstrap config fetches are small request/response exchanges, not bidirectional tunnels — and meek's MeekStream is respond-first: h2_request_stream sends request headers, then awaits the response before exposing the write half. That fits a meek server or a GET, but an ordinary POST origin (Lantern's config-new) reads the request body before responding, so a config-new fetch over MeekStream would deadlock (the body is never sent before the awaited response).

This adds a one-shot request primitive, separate from the meek transport (which stays for true tunneling):

meek (respond-first):   headers ──▶ │ await response │ ──▶ then stream body   ✗ deadlocks a read-then-respond POST
oneshot (request-first): headers + body (end_stream) ──▶ │ await response │   ✓ works against config-new

API (flint-fronted)

  • OneshotRequest (method/path/headers/body/max_body) + HttpResponse (status/headers/body, with a case-insensitive header() lookup).
  • h2_oneshot(io, authority, &req) — sends the complete request (headers + body, end-of-stream), then awaits and collects the response.
  • DirectH2Dialer::request / FrontedTlsDialer::request (+ _with dial-injection seams), sharing a race_oneshot helper that runs the full dial + request per candidate (a candidate wins only after a complete response). Direct addresses the origin; fronted addresses the provider's fronted host with the decoy SNI on the wire.
  • flint-kindling re-exports OneshotRequest, HttpResponse, h2_oneshot.

Why

This is the shape spark's config-fetch bootstrap needs: race a direct one-shot request against a fronted one-shot request (e.g. via the aliyun provider), first usable config wins — without the meek deadlock.

Tests

  • direct_h2_oneshot_sends_body_before_awaiting_response — a read-body-then-respond server (the config-new ordering) over the direct path; passes on the oneshot, would deadlock on meek.
  • fronted_tls_oneshot_request_over_verified_front — same, over the fronted-TLS path, asserting the decoy SNI on the wire + the fronted Host on the inner request.
  • cargo test -p flint-fronted --features boring (32 pass) + -p flint-kindling (9 pass), clippy + fmt clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EztFQJYZxiWTk2PQwo9qWh

Summary by CodeRabbit

  • New Features

    • Added one-shot HTTP/2 request support for both direct and fronted connections.
    • Introduced request/response helpers for sending a full request and reading the complete response in one call.
    • Added support for setting request headers, request bodies, and a maximum response body size.
    • Response headers can now be looked up in a case-insensitive way.
  • Bug Fixes

    • Improved request flow so the full body is sent before waiting for the response, helping ensure more reliable exchanges.

Bootstrap config fetches are small request/response exchanges, not bidirectional
tunnels — and meek's MeekStream is respond-first: it sends request headers, then
awaits the response *before* exposing the write half. That fits a meek server or
a GET, but an ordinary POST origin (e.g. Lantern's config-new) reads the request
body before responding, so a config-new fetch over MeekStream would deadlock
(the body is never sent before the awaited response).

Add a one-shot request primitive, distinct from the meek transport:

- `OneshotRequest` (method/path/headers/body/max_body) + `HttpResponse`.
- `h2_oneshot(io, authority, &req)`: sends the COMPLETE request (headers + body,
  end-of-stream) and only then awaits + collects the response. Request-first, so
  it works against a read-body-then-respond origin.
- `DirectH2Dialer::request` / `FrontedTlsDialer::request` (+ `_with` test seams),
  sharing a `race_oneshot` helper that runs the full dial + request per candidate
  (a candidate wins only after a complete response). Direct addresses the origin;
  fronted addresses the provider's fronted host with the decoy SNI on the wire.
- flint-kindling re-exports `OneshotRequest`, `HttpResponse`, `h2_oneshot`.

This is the shape spark's config-fetch needs: race a direct one-shot request
against a fronted one-shot request, first usable config wins. The meek transport
stays for true bidirectional tunneling.

Tests: an e2e proving a read-body-then-respond server works over the oneshot
(would deadlock over meek), for both the direct and fronted-TLS paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EztFQJYZxiWTk2PQwo9qWh

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Adds a one-shot HTTP/2 request primitive to support bootstrap-style request/response exchanges (not tunnels), specifically avoiding the “respond-first” deadlock that can occur when using meek-style request streams against read-body-then-respond POST origins.

Changes:

  • Introduces OneshotRequest, HttpResponse, and h2_oneshot() to send headers+body (end-of-stream) before awaiting the response.
  • Adds request/request_with APIs to DirectH2Dialer and FrontedTlsDialer, sharing a race_oneshot helper that races full dial+request attempts.
  • Re-exports the oneshot types/functions from flint-kindling.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
crates/flint-fronted/src/lib.rs Implements the oneshot request/response API, dialer helpers, and tests to validate request-body ordering and fronted SNI/Host behavior.
crates/flint-kindling/src/lib.rs Re-exports the new oneshot request/response types and h2_oneshot for downstream consumers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/flint-fronted/src/lib.rs
Comment thread crates/flint-fronted/src/lib.rs
Comment thread crates/flint-fronted/src/lib.rs
Address Copilot review feedback on #9:
- skip a caller-supplied `Host` header so the authority-derived Host/:authority
  is authoritative (a duplicate would break fronting);
- send the request body respecting HTTP/2 flow control (reserve capacity, await
  the peer window, send in bounded chunks) instead of one send_data that could
  exceed the stream's initial send window for a large body;
- skip non-UTF-8 response header values instead of silently coercing them to
  empty strings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EztFQJYZxiWTk2PQwo9qWh
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e4eed957-9be5-4702-a0b7-0d466cc0c71b

📥 Commits

Reviewing files that changed from the base of the PR and between 5c13d26 and b31432c.

📒 Files selected for processing (2)
  • crates/flint-fronted/src/lib.rs
  • crates/flint-kindling/src/lib.rs

📝 Walkthrough

Walkthrough

Adds oneshot HTTP/2 request/response APIs, shared transport logic, dialer entry points for fronted and direct paths, re-exports, and integration tests covering request ordering and fronted TLS behavior.

Changes

Oneshot HTTP/2 request flow

Layer / File(s) Summary
Public oneshot request and response types
crates/flint-fronted/src/lib.rs, crates/flint-kindling/src/lib.rs
OneshotRequest and HttpResponse are added, and the kindling crate re-exports the new oneshot API surface.
HTTP/2 oneshot exchange
crates/flint-fronted/src/lib.rs
h2_oneshot performs the h2 handshake, sends request headers and body, and collects the response body with a max-size cap.
Fronted and direct dialer entry points
crates/flint-fronted/src/lib.rs
FrontedTlsDialer and DirectH2Dialer add request/request_with methods that build candidates and call race_oneshot.
Oneshot integration tests
crates/flint-fronted/src/lib.rs
Integration tests cover body-send ordering for direct h2 and verified fronted TLS oneshot behavior.

Sequence Diagram(s)

sequenceDiagram
  participant FrontedTlsDialer
  participant race_oneshot
  participant h2_oneshot
  participant HTTP2Peer
  FrontedTlsDialer->>race_oneshot: materialize front candidates
  race_oneshot->>h2_oneshot: dial candidate and run OneshotRequest
  h2_oneshot->>HTTP2Peer: send headers and body
  HTTP2Peer-->>h2_oneshot: response headers and body
  h2_oneshot-->>race_oneshot: HttpResponse
  race_oneshot-->>FrontedTlsDialer: first successful response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • getlantern/flint#8: Adds the earlier direct-h2 request/request_with path that this PR extends with fronted oneshot handling and shared race_oneshot logic.

Poem

A bunny hopped through headers bright,
With one-shot pings in h2 flight.
Fronted whispers, direct beams too,
Then a warm HttpResponse hopped on through. 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a one-shot HTTP request path separate from the meek tunnel.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fisk/kindling-oneshot-request

Comment @coderabbitai help to get the list of available commands.

@myleshorton myleshorton merged commit 76c5cd3 into main Jun 25, 2026
2 checks passed
@myleshorton myleshorton deleted the fisk/kindling-oneshot-request branch June 25, 2026 16:15
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