Skip to main content

hclient_core/
caps.rs

1use http::HeaderName;
2
3/// Who follows a redirect chain: nobody, `Client`, or the backend.
4///
5/// Only [`Internal`](Self::Internal) is branched on: `Client::build()`
6/// refuses a `RedirectPolicy` against a backend that walks the chain
7/// itself, because a policy it cannot honour must not be silently ignored.
8/// Between [`None`](Self::None) and [`Transparent`](Self::Transparent) the
9/// field is a claim a caller reads and nothing in this workspace can
10/// contradict — so a variant here earns its place from a backend that
11/// carries it, not from being describable.
12///
13/// # Implementing this
14///
15/// **The redirect policy never crosses the seam.** `Client` merges the
16/// client-level and per-request `RedirectPolicy` and does not write the
17/// result into the request's extensions, so a transport cannot read one. A
18/// backend that wanted to apply the caller's policy itself would see only
19/// what a `RequestBuilder` happened to leave in the extension bag — never
20/// one set on the client — so there is deliberately no variant for it.
21///
22/// A backend that follows redirects internally reports `Internal` and
23/// gives up what `Client`'s stage does per hop: `SENSITIVE_HEADERS`
24/// stripped across an origin, cookies re-derived rather than carried, and
25/// the `AllowEarlyData` mark taken off. Answering the `3xx` to the caller
26/// instead — `Transparent` — keeps all of it.
27///
28/// Apple's `URLSession` is the worked example: a background session has no
29/// redirect hook to install and is `Internal`; a foreground one answers
30/// `nil` from
31/// `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`
32/// so the `3xx` becomes the response, and is `Transparent`.
33///
34/// # Adding a variant
35///
36/// This enum is deliberately not `#[non_exhaustive]` — see
37/// [`CancelSupport`] — so a new variant breaks an external `match`. That
38/// cost is the point: it should arrive **with** the backend that carries
39/// it. A `libcurl` backend (`CURLOPT_FOLLOWLOCATION` plus
40/// `CURLOPT_MAXREDIRS` is a genuinely declarative policy) or WinHTTP would
41/// be candidates, and both would also need the seam to start carrying the
42/// merged policy.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum RedirectSupport {
45    /// No redirects, and nothing to observe.
46    ///
47    /// The conservative base: `Capabilities::default()` returns this same
48    /// value, so "the backend said nothing about redirects" and "the
49    /// backend said `None`" are the same observation. This is exactly why
50    /// "3xx arrives as-is" gets its own `Transparent`: conflating "the
51    /// field wasn't filled in" with a substantive claim about backend
52    /// behavior means having a capability that lies.
53    #[default]
54    None,
55    /// The backend doesn't follow redirects itself: the 3xx arrives at us
56    /// as an ordinary response, and following the chain is the job of the
57    /// redirect stage in `Client`.
58    ///
59    /// Not the same as `None`, even though `Capabilities::default()` also
60    /// returns `None`: here redirects are fully observable and controllable,
61    /// just not by the backend. `RedirectPolicy` works and does exactly
62    /// what it promises.
63    ///
64    /// This is what `wasi:http` does: the `3xx` reaches the guest as-is.
65    ///
66    /// **Reported by** `hclient-wasi`, `hclient-h3`, `hclient-native` and
67    /// `hclient-urlsession` — the last by refusing each hop in its
68    /// delegate, which is a choice the platform allows rather than one it
69    /// makes; see that crate's own doc.
70    Transparent,
71    /// The backend follows redirects itself; we neither control nor see it.
72    ///
73    /// **The example is in this workspace**: `hclient-fetch` reports this
74    /// variant. A browser's `fetch()` with `redirect: "follow"` (the
75    /// default, and the only thing that crate ever sends — its
76    /// `convert.rs` never calls `RequestInit::set_redirect`) follows the
77    /// redirect inside the browser, and the JS code sees only the final
78    /// response, with no way to intercept the intermediate hops.
79    ///
80    /// For such a backend, `Client`'s redirect stage will never see a
81    /// single 3xx, and whatever `RedirectPolicy` was set would be a silent
82    /// no-op. So `check_supported` **does** check this field
83    /// (`hclient/src/config.rs`, `check_redirect_supported`): a
84    /// `RedirectPolicy` the caller actually asked for — client-level at
85    /// `build()`, or per-request at `execute()`, whichever is in effect —
86    /// against an `Internal` backend is an `UnsupportedCapability { what:
87    /// "redirect_policy" }`, not a setting that quietly does nothing. A
88    /// caller who configured nothing is unaffected: that is why
89    /// `Config::redirect` is an `Option`.
90    ///
91    /// It is also the variant an `hclient-urlsession` **background** session
92    /// must report, and there it is forced rather than chosen: the redirect
93    /// delegate is not called for background tasks at all.
94    Internal,
95}
96
97/// Whether dropping the future returned by
98/// [`Transport::execute`](crate::unversioned::Transport::execute) stops the
99/// exchange — see that method's doc comment for the contract itself, of
100/// which this enum is the one honest way out.
101///
102/// # Why two variants and not three
103///
104/// A third variant could split `Supported` by who performs the
105/// cancellation: the transport tearing down a socket it owns, versus the
106/// transport asking an ambient host to stop. It is not here because no
107/// caller decision turns on the difference. A capability answers a question
108/// the caller actually asks — here, "can I rely on a drop ending the
109/// exchange?" — and *who* ends it is an implementation detail. Both shapes
110/// give a guarantee of exactly the same strength, including its limit:
111/// bytes already sent are already sent, and the server may have acted on
112/// them either way.
113///
114/// The distinction is worth knowing even though it is not worth a variant:
115/// `hclient-native` owns the socket and closes it itself, while
116/// `hclient-fetch` and `hclient-wasi` ask the browser and the `wasi:http`
117/// host — `AbortController::abort()` and the Component Model's
118/// `subtask.cancel`. Only the first kind can pool connections, which is
119/// why the pool lives in `hclient-native` and nowhere else.
120///
121/// Not `#[non_exhaustive]`, deliberately: no other enum in this file is,
122/// and consistency across the capability set is worth more than reserving
123/// the right to add a variant to this one alone.
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
125pub enum CancelSupport {
126    /// Dropping the future does not stop the exchange: it may run to
127    /// completion, unobserved, on a connection this transport no longer
128    /// reports on.
129    ///
130    /// The conservative base — [`Capabilities::default()`] returns this — and
131    /// here, unlike [`RedirectSupport::None`], that costs nothing. **A
132    /// default must never be stronger than the truth**: a backend that
133    /// never touches this field is read as "do not rely on a drop stopping
134    /// anything",
135    /// which is the safe reading of silence and is also exactly what a
136    /// backend that genuinely cannot cancel means. The two coincide, so
137    /// there is nothing to tell apart — whereas for redirects they did not:
138    /// `None` there is a substantive "redirects are impossible", which is a
139    /// far stronger claim than "the field was not filled in", and a
140    /// transparent backend forced to say it was misread.
141    #[default]
142    None,
143    /// Dropping the future stops the exchange, as far as this transport
144    /// controls it.
145    ///
146    /// What that does and does not promise is the contract on
147    /// [`Transport::execute`](crate::unversioned::Transport::execute); the
148    /// short version is that our side stops, and the server's side is not
149    /// ours to promise anything about.
150    Supported,
151}
152
153/// Whether a request may travel over a connection an earlier request
154/// already used, or whether every request opens a socket of its own.
155///
156/// # Why two variants and not three
157///
158/// The v0.2 design document asked for three, on
159/// [`RedirectSupport`]'s precedent: reuse that is ours and configurable,
160/// reuse that belongs to an ambient host and is not ours to control
161/// (`hclient-fetch`, `hclient-wasi`), and none. The middle one is not here,
162/// and the reason is a sharper reading of [`RedirectSupport`] than "the
163/// owner differs".
164///
165/// [`RedirectSupport::Internal`] earns its variant because
166/// `check_supported` **refuses** on it: `ClientBuilder::redirect` exists,
167/// it is a portable, client-level setting, and a backend that follows
168/// redirects internally would silently ignore it — so the variant is what
169/// turns a silent no-op into an `UnsupportedCapability`. That is a caller
170/// decision, made by code that can be pointed at.
171///
172/// No such setting exists for reuse. The pool is configured on the
173/// concrete transport that owns it (`hclient_native::Native::pool`),
174/// because a pool's idle timeout is a property of a connection between
175/// requests and not of any one request — so there is nothing for
176/// `check_supported` to refuse, and a caller holding a generic `T:
177/// Transport` learns nothing actionable from *who* keeps the connection
178/// alive. The question the design document itself named — "are my requests
179/// going over reused connections, because it changes how I batch work" —
180/// is answered by the two variants below, and adding "who owns it" would
181/// re-add exactly the axis [`CancelSupport`] rejected one capability
182/// earlier.
183///
184/// **The condition under which the third variant arrives**, written down
185/// so the next reader does not have to re-derive it: as soon as there is a
186/// portable, client-level pool setting that a host-managed backend would
187/// have to reject, the variant arrives *together with that setting and
188/// with its arm in `check_supported`* — the same order in which
189/// [`RedirectSupport::Transparent`] arrived, once a backend existed that
190/// was being misread without it. Not before: a variant no caller can
191/// branch on is a distinction the capability set has to carry forever for
192/// nothing.
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
194pub enum ReuseSupport {
195    /// Every request opens a new connection, and closes it when it is done.
196    ///
197    /// The conservative base — [`Capabilities::default()`] returns this — and,
198    /// as with [`CancelSupport::None`], silence and the substantive claim
199    /// coincide: a caller who reads this plans for a handshake per request,
200    /// which is exactly what a backend that never filled the field in will
201    /// give them.
202    #[default]
203    None,
204    /// Requests to the same origin may travel over a connection an earlier
205    /// request already used.
206    ///
207    /// Says nothing about *when* one is reused — that depends on what is
208    /// idle at the moment, and no caller can predict it per request. What
209    /// it does promise is the thing a caller batches on: a second request
210    /// to an origin need not pay for a TCP and TLS handshake again.
211    Supported,
212}
213
214/// Whether the transport hands back a response body it has already
215/// decoded, or the bytes exactly as the server put them on the wire.
216///
217/// The question a caller asks of this is "must I reverse a
218/// `Content-Encoding` myself, and may I ask for one?" — both halves at
219/// once, because they are one fact about the transport. `hclient`'s
220/// `Client` is that caller: it reads this field and nothing else to decide
221/// whether to advertise `Accept-Encoding` and whether to decode.
222///
223/// # Why this is NOT read off `forbidden_request_headers`
224///
225/// `hclient-fetch` lists [`http::header::ACCEPT_ENCODING`] among its
226/// forbidden request headers, and it also decompresses internally, so on
227/// that one backend the two answers coincide — which is exactly what makes
228/// deriving one from the other tempting and wrong. "This header cannot be
229/// sent" and "the body reaching you is already decoded" are different
230/// claims: a transport that forbids the header while decompressing nothing
231/// is perfectly coherent (a proxy-shaped backend that pins its own
232/// `Accept-Encoding`, say), and a client that inferred "already decoded"
233/// from "header forbidden" would hand that caller compressed bytes
234/// labelled as plaintext. That is the "capability that lies" defect this
235/// workspace has caught four times, which is why this is its own field.
236///
237/// The reverse inference is just as wrong and is the one `Client`
238/// implements: a `None` transport that forbids `Accept-Encoding` gets no
239/// header from us and still gets its response decoded, because a
240/// `Content-Encoding` the server applied unbidden is still ours to reverse.
241///
242/// # Why two variants and not three
243///
244/// [`CancelSupport`]'s rule, applied a third time: a variant exists only
245/// if a caller decision turns on it. The third variant that suggests
246/// itself is "the transport can decompress, if asked" — configurable
247/// rather than automatic. No transport in this workspace or outside it
248/// works that way today, and there is no client-level setting for it to
249/// answer: `Client` does not offer "decompress, but at the transport
250/// layer". A variant no caller can branch on is a distinction the
251/// capability set carries forever for nothing.
252///
253/// **The condition under which it arrives**, on
254/// [`RedirectSupport::Transparent`]'s precedent: together with the setting
255/// that asks for it and its arm in `check_supported`, once a backend
256/// exists that is being misread without it. Not before.
257///
258/// # Silence and the substantive claim coincide here
259///
260/// [`Self::None`] is what [`Capabilities::default()`] returns, so "the
261/// backend never filled this in" and "the backend hands the bytes over
262/// untouched" are the same value — and, as with [`CancelSupport::None`]
263/// and [`ReuseSupport::None`], that costs nothing, because the two mean
264/// the same thing to a caller: decode it yourself. The
265/// [`RedirectSupport`] problem, where `None` was a strictly stronger claim
266/// than silence and a `Transparent` backend was misread for lack of a
267/// third value, does not arise.
268///
269/// Not `#[non_exhaustive]`, for consistency with every other enum in this
270/// file.
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
272pub enum DecompressionSupport {
273    /// The response body arrives exactly as it came off the wire: a
274    /// `Content-Encoding` the server applied is still applied, and
275    /// reversing it belongs to whoever reads the body.
276    ///
277    /// The conservative base — [`Capabilities::default()`] returns this — and
278    /// the honest answer for every transport that moves bytes rather than
279    /// interpreting them: `hclient-native` (hyper hands the body through
280    /// as it arrives) and `hclient-wasi` (`wasi:http` 0.3 defines no
281    /// content-coding behaviour of its own) are both this.
282    #[default]
283    None,
284    /// The transport decodes `Content-Encoding` itself, before a single
285    /// byte reaches us, and chooses what to ask for — so `Accept-Encoding`
286    /// is not ours to set either, and decoding again would corrupt every
287    /// compressed response.
288    ///
289    /// Named after [`RedirectSupport::Internal`], and for the same shape
290    /// of reason: the backend does it, we neither control nor see it. The
291    /// example is again the browser — `hclient-fetch` reports this,
292    /// derived from the same in-crate fact its `Body::size_hint` already
293    /// rests on (a `Content-Length` under a `Content-Encoding` describes
294    /// bytes this transport never yields, because the browser has already
295    /// reversed the coding).
296    ///
297    /// Note what this does NOT promise: that the response headers were
298    /// tidied up afterwards. `fetch` leaves `Content-Encoding` and
299    /// `Content-Length` on the response describing the wire, not the body
300    /// you get — which is precisely why the size hint has to distrust
301    /// them.
302    Internal,
303}
304
305#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
306pub enum TlsSupport {
307    #[default]
308    None,
309    ServerTrustCallbackOnly,
310    Full,
311}
312
313#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
314pub struct TimeoutSupport {
315    /// Whether [`Timeouts::resolve`] is enforced. Honestly `false` on
316    /// every ambient backend: `wasi:http` and `fetch` do the resolving
317    /// inside the host, so there is no moment for a client to bound.
318    pub resolve: bool,
319    pub connect: bool,
320    pub first_byte: bool,
321    pub between_bytes: bool,
322}
323
324/// The timeout triple — `wasi:http`'s shape, the richest of the ambient
325/// models.
326///
327/// Collapses to a single `AbortController` in fetch; on native it splits
328/// into connector / response-wait / body-idle. A single `Duration` throws
329/// away information the WASI backend knows how to use.
330///
331/// Lives in `hclient-core` because transports read it from the request's
332/// `http::Extensions`, and they don't depend on `hclient`.
333#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
334pub struct Timeouts {
335    /// A bound on **getting an address to try**, separate from the connect
336    /// budget that follows it.
337    ///
338    /// # What it bounds, which is not a phase boundary
339    ///
340    /// Happy Eyeballs interleaves resolution with connecting on purpose —
341    /// the resolver is a `Stream` and `hclient-native` starts connecting to
342    /// the first address while the rest are still arriving — so there is no
343    /// instant at which *resolution finished*, and a bound on one would
344    /// have nothing to attach to. What this bounds is the wait for the
345    /// **first** address from either family, which is exactly the failure a
346    /// caller cannot otherwise diagnose: a resolver that hangs looks like
347    /// an origin that is unreachable, and only the first is worth a
348    /// different retry.
349    ///
350    /// It therefore does **not** apply where the connection does not depend
351    /// on the resolver — an IP literal, and an HTTPS record carrying
352    /// address hints, both of which give a connector somewhere to go
353    /// without an answer.
354    ///
355    /// # Why not simply a smaller `connect`
356    ///
357    /// Because the two answer different questions and a caller who cares
358    /// wants both: `resolve` says *how long may I wait to learn where to
359    /// go*, `connect` says *and how long may going there take*. Folding
360    /// them loses which one failed, which is the whole gap. Overlapping
361    /// budgets are the caller's to reconcile; nothing here subtracts one
362    /// from the other, because a resolver that answered in 10 ms has not
363    /// spent any of the connect budget in any sense a connector can see.
364    pub resolve: Option<core::time::Duration>,
365    pub connect: Option<core::time::Duration>,
366    pub first_byte: Option<core::time::Duration>,
367    pub between_bytes: Option<core::time::Duration>,
368}
369
370/// Whether a transport can put a request into TLS 1.3 early data (0-RTT).
371///
372/// # This is the floor, and it says less than it looks like
373///
374/// [`Self::Supported`] means only *"this transport is able to offer early
375/// data"*. It never means a particular request went into early data, and it
376/// never means one was accepted. In QUIC the acceptance verdict arrives
377/// **after the response** — measured at 8.63 ms against a response at
378/// 8.58 ms — so it is a future, not a
379/// property of a transport, and nothing about it can live in a value that
380/// [`Transport::capabilities`](crate::unversioned::Transport::capabilities)
381/// determines once at construction.
382///
383/// # Why the default is `None` with unusual force
384///
385/// Every other capability here follows the rule that a default must not be
386/// stronger than the truth, and the cost of breaking it is a buffered copy,
387/// a lost optimisation, or — for `full_duplex` — a deadlock. This one costs
388/// **replay exposure**: early data is data an attacker who captured it can
389/// send again, at a moment of their choosing, to a server that will act on
390/// it. So [`Capabilities::default()`] reports `None`, every transport that
391/// ships today reports `None`, and a transport that forgets this field
392/// reports `None`.
393///
394/// # Reporting `Supported` is not sufficient to put anything in early data
395///
396/// It is necessary and nothing more. The gate is the caller's, per request
397/// — see [`AllowEarlyData`] — and a transport that reports `Supported` must
398/// still refuse to place a request the caller did not mark.
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
400pub enum EarlyDataSupport {
401    /// This transport never offers early data. The conservative base, what
402    /// [`Capabilities::default()`] returns, and the honest answer for every
403    /// transport in this workspace except `hclient-h3`.
404    #[default]
405    None,
406    /// This transport can offer early data for a request the caller has
407    /// marked with [`AllowEarlyData`]. See this enum's doc for the three
408    /// things it still does not mean.
409    Supported,
410}
411
412/// The caller's per-request statement that this request may go into TLS 1.3
413/// early data (0-RTT).
414///
415/// Put into `http::Extensions` on the request. Absent, the request waits
416/// for the handshake to complete, and **there is no configuration in which
417/// a request the caller did not mark ends up in early data**. Present
418/// against a transport reporting [`EarlyDataSupport::None`], it is a typed
419/// [`UnsupportedCapability`] rather than a silent no-op.
420///
421/// # What marking a request asserts, and what it does not
422///
423/// **It is an assertion that replaying this request is SAFE — not that
424/// replaying it is POSSIBLE.** Those are different questions, and only the
425/// caller can answer the first one.
426///
427/// [`RequestBody::retry_kind`](crate::RequestBody::retry_kind) answers the
428/// second: `Free`, `ViaFactory`, `Impossible` — *can I send these bytes
429/// again*. A transport needs that answer, because a rejected 0-RTT request
430/// has to be replayed after the handshake and a
431/// [`RetryKind::Impossible`](crate::RetryKind::Impossible) body cannot be.
432/// So `RetryKind` is a **correctness** precondition here, and it is checked
433/// as one.
434///
435/// It is emphatically **not** a safety condition, and reading it as one is
436/// the mistake to avoid. `POST /transfer` with a
437/// fully buffered body is `RetryKind::Free` — trivially replayable, and
438/// precisely the request that must never enter early data, because *an
439/// attacker* can replay it too. quinn says the same in one line: *"this
440/// enables transmission of 0-RTT data, which is vulnerable to replay
441/// attacks, and should therefore never invoke non-idempotent operations"*.
442///
443/// The notion that would answer the safety question — method safety and
444/// idempotency — deliberately does not exist in this codebase, and its
445/// absence is written down where the one v0.2 retry lives. RFC 8470 §2 puts
446/// the default on the conservative side (*"clients MAY send requests with
447/// safe HTTP methods … and MUST NOT send unsafe methods (or methods whose
448/// safety is not known) in early data"*) and, in the same sentence, says
449/// why a method table cannot be the whole answer: *"absent other
450/// information"*. `GET` is not safe on plenty of real APIs, and only the
451/// caller knows which. Hence this extension: **a caller-visible decision,
452/// with a method check beneath it, rather than a table hidden in a
453/// transport.**
454///
455/// # The third failure path
456///
457/// A request placed in early data can fail in three places, not one: no
458/// usable key material (nothing was risked, fall back silently), the server
459/// rejecting the 0-RTT keys (replay on the same connection once the
460/// handshake finishes — the transport's job, invisible to the caller), and
461/// **HTTP `425 Too Early`** (RFC 8470 §5.2), which arrives a full round
462/// trip later and must be retried *not* in early data. The third is a
463/// status-code branch in the client, not in a transport.
464///
465/// **A retry built for a `425` must remove this extension from the request
466/// it replays.** RFC 8470 requires it, and it is not a formality: on
467/// `hclient-h3` this mark is part of the connection pool's key, so a
468/// replay that kept it would ask for the early-data connection and — if
469/// that one has been evicted or closed since — would open a fresh one and
470/// go out in early data again, to the server that just refused to risk it.
471/// See `hclient_h3::early`.
472///
473/// # The other boundary: an origin
474///
475/// The mark does not cross one, and `hclient`'s redirect stage drops it on
476/// the same condition that drops `Cookie` and `Authorization` — the host or
477/// scheme changed.
478///
479/// The asymmetry is the point and the two halves are easy to conflate. This
480/// is a claim about what a request does **at a server**, so a caller who
481/// marked a request for origin A never judged origin B, and carrying it
482/// across would act on a judgement nobody made. A *method* change is the
483/// opposite case and the mark stays: a `303` rewriting `POST` to `GET`
484/// leaves a request strictly less consequential than the one already
485/// vouched for.
486#[derive(Debug, Clone, Copy, PartialEq, Eq)]
487pub struct AllowEarlyData;
488
489/// The caller's per-request statement that this request needs a particular
490/// HTTP version, and must fail rather than go out over another one.
491///
492/// Put into `http::Extensions` on the request, and read by the transport at
493/// the moment the protocol becomes known — which is **before the head is
494/// written**, on every transport here that honours one. Absent, the
495/// transport picks as it always did.
496///
497/// # It is [`AllowEarlyData`]'s mechanism with the polarity reversed
498///
499/// Same shape: a mark in the request's extensions that a transport reads
500/// and acts on before sending, `Copy`, defined in this crate because
501/// transports read it and do not depend on `hclient`. The difference is
502/// that one is a permission and this is a requirement, and that difference
503/// is why both have to be per request rather than per client — see below.
504///
505/// # Why a demand and not a question
506///
507/// [`Capabilities::full_duplex`] and its neighbours report the **floor**:
508/// the value that holds on the worst protocol a transport might negotiate.
509/// That is right for a static answer and cannot be otherwise — Cargo
510/// unifies features across a graph, so a library built on `hclient` can
511/// never know whether some other crate turned `http2` on — but it leaves a
512/// caller who genuinely needs HTTP/2 with no way to act.
513///
514/// The two answers that do not work:
515///
516/// - **Per response.** `Response::version()` already answers it, honestly,
517///   and *after the fact*. A caller structured for bidirectional streaming
518///   has to decide before it sends.
519/// - **Per connection.** There is no connection handle in the public API,
520///   so it means either a new seam or a query answered from a pool — and
521///   the pooled answer is racy in the way that matters: the entry can be
522///   evicted between the answer and the request that relied on it. It
523///   would be a fact about the past presented as a promise about the next
524///   request.
525///
526/// This is the third: the caller states the requirement, and the transport
527/// converts "the floor says no" into "this connection says yes" for one
528/// request, or fails it before committing to a shape that would deadlock.
529///
530/// # Why it cannot be a client-level setting
531///
532/// Turning an ALPN outcome into a request failure is **correct for gRPC**,
533/// whose RPC cannot proceed over HTTP/1.1 at all, and **wrong for a
534/// browser-shaped client**, which should degrade quietly. Only the caller
535/// knows which of the two it is — the same argument that put
536/// [`AllowEarlyData`] in the caller's hands rather than in a transport's
537/// configuration.
538///
539/// # Exact match, deliberately, not a minimum
540///
541/// `RequireVersion(HTTP_2)` is satisfied by HTTP/2 and by nothing else. It
542/// is tempting to read it as "at least", and there is no ordering that
543/// makes that mean anything: a caller who needs h2 framing does not want
544/// HTTP/3 instead, and a caller who needs HTTP/1.1 — to keep an upgrade
545/// path open, say — wants strictly less than HTTP/2, not more. A "minimum"
546/// reading would satisfy the first demand with the wrong protocol and be
547/// unable to express the second at all.
548///
549/// # Refusal, and the two shapes it takes
550///
551/// - The **backend cannot honour demands at all**
552///   ([`Capabilities::version_select`] is `false` — `hclient-fetch` and
553///   `hclient-wasi`, neither of which chooses or even learns the version):
554///   a typed [`UnsupportedCapability`] from `Client`, the same arm a
555///   `RedirectPolicy` against
556///   [`RedirectSupport::Internal`] takes. It fires whatever version was
557///   demanded, because the backend cannot answer for any of them.
558/// - The **backend honours demands and this connection does not match**:
559///   a typed [`VersionNotAvailable`] under
560///   [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported), raised by
561///   the transport before the head goes out.
562///
563/// A transport that always speaks one version still *honours* demands —
564/// `hclient-h3` reports `version_select: true` and answers
565/// `RequireVersion(HTTP_3)` by proceeding and everything else with
566/// [`VersionNotAvailable`]. Reporting `false` there would refuse the one
567/// demand it trivially satisfies.
568///
569/// # The origin boundary, and why this one crosses it
570///
571/// [`AllowEarlyData`] comes off on a cross-origin redirect, because
572/// "replaying this is safe" is a claim about what a request does *at a
573/// server* and the caller judged only the first one. **This mark is not
574/// that kind of claim.** It is a statement about the caller's own code —
575/// "the thing I am about to do needs this protocol" — and it is equally
576/// true at hop 1 and at hop 4. Dropping it across an origin would mean a
577/// redirect could silently deliver over HTTP/1.1 exactly the request that
578/// said it could not use HTTP/1.1, which is the failure the mark exists to
579/// prevent, arriving through the one door left open.
580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
581pub struct RequireVersion(pub http::Version);
582
583/// A [`RequireVersion`] demand the connection in hand does not satisfy.
584///
585/// Carries both halves, because "HTTP/2 was required" and "HTTP/1.1 is
586/// what this connection negotiated" are separately actionable — the first
587/// is the caller's own request coming back, the second is a fact about the
588/// server or the TLS configuration.
589///
590/// One type in this crate rather than one per backend (the shape
591/// `hclient_h3::RequestTrailersNotSent` takes), because a caller
592/// downcasting on it must not have to know which transport is underneath:
593/// the demand is portable, so its refusal is too.
594#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
595#[error(
596    "the request required {required:?} and this connection negotiated {negotiated:?}; \
597     it was refused before the head was written"
598)]
599pub struct VersionNotAvailable {
600    pub required: http::Version,
601    pub negotiated: http::Version,
602}
603
604/// The one comparison, shared by every transport that honours a demand.
605///
606/// `Ok(())` when there is no demand or `negotiated` satisfies it; a typed
607/// [`VersionNotAvailable`] under
608/// [`ErrorKind::Unsupported`](crate::ErrorKind::Unsupported) otherwise.
609///
610/// A function here rather than a `==` at each call site so that the rule —
611/// exact match, absence means no demand — has one definition. Two
612/// transports enforce it today and they must not drift.
613///
614/// **What it does not do is decide *when* to call it.** That is the whole
615/// content of the guarantee: `check_version` at the wrong point is a check
616/// that reports a violation after the bytes are already gone. Each caller
617/// places it where the protocol is first known and no head has been
618/// written, and pins that placement with a test that asserts the server
619/// saw nothing.
620pub fn check_version(
621    extensions: &http::Extensions,
622    negotiated: http::Version,
623) -> Result<(), crate::Error> {
624    match extensions.get::<RequireVersion>() {
625        Some(&RequireVersion(required)) if required != negotiated => Err(crate::Error::new(
626            crate::ErrorKind::Unsupported,
627            VersionNotAvailable {
628                required,
629                negotiated,
630            },
631        )),
632        _ => Ok(()),
633    }
634}
635
636/// What the transport can do **in this process, right now**.
637///
638/// A runtime fact, not a `cfg!`: one wasm binary runs in both Chrome
639/// (streaming request body available since 131) and Safari (not available).
640///
641/// # Two kinds of field
642///
643/// Every field here is one of two things, and reading them as one kind is
644/// what makes a field like [`proxy`](Self::proxy) look dead when it is
645/// not.
646///
647/// - **A gate.** The field guards a setting a caller made on the
648///   *`Client`*, and `ClientBuilder::build` refuses when the transport
649///   cannot honour it — the model this whole type exists for, taken from
650///   `wasi:http`'s own setters returning
651///   `result<_, request-options-error::not-supported>`. A gate with no
652///   branch is the *silently ignored setting* defect, and this project has
653///   closed four of them: `redirects`, `owns_cookie_jar`, `owns_cache` and
654///   the `timeouts` triple each earned a branch the day the setting
655///   arrived.
656/// - **A report.** The field states a fact about the transport, and
657///   nothing at the client level could refuse it, because the setting it
658///   describes is configured *on the transport*. `proxy`, `client_certs`,
659///   `tls_config`, `early_data`, `connection_reuse`, `cancel_on_drop`,
660///   `full_duplex`, `streaming_request_body`, the two trailer flags and
661///   `version_reported` are all this kind. Its reader is the caller.
662///
663/// **A report is not a dead field.** `upgrade` was deleted for having no
664/// reader, and the difference is that its four variants encoded a
665/// distinction with one reachable side — where a report has both values
666/// reachable and answers a question only it can answer.
667///
668/// The classification is enforced rather than described:
669/// `every_capability_is_a_gate_or_a_report` in this module destructures
670/// the struct with no `..`, so a field added later is a compile error
671/// until somebody decides which kind it is.
672#[non_exhaustive]
673#[derive(Debug, Clone, Default)]
674pub struct Capabilities {
675    /// Whether a request body may be written as it is produced.
676    ///
677    /// Reported. `Client` does not gate on it — see this type's doc for
678    /// why some fields do and some do not — which is why
679    /// `hclient-urlsession` refuses a `Streaming` body with a typed error
680    /// of its own rather than relying on a check that does not happen.
681    pub streaming_request_body: bool,
682    /// Whether the response may begin arriving before the request body has
683    /// finished. Reported.
684    pub full_duplex: bool,
685    /// Reported.
686    pub request_trailers: bool,
687    /// Reported.
688    pub response_trailers: bool,
689    /// Who follows a redirect — see [`RedirectSupport`].
690    ///
691    /// **A gate**: `RedirectPolicy` and a redirect predicate are `Client`
692    /// settings, and [`RedirectSupport::Internal`] means the transport has
693    /// already followed the chain by the time anything is handed back, so
694    /// either setting would silently not apply.
695    pub redirects: RedirectSupport,
696    /// What dropping an in-flight `execute` future does — see
697    /// [`CancelSupport`] and the contract on
698    /// [`Transport::execute`](crate::unversioned::Transport::execute).
699    pub cancel_on_drop: CancelSupport,
700    /// Whether a connection is reused across requests — see
701    /// [`ReuseSupport`].
702    pub connection_reuse: ReuseSupport,
703    /// Whether the transport already decoded the response body's
704    /// `Content-Encoding` — see [`DecompressionSupport`].
705    pub response_decompression: DecompressionSupport,
706    /// Whether the transport can put a marked request into TLS 1.3 early
707    /// data — see [`EarlyDataSupport`], which says less than its name
708    /// suggests and says so at length.
709    pub early_data: EarlyDataSupport,
710    /// What TLS configuration this transport accepts — see [`TlsSupport`].
711    ///
712    /// **Reported, not a gate.** A `Client` has no TLS setting to refuse:
713    /// the trust store, the client certificate and the ALPN list are all
714    /// configured on the `TlsConnect` a transport was built with. See this
715    /// type's own doc for the two kinds of field.
716    pub tls_config: TlsSupport,
717    /// Whether the TLS configuration this transport holds presents a
718    /// client certificate.
719    ///
720    /// Reported, for [`tls_config`](Self::tls_config)'s reason. Read off
721    /// `TlsIdentity::presents_client_certs` by the backends rather than
722    /// from a constant, which is what stopped one connector giving two
723    /// answers depending on which stack held it.
724    pub client_certs: bool,
725    /// Whether this transport sends through a proxy.
726    ///
727    /// **Reported, and it will never be a gate.** The
728    /// setting it would guard is `Native::proxy`, which is on the
729    /// transport that would answer the question, so there is nothing at
730    /// the client level to refuse. That makes it unlike
731    /// [`owns_cookie_jar`](Self::owns_cookie_jar), where the client owns
732    /// the setting and the transport owns the conflict.
733    ///
734    /// It is not [`upgrade`](https://docs.rs/hclient-core)'s case either,
735    /// the four-variant enum deleted for having no reader: both values
736    /// here are reachable, and the reader is the caller — *will my
737    /// requests go through a proxy* is a question a diagnostic asks and
738    /// only this field answers.
739    pub proxy: bool,
740    /// Whether the transport keeps its own cookie jar: attaching `Cookie`
741    /// to outgoing requests and processing `Set-Cookie` on incoming ones,
742    /// without being asked.
743    ///
744    /// `true` for `hclient-fetch` — the browser does both, and `Cookie` is
745    /// on that backend's `forbidden_request_headers`, so a client-side jar
746    /// there would not merely be redundant, it would send every cookie
747    /// twice and store every `Set-Cookie` twice. `false` for
748    /// `hclient-native` and `hclient-wasi`.
749    ///
750    /// # Why a `bool` and not an enum
751    ///
752    /// The same question [`CancelSupport`] and [`ReuseSupport`] were made
753    /// to answer: a variant exists only if a caller decision turns on it.
754    /// This field answers exactly one decision — "do I run a jar of my own
755    /// for this transport?" — and it is binary. The two axes an enum would
756    /// add do not carry decisions:
757    ///
758    /// - *Who* owns it (the browser, an ambient host) is the split
759    ///   [`CancelSupport`] already rejected once, for the same reason.
760    /// - Attaching versus storing could in principle come apart, and in
761    ///   practice never has: a backend that attaches cookies it did not
762    ///   store, or stores cookies it will not attach, is not a shape any
763    ///   of the three backends here or any ambient HTTP API takes.
764    ///
765    /// What it does *not* answer — deliberately, and this is where a third
766    /// state would arrive if it ever arrives — is whether a jar-owning
767    /// backend can be asked to stop, or its jar inspected. There is no
768    /// portable setting for either, so there is nothing to refuse. When a
769    /// client-level cookie setting exists, it earns its refusal here the
770    /// way [`RedirectSupport::Internal`] earned its variant: the setting,
771    /// the variant and the `check_supported` arm arrive together.
772    pub owns_cookie_jar: bool,
773    /// Whether the transport keeps its own HTTP response cache: serving a
774    /// stored response instead of sending, and storing what it fetches,
775    /// without being asked.
776    ///
777    /// `true` for `hclient-fetch` — the browser has an HTTP cache and
778    /// applies it inside `fetch()`. `false` for `hclient-native`,
779    /// `hclient-h3` and `hclient-wasi`, none of which stores a response
780    /// anywhere. `wasi:http`'s host may well have a cache; the guest
781    /// cannot see it, and a capability is a claim about what this code
782    /// does rather than about what is downstream of it — the same line
783    /// `owns_cookie_jar` holds for the same backend.
784    ///
785    /// # This field had no reader for four verticals
786    ///
787    /// It shipped in v0.1 as `false` everywhere but one backend, branched
788    /// on nowhere, and was on the same list `version_select` was rescued
789    /// from — *a variant exists only if a caller decision turns on it*. The
790    /// decision that arrived is `ClientBuilder::cache`, and a client-side
791    /// cache against a transport reporting `true` is an
792    /// [`UnsupportedCapability`] at `build()`, the same arm
793    /// `owns_cookie_jar` takes for a jar and [`RedirectSupport::Internal`]
794    /// takes for a redirect policy.
795    ///
796    /// # Why a `bool` and not an enum
797    ///
798    /// [`Self::owns_cookie_jar`]'s answer, one field up, applies verbatim:
799    /// this field settles exactly one decision — *do I run a cache of my
800    /// own for this transport?* — and it is binary. *Who* owns it is the
801    /// split [`CancelSupport`] rejected; storing versus serving could in
802    /// principle come apart and in practice never has.
803    ///
804    /// What it deliberately does **not** answer is whether a cache-owning
805    /// backend can be asked to bypass, revalidate or clear. There is no
806    /// portable setting for any of the three — `fetch()`'s `cache` option
807    /// is a browser API a `Transport` seam has no counterpart for — so
808    /// there is nothing to refuse. That is where a third state would
809    /// arrive if it ever arrives.
810    pub owns_cache: bool,
811    /// Whether the transport honours a per-request [`RequireVersion`]
812    /// demand: reads it, and either serves the request over that version
813    /// or fails it with [`VersionNotAvailable`] **before the head is
814    /// written**.
815    ///
816    /// # It says "honours", not "chooses"
817    ///
818    /// A transport that only ever speaks one version reports `true` if it
819    /// answers demands — `hclient-h3` does, by proceeding on
820    /// `RequireVersion(HTTP_3)` and refusing everything else. Reporting
821    /// `false` there would make `Client` refuse the one demand it
822    /// trivially satisfies, which is the opposite of honest.
823    ///
824    /// `false` is for a transport that cannot answer at all:
825    /// `hclient-fetch` and `hclient-wasi` neither select the version nor
826    /// learn it (both also report `version_reported: false`), so a demand
827    /// against either becomes an [`UnsupportedCapability`] from `Client` —
828    /// the same arm a `RedirectPolicy` against
829    /// [`RedirectSupport::Internal`] takes.
830    ///
831    /// # Why this field exists
832    ///
833    /// The rule is that *a capability exists only if a caller decision
834    /// turns on it* — `RedirectSupport` lost two variants to it.
835    /// [`RequireVersion`] is the decision this one answers, and it is the
836    /// reason the demand and this
837    /// field's first `true` land in one change.
838    pub version_select: bool,
839    /// Whether `Response::version()` is something the transport observed.
840    ///
841    /// `false` says the value on the response is `http`'s builder default
842    /// standing in for a fact the backend never learned — the browser will
843    /// not tell a page which protocol it spoke, and `wasi:http@0.3.0` has
844    /// no version concept at all.
845    ///
846    /// The observability seam asks the same question one field over and
847    /// answers it in the event rather than here, because a
848    /// [`Hooks`](crate::unversioned::Hooks) impl is handed an
849    /// [`Event`](crate::unversioned::Event) and no capabilities:
850    /// [`Head::version`](crate::unversioned::Head::version) is `Some`
851    /// exactly when this field is `true`. Two spellings of one fact, in
852    /// the two places that can each be read on their own.
853    pub version_reported: bool,
854    pub timeouts: TimeoutSupport,
855    pub informational_1xx: bool,
856    pub forbidden_request_headers: &'static [HeaderName],
857}
858
859/// A setting the chosen transport cannot honor.
860///
861/// Returned from `build()` rather than silently ignored. The model is
862/// wasi:http itself, whose setters return `request-options-error::not-supported`.
863#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
864#[error("backend `{backend}` does not support `{what}`")]
865pub struct UnsupportedCapability {
866    pub what: &'static str,
867    pub backend: &'static str,
868}
869
870#[cfg(test)]
871mod tests {
872    use super::*;
873    use std::error::Error as StdError;
874
875    /// **Every field is a gate or a report, and adding one without saying
876    /// which is a compile error.**
877    ///
878    /// The distinction is [`Capabilities`]' own doc; this is what keeps it
879    /// from going stale. Destructured with no `..` rest pattern —
880    /// `#[non_exhaustive]` blocks that only from outside the crate, which
881    /// is why the classification has to live here rather than in
882    /// `hclient`, where the branches themselves are.
883    ///
884    /// The lists are asserted against each other rather than merely
885    /// written: a field named in both, or in neither, fails a line.
886    #[test]
887    fn every_capability_is_a_gate_or_a_report() {
888        let c = Capabilities::default();
889        let Capabilities {
890            // ── gates: a `Client` setting the transport can refuse ──
891            //
892            // Each of these has a branch in `hclient::caps::check_supported` and
893            // a test naming the setting it refuses.
894            redirects,
895            response_decompression,
896            owns_cookie_jar,
897            owns_cache,
898            version_select,
899            timeouts,
900            forbidden_request_headers,
901            // `informational_1xx` is a gate in the other direction: no
902            // `Client` setting turns it on, and what it guards is a
903            // *claim* — `Native::hooks` clears it, because a transport
904            // reporting `true` while reporting nothing is a capability
905            // that lies.
906            informational_1xx,
907
908            // ── reports: a fact whose setting lives on the transport ──
909            streaming_request_body,
910            full_duplex,
911            request_trailers,
912            response_trailers,
913            cancel_on_drop,
914            connection_reuse,
915            early_data,
916            tls_config,
917            client_certs,
918            proxy,
919            version_reported,
920        } = &c;
921
922        // The gates, at their conservative base: each is the value that
923        // refuses a caller's setting rather than silently dropping it.
924        assert_eq!(*redirects, RedirectSupport::None);
925        assert_eq!(*response_decompression, DecompressionSupport::None);
926        assert!(!owns_cookie_jar);
927        assert!(!owns_cache);
928        assert!(!version_select);
929        assert!(!timeouts.resolve && !timeouts.connect);
930        assert!(!timeouts.first_byte && !timeouts.between_bytes);
931        assert!(forbidden_request_headers.is_empty());
932        assert!(!informational_1xx);
933
934        // The reports, likewise understated: a report that over-claims
935        // costs a caller correctness, one that under-claims costs an
936        // opportunity — the floor rule, which is why `none()` is the base
937        // every transport starts from.
938        assert!(!streaming_request_body);
939        assert!(!full_duplex);
940        assert!(!request_trailers);
941        assert!(!response_trailers);
942        assert_eq!(*cancel_on_drop, CancelSupport::None);
943        assert_eq!(*connection_reuse, ReuseSupport::None);
944        assert_eq!(*early_data, EarlyDataSupport::None);
945        assert_eq!(*tls_config, TlsSupport::None);
946        assert!(!client_certs);
947        assert!(!proxy);
948        assert!(!version_reported);
949    }
950
951    #[test]
952    fn the_default_is_the_conservative_base() {
953        // Every field, spelled out individually — not
954        // `assert_eq!` on the whole struct via a derived `PartialEq`, which
955        // `Capabilities` deliberately does not implement (it's
956        // `#[non_exhaustive]` so its shape stays ours to change, and a
957        // struct-wide `PartialEq` would be a public trait impl added purely
958        // for a test's convenience).
959        //
960        // Destructured with no `..` rest pattern — `#[non_exhaustive]` only
961        // blocks that from outside the crate, and this test lives inside it.
962        // The field count is deliberately not written here: the destructure
963        // below *is* the count, and unlike a number it cannot go stale.
964        // Per-field assertions would NOT catch a new field: adding one and
965        // setting it `true` in `none()` leaves them compiling and passing.
966        // Only the exhaustive destructure does — omitting a field from the
967        // pattern is a compile error naming it, because `..` is not there
968        // to absorb it silently.
969        let Capabilities {
970            streaming_request_body,
971            full_duplex,
972            request_trailers,
973            response_trailers,
974            redirects,
975            cancel_on_drop,
976            connection_reuse,
977            response_decompression,
978            early_data,
979            tls_config,
980            client_certs,
981            proxy,
982            owns_cookie_jar,
983            owns_cache,
984            version_select,
985            version_reported,
986            timeouts,
987            informational_1xx,
988            forbidden_request_headers,
989        } = Capabilities::default();
990        assert!(!streaming_request_body);
991        assert!(!full_duplex);
992        assert!(!request_trailers);
993        assert!(!response_trailers);
994        assert_eq!(redirects, RedirectSupport::None);
995        assert_eq!(cancel_on_drop, CancelSupport::None);
996        assert_eq!(connection_reuse, ReuseSupport::None);
997        assert_eq!(response_decompression, DecompressionSupport::None);
998        assert_eq!(
999            early_data,
1000            EarlyDataSupport::None,
1001            "the one capability whose over-claim costs replay exposure rather \
1002             than a buffered copy"
1003        );
1004        assert_eq!(tls_config, TlsSupport::None);
1005        assert!(!client_certs);
1006        assert!(!proxy);
1007        assert!(!owns_cookie_jar);
1008        assert!(!owns_cache);
1009        assert!(!version_select);
1010        assert!(!version_reported);
1011        assert_eq!(
1012            timeouts,
1013            TimeoutSupport {
1014                resolve: false,
1015                connect: false,
1016                first_byte: false,
1017                between_bytes: false,
1018            }
1019        );
1020        assert!(!informational_1xx);
1021        assert!(forbidden_request_headers.is_empty());
1022    }
1023
1024    #[test]
1025    fn unsupported_names_both_the_feature_and_the_backend() {
1026        let e = UnsupportedCapability {
1027            what: "connect_timeout",
1028            backend: "wasi:http",
1029        };
1030        let msg = e.to_string();
1031        assert!(msg.contains("connect_timeout"), "{msg}");
1032        assert!(msg.contains("wasi:http"), "{msg}");
1033    }
1034
1035    #[test]
1036    fn timeout_support_is_per_phase_not_a_single_flag() {
1037        let t = TimeoutSupport {
1038            resolve: true,
1039            connect: true,
1040            first_byte: true,
1041            between_bytes: false,
1042        };
1043        assert!(t.connect && t.first_byte && !t.between_bytes);
1044    }
1045
1046    /// No mark, no opinion. The absence of a demand is the overwhelmingly
1047    /// common case and it must not cost a request anything, on any
1048    /// version — including the ones nothing in this workspace speaks, so
1049    /// that the rule is "absent means silent" rather than "absent means
1050    /// the ones we happened to list".
1051    #[test]
1052    fn an_unmarked_request_is_satisfied_by_every_version() {
1053        let e = http::Extensions::new();
1054        for v in [
1055            http::Version::HTTP_09,
1056            http::Version::HTTP_10,
1057            http::Version::HTTP_11,
1058            http::Version::HTTP_2,
1059            http::Version::HTTP_3,
1060        ] {
1061            assert!(check_version(&e, v).is_ok(), "{v:?}");
1062        }
1063    }
1064
1065    #[test]
1066    fn a_demand_the_connection_meets_passes() {
1067        let mut e = http::Extensions::new();
1068        e.insert(RequireVersion(http::Version::HTTP_2));
1069        assert!(check_version(&e, http::Version::HTTP_2).is_ok());
1070    }
1071
1072    /// The refusal carries both halves and is `Unsupported`, not `Other`:
1073    /// a caller sorting failures by `kind()` must be able to tell "this
1074    /// connection cannot do what I asked" from a genuine transport
1075    /// failure without a downcast.
1076    #[test]
1077    fn a_demand_the_connection_misses_is_a_typed_unsupported() {
1078        let mut e = http::Extensions::new();
1079        e.insert(RequireVersion(http::Version::HTTP_2));
1080        let err = check_version(&e, http::Version::HTTP_11).unwrap_err();
1081        assert_eq!(*err.kind(), crate::ErrorKind::Unsupported);
1082        let named = StdError::source(&err)
1083            .and_then(|s| s.downcast_ref::<VersionNotAvailable>())
1084            .expect("the source must be the typed refusal, not an opaque string");
1085        assert_eq!(
1086            *named,
1087            VersionNotAvailable {
1088                required: http::Version::HTTP_2,
1089                negotiated: http::Version::HTTP_11,
1090            }
1091        );
1092    }
1093
1094    /// Exact match in **both** directions, and the second one is the
1095    /// interesting half: a caller demanding HTTP/1.1 — to keep an upgrade
1096    /// path open — must not be quietly served over HTTP/2 on the grounds
1097    /// that HTTP/2 is "newer". A `>=` comparison would pass this test's
1098    /// sibling above and fail here, which is why the pair is written out
1099    /// rather than parameterised into one loop over "mismatches".
1100    #[test]
1101    fn a_newer_version_does_not_satisfy_a_demand_for_an_older_one() {
1102        let mut e = http::Extensions::new();
1103        e.insert(RequireVersion(http::Version::HTTP_11));
1104        let err = check_version(&e, http::Version::HTTP_2).unwrap_err();
1105        assert_eq!(*err.kind(), crate::ErrorKind::Unsupported);
1106    }
1107
1108    /// The message names both versions. Not a `Display` assertion for its
1109    /// own sake: `VersionNotAvailable` reaches a log or a `{e}` far more
1110    /// often than it reaches a downcast, and a message naming only one of
1111    /// the two versions leaves the reader unable to tell which end was
1112    /// wrong.
1113    #[test]
1114    fn the_refusal_message_names_both_versions() {
1115        let msg = VersionNotAvailable {
1116            required: http::Version::HTTP_2,
1117            negotiated: http::Version::HTTP_11,
1118        }
1119        .to_string();
1120        assert!(msg.contains("HTTP/2.0"), "{msg}");
1121        assert!(msg.contains("HTTP/1.1"), "{msg}");
1122    }
1123}