Skip to main content

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//! By default, trillium-client auto-discovers the best HTTP version for each request:
35//!
36//! - Over `https://` with a TLS connector that advertises `h2` in ALPN *and* exposes the server's selection
37//!   back to trillium (the default for [`trillium_rustls::RustlsConfig`](https://docs.trillium.rs/trillium_rustls/struct.RustlsConfig.html)
38//!   and [`trillium_openssl::OpenSslConfig`](https://docs.trillium.rs/trillium_openssl/struct.OpenSslConfig.html)):
39//!   the server picks h2 or h1.1 during the TLS handshake. Whatever ALPN selects is what the client
40//!   uses.
41//! - Over `https://` with `h2` removed from the ALPN list (e.g. `RustlsConfig::without_http2()`):
42//!   h1 only.
43//! - Over `https://` with a TLS connector that doesn't surface ALPN selection
44//!   (`trillium_native_tls`): h1 only by default, since trillium can't tell whether the server
45//!   picked h2. Use the `Version::Http2` hint described below to force h2 over TLS in that case.
46//! - Over `https://` when the [`Client`] was built with
47//!   [`Client::new_with_quic`](Client::new_with_quic): the client may use h3 for origins that have
48//!   advertised it via [`Alt-Svc`][altsvc], that publish an `alpn=h3` SVCB/HTTPS DNS record (when
49//!   an encrypted resolver is configured — see [Encrypted DNS](#encrypted-dns)), or that the user
50//!   has hinted (see below).
51//! - Over `http://`: h1 only. There is no h2c probing without explicit prior knowledge.
52//!
53//! [altsvc]: https://datatracker.ietf.org/doc/html/rfc7838
54//!
55//! ### Prior-knowledge hints
56//!
57//! Setting [`Conn::http_version`](Conn::with_http_version) before sending the request
58//! signals **prior knowledge** of what the server speaks. By default no hint is set, which means
59//! "use auto-discovery." Setting any explicit version **pins** the protocol and suppresses
60//! auto-discovery — no Alt-Svc h3, no ALPN/pooled h2 promotion — and constrains the connection's
61//! ALPN to match (an h1 pin advertises only `http/1.1`, an h2 pin only `h2`), so the pin is honored
62//! over TLS rather than overridden by ALPN. The [`http_version`](Conn::http_version) accessor
63//! reports the unset default as [`Version::Http1_1`].
64//!
65//! | hint | URL scheme | behavior | curl equivalent |
66//! |---|---|---|---|
67//! | `Version::Http3` | `https` | Skip the [`Alt-Svc`][altsvc] cache and dial QUIC directly. Falls back to auto-discovery (h2 / h1) if QUIC connect fails. Requires [`Client::new_with_quic`](Client::new_with_quic). | `--http3` |
68//! | `Version::Http2` | `https` | TLS handshake advertising only `h2` in ALPN, then start the h2 driver immediately without checking the negotiated ALPN. **No fallback** — a non-h2-speaking server surfaces as an IO error. Also works with TLS connectors that don't surface ALPN selection. | (curl bundles this with `--http2-prior-knowledge`'s cleartext mode) |
69//! | `Version::Http2` | `http` | h2c immediate preface (cleartext h2 prior knowledge). **No fallback**. | `--http2-prior-knowledge` |
70//! | `Version::Http1_1` | any | Force HTTP/1.1: no h3 Alt-Svc, no h2 ALPN/pool promotion. | `--http1.1` |
71//! | `Version::Http1_0` | any | h1.0 wire format (no `Host`, no chunked encoding, etc.). | `--http1.0` |
72//! | _unset_ (default) | any | Auto-discovery as described above. | (default) |
73//!
74//! Hints are per-[`Conn`]; mix them freely on requests sharing one [`Client`].
75//!
76//! ### Forcing h1.1
77//!
78//! Set the [`Version::Http1_1`] hint on the request — the per-request equivalent of curl's
79//! `--http1.1`. It pins HTTP/1.1 even when the connector would otherwise negotiate h2 via ALPN or
80//! use h3 via Alt-Svc, by advertising only `http/1.1` in this connection's ALPN. (Over
81//! `trillium_native_tls`, which doesn't yet honor per-connection ALPN, the pin still skips h2/h3
82//! promotion but can't constrain the handshake — in practice harmless, since native-tls advertises
83//! no ALPN by default.) To opt out of h2 ALPN advertisement at the connection level for *all*
84//! requests on a client, that remains a TLS configuration concern: use
85//! [`RustlsConfig::without_http2()`](https://docs.trillium.rs/trillium_rustls/struct.RustlsConfig.html#method.without_http2)
86//! (or the equivalent on whichever TLS crate you're using) when constructing the
87//! [`Client`].
88//!
89//! ## WebSockets and WebTransport
90//!
91//! With the `websockets` cargo feature, `Conn::into_websocket` transforms a built conn into
92//! a `WebSocketConn` (RFC 6455 over h1, RFC 8441 extended CONNECT over h2). With the
93//! `webtransport` cargo feature, `Client::webtransport(url)` + `Conn::into_webtransport()`
94//! open a multiplexed WebTransport-over-h3 session (RFC 9220 +
95//! draft-ietf-webtrans-http3). Multiple WebTransport sessions to the same origin coalesce
96//! onto a single underlying QUIC connection — see the `webtransport` module for details.
97//!
98//! ## Server-Sent Events
99//!
100//! With the `sse` cargo feature, [`Conn::into_sse`](sse) executes a request and reads the
101//! response body as a `text/event-stream`, returning an [`EventStream`] — a [`Stream`] of
102//! [`Event`]s parsed per the [SSE specification][sse-spec]. Unlike the WebSocket and WebTransport
103//! upgrades, SSE is not a protocol switch: an event stream is an ordinary response whose body is
104//! read incrementally, so it works the same over HTTP/1.x, HTTP/2, and HTTP/3. This is a
105//! single-response stream — it ends when the connection closes and does not implement the
106//! [`EventSource`][es] automatic-reconnection behavior. See the [`sse`] module for details.
107//!
108//! [`Stream`]: https://docs.rs/futures-core/latest/futures_core/stream/trait.Stream.html
109//! [sse-spec]: https://html.spec.whatwg.org/multipage/server-sent-events.html
110//! [es]: https://developer.mozilla.org/en-US/docs/Web/API/EventSource
111//!
112//! ## Encrypted DNS
113//!
114//! With the `hickory` cargo feature, the client can route all of its DNS through an encrypted
115//! resolver of your choice rather than sending plaintext queries to the operating system's
116//! resolver. `Client::with_doh` uses DNS-over-HTTPS ([RFC 8484]), `Client::with_dot` DNS-over-TLS
117//! ([RFC 7858]), and `Client::with_doq` DNS-over-QUIC ([RFC 9250]); a client uses at most one, and
118//! a later call replaces an earlier one. DoH lookups ride the client's own connection pool, so they
119//! reuse and multiplex like any other request. A single resolution is cached and shared across
120//! HTTP/1, HTTP/2, and HTTP/3.
121//!
122//! Resolution is fail-closed: once a resolver is configured, a lookup it can't answer fails the
123//! request rather than falling back to the system resolver, so a query never leaks to a (possibly
124//! plaintext) local resolver. The resolver's own host is the one exception — it's resolved once via
125//! the underlying connector to bootstrap the connection; give the resolver as an IP address to skip
126//! even that.
127//!
128//! SVCB and HTTPS DNS records ([RFC 9460]) are fetched too, letting a server advertise HTTP/3
129//! support directly in DNS. A domain publishing `alpn=h3` is reached over HTTP/3 on the first
130//! request by an HTTP/3-capable client ([`Client::new_with_quic`]), with no [`Alt-Svc`][altsvc]
131//! round-trip. The connection to a DoH resolver itself negotiates h1/h2 by default;
132//! `Client::with_doh3` pins it to HTTP/3 for resolvers that serve DoH over HTTP/3 without
133//! advertising it. `with_dot` requires a TLS connector and `with_doq` an HTTP/3-capable client.
134//!
135//! [RFC 8484]: https://www.rfc-editor.org/rfc/rfc8484
136//! [RFC 7858]: https://www.rfc-editor.org/rfc/rfc7858
137//! [RFC 9250]: https://www.rfc-editor.org/rfc/rfc9250
138//! [RFC 9460]: https://www.rfc-editor.org/rfc/rfc9460
139
140#[cfg(test)]
141#[doc = include_str!("../README.md")]
142mod readme {}
143mod client;
144mod client_handler;
145mod conn;
146mod conn_handler_ext;
147#[cfg(feature = "hickory")]
148mod dns;
149mod h3;
150mod into_url;
151mod pool;
152mod response_body;
153#[cfg(feature = "sse")]
154pub mod sse;
155mod util;
156#[cfg(feature = "websockets")]
157pub mod websocket;
158#[cfg(feature = "webtransport")]
159pub mod webtransport;
160
161pub use client::Client;
162pub use client_handler::ClientHandler;
163#[cfg(any(feature = "serde_json", feature = "sonic-rs"))]
164pub use conn::ClientSerdeError;
165pub use conn::{Conn, USER_AGENT, UnexpectedStatusError};
166pub use conn_handler_ext::ConnExt;
167pub use into_url::IntoUrl;
168// open an issue if you have a reason for pool to be public
169pub(crate) use pool::Pool;
170pub use response_body::ResponseBody;
171#[cfg(feature = "sse")]
172pub use sse::{Event, EventStream, SseError, SseErrorKind};
173pub use trillium_http::{
174    Body, BodySource, Error, HeaderName, HeaderValue, HeaderValues, Headers, KnownHeaderName,
175    Method, Result, Status, Version,
176};
177pub use trillium_server_common::{
178    ArcedConnector, ArcedQuicClientConfig, Connector, QuicClientConfig, Url, url,
179};
180#[cfg(feature = "websockets")]
181pub use trillium_websockets::{WebSocketConfig, WebSocketConn, async_tungstenite, tungstenite};
182#[cfg(feature = "websockets")]
183pub use websocket::WebSocketUpgradeError;
184
185#[cfg(all(feature = "serde_json", feature = "sonic-rs"))]
186compile_error!("cargo features \"serde_json\" and \"sonic-rs\" are mutually exclusive");
187
188#[cfg(feature = "serde_json")]
189#[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
190pub use serde_json::{Value, json};
191#[cfg(feature = "sonic-rs")]
192#[cfg_attr(docsrs, doc(cfg(feature = "sonic-rs")))]
193pub use sonic_rs::{Value, json};
194
195/// constructs a new [`Client`] -- alias for [`Client::new`]
196pub fn client(connector: impl Connector) -> Client {
197    Client::new(connector)
198}