Skip to main content

tachyon_web/server/
http.rs

1use crate::http::response::Body;
2#[cfg(feature = "tls")]
3use crate::server::TLS_HANDSHAKE_TIMEOUT;
4use crate::server::{IS_LOCAL_WORKER, REQUEST_TIMEOUT, Server};
5use bytes::Bytes;
6use hyper::body::{Body as HyperBody, Frame, SizeHint};
7use hyper::service::service_fn;
8use hyper::{Request, Response};
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12use std::task::{Context, Poll};
13use tokio::net::TcpListener;
14#[cfg(feature = "tls")]
15use tokio_rustls::TlsAcceptor;
16
17pin_project_lite::pin_project! {
18    /// Wraps an incoming request body with an absolute deadline for fully reading it.
19    ///
20    /// Bodies are streamed lazily now (see `hyper_handler`), so a handler that never
21    /// touches the body (e.g. an arity-0 route) would otherwise never be bounded by any
22    /// timeout — the client could send headers, declare a `Content-Length`, and simply
23    /// never send the body, holding the connection open indefinitely. This restores the
24    /// same bound `REQUEST_TIMEOUT` previously enforced by eager buffering, without
25    /// requiring anything to actually poll the body for it to apply.
26    ///
27    /// The `Sleep` itself is only allocated the first time `poll_frame` is actually
28    /// called, not eagerly per request: hyper's connection driver checks
29    /// `is_end_stream()` (forwarded straight to `inner`, below) before ever polling a
30    /// body it already knows is empty (e.g. any GET with no `Content-Length`), so an
31    /// eagerly-constructed timer would be registered and dropped unpolled on every
32    /// such request — a real timer-wheel allocation wasted on the single most common
33    /// request shape. Deferring construction to first poll costs nothing: a body that's
34    /// never polled was never at risk of stalling in the first place.
35    struct DeadlineBody {
36        #[pin]
37        inner: hyper::body::Incoming,
38        deadline: Option<Pin<Box<tokio::time::Sleep>>>,
39    }
40}
41
42impl HyperBody for DeadlineBody {
43    type Data = Bytes;
44    type Error = crate::http::error::Error;
45
46    fn poll_frame(
47        self: Pin<&mut Self>,
48        cx: &mut Context<'_>,
49    ) -> Poll<Option<Result<Frame<Self::Data>, Self::Error>>> {
50        let this = self.project();
51        let deadline = this
52            .deadline
53            .get_or_insert_with(|| Box::pin(tokio::time::sleep(REQUEST_TIMEOUT)));
54        if deadline.as_mut().poll(cx).is_ready() {
55            return Poll::Ready(Some(Err(crate::http::error::Error::Rejection {
56                status: hyper::StatusCode::REQUEST_TIMEOUT,
57                message: "Timed out reading request body".to_string(),
58            })));
59        }
60        match this.inner.poll_frame(cx) {
61            Poll::Ready(Some(Ok(frame))) => Poll::Ready(Some(Ok(frame))),
62            Poll::Ready(Some(Err(e))) => Poll::Ready(Some(Err(e.into()))),
63            Poll::Ready(None) => Poll::Ready(None),
64            Poll::Pending => Poll::Pending,
65        }
66    }
67
68    fn is_end_stream(&self) -> bool {
69        self.inner.is_end_stream()
70    }
71
72    fn size_hint(&self) -> SizeHint {
73        self.inner.size_hint()
74    }
75}
76
77#[cfg(feature = "http2")]
78#[derive(Clone, Copy, Debug)]
79struct LocalExecutor;
80
81#[cfg(feature = "http2")]
82impl<F> hyper::rt::Executor<F> for LocalExecutor
83where
84    F: Future + Send + 'static,
85    F::Output: Send + 'static,
86{
87    fn execute(&self, fut: F) {
88        IS_LOCAL_WORKER.with(|flag| {
89            if flag.get() {
90                drop(tokio::task::spawn_local(fut));
91            } else {
92                drop(tokio::spawn(fut));
93            }
94        });
95    }
96}
97
98impl<S> Server<S>
99where
100    S: Clone + Send + Sync + 'static,
101{
102    /// Serve HTTP/1.1 (and, with the `http2` feature, HTTP/2 over cleartext —
103    /// "h2c", detected via the connection preface with no ALPN needed) over
104    /// plaintext TCP on the given listener.
105    ///
106    /// Without the `http2` feature this uses `hyper::server::conn::http1::Builder`
107    /// directly — no protocol sniffing, no `auto` dispatch overhead. With it, it
108    /// uses `hyper_util`'s `auto::Builder`, which peeks at the first bytes of each
109    /// connection to detect an HTTP/2 client connection preface and falls back to
110    /// HTTP/1.1 otherwise. Either builder is constructed once and cloned per
111    /// connection (cheap: only pointer-sized fields).
112    ///
113    /// h2c has no browser support (browsers only ever negotiate HTTP/2 via TLS
114    /// ALPN) but is exactly what most non-browser HTTP/2 clients (gRPC, `curl
115    /// --http2-prior-knowledge`, many internal service meshes) expect when TLS is
116    /// terminated upstream (e.g. behind a load balancer) or simply not wanted.
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if FIPS compliance enforcement fails. Per-connection I/O
121    /// errors (accept failures, handshake failures, etc.) are logged and do not
122    /// terminate the accept loop.
123    pub async fn serve_http(self, listener: TcpListener) -> Result<(), std::io::Error> {
124        crate::server::enforce_fips_compliance()?;
125        let state = Arc::new(self);
126        let connection_semaphore = Arc::new(tokio::sync::Semaphore::new(state.max_connections));
127
128        // Build once outside the loop — `clone()` inside is a few pointer copies.
129        // Three cases, matching whichever of `http1`/`http2` are enabled (at least
130        // one always is — see the crate-level `compile_error!` in `lib.rs`):
131        #[cfg(all(feature = "http1", feature = "http2"))]
132        let builder = {
133            // Both enabled: `auto::Builder` sniffs each connection's first bytes
134            // for the HTTP/2 client preface and falls back to HTTP/1.1 otherwise.
135            let mut b = hyper_util::server::conn::auto::Builder::new(LocalExecutor);
136            let _ = b
137                .http1()
138                .timer(hyper_util::rt::TokioTimer::new())
139                .header_read_timeout(REQUEST_TIMEOUT)
140                .keep_alive(true)
141                .max_buf_size(8192)
142                .writev(true);
143            let _ = b
144                .http2()
145                .timer(hyper_util::rt::TokioTimer::new())
146                .initial_stream_window_size(65535)
147                .initial_connection_window_size(1024 * 1024)
148                .max_frame_size(16384)
149                .max_concurrent_streams(200)
150                .keep_alive_timeout(REQUEST_TIMEOUT);
151            // RFC 8441: let `ws::WebSocketUpgrade` accept WebSocket-over-HTTP/2 requests.
152            #[cfg(feature = "ws")]
153            let _ = b.http2().enable_connect_protocol();
154            b
155        };
156        #[cfg(all(feature = "http1", not(feature = "http2")))]
157        let builder = {
158            // http1 only: the low-level builder directly, no protocol-sniffing overhead.
159            let mut b = hyper::server::conn::http1::Builder::new();
160            let _ = b
161                .timer(hyper_util::rt::TokioTimer::new())
162                .header_read_timeout(REQUEST_TIMEOUT)
163                .keep_alive(true)
164                .max_buf_size(8192)
165                .writev(true);
166            b
167        };
168        #[cfg(all(feature = "http2", not(feature = "http1")))]
169        let builder = {
170            // http2 only: h2c with no HTTP/1.1 fallback at all — a client that
171            // isn't speaking HTTP/2 with prior knowledge simply fails to connect.
172            let mut b = hyper::server::conn::http2::Builder::new(LocalExecutor);
173            let _ = b
174                .timer(hyper_util::rt::TokioTimer::new())
175                .initial_stream_window_size(65535)
176                .initial_connection_window_size(1024 * 1024)
177                .max_frame_size(16384)
178                .max_concurrent_streams(200)
179                .keep_alive_timeout(REQUEST_TIMEOUT);
180            #[cfg(feature = "ws")]
181            let _ = b.enable_connect_protocol();
182            b
183        };
184
185        loop {
186            let Ok(permit) = connection_semaphore.clone().acquire_owned().await else {
187                break;
188            };
189
190            let (stream, peer) = match listener.accept().await {
191                Ok(c) => c,
192                Err(e) => {
193                    drop(permit);
194                    tracing::error!("[http] Accept error: {}", e);
195                    if crate::server::is_resource_exhaustion(&e) {
196                        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
197                    }
198                    continue;
199                }
200            };
201            let _ = stream.set_nodelay(true);
202            #[cfg(target_os = "linux")]
203            {
204                let sock_ref = socket2::SockRef::from(&stream);
205                let _ = sock_ref.set_tcp_quickack(true);
206            }
207            let state = state.clone();
208            let builder = builder.clone();
209
210            let serve_fut = async move {
211                let io = hyper_util::rt::TokioIo::new(stream);
212                let svc = service_fn(move |req| hyper_handler(state.clone(), req, peer));
213                #[cfg(all(feature = "http1", feature = "http2"))]
214                let result = builder.serve_connection_with_upgrades(io, svc).await;
215                #[cfg(all(feature = "http1", not(feature = "http2")))]
216                let result = builder.serve_connection(io, svc).with_upgrades().await;
217                #[cfg(all(feature = "http2", not(feature = "http1")))]
218                let result = builder.serve_connection(io, svc).await;
219                if let Err(e) = result {
220                    tracing::debug!("[http] Connection error: {}", e);
221                }
222                drop(permit);
223            };
224
225            IS_LOCAL_WORKER.with(|flag| {
226                if flag.get() {
227                    drop(tokio::task::spawn_local(serve_fut));
228                } else {
229                    drop(tokio::spawn(serve_fut));
230                }
231            });
232        }
233        Ok(())
234    }
235
236    /// Serve HTTP/1.1 and HTTP/2 over TLS (HTTPS) on the given listener and acceptor.
237    ///
238    /// # Errors
239    ///
240    /// Returns an error if FIPS compliance enforcement fails. Per-connection I/O
241    /// errors (accept failures, handshake failures, etc.) are logged and do not
242    /// terminate the accept loop.
243    #[cfg(feature = "tls")]
244    pub async fn serve_https(
245        self,
246        listener: TcpListener,
247        acceptor: TlsAcceptor,
248    ) -> Result<(), std::io::Error> {
249        crate::server::enforce_fips_compliance()?;
250        let state = Arc::new(self);
251        let connection_semaphore = Arc::new(tokio::sync::Semaphore::new(state.max_connections));
252
253        loop {
254            let Ok(permit) = connection_semaphore.clone().acquire_owned().await else {
255                break;
256            };
257
258            let (tcp_stream, peer) = match listener.accept().await {
259                Ok(c) => c,
260                Err(e) => {
261                    drop(permit);
262                    tracing::error!("[https] Accept error: {}", e);
263                    if crate::server::is_resource_exhaustion(&e) {
264                        tokio::time::sleep(std::time::Duration::from_millis(100)).await;
265                    }
266                    continue;
267                }
268            };
269            let _ = tcp_stream.set_nodelay(true);
270            #[cfg(target_os = "linux")]
271            {
272                let sock_ref = socket2::SockRef::from(&tcp_stream);
273                let _ = sock_ref.set_tcp_quickack(true);
274            }
275            let acceptor = acceptor.clone();
276            let state = state.clone();
277
278            let serve_fut = async move {
279                let tls_stream =
280                    match tokio::time::timeout(TLS_HANDSHAKE_TIMEOUT, acceptor.accept(tcp_stream))
281                        .await
282                    {
283                        Ok(Ok(stream)) => stream,
284                        Ok(Err(e)) => {
285                            tracing::debug!("[https] TLS handshake error: {}", e);
286                            drop(permit);
287                            return;
288                        }
289                        Err(_) => {
290                            tracing::debug!("[https] TLS handshake timed out");
291                            drop(permit);
292                            return;
293                        }
294                    };
295
296                // Inspect TLS connection ALPN before consuming the stream.
297                // Copy bytes out so the borrow ends before the move.
298                #[cfg(feature = "http2")]
299                let is_h2 = {
300                    let (_, connection) = tls_stream.get_ref();
301                    connection.alpn_protocol() == Some(b"h2")
302                };
303
304                let io = hyper_util::rt::TokioIo::new(tls_stream);
305                let svc = service_fn(move |req| hyper_handler(state.clone(), req, peer));
306
307                #[cfg(feature = "http2")]
308                if is_h2 {
309                    // Use low-level HTTP/2 connection builder
310                    let mut builder = hyper::server::conn::http2::Builder::new(LocalExecutor);
311                    let _ = builder
312                        .timer(hyper_util::rt::TokioTimer::new())
313                        .initial_stream_window_size(65535)
314                        .initial_connection_window_size(1024 * 1024)
315                        .max_frame_size(16384)
316                        .max_concurrent_streams(200)
317                        .keep_alive_timeout(REQUEST_TIMEOUT);
318                    #[cfg(feature = "ws")]
319                    let _ = builder.enable_connect_protocol();
320
321                    if let Err(e) = builder.serve_connection(io, svc).await {
322                        tracing::debug!("[https] HTTP/2 Connection error: {}", e);
323                    }
324                    drop(permit);
325                    return;
326                }
327
328                // Fallback path for a connection that didn't negotiate h2 over ALPN.
329                // With the `http1` feature this is the common case (HTTP/1.1 over
330                // TLS); without it, ALPN only ever advertised "h2" (see
331                // `alpn_protocols` in `server/mod.rs`), so a non-h2 connection here
332                // means a non-compliant client picked a protocol we didn't offer —
333                // there's no builder to serve it with, so the connection is dropped.
334                #[cfg(feature = "http1")]
335                {
336                    // Use low-level HTTP/1.1 connection builder (bypasses auto-negotiation overhead)
337                    let mut builder = hyper::server::conn::http1::Builder::new();
338                    let _ = builder
339                        .timer(hyper_util::rt::TokioTimer::new())
340                        .header_read_timeout(REQUEST_TIMEOUT)
341                        .keep_alive(true)
342                        .max_buf_size(8192);
343
344                    if let Err(e) = builder.serve_connection(io, svc).with_upgrades().await {
345                        tracing::debug!("[https] HTTP/1.1 Connection error: {}", e);
346                    }
347                }
348                drop(permit);
349            };
350
351            IS_LOCAL_WORKER.with(|flag| {
352                if flag.get() {
353                    drop(tokio::task::spawn_local(serve_fut));
354                } else {
355                    drop(tokio::spawn(serve_fut));
356                }
357            });
358        }
359        Ok(())
360    }
361
362    /// Serve HTTP/1.1 and HTTP/2 over TLS (HTTPS) on the given listener with a custom `rustls::ServerConfig`.
363    ///
364    /// # Errors
365    ///
366    /// Returns an error if FIPS compliance enforcement fails. Per-connection I/O
367    /// errors (accept failures, handshake failures, etc.) are logged and do not
368    /// terminate the accept loop.
369    #[cfg(feature = "tls")]
370    pub async fn serve_https_config(
371        self,
372        listener: TcpListener,
373        config: rustls::ServerConfig,
374    ) -> Result<(), std::io::Error> {
375        crate::server::enforce_fips_compliance()?;
376        let acceptor = TlsAcceptor::from(Arc::new(config));
377        self.serve_https(listener, acceptor).await
378    }
379}
380
381pub(super) async fn hyper_handler<S>(
382    state: Arc<Server<S>>,
383    req: Request<hyper::body::Incoming>,
384    peer: std::net::SocketAddr,
385) -> Result<Response<Body>, std::io::Error>
386where
387    S: Clone + Send + Sync + 'static,
388{
389    let (parts, incoming_body) = req.into_parts();
390
391    let body = if HyperBody::is_end_stream(&incoming_body) {
392        Body::empty()
393    } else {
394        Body::stream(DeadlineBody {
395            inner: incoming_body,
396            deadline: None,
397        })
398    };
399
400    let mut rebuild_req = Request::from_parts(parts, body);
401    #[cfg(feature = "original-uri")]
402    {
403        let orig_uri = rebuild_req.uri().clone();
404        rebuild_req
405            .extensions_mut()
406            .insert(crate::routing::extract::OriginalUri(orig_uri));
407    }
408    rebuild_req
409        .extensions_mut()
410        .insert(crate::routing::extract::ConnectInfo(peer));
411    rebuild_req
412        .extensions_mut()
413        .insert(crate::routing::extract::MaxBodySize(state.max_body_size));
414
415    let resp = state.router.handle_request(rebuild_req).await;
416
417    if let Some((min, max)) = state.response_jitter {
418        tokio::time::sleep(crate::server::jittered_delay(min, max)).await;
419    }
420
421    Ok(resp)
422}