Skip to main content

base_http_server/
lib.rs

1// Released under the MIT License.
2// Copyright, 2026, by Samuel Williams.
3
4//! Application handlers and supervised HTTP/1 connections over Base scheduler
5//! capabilities. All connection futures are owned and joined by the server.
6#![doc = include_str!("../readme.md")]
7#![forbid(unsafe_code)]
8
9use base_endpoint::stream::{ReadableStream, WritableStream};
10use base_http::{
11    InterimHandler, InterimResponses, Request, Response, Scheme, Status, body::ReadableBody,
12};
13use base_http1::{RequestReader, ResponseWriter};
14use base_scheduler::{
15    Cancellation, SocketAccept, SocketClose, SocketStreamReading, SocketStreamWriting,
16};
17use futures_util::{
18    future::{Either, select},
19    stream::{FuturesUnordered, StreamExt},
20};
21use std::{
22    fmt,
23    future::{Future, poll_fn},
24    pin::pin,
25    task::Poll,
26};
27
28/// Generic bodies and hooks keep handlers independent of the HTTP wire version
29/// and scheduler. A returned body may retain the request body, including borrows.
30pub trait Handler: Sync {
31    type Error: std::error::Error + Send + Sync + 'static;
32    type Body<B: ReadableBody>: ReadableBody;
33
34    fn call<B: ReadableBody, I: InterimHandler>(
35        &self,
36        request: Request<B>,
37        interim: &mut InterimResponses<I>,
38        cancellation: &Cancellation,
39    ) -> impl Future<Output = Result<Response<Self::Body<B>>, Self::Error>> + Send;
40}
41
42#[derive(Clone, Debug)]
43pub struct Options {
44    pub http1: base_http1::Options,
45    /// HTTP engine selection, shared by plain and encrypted establishment.
46    pub protocols: base_http_protocol::HttpProtocols,
47    pub max_connections: usize,
48}
49impl Default for Options {
50    fn default() -> Self {
51        Self {
52            http1: base_http1::Options::default(),
53            protocols: base_http_protocol::HttpProtocols::default(),
54            max_connections: 256,
55        }
56    }
57}
58
59/// A server failure. Protocol and negotiation errors are transparent; other wrapping variants describe this layer and report their cause as the source, so a cause chain never repeats a message.
60#[derive(Debug)]
61pub enum Error {
62    Protocol(base_http1::Error),
63    Handler(Box<dyn std::error::Error + Send + Sync>),
64    Io(std::io::Error),
65    Cancelled,
66    Negotiation(base_http_protocol::NegotiationError),
67}
68impl fmt::Display for Error {
69    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70        match self {
71            Self::Protocol(e) => e.fmt(f),
72            Self::Handler(_) => f.write_str("request handler failed"),
73            Self::Io(_) => f.write_str("HTTP server I/O failed"),
74            Self::Cancelled => f.write_str("HTTP server cancelled"),
75            Self::Negotiation(error) => error.fmt(f),
76        }
77    }
78}
79impl std::error::Error for Error {
80    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
81        match self {
82            Self::Protocol(e) => e.source(),
83            Self::Handler(e) => Some(e.as_ref()),
84            Self::Io(e) => Some(e),
85            Self::Cancelled => None,
86            Self::Negotiation(error) => error.source(),
87        }
88    }
89}
90impl From<base_http1::Error> for Error {
91    fn from(e: base_http1::Error) -> Self {
92        match e {
93            base_http1::Error::Cancelled => Self::Cancelled,
94            e => Self::Protocol(e),
95        }
96    }
97}
98impl From<std::io::Error> for Error {
99    fn from(e: std::io::Error) -> Self {
100        Self::Io(e)
101    }
102}
103impl From<base_scheduler::OperationError> for Error {
104    fn from(error: base_scheduler::OperationError) -> Self {
105        base_http1::Error::from(error).into()
106    }
107}
108
109async fn cancellable<F: Future>(
110    future: F,
111    cancellation: &Cancellation,
112) -> Result<F::Output, Error> {
113    cancellation.check().map_err(|_| Error::Cancelled)?;
114    if cancellation.is_never() {
115        return Ok(future.await);
116    }
117    let mut future = pin!(future);
118    let mut cancelled = pin!(cancellation.cancelled());
119    poll_fn(|cx| {
120        if cancelled.as_mut().poll(cx).is_ready() {
121            return Poll::Ready(Err(Error::Cancelled));
122        }
123        future.as_mut().poll(cx).map(Ok)
124    })
125    .await
126}
127
128/// Own one accepted socket through request admission, handler execution, output,
129/// uncancellable lane retirement, and finally exclusive socket closure. Await
130/// completion after cooperative cancellation; dropping this future is not a join.
131pub async fn serve_connection<S, H>(
132    scheduler: &S,
133    socket: S::Socket,
134    handler: &H,
135    options: &base_http1::Options,
136    cancellation: &Cancellation,
137) -> Result<u64, Error>
138where
139    S: SocketStreamReading + SocketStreamWriting + SocketClose,
140    S::Socket: 'static,
141    H: Handler,
142{
143    let outcome = connection_outcome(scheduler, socket, handler, options, cancellation).await;
144    outcome.result.map(|()| outcome.requests)
145}
146
147struct Outcome {
148    requests: u64,
149    result: Result<(), Error>,
150}
151
152async fn connection_outcome<S, H>(
153    scheduler: &S,
154    socket: S::Socket,
155    handler: &H,
156    options: &base_http1::Options,
157    cancellation: &Cancellation,
158) -> Outcome
159where
160    S: SocketStreamReading + SocketStreamWriting + SocketClose,
161    S::Socket: 'static,
162    H: Handler,
163{
164    let mut requests = 0;
165    // The outer owner closes the socket even when capability construction fails.
166    let result = async {
167        options.validate()?;
168        let mut reader = scheduler.socket_stream_reader(&socket)?;
169        let writer = match scheduler.socket_stream_writer(&socket) {
170            Ok(writer) => writer,
171            Err(error) => {
172                // Report the construction failure in preference to a retirement failure.
173                let _ = ReadableStream::close(&mut reader).await;
174                return Err(Error::Io(error));
175            }
176        };
177        // The plaintext engine is HTTP/1, whose codecs fail to construct only when `options.validate` above fails.
178        let (input, output) = base_http_protocol::HttpProtocols::default()
179            .plaintext()
180            .server(reader, writer, options.clone())
181            .expect("validated HTTP/1 options construct server codecs");
182        serve_codecs(
183            input,
184            output,
185            handler,
186            cancellation,
187            &mut requests,
188            &Scheme::HTTP,
189        )
190        .await
191    }
192    .await;
193    let closed = scheduler.socket_close(socket).await;
194    Outcome {
195        requests,
196        result: result.and_then(|()| Ok(closed?)),
197    }
198}
199
200/// Serve exchanges over established codecs, then retire both directions.
201async fn serve_codecs<R, W, H>(
202    mut input: RequestReader<R>,
203    mut output: ResponseWriter<W>,
204    handler: &H,
205    cancellation: &Cancellation,
206    count: &mut u64,
207    scheme: &Scheme,
208) -> Result<(), Error>
209where
210    R: ReadableStream,
211    W: WritableStream,
212    H: Handler,
213{
214    let result = exchanges(
215        &mut input,
216        &mut output,
217        handler,
218        cancellation,
219        count,
220        scheme,
221    )
222    .await;
223    // Both cleanups run regardless of the exchange or the other cleanup result.
224    let input_closed = input.close().await;
225    let output_closed = output.close().await;
226    result?;
227    input_closed?;
228    output_closed?;
229    Ok(())
230}
231
232async fn exchanges<R, W, H>(
233    input: &mut RequestReader<R>,
234    output: &mut ResponseWriter<W>,
235    handler: &H,
236    cancellation: &Cancellation,
237    count: &mut u64,
238    scheme: &Scheme,
239) -> Result<(), Error>
240where
241    R: ReadableStream,
242    W: WritableStream,
243    H: Handler,
244{
245    loop {
246        let (mut request, exchange) = match input.read_request(cancellation).await {
247            Ok(Some(pair)) => pair,
248            Ok(None) => return Ok(()),
249            Err(error) => {
250                if !matches!(error, base_http1::Error::Cancelled) {
251                    let status = if matches!(error, base_http1::Error::Limit(_)) {
252                        Status::new(431).unwrap()
253                    } else {
254                        Status::BAD_REQUEST
255                    };
256                    // A bad request cannot be reused even if rejection succeeds.
257                    let _ = output.reject(status, cancellation).await;
258                }
259                return Err(error.into());
260            }
261        };
262        request.scheme.get_or_insert_with(|| scheme.clone());
263        let result = {
264            let mut interim = InterimResponses::new(output.interim(exchange.version()));
265            let result = {
266                let called = pin!(handler.call(request, &mut interim, cancellation));
267                cancellable(called, cancellation).await?
268            };
269            interim.finish();
270            result
271        };
272        let mut response = result.map_err(|error| {
273            // Cancellation can arrive between polling the outer signal and
274            // polling a handler which propagates a cancelled body operation.
275            if cancellation.is_cancelled() {
276                Error::Cancelled
277            } else {
278                Error::Handler(Box::new(error))
279            }
280        })?;
281        let persistent = output
282            .write_response(&exchange, &mut response, cancellation)
283            .await?;
284        drop(response);
285        *count += 1;
286        if !persistent || !input.is_reusable() {
287            return Ok(());
288        }
289    }
290}
291
292#[derive(Default, Debug)]
293pub struct Statistics {
294    pub connections: u64,
295    pub requests: u64,
296    pub failed_connections: u64,
297}
298
299/// Serve an already-bound listener with bounded concurrent connection futures.
300/// No hidden tasks or detached jobs are created. Cancellation stops acceptance,
301/// cancels active exchanges, joins retirement, and closes the owned listener.
302/// Individual peer/handler failures are counted without stopping other peers.
303pub async fn serve<S, H>(
304    scheduler: &S,
305    listener: S::Socket,
306    handler: &H,
307    options: &Options,
308    cancellation: &Cancellation,
309) -> Result<Statistics, Error>
310where
311    S: SocketAccept + SocketStreamReading + SocketStreamWriting + SocketClose,
312    S::Socket: 'static,
313    H: Handler,
314{
315    serve_with(
316        scheduler,
317        listener,
318        options,
319        cancellation,
320        |socket, cancellation| async move {
321            connection_outcome(scheduler, socket, handler, &options.http1, &cancellation).await
322        },
323    )
324    .await
325}
326
327async fn serve_with<S, F, Fut>(
328    scheduler: &S,
329    listener: S::Socket,
330    options: &Options,
331    cancellation: &Cancellation,
332    connection: F,
333) -> Result<Statistics, Error>
334where
335    S: SocketAccept + SocketClose,
336    F: Fn(S::Socket, Cancellation) -> Fut,
337    Fut: Future<Output = Outcome>,
338{
339    let connections_cancel = cancellation.child();
340    let mut connections = FuturesUnordered::new();
341    let mut statistics = Statistics::default();
342    let result = async {
343        options.http1.validate()?;
344        if options.max_connections == 0 {
345            return Err(Error::Protocol(base_http1::Error::Invalid(
346                "zero connection limit",
347            )));
348        }
349        loop {
350            if connections.len() == options.max_connections {
351                record(
352                    &mut statistics,
353                    cancellable(connections.next(), cancellation)
354                        .await?
355                        .unwrap(),
356                );
357                continue;
358            }
359            // Keep this accept alive while existing connections complete. A
360            // backend may already own an accepted socket between polls; dropping
361            // and recreating acceptance can silently discard that new peer.
362            let accepted = {
363                let mut accept = pin!(scheduler.socket_accept(&listener, cancellation));
364                loop {
365                    let completed = pin!(async {
366                        if connections.is_empty() {
367                            std::future::pending().await
368                        } else {
369                            connections.next().await.unwrap()
370                        }
371                    });
372                    match select(accept.as_mut(), completed).await {
373                        Either::Left((result, _)) => break result,
374                        Either::Right((result, _)) => record(&mut statistics, result),
375                    }
376                }
377            };
378            let (socket, _) = accepted.map_err(base_http1::Error::from)?;
379            statistics.connections += 1;
380            connections.push(connection(socket, connections_cancel.clone()));
381        }
382    }
383    .await;
384    connections_cancel.cancel();
385    while let Some(result) = connections.next().await {
386        record(&mut statistics, result);
387    }
388    let closed = scheduler.socket_close(listener).await;
389    closed?;
390    match result {
391        Ok(()) | Err(Error::Cancelled) => Ok(statistics),
392        Err(error) => Err(error),
393    }
394}
395
396fn record(statistics: &mut Statistics, outcome: Outcome) {
397    statistics.requests += outcome.requests;
398    match outcome.result {
399        Ok(()) => {}
400        Err(Error::Cancelled) => {}
401        Err(_) => statistics.failed_connections += 1,
402    }
403}
404
405/// Serve HTTP/1 over an already authenticated TLS connection. ALPN is
406/// interpreted by HTTP before any request is admitted.
407#[cfg(feature = "rustls")]
408pub async fn serve_tls_connection<S, H>(
409    scheduler: &S,
410    connection: base_endpoint::tls::rustls::RustlsConnection<S::Socket>,
411    handler: &H,
412    options: &Options,
413    cancellation: &Cancellation,
414) -> Result<u64, Error>
415where
416    S: SocketStreamReading + SocketStreamWriting + SocketClose,
417    S::Socket: 'static,
418    H: Handler,
419{
420    let outcome =
421        tls_connection_outcome(scheduler, connection, handler, options, cancellation).await;
422    outcome.result.map(|()| outcome.requests)
423}
424
425#[cfg(feature = "rustls")]
426async fn tls_connection_outcome<S, H>(
427    scheduler: &S,
428    connection: base_endpoint::tls::rustls::RustlsConnection<S::Socket>,
429    handler: &H,
430    options: &Options,
431    cancellation: &Cancellation,
432) -> Outcome
433where
434    S: SocketStreamReading + SocketStreamWriting + SocketClose,
435    S::Socket: 'static,
436    H: Handler,
437{
438    use base_endpoint::tls::TlsSession;
439    let selected = connection.negotiated_protocol().map(<[u8]>::to_vec);
440    let mut requests = 0;
441    let result = connection
442        .with_duplex(scheduler, cancellation, |reader, writer| async {
443            let protocol = options.protocols.negotiated(selected.as_deref())?;
444            let (input, output) = protocol.server(reader, writer, options.http1.clone())?;
445            serve_codecs(
446                input,
447                output,
448                handler,
449                cancellation,
450                &mut requests,
451                &Scheme::HTTPS,
452            )
453            .await
454        })
455        .await;
456    Outcome { requests, result }
457}
458
459/// Configure HTTP/1 ALPN and serve TLS on an owned TCP listener. Handshakes
460/// occupy bounded connection slots and progress concurrently with acceptance.
461#[cfg(feature = "rustls")]
462pub async fn serve_tls<S, H>(
463    scheduler: &S,
464    listener: S::Socket,
465    config: std::sync::Arc<rustls::ServerConfig>,
466    handler: &H,
467    options: &Options,
468    cancellation: &Cancellation,
469) -> Result<Statistics, Error>
470where
471    S: SocketAccept + SocketStreamReading + SocketStreamWriting + SocketClose,
472    S::Socket: 'static,
473    H: Handler,
474{
475    use base_endpoint::tls::{TlsAcceptor, rustls::RustlsAcceptor};
476    let mut config = (*config).clone();
477    config.alpn_protocols = options.protocols.alpn_protocols();
478    let acceptor = RustlsAcceptor::new(std::sync::Arc::new(config));
479    serve_with(
480        scheduler,
481        listener,
482        options,
483        cancellation,
484        |socket, cancellation| {
485            let acceptor = &acceptor;
486            async move {
487                match acceptor.accept(scheduler, socket, &cancellation).await {
488                    Ok(connection) => {
489                        tls_connection_outcome(
490                            scheduler,
491                            connection,
492                            handler,
493                            options,
494                            &cancellation,
495                        )
496                        .await
497                    }
498                    Err(error) => Outcome {
499                        requests: 0,
500                        result: Err(error.into()),
501                    },
502                }
503            }
504        },
505    )
506    .await
507}
508
509impl From<base_http_protocol::NegotiationError> for Error {
510    fn from(error: base_http_protocol::NegotiationError) -> Self {
511        Self::Negotiation(error)
512    }
513}
514
515#[cfg(test)]
516mod tests {
517    use super::*;
518    use std::{error::Error as _, io};
519
520    #[test]
521    fn protocol_errors_are_transparent_and_io_errors_have_context() {
522        let error = Error::from(base_http1::Error::Io(io::Error::from(
523            io::ErrorKind::ConnectionReset,
524        )));
525        assert_eq!(error.to_string(), "HTTP/1 connection I/O failed");
526        assert_eq!(error.source().unwrap().to_string(), "connection reset");
527
528        let error = Error::from(io::Error::from(io::ErrorKind::ConnectionReset));
529        assert_eq!(error.to_string(), "HTTP server I/O failed");
530        assert_eq!(error.source().unwrap().to_string(), "connection reset");
531    }
532
533    #[test]
534    fn handler_errors_are_reported_as_the_source() {
535        let error = Error::Handler(Box::new(io::Error::other("database unavailable")));
536        assert_eq!(error.to_string(), "request handler failed");
537        assert_eq!(error.source().unwrap().to_string(), "database unavailable");
538    }
539
540    #[test]
541    fn scheduler_operation_errors_keep_cancellation_distinct() {
542        let error = Error::from(base_scheduler::OperationError::from(
543            base_scheduler::Cancelled,
544        ));
545        assert_eq!(format!("{error:?}"), "Cancelled");
546        assert_eq!(error.to_string(), "HTTP server cancelled");
547        assert!(error.source().is_none());
548
549        let error = Error::from(base_scheduler::OperationError::from(io::Error::from(
550            io::ErrorKind::ConnectionReset,
551        )));
552        let expected = Error::Protocol(base_http1::Error::Io(io::Error::from(
553            io::ErrorKind::ConnectionReset,
554        )));
555        assert_eq!(format!("{error:?}"), format!("{expected:?}"));
556        assert_eq!(error.source().unwrap().to_string(), "connection reset");
557    }
558
559    #[test]
560    fn negotiation_errors_are_transparent() {
561        let error = Error::from(base_http_protocol::NegotiationError::Missing);
562        assert_eq!(format!("{error:?}"), "Negotiation(Missing)");
563        assert_eq!(
564            error.to_string(),
565            "HTTP application protocol was not negotiated"
566        );
567        assert!(error.source().is_none());
568    }
569}