docs(fetch_winhttp): add design document for WinHTTP custom transport - #595
docs(fetch_winhttp): add design document for WinHTTP custom transport#595sandersaares wants to merge 25 commits into
Conversation
Design-only phase: introduces the Windows-only fetch_winhttp custom transport crate as a placeholder (no implementation yet) carrying the DESIGN.md architecture document for the WinHTTP-based fetch transport. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
Apply editorial restructuring from the final rubber-duck pass: - add a concise "Architecture at a glance" (section 1.3) so the mental model precedes the detail sections - deduplicate the threading (4), context-ownership (5.4/11.3), and connection-lifetime (7.5) explanations, cross-referencing the canonical section instead of re-deriving - convert 7.5's rejected alternatives and 14.2's per-factor test list into scannable tables, keeping prose only for the sequenced factors - refine 17 to distinguish network-phase timers (transport concern) from end-to-end pipeline deadlines Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
Fixes flagged by a second rubber-duck editorial pass: - Remove client-cert benefit claim from S1 that implied v1 mTLS (contradicts S9.1) - State the full session-level callback notification mask once (S5.3) instead of only naming FLAG_HANDLES; reference it from the S3 table and test plan - Correct the S14.2 session-lifetime test to match S5.3: the body reader's Arc keeps the session wrapper alive in-flight, and native parent refcounting (not the Rust Arc) carries the OS session through the final HANDLE_CLOSING - Fix atomic cross-ref S5.1 -> S5.5 - Distinguish dropping the execute future vs the response body in the cancellation hazard and test plan - Clarify one-session-per-built-client wording (S1.3) - Point WinHttpOptions comment at S12 as well - Drop misleading '64-bit Windows' qualifier on DWORD_PTR - Clarify S16 boundary: observable HTTP behaviors are contractual; only the machinery is implementation detail - Deduplicate the 'one transport-scheduled delay' claim across S12 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
✅ Version increments look sufficient
|
There was a problem hiding this comment.
Pull request overview
Introduces a new fetch_winhttp crate as a Windows-only fetch transport design artifact, allowing review of the proposed WinHTTP-based architecture before any implementation is added.
Changes:
- Adds the
fetch_winhttpcrate with a placeholderlib.rsand a detaileddocs/DESIGN.mddescribing the planned transport. - Wires the new crate into workspace metadata (workspace dependency list, root README and CHANGELOG, Cargo.lock).
- Updates workspace spellcheck dictionary for WinHTTP/design terminology used in the new documentation.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Adds fetch_winhttp to the top-level crate list. |
| crates/fetch_winhttp/src/lib.rs | Adds placeholder crate docs and rustdoc logo/favicon configuration. |
| crates/fetch_winhttp/README.md | Adds crate README describing scope/status and linking to the design doc. |
| crates/fetch_winhttp/logo.png | Adds crate logo asset via Git LFS pointer. |
| crates/fetch_winhttp/favicon.ico | Adds crate favicon asset via Git LFS pointer. |
| crates/fetch_winhttp/docs/DESIGN.md | Adds the WinHTTP transport design document and test plan. |
| crates/fetch_winhttp/CHANGELOG.md | Adds initial crate changelog stub. |
| crates/fetch_winhttp/Cargo.toml | Adds the new crate manifest and docs.rs metadata. |
| CHANGELOG.md | Adds fetch_winhttp to the workspace changelog index. |
| Cargo.toml | Adds fetch_winhttp to [workspace.dependencies]. |
| Cargo.lock | Adds the lockfile entry for the new crate. |
| .spelling | Adds WinHTTP- and design-related terms to the spellcheck allowlist. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The design-only placeholder crate had no tests, so `cargo nextest run` failed with "no tests to run" (exit 4) in the pr-test CI job. Add a trivial placeholder test until the real transport and its test suite land. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
… in AGENTS - Reword the double-verb "is ... is" sentence in DESIGN.md S1.2. - AGENTS.md: state that auto-generated crate READMEs must not be hand-edited or flagged for formatting/wording/casing in review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
| @@ -0,0 +1,1381 @@ | |||
| # `fetch_winhttp` design | |||
There was a problem hiding this comment.
nit: 1400 lines of design, can we shorten it?
There was a problem hiding this comment.
Do you have specific suggestions for which sections are unnecessary or overly verbose? Hard to take action based on line count alone.
There was a problem hiding this comment.
Since I had the same issue, I used an agent to help me outline my thoughts here. Please take some time go through them. If you think anything is unnecessary or verbose, I'd love to hear specific suggestions how to improve it!
There was a problem hiding this comment.
I read Ralf's analysis and more or less agree with that assessment pages. I would love to see a shortened section, max 3-4 pages with core tenets and requirements.
Many parts of this document read more as an implementation plan for agents.
There was a problem hiding this comment.
What would really help is to see actual APIs and how these are used in fetch.
// do we need some arguments for win http?
HttpClient::builder_winhttp(...)
.winhttp(...) // is there win-http specific configuration
.tls(TlsOptions::default()) // !!! THESE ARE IGNORED, we need to address this in fetch
.build()Maybe even highlight fetch knobs that are not applied.
There was a problem hiding this comment.
Factored document into 3 separate documents to simplify reviewing from different viewpoints. Added API example for usage in fetch. The API shape is not something I am particularly happy with but I think this needs fetch-level design work to make working with transport-specific concerns more ergonomic, not something specific to winhttp integration.
@ralfbiedert I found it quite hard to extract actionable feedback from the attached analysis. There is a lot of just description in there and a hilarious amount of "the key problem with this plan is that it is a plan so we don't really know for sure yet", which obscures the more valuable points. I was able to extract some specific items from there but it feels like it was missing a "robot, condense this analysis to actionable suggestions" turn at the end.
There was a problem hiding this comment.
Thanks Sander, the shortened design is now much more digestible. Will go through it.
|
|
||
| - **No first-class way to plug in a downstream transport.** `HttpClient` and its | ||
| builder live in `fetch`, so a transport crate cannot add an inherent | ||
| `HttpClient::winhttp(..)` constructor (orphan rule). We expose free functions |
There was a problem hiding this comment.
it's better to expose these as extensions so you get a nice HttpClient::builder_win_http experience
There was a problem hiding this comment.
Integrated into doc.
| The slightly more verbose hello-world is worth the consistency. | ||
|
|
||
| - **TLS configuration lives at the wrong layer.** `fetch`'s generic `TlsOptions` | ||
| carries rustls/native-tls material that only the Hyper transport understands; |
There was a problem hiding this comment.
TlsOptions are agnostic to hyper, the plumbing that consumes TlsOptions and adapts them to hypers lives in fetch_hyper
There was a problem hiding this comment.
Clarified in text that "hyper" here refers to "fetch_hyper" (the fetch feedback is that TLS is a property of transports like fetch_hyper not of the pipeline composer fetch).
…ailable The `+ Send` bound on `bytesbuf_io`'s `ReadAsFuturesStream` in-flight read future is now merged, so state it as a provided guarantee rather than a merge-queue assumption (S11.2, S15). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
|
|
||
| - **One shared OS session per built client**, opened at build time, `Arc`-shared by | ||
| all per-core instances, and immutable after setup (§4.2). | ||
| - **One transport instance per core.** `fetch` clones and relocates the transport |
There was a problem hiding this comment.
we might leave this configurable, for clients with low-traffic, there is no need to create per-core instances
There was a problem hiding this comment.
Might be worth noting down as a low-traffic client optimization pass (possibly deferred), though needs measurements/benchmarks to evaluate impact. Which of our current known customer scenarios would you classify as the most representative of this situation and how would you characterize their usage of fetch (i.e. what aspects would be important to replicate in a benchmark)?
…ttp-design # Conflicts: # .spelling
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #595 +/- ##
=======================================
Coverage 100.0% 100.0%
=======================================
Files 442 443 +1
Lines 42616 42617 +1
=======================================
+ Hits 42616 42617 +1
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…action Both complaints were framed backwards. Restate them as fetch over-abstracting, not under-modeling: - TLS (S1.2, S17): different transports inherently support different TLS config models; that is a fact of life, not a fetch design choice. The shortcoming is fetch trying to configure TLS uniformly at the transport-agnostic level; it belongs on the transport. - Timeouts (S12.1, S17): fetch lacking fine-grained resolve/send timeouts is fine and expected - transports differ in which timers they support and what each includes/excludes. fetch should keep only pipeline-level deadlines and leave all network-phase timers to each transport, so exposing them via WinHttpOptions is the correct home, not a workaround. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
…refs - Rewrite S5.3 to lead with the one ownership rule in bold, then three tight phases (before/after SetOption, validity), instead of a dense wall of prose. - Reference plurality's Box::into_raw/from_raw as available; drop the now-merged PR #585 links in S5.3 and S15. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
Drop the pools' internal mechanics (EventPool's Arc-over-slab, MutexGuard-!Send reasoning, reference-counted backing details, thread-per-core aside) and keep only the contract-level facts: two transport-owned pools, what each rents, their Send/Sync properties, and why the plurality Pool needs a coarse uncontended Mutex. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
…traction Reframe the S17 connection-management bullet to match the TLS/timeout framing and lead with max_connections as the concrete example: it presumes a fetch-managed pool, but different transports pool connections differently (WinHTTP owns its own pool with its own knobs), so there is no single model to configure uniformly at the fetch level - it belongs on the transport. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
'Advisory' conflated two different cases. Replace with precise labels: - max_connections is 'Approximate' - the limit is applied but enforced per authority (WinHTTP) vs per pool (fetch), so a multi-host pool's effective cap differs. - connection_idle_timeout is 'Not honored' - WinHTTP exposes no idle-TTL knob, so the configured value has no effect and WinHTTP uses its own default. Also relabel the unhonored connection_lifetime rows and update the closing note. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
…example Keep-alive is a property of how each transport's stack maintains its own connections (WinHTTP applies its own per-protocol keep-alive with its own floors), so include ConnectionKeepAlive alongside max_connections/connection_lifetime/ connection_idle_timeout as connection-management config that over-abstracts at the fetch level and belongs on the transport. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
…olicy The connection_lifetime-specific warning paragraph described a general behavior. Promote it to a shared policy subchapter (S17.1) under the fetch API feedback: for any configured option the transport cannot faithfully honor, warn at build time (tracing + telemetry) and proceed, rather than silently ignoring (hides a broken guarantee) or hard-erroring (breaks drop-in use). Condense S7.5 to reference S17.1. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
…rk in S11.2 The sentence described something that is not done; it adds nothing and is not emphasizing anything. Start the paragraph with the substantive pull-based/ backpressure point instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
…tive-timer backstop Rewrite S12.2 from a single dense paragraph into short, focused paragraphs and retitle it "The outer connect timeout" (it enforces a timeout, not a "delay"). Its core message is now up front: the native connect timer is per-attempt but fetch callers expect a total deadline, so the transport races the connect phase against one tick::Clock timer. Replace the unexplained "as backstop" note in the S12.1 table with a new S12.3 that states explicitly why the native receive timers are set: WinHTTP's own receive timers cannot be disabled, so we align them with the fetch value to keep the layers in agreement and to provide OS-level liveness if the caller's async executor stalls. Renumber the test subchapter to S12.4 and update cross-refs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
Telemetry will go through the observed crate rather than tracing. Add an observed::Sink field to WinHttpDeps (it is Clone/ThreadAware, so it relocates per core like the rest of the config) and note the transport emits telemetry through it (detailed in v1.1). Replace the opentelemetry/tracing dependencies with observed and reword the S17.1 warning to emit an observed event. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
Resolve Cargo.toml [workspace.dependencies] conflict: origin/main merged the separate local-dependency block into the single alphabetical dependency list. Re-apply our only delta - the fetch_winhttp local dependency entry - in its alphabetical position after fetch_tls. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
ralfbiedert
left a comment
There was a problem hiding this comment.
Approved. The design is internally coherent, including FFI ownership, callback handoff, cancellation, and timeout reasoning. For implementation follow-up: keep the cross-crate design critique out of public rustdoc, verify docs.rs/fetch links, and hold the eventual unsafe FFI layer to the proposed mock/Miri validation.
Separate the hybrid DESIGN.md into a user-facing contract (design.md) and an implementation guide (implementation.md), and extract fetch API feedback into crates/fetch/docs/stabilization.md. Make design.md purely contractual, relocating all WinHTTP option mechanics to implementation.md, and reduce repetition across all three docs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
In stabilization.md item 2, make explicit that the transport whose TLS the generic TlsOptions serves is fetch_hyper (the Hyper-based fetch transport), framing the issue as fetch vs fetch_hyper/fetch_winhttp having different, non-interchangeable TLS models that cannot live on the fetch layer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
| first-class inherent methods (`HttpClient::builder_tokio`/`new_tokio`), while a | ||
| downstream transport is reachable only through an imported trait. The extension trait is a | ||
| workaround, not a fix - it papers over the call syntax but leaves the asymmetry, and the | ||
| asymmetry itself is the wart: all HTTP-client composition options should be on equal |
There was a problem hiding this comment.
The convenience for common workloads is intentional. We don't want to go through extra loops for 90% of users that just want to use tokio with whatever default transport we choose.
Also, let's think about the current setup more as a stopgap while we work on fine-tuning and stabilizing other parts of our stack (runtime, security). Once that's in, we will also have built-in builder_oxidizer + new_oxidizer APIs.
The workspace README check (doc2readme --check) failed because multitude, plurality and routerama READMEs were generated by an older doc2readme and had drifted from the pinned CARGO_DOC2README_VERSION (0.7.2). Regenerate them so the check passes. Generated content only; no hand edits. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
| avoid a Hyper-colored default, and treat every transport uniformly. The slightly more | ||
| verbose hello-world is worth the consistency. | ||
|
|
||
| ## 2. Transport-specific TLS configuration |
There was a problem hiding this comment.
Current bundling to fetch crate might be unfortunate because indeed the fetch_tls might not be useable in current form with winhttp.
However, my thinking is that fetch_tls might server as a set of descriptors that can be interpreted by different TLS technologies.
One thing we need to address is how the different capabilities should be represented. (for example, certificate validation can inspected by callbacks for rustls but not for native-tls.
| `fetch_winhttp` have fundamentally different, non-interchangeable TLS configuration, so | ||
| it cannot be handled at the `fetch` layer at all. Per the split above, TLS should be | ||
| configured per transport, on the transport being plugged in; the | ||
| `HttpClient::build(transport().tls(cfg))` shape would express this cleanly. |
There was a problem hiding this comment.
| `HttpClient::build(transport().tls(cfg))` shape would express this cleanly. | |
| `HttpClient::builder_tokio(deps, tls)` |
this is also possible alternative, do not tie tls options to builder but rather explicitly require them on construction
|
|
||
| ## 4. Scope-based timeout configuration | ||
|
|
||
| Timeouts are over-abstracted, not under-modeled. `fetch` models a connect |
There was a problem hiding this comment.
If we ignore all technical details of timeouts across different timeouts, one timeout that the end consumer of fetch cares about is "connect timeout".
While I believe configuration knobs are useful, on the level of common API, connect timeout is something we should keep and let transports interpret it.
Last resort for transport is to leverage drop safety, if the future is cancelled, transport should handle it properly.
| on the builder, e.g. | ||
| `HttpClient::builder_winhttp().tls(|tls| tls.accept_invalid_certs()).timeouts(|t| t.read(3000)).custom_pipeline(..).build()`, | ||
| or a compositional form where the transport is itself a builder plugged into the client: | ||
| `HttpClient::builder().transport(WinHttpTransport::builder().tls(|tls| ..).build()).build()`. |
There was a problem hiding this comment.
Maybe HttpClient::builder() could return HttpClientBuilder<NeedsTransport>. APIs for HttpClientBuilder<NeedsTransport> could expose configuration for pipeline, i.e. things that are not transport agnostic.
| @@ -0,0 +1,100 @@ | |||
| # `fetch` API stabilization feedback | |||
There was a problem hiding this comment.
TLDR: many good points but one major point I want to highlight. fetch is batteries includes so we DO play favorites. We will pick our pick for default transport for each supporter runtime and support for custom transports should not impact the ergonomics for our default choices.
This can be done ergonomically where HttpClient::builder(...) could return HttpClientBuilder<NeedsTransport> while HttpClient::builder_tokio(...) returns something like HttpClientBuilder<DefaultTransport> where we plug-in our recommended transport.
There was a problem hiding this comment.
also the generic parameter should be defaulted, i.e. HttpClientBuilder<T = DefaultTransport>
(we can live with generics on HttpClientBuilder, generics on HttpClient are probably a no-go)
There was a problem hiding this comment.
I also think that is ok for us to expose lowest common denominator for our default transport and then also enable deeper customization through specific transport knobs:
HttpClient::builder(...).hyper(hyper_transport)
| let client = HttpClient::new_winhttp(); | ||
|
|
||
| // Configured: | ||
| let client = HttpClient::builder_winhttp(WinHttpDeps { |
There was a problem hiding this comment.
For use of ..Default::default() we cannot apply #[non_exhaustive]. Are we sure we won't be adding new fields later?
There was a problem hiding this comment.
[Copilot speaking] Agreed - we should expect to add fields. Reworked design.md §1.1 so WinHttpDeps, WinHttpOptions and WinHttpTlsConfig are all #[non_exhaustive] and built through builders (WinHttpDeps::builder().tls(...).options(...).sink(...).build()) instead of a struct literal with ..Default::default(), so new knobs can land compatibly. Fixed in 2883b8d.
…ttp-design # Conflicts: # crates/multitude/README.md # crates/plurality/README.md # crates/routerama/README.md
…FI context ordering Clarify WINHTTP_OPTION_CONTEXT_VALUE must be installed before the first async call so HANDLE_CLOSING always carries the context. Add a leak-freedom soak test to the integration plan. Add a Telemetry chapter documenting the metrics-vs-logs split and cold-connect error attribution (log-only). Map fetch's ConnectionKeepAlive onto WINHTTP_OPTION_HTTP2/3_KEEPALIVE with the imperfect-fit residue routed through the warn policy. Record stale-connection retry as a deferred v2 connection-management consideration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
|
|
||
| (The specific OS options behind each behavior are implementation.md §10.3.) | ||
|
|
||
| ## 6. Timeouts and time |
There was a problem hiding this comment.
from the perspective of transport, it needs to ensure drop safety, i.e. when fetch cancels the future, transport needs to honor it.
Also, fetch attaches timeout information to each request that can be inspected and may be even applied to transport?
See:
https://docs.rs/http_extensions/latest/http_extensions/timeout/index.html
There was a problem hiding this comment.
saw this mentioned bellow, drop safety still applies
There was a problem hiding this comment.
[Copilot speaking] Both points addressed in 2883b8d.
Drop safety: honored. Dropping the request future closes the WinHTTP handle, which cancels the in-flight operation and delivers the terminal HANDLE_CLOSING that reclaims the request context (design.md §7 cancellation model; implementation.md §4). I added an explicit note in design.md §6.1 that cancellation is the transport's backstop for any timeout it does not enforce natively.
Per-request timeout inspection: adopted where the semantics actually match, which turned out to be a split:
- BodyTimeout (idle gap between body chunks, reset on progress) matches WINHTTP_OPTION_RECEIVE_TIMEOUT (per-receive-operation idle timer, reset each read), so the transport now reads it from the request extensions and programs that native timer per-request.
- ResponseTimeout is a total over connect + send + receive-headers. WINHTTP_OPTION_RECEIVE_RESPONSE_TIMEOUT covers only the post-send wait for the first response byte (it excludes connect and send), so it is not a faithful remap. ResponseTimeout therefore stays enforced above the transport (as fetch/http_extensions already do) and the native receive-response timer is set only as a looser liveness backstop.
Mapping detail is in implementation.md §10.4.
| `fetch_hyper` does; both surface as `HttpError::timeout`. | ||
| - **Seatbelt request timeout**: enforced above the transport; the transport is not | ||
| involved. | ||
| - **Resolve and send timeouts**: `fetch` has no concept for these, so they are exposed as |
There was a problem hiding this comment.
SendTimeout is really just the ResposeTimout in disguise. We send request and wait for the response without touching the response body. After that the BodyTimeout applies.
There was a problem hiding this comment.
[Copilot speaking] Agreed - fixed in 51169ff. Removed the standalone "send timeout" framing: §6.1 now states send is not a distinct concept (send + await-headers is exactly the span ResponseTimeout governs, then BodyTimeout takes over), leaving only a Resolve transport-specific knob. Updated the implementation.md §10.4 timeout table to match (WinHTTP send field left as a loose backstop, not driven by a dedicated fetch knob).
|
Thanks for fine-tuning the design for humans, the split into design + implementation might be something we should leverage more to avoid overly technical design docs. I think this is good to go, minor comments. The fetch stabilization is a different topic we need address. Custom winhttp transport implementation will give us a lot of useful insights. |
…oring Address PR review feedback. Make WinHttpDeps/WinHttpOptions/WinHttpTlsConfig builder-constructed and `#[non_exhaustive]` so config types can gain fields without a breaking change, replacing the `..Default::default()` struct-literal example in design.md §1.1. Honor per-request timeouts where WinHTTP semantics match: BodyTimeout maps to WINHTTP_OPTION_RECEIVE_TIMEOUT (per-receive-operation idle timer, reset each read) and is programmed per-request; ResponseTimeout is a connect+send+headers total with no matching native timer, so it stays enforced above the transport with RECEIVE_RESPONSE_TIMEOUT as a looser backstop only. Both are read from request extensions. Note cancellation as the drop-safety backstop for non-native timeouts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
Address PR review: a distinct "send timeout" is redundant - sending the request and awaiting response headers is the span ResponseTimeout already governs. Split the design.md §6.1 bullet into Send (not a distinct concept) and Resolve (the sole transport-specific knob), and update the implementation.md §10.4 timeout table and §7.2 test row accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8dbb7b85-e123-4586-8df2-a2a6a5b92194
martintmk
left a comment
There was a problem hiding this comment.
I think this is good to go, fetch API design is different discussion
| .accept_invalid_certs(true) // Schannel knobs, §4 | ||
| .build()) | ||
| .options(WinHttpOptions::builder() | ||
| .connect_timeout(Duration::from_secs(10)) // protocol/timeout tuning, §3, §5, §6 |
There was a problem hiding this comment.
🤖: connect_timeout is configured here via WinHttpOptions, but §6.1/§6.2 and implementation.md §10.4 say the transport reads TransportOptions.connect_timeout (implementation.md's timeout test table also alternates between the two). Please pick one source of truth or specify precedence — otherwise this public example and the implementation contract can yield different connect-timeout values.
| `fetch`'s generic `TlsOptions`/`TlsBackend` carries rustls/native-tls material | ||
| (crypto providers, verifiers, client-cert resolvers) that is meaningless to | ||
| Schannel. WinHTTP does TLS itself and accepts only a small set of knobs, so | ||
| `fetch_winhttp` therefore ignores `fetch`'s TLS configuration entirely and takes its |
There was a problem hiding this comment.
🤖: Silently ignoring fetch's TlsOptions is the one dropped option with security consequences. CustomContext.tls is always populated, so a caller who set non-default TLS material — a private CA root, a client identity (mTLS), or cert-pinning — silently gets a default system-trust connection with no signal, even though the same code 'works' on fetch_hyper. Please make the design explicit that a non-default TlsOptions triggers the §11 build-time warning, and consider a hard error (not just a warn) when security-affecting TLS material is present, since this is a silent security weakening rather than a benign fidelity gap.
| ``` | ||
|
|
||
| `CustomContext` hands the factory a `HttpBodyBuilder` (carrying the clock and | ||
| read-buffer pool), a `PoolIndex`, the generic `TransportOptions`/`TlsOptions`, a |
There was a problem hiding this comment.
🤖: Ignoring PoolIndex collapses fetch's configured multiple_pools into a single WinHTTP connection pool: fetch invokes the custom transport factory once per pool slot specifically so routing to a PoolIndex selects an independent transport pool, but every handler here shares the one OS session — so nominally separate pools all use the same OS connection pool. That contradicts the fetch contract and this design's §11 warning policy. Either key WinHTTP sessions by PoolIndex (one session per pool slot, shared across cores within a slot), or add multiple_pools to the explicitly-unhonored options and emit the §11 warning instead of silently collapsing it.
| `Meter`, and the caller's `Extras`. `fetch_winhttp` ignores `PoolIndex` (per-core | ||
| placement comes from `Isolation::Isolated`, §3.2) and ignores `CustomContext::tls` (it | ||
| takes its own `WinHttpTlsConfig` instead; see design.md §1.2). | ||
|
|
There was a problem hiding this comment.
🤖: Opening and capturing the session in builder_winhttp breaks the per-built-client isolation guarantee (design.md §2) when the builder is cloned: HttpClientBuilder is Clone, so two clones can be built into independent clients while retaining this same captured Arc<WinHttpSession> — and therefore the same WinHTTP pool. Since that isolation is a stated TLS-policy security boundary (a strict client must not share a pooled connection with an accept_invalid_certs one), please make the session build-scoped (e.g. create per-client shared state at build, lazily initialized from the finalized CustomContext) rather than builder-scoped, and add an integration test asserting that two independently built clients targeting the same authority establish separate connections.
| - Streaming upload (unknown length -> chunked) and streaming download; assert | ||
| incremental delivery, not just final bytes. | ||
| - Real gzip/deflate responses are transparently decoded. | ||
| - `https` against a localhost TLS server with a self-signed cert: fails by |
There was a problem hiding this comment.
🤖: The integration plan only exercises accept_invalid_certs, but the design exposes accept_invalid_certs and accept_invalid_hostnames as independent security relaxations. Please add a localhost TLS scenario with both an untrusted cert and a hostname mismatch, covering all four flag combinations: either flag alone must leave the other validation active, and only both together should succeed. That catches accidentally coupling the two Schannel ignore flags.
[Copilot speaking]
Adds a placeholder
fetch_winhttpcrate whose sole content is a design document:crates/fetch_winhttp/docs/DESIGN.md. No transport code yet - this PR is to review the design before implementation.fetch_winhttpis a Windows-only customfetchtransport that drives the OS WinHTTP API in asynchronous mode as a Tokio-free alternative tofetch_hyper. OS calls go through a bindings facade (realwindows-crate impl plus a mock for unit tests), mirroring the pattern used inoxidizer_io.The design covers the threading model (one shared OS session per built client, one transport instance per core, callbacks completing inline for processor affinity), the cancellation and FFI-ownership model (pooled
RequestContextreclaimed on the finalHANDLE_CLOSING), error handling, HTTP protocol negotiation, TLS (WinHTTP/Schannel-managed, with an insecure mode), compression, and request/response body streaming modelled asbytesbuf_ioreads/writes. It also includes a test plan (mock-bindings unit tests plus localhost integration tests) and a chapter of feedback on thefetchAPI where its Hyper/Tokio-shaped assumptions force workarounds.The document has been through several rounds of rubber-duck review; opening now for wider human review of the design.