Skip to content

docs(fetch_winhttp): add design document for WinHTTP custom transport - #595

Open
sandersaares wants to merge 25 commits into
mainfrom
u/sasaares/fetch-winhttp-design
Open

docs(fetch_winhttp): add design document for WinHTTP custom transport#595
sandersaares wants to merge 25 commits into
mainfrom
u/sasaares/fetch-winhttp-design

Conversation

@sandersaares

Copy link
Copy Markdown
Member

[Copilot speaking]

Adds a placeholder fetch_winhttp crate 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_winhttp is a Windows-only custom fetch transport that drives the OS WinHTTP API in asynchronous mode as a Tokio-free alternative to fetch_hyper. OS calls go through a bindings facade (real windows-crate impl plus a mock for unit tests), mirroring the pattern used in oxidizer_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 RequestContext reclaimed on the final HANDLE_CLOSING), error handling, HTTP protocol negotiation, TLS (WinHTTP/Schannel-managed, with an insecure mode), compression, and request/response body streaming modelled as bytesbuf_io reads/writes. It also includes a test plan (mock-bindings unit tests plus localhost integration tests) and a chapter of feedback on the fetch API 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.

sandersaares and others added 3 commits July 22, 2026 15:50
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
Copilot AI review requested due to automatic review settings July 22, 2026 13:35
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown

✅ Version increments look sufficient

cargo semver-checks compared the 1 crate(s) this PR publishes against their previous version-bump commit in git history. Every version increment is sufficient for the detected API changes.

Crate Baseline Baseline commit This PR Minimum required Status
fetch_winhttp new crate 0.1.0 0.1.0 ✅ ok

This check is informational and does not block the merge.

View the check run

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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_winhttp crate with a placeholder lib.rs and a detailed docs/DESIGN.md describing 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.

Comment thread crates/fetch_winhttp/src/lib.rs
Comment thread crates/fetch_winhttp/README.md Outdated
Comment thread crates/fetch_winhttp/docs/DESIGN.md Outdated
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
Copilot AI review requested due to automatic review settings July 22, 2026 13:57
… 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
Comment thread crates/fetch_winhttp/docs/DESIGN.md Outdated
@@ -0,0 +1,1381 @@
# `fetch_winhttp` design

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: 1400 lines of design, can we shorten it?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Do you have specific suggestions for which sections are unnecessary or overly verbose? Hard to take action based on line count alone.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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!

fetch_winhttp_design_analysis.md

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks Sander, the shortened design is now much more digestible. Will go through it.

Comment thread crates/fetch_winhttp/docs/DESIGN.md Outdated

- **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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

it's better to expose these as extensions so you get a nice HttpClient::builder_win_http experience

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Integrated into doc.

Comment thread crates/fetch_winhttp/docs/DESIGN.md Outdated
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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TlsOptions are agnostic to hyper, the plumbing that consumes TlsOptions and adapts them to hypers lives in fetch_hyper

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

we might leave this configurable, for clients with low-traffic, there is no need to create per-core instances

@sandersaares sandersaares Jul 23, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

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)?

@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.0%. Comparing base (53018e0) to head (51169ff).

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     
Flag Coverage Δ
linux 100.0% <100.0%> (?)
linux-arm 100.0% <100.0%> (?)
windows 100.0% <100.0%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

sandersaares and others added 11 commits July 23, 2026 09:03
…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 ralfbiedert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

sandersaares and others added 2 commits July 27, 2026 10:08
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
`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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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()`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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)

Comment thread crates/fetch_winhttp/docs/design.md Outdated
let client = HttpClient::new_winhttp();

// Configured:
let client = HttpClient::builder_winhttp(WinHttpDeps {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For use of ..Default::default() we cannot apply #[non_exhaustive]. Are we sure we won't be adding new fields later?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[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.

sandersaares and others added 2 commits July 27, 2026 11:49
…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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

saw this mentioned bellow, drop safety still applies

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[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.

Comment thread crates/fetch_winhttp/docs/design.md Outdated
`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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

[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).

@martintmk

Copy link
Copy Markdown
Member

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.

sandersaares and others added 2 commits July 27, 2026 13:18
…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 martintmk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖: 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖: 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖: 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).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖: 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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖: 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.

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.

5 participants