trillium_client/lib.rs
1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![forbid(unsafe_code)]
3#![deny(
4 clippy::dbg_macro,
5 missing_copy_implementations,
6 rustdoc::missing_crate_level_docs,
7 missing_debug_implementations,
8 missing_docs,
9 nonstandard_style,
10 unused_qualifications
11)]
12
13//! trillium client is an HTTP client that uses the same `conn` approach as
14//! [`trillium`](https://trillium.rs) but which can be used
15//! independently for any HTTP client application.
16//!
17//! ## Connector
18//!
19//! [`trillium_client::Client`](Client) is built with a [`Connector`]. Each runtime crate
20//! ([`trillium_smol`](https://docs.trillium.rs/trillium_smol),
21//! [`trillium_tokio`](https://docs.trillium.rs/trillium_tokio),
22//! [`trillium_async_std`](https://docs.trillium.rs/trillium_async_std)) offers
23//! a Connector implementation, which can optionally be combined with a
24//! tls crate such as
25//! [`trillium_rustls`](https://docs.trillium.rs/trillium_rustls),
26//! [`trillium_native_tls`](https://docs.trillium.rs/trillium_native_tls), or
27//! [`trillium_openssl`](https://docs.trillium.rs/trillium_openssl).
28//!
29//! See the documentation for [`Client`] and [`Conn`] for further usage
30//! examples.
31//!
32//! ## Protocol selection
33//!
34//! Each request picks its HTTP version by four rules:
35//!
36//! 1. **Reuse before establish, best protocol first.** A live pooled connection to the origin is
37//! used before a new one is opened, preferring HTTP/3, then HTTP/2, then HTTP/1.1.
38//! 2. **New connections need prior knowledge for h3, and ALPN for h2.** A new connection uses
39//! HTTP/3 only when the origin is known to speak it: an `Http3` hint, an [`Alt-Svc`][altsvc]
40//! header from an earlier response, or an `alpn=h3` SVCB/HTTPS DNS record (see
41//! [Encrypted DNS](#encrypted-dns)). This requires a client built with
42//! [`Client::new_with_quic`]. Otherwise, over `https://` the server chooses h2 or h1.1 during
43//! the TLS handshake and the client uses whatever ALPN selected. Over `http://` the client
44//! speaks HTTP/1.1, unless h2 is hinted.
45//! 3. **A hint is where to start, not where to stop.** If the hinted protocol can't be reached (an
46//! h3 endpoint that doesn't answer) or can't carry the request (an h2 or h3 peer without
47//! extended CONNECT for a websocket handshake), the client continues to the next protocol down.
48//! [`Conn::with_strict_http_version`] turns that continuation into an error.
49//! 4. **The URL scheme never changes.** Continuing to an earlier protocol stays on the same scheme:
50//! h3 continues to h2 or h1.1 over TLS, and cleartext h2 continues to cleartext h1.1. Nothing is
51//! ever downgraded from TLS to cleartext.
52//!
53//! ```text
54//! ┌─ h3 known (hint, Alt-Svc, DNS) ─► QUIC ─ok─► HTTP/3 ─┐
55//! │ │fail │ can't carry
56//! request ────┤ ▼ │ the request
57//! ├─ pooled h2 ───────────────────► HTTP/2 ──────────────┤
58//! │ ▲ │
59//! └─ new connection ── ALPN h2 ────────┘ ▼
60//! │ ALPN http/1.1, or cleartext HTTP/1.1 (new
61//! ▼ connection)
62//! HTTP/1.1
63//! ```
64//!
65//! Over `https://` with a TLS connector that doesn't surface ALPN selection
66//! (`trillium_native_tls`), the client can't tell whether the server picked h2, so it uses h1.1
67//! unless h2 is hinted. To opt out of h2 for every request on a client, remove it from the TLS
68//! configuration's ALPN list (for example `RustlsConfig::without_http2()`).
69//!
70//! [altsvc]: https://datatracker.ietf.org/doc/html/rfc7838
71//!
72//! ### Version hints
73//!
74//! [`Conn::with_http_version`] names the protocol to try first. It also constrains the new
75//! connection's ALPN to match, so the hint is honored over TLS rather than overridden by the
76//! server's ALPN choice. The [`http_version`](Conn::http_version) accessor reports the unset
77//! default as [`Version::Http1_1`]. Hints are per-[`Conn`]; mix them freely on requests sharing
78//! one [`Client`].
79//!
80//! | hint | behavior | curl equivalent |
81//! |---|---|---|
82//! | `Version::Http3` | Dial QUIC directly, skipping the Alt-Svc cache. Continues to h2 / h1.1 if the QUIC connection fails. | `--http3` |
83//! | `Version::Http2` over `https` | TLS handshake advertising only `h2`, then the h2 preface without checking ALPN. Works with TLS connectors that don't surface ALPN. A server that doesn't speak h2 surfaces as an IO error: the preface commits the connection. | `--http2-prior-knowledge` |
84//! | `Version::Http2` over `http` | Cleartext h2 (h2c) preface. Same commitment as above. | `--http2-prior-knowledge` |
85//! | `Version::Http1_1` | HTTP/1.1 only: no h3, no h2. | `--http1.1` |
86//! | `Version::Http1_0` | HTTP/1.0 wire format (no `Host`, no chunked encoding). | `--http1.0` |
87//! | _unset_ | Rules 1 and 2 above. | (default) |
88//!
89//! ### Strict mode
90//!
91//! [`Conn::with_strict_http_version`] (or [`Client::with_strict_http_version`] for every conn)
92//! makes a request fail when the protocol it was matched to can't carry it, instead of
93//! continuing to an earlier protocol. Off by default. It applies to the websocket handshake
94//! below; the h2 prior-knowledge commitment and the h3 connection-failure continuation are the
95//! same either way.
96//!
97//! ## WebSockets and WebTransport
98//!
99//! With the `websockets` cargo feature, `Conn::into_websocket` performs a websocket handshake
100//! and returns a `WebSocketConn`. Over HTTP/1.1 this is the RFC 6455 `Upgrade` handshake; over
101//! HTTP/2 and HTTP/3 it is an extended CONNECT (RFC 8441, RFC 9220). The version follows the
102//! rules above: a server that speaks h2 or h3 but does not advertise extended CONNECT is retried
103//! as an HTTP/1.1 upgrade on a new connection, or fails under strict mode. With the
104//! `webtransport` cargo feature, `Client::webtransport(url)` + `Conn::into_webtransport()`
105//! open a multiplexed WebTransport-over-h3 session (RFC 9220 +
106//! draft-ietf-webtrans-http3); WebTransport exists only on HTTP/3, so those conns are strict.
107//! Multiple WebTransport sessions to the same origin coalesce
108//! onto a single underlying QUIC connection — see the `webtransport` module for details.
109//!
110//! ## Server-Sent Events
111//!
112//! With the `sse` cargo feature, [`Conn::into_sse`](sse) executes a request and reads the
113//! response body as a `text/event-stream`, returning an [`EventStream`] — a [`Stream`] of
114//! [`Event`]s parsed per the [SSE specification][sse-spec]. Unlike the WebSocket and WebTransport
115//! upgrades, SSE is not a protocol switch: an event stream is an ordinary response whose body is
116//! read incrementally, so it works the same over HTTP/1.x, HTTP/2, and HTTP/3. This is a
117//! single-response stream — it ends when the connection closes and does not implement the
118//! [`EventSource`][es] automatic-reconnection behavior. See the [`sse`] module for details.
119//!
120//! [`Stream`]: https://docs.rs/futures-core/latest/futures_core/stream/trait.Stream.html
121//! [sse-spec]: https://html.spec.whatwg.org/multipage/server-sent-events.html
122//! [es]: https://developer.mozilla.org/en-US/docs/Web/API/EventSource
123//!
124//! ## Encrypted DNS
125//!
126//! With the `hickory` cargo feature, the client can route all of its DNS through an encrypted
127//! resolver of your choice rather than sending plaintext queries to the operating system's
128//! resolver. `Client::with_doh` uses DNS-over-HTTPS ([RFC 8484]), `Client::with_dot` DNS-over-TLS
129//! ([RFC 7858]), and `Client::with_doq` DNS-over-QUIC ([RFC 9250]); a client uses at most one, and
130//! a later call replaces an earlier one. DoH lookups ride the client's own connection pool, so they
131//! reuse and multiplex like any other request. A single resolution is cached and shared across
132//! HTTP/1, HTTP/2, and HTTP/3.
133//!
134//! Resolution is fail-closed: once a resolver is configured, a lookup it can't answer fails the
135//! request rather than falling back to the system resolver, so a query never leaks to a (possibly
136//! plaintext) local resolver. The resolver's own host is the one exception — it's resolved once via
137//! the underlying connector to bootstrap the connection; give the resolver as an IP address to skip
138//! even that.
139//!
140//! SVCB and HTTPS DNS records ([RFC 9460]) are fetched too, letting a server advertise HTTP/3
141//! support directly in DNS. A domain publishing `alpn=h3` is reached over HTTP/3 on the first
142//! request by an HTTP/3-capable client ([`Client::new_with_quic`]), with no [`Alt-Svc`][altsvc]
143//! round-trip. The connection to a DoH resolver itself negotiates h1/h2 by default;
144//! `Client::with_doh3` pins it to HTTP/3 for resolvers that serve DoH over HTTP/3 without
145//! advertising it. `with_dot` requires a TLS connector and `with_doq` an HTTP/3-capable client.
146//!
147//! [RFC 8484]: https://www.rfc-editor.org/rfc/rfc8484
148//! [RFC 7858]: https://www.rfc-editor.org/rfc/rfc7858
149//! [RFC 9250]: https://www.rfc-editor.org/rfc/rfc9250
150//! [RFC 9460]: https://www.rfc-editor.org/rfc/rfc9460
151
152#[cfg(test)]
153#[doc = include_str!("../README.md")]
154mod readme {}
155mod client;
156mod client_handler;
157mod conn;
158mod conn_handler_ext;
159#[cfg(feature = "hickory")]
160mod dns;
161mod h3;
162mod into_url;
163mod pool;
164mod reaper;
165mod response_body;
166#[cfg(feature = "sse")]
167pub mod sse;
168mod util;
169#[cfg(feature = "websockets")]
170pub mod websocket;
171#[cfg(feature = "webtransport")]
172pub mod webtransport;
173
174pub use client::Client;
175pub use client_handler::ClientHandler;
176#[cfg(any(feature = "serde_json", feature = "sonic-rs"))]
177pub use conn::ClientSerdeError;
178pub use conn::{Conn, USER_AGENT, UnexpectedStatusError};
179pub use conn_handler_ext::ConnExt;
180pub use into_url::IntoUrl;
181// open an issue if you have a reason for pool to be public
182pub(crate) use pool::Pool;
183pub use response_body::ResponseBody;
184#[cfg(feature = "sse")]
185pub use sse::{Event, EventStream, SseError, SseErrorKind};
186pub use trillium_http::{
187 Body, BodySource, Error, HeaderName, HeaderValue, HeaderValues, Headers, KnownHeaderName,
188 Method, Result, Status, Version,
189};
190pub use trillium_server_common::{
191 ArcedConnector, ArcedQuicClientConfig, Connector, QuicClientConfig, Url, url,
192};
193#[cfg(feature = "websockets")]
194pub use trillium_websockets::{WebSocketConfig, WebSocketConn, async_tungstenite, tungstenite};
195#[cfg(feature = "websockets")]
196pub use websocket::WebSocketUpgradeError;
197
198#[cfg(all(feature = "serde_json", feature = "sonic-rs"))]
199compile_error!("cargo features \"serde_json\" and \"sonic-rs\" are mutually exclusive");
200
201#[cfg(feature = "serde_json")]
202#[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
203pub use serde_json::{Value, json};
204#[cfg(feature = "sonic-rs")]
205#[cfg_attr(docsrs, doc(cfg(feature = "sonic-rs")))]
206pub use sonic_rs::{Value, json};
207
208/// constructs a new [`Client`] -- alias for [`Client::new`]
209pub fn client(connector: impl Connector) -> Client {
210 Client::new(connector)
211}