Skip to main content

rust_web_server/early_hints/
mod.rs

1//! `103 Early Hints` (RFC 8297, 18.12.0, #38): `Link` headers a client can
2//! act on — preload a stylesheet, preconnect to an origin — while the
3//! response is still being made.
4//!
5//! A handler or middleware calls [`send`] at any point before it returns;
6//! the connection writes a `103` head straight away and the final response
7//! after it. [`EarlyHintsLayer`] sends a fixed list for every request it
8//! sees:
9//!
10//! ```rust,no_run
11//! use rust_web_server::app::App;
12//! use rust_web_server::core::New;
13//! use rust_web_server::early_hints::EarlyHintsLayer;
14//!
15//! let app = App::new().wrap(EarlyHintsLayer::new([
16//!     "</app.css>; rel=preload; as=style",
17//!     "<https://cdn.example.com>; rel=preconnect",
18//! ]));
19//! ```
20//!
21//! Sent on HTTP/2 and HTTP/3; on HTTP/1.1 only with
22//! `RWS_CONFIG_EARLY_HINTS_HTTP1=true` (browsers use them only over h2/h3,
23//! and some old HTTP/1.1 clients mishandle extra `1xx` responses), never on
24//! HTTP/1.0. The `http1`-only build writes them just before the final
25//! response. Where nothing can send them — [`TestClient`](crate::test_client),
26//! an app called directly — [`send`] does nothing and returns `false`.
27//! At most [`MAX_PER_REQUEST`] per request; a value with CR, LF or NUL is
28//! refused.
29
30use std::cell::RefCell;
31use std::sync::atomic::{AtomicUsize, Ordering};
32use std::sync::Arc;
33
34use crate::application::Application;
35use crate::middleware::Middleware;
36use crate::request::Request;
37use crate::response::Response;
38use crate::server::ConnectionInfo;
39
40/// `103` heads one request may send.
41pub const MAX_PER_REQUEST: usize = 4;
42
43/// Where one request's hints go: the connection that serves it.
44pub(crate) struct Sink {
45    deliver: Box<dyn Fn(Vec<String>) + Send + Sync>,
46    sent: AtomicUsize,
47}
48
49impl Sink {
50    pub(crate) fn new(deliver: impl Fn(Vec<String>) + Send + Sync + 'static) -> Arc<Sink> {
51        Arc::new(Sink { deliver: Box::new(deliver), sent: AtomicUsize::new(0) })
52    }
53
54    /// Start counting again (a sink reused for the next request).
55    pub(crate) fn reset(&self) {
56        self.sent.store(0, Ordering::Relaxed);
57    }
58
59    fn send(&self, links: Vec<String>) -> bool {
60        if self.sent.fetch_add(1, Ordering::Relaxed) >= MAX_PER_REQUEST {
61            return false;
62        }
63        (self.deliver)(links);
64        true
65    }
66}
67
68thread_local! {
69    static CURRENT: RefCell<Option<Arc<Sink>>> = const { RefCell::new(None) };
70}
71
72#[cfg(feature = "http2")]
73tokio::task_local! {
74    static TASK: Option<Arc<Sink>>;
75}
76
77/// Run `f` with `sink` as this thread's destination for [`send`] (the
78/// previous one restored afterwards, also on panic).
79pub(crate) fn with_sink<R>(sink: Option<Arc<Sink>>, f: impl FnOnce() -> R) -> R {
80    struct Restore(Option<Arc<Sink>>);
81    impl Drop for Restore {
82        fn drop(&mut self) {
83            let previous = self.0.take();
84            CURRENT.with(|c| *c.borrow_mut() = previous);
85        }
86    }
87    let previous = CURRENT.with(|c| std::mem::replace(&mut *c.borrow_mut(), sink));
88    let _restore = Restore(previous);
89    f()
90}
91
92/// Run `future` with `sink` as its destination for [`send`] (async
93/// handlers, the async forwarder).
94#[cfg(feature = "http2")]
95pub(crate) async fn scope<F: std::future::Future>(sink: Option<Arc<Sink>>, future: F) -> F::Output {
96    TASK.scope(sink, future).await
97}
98
99fn current() -> Option<Arc<Sink>> {
100    #[cfg(feature = "http2")]
101    if let Ok(Some(sink)) = TASK.try_with(|s| s.clone()) {
102        return Some(sink);
103    }
104    CURRENT.with(|c| c.borrow().clone())
105}
106
107/// Whether [`send`] would reach a client from here.
108pub fn available() -> bool {
109    current().is_some()
110}
111
112/// Send a `103 Early Hints` with these `Link` header values now, before the
113/// final response. `false` when there is nowhere to send it (see the module
114/// docs), the request already sent [`MAX_PER_REQUEST`], or a value is empty
115/// or holds CR, LF or NUL.
116pub fn send<S: AsRef<str>>(links: &[S]) -> bool {
117    send_from(Source::Handler, links)
118}
119
120/// Where a hint came from, for `rws_early_hints_total{source}`.
121#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub(crate) enum Source {
123    /// [`send`] called by application code.
124    Handler,
125    /// A fixed list: [`EarlyHintsLayer`] or a config route's `early_hints`.
126    Configured,
127    /// A backend's own `103`, passed on by a proxy.
128    Upstream,
129}
130
131impl Source {
132    fn label(self) -> &'static str {
133        match self {
134            Source::Handler => "handler",
135            Source::Configured => "configured",
136            Source::Upstream => "upstream",
137        }
138    }
139}
140
141static SENT: [AtomicUsize; 3] = [AtomicUsize::new(0), AtomicUsize::new(0), AtomicUsize::new(0)];
142
143pub(crate) fn send_from<S: AsRef<str>>(source: Source, links: &[S]) -> bool {
144    if links.is_empty() || !links.iter().all(|l| valid(l.as_ref())) {
145        return false;
146    }
147    let Some(sink) = current() else { return false };
148    let sent = sink.send(links.iter().map(|l| l.as_ref().to_string()).collect());
149    if sent {
150        SENT[source as usize].fetch_add(1, Ordering::Relaxed);
151    }
152    sent
153}
154
155/// The `Link` values of a backend's `103` head; empty for any other head.
156pub(crate) fn upstream_links(head: &[u8]) -> Vec<String> {
157    let text = String::from_utf8_lossy(head);
158    let mut lines = text.split("\r\n");
159    let is_103 = lines.next().and_then(|l| l.split(' ').nth(1)) == Some("103");
160    if !is_103 {
161        return Vec::new();
162    }
163    lines
164        .filter_map(|line| line.split_once(':'))
165        .filter(|(name, _)| name.trim().eq_ignore_ascii_case("link"))
166        .map(|(_, value)| value.trim().to_string())
167        .filter(|value| valid(value))
168        .collect()
169}
170
171/// Pass a backend's `103` on to the client, when the head is one and
172/// there is somewhere to send it.
173pub(crate) fn forward_upstream(head: &[u8]) {
174    let links = upstream_links(head);
175    if !links.is_empty() {
176        send_from(Source::Upstream, &links);
177    }
178}
179
180/// `rws_early_hints_total{source}`; empty until a hint is sent.
181pub(crate) fn prometheus_text() -> String {
182    let counts: Vec<usize> = SENT.iter().map(|c| c.load(Ordering::Relaxed)).collect();
183    if counts.iter().all(|n| *n == 0) {
184        return String::new();
185    }
186    let mut out = String::from("# HELP rws_early_hints_total 103 Early Hints responses sent, by where the hints came from\n# TYPE rws_early_hints_total counter\n");
187    for source in [Source::Handler, Source::Configured, Source::Upstream] {
188        out.push_str(&format!("rws_early_hints_total{{source=\"{}\"}} {}\n", source.label(), counts[source as usize]));
189    }
190    out
191}
192
193/// A usable `Link` value: not empty, no CR, LF or NUL.
194pub fn valid(link: &str) -> bool {
195    !link.trim().is_empty() && !link.bytes().any(|b| matches!(b, b'\r' | b'\n' | 0))
196}
197
198/// Whether this HTTP/1.x request may get `103`s: HTTP/1.1 with
199/// `early_hints_http1` on.
200pub(crate) fn http1_wanted(request: &Request) -> bool {
201    request.http_version == "HTTP/1.1" && crate::server_config::ServerConfig::current().early_hints_http1
202}
203
204/// The HTTP/1.1 `103` head for `links`.
205pub(crate) fn http1_head(links: &[String]) -> Vec<u8> {
206    let mut head = b"HTTP/1.1 103 Early Hints\r\n".to_vec();
207    for link in links {
208        head.extend_from_slice(b"Link: ");
209        head.extend_from_slice(link.as_bytes());
210        head.extend_from_slice(b"\r\n");
211    }
212    head.extend_from_slice(b"\r\n");
213    head
214}
215
216/// Middleware that sends the same `Link` hints for every request, then
217/// hands it on.
218#[derive(Clone, Debug)]
219pub struct EarlyHintsLayer {
220    links: Arc<Vec<String>>,
221}
222
223impl EarlyHintsLayer {
224    /// `links` are `Link` header values, e.g.
225    /// `</app.css>; rel=preload; as=style`. Invalid ones are dropped.
226    pub fn new<I, S>(links: I) -> EarlyHintsLayer
227    where
228        I: IntoIterator<Item = S>,
229        S: Into<String>,
230    {
231        EarlyHintsLayer { links: Arc::new(links.into_iter().map(Into::into).filter(|l: &String| valid(l)).collect()) }
232    }
233
234    pub fn links(&self) -> &[String] {
235        &self.links
236    }
237}
238
239impl Middleware for EarlyHintsLayer {
240    fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
241        if !self.links.is_empty() {
242            send_from(Source::Configured, &self.links);
243        }
244        next.execute(request, connection)
245    }
246
247    /// Queueing a hint never blocks.
248    fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
249        true
250    }
251
252    fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
253        true
254    }
255}
256
257#[cfg(test)]
258mod tests;