fronted/kindling: add one-shot HTTP request (separate from meek tunnel)#9
Conversation
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
There was a problem hiding this comment.
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, andh2_oneshot()to send headers+body (end-of-stream) before awaiting the response. - Adds
request/request_withAPIs toDirectH2DialerandFrontedTlsDialer, sharing arace_oneshothelper 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.
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
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds 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. ChangesOneshot HTTP/2 request flow
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Summary
Bootstrap config fetches are small request/response exchanges, not bidirectional tunnels — and meek's
MeekStreamis respond-first:h2_request_streamsends 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'sconfig-new) reads the request body before responding, so a config-new fetch overMeekStreamwould 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):
API (flint-fronted)
OneshotRequest(method/path/headers/body/max_body) +HttpResponse(status/headers/body, with a case-insensitiveheader()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(+_withdial-injection seams), sharing arace_oneshothelper 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.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 frontedHoston 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
Bug Fixes