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}