rust_web_server/early_hints/
mod.rs1use 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
40pub const MAX_PER_REQUEST: usize = 4;
42
43pub(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 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
77pub(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#[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
107pub fn available() -> bool {
109 current().is_some()
110}
111
112pub fn send<S: AsRef<str>>(links: &[S]) -> bool {
117 send_from(Source::Handler, links)
118}
119
120#[derive(Clone, Copy, Debug, PartialEq, Eq)]
122pub(crate) enum Source {
123 Handler,
125 Configured,
127 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
155pub(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
171pub(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
180pub(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
193pub fn valid(link: &str) -> bool {
195 !link.trim().is_empty() && !link.bytes().any(|b| matches!(b, b'\r' | b'\n' | 0))
196}
197
198pub(crate) fn http1_wanted(request: &Request) -> bool {
201 request.http_version == "HTTP/1.1" && crate::server_config::ServerConfig::current().early_hints_http1
202}
203
204pub(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#[derive(Clone, Debug)]
219pub struct EarlyHintsLayer {
220 links: Arc<Vec<String>>,
221}
222
223impl EarlyHintsLayer {
224 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 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;