💾 Archived View for alchemi.dev › en › projects › kochab › files › src › handling.rs captured on 2023-09-28 at 15:57:40.

View Raw

More Information

⬅️ Previous capture (2022-07-16)

-=-=-=-=-=-=-

//! Types for handling requests
//!
//! The main type is the [`Handler`], which wraps a more specific type of handler and
//! manages delegating responses to it.
//!
//! For most purposes, you should never have to manually create any of these structs
//! yourself, though it may be useful to look at the implementations of [`From`] on
//! [`Handler`], as these are the things that can be used as handlers for routes.
use anyhow::Result;

use std::{
    pin::Pin,
    future::Future,
    task::Poll,
    panic::{catch_unwind, AssertUnwindSafe},
};
#[cfg(feature = "serve_dir")]
use std::path::PathBuf;

use crate::{Document, types::{Body, Response, Request}};

/// A struct representing something capable of handling a request.
///
/// In the future, this may have multiple varieties, but at the minute, it just wraps an
/// [`Fn`](std::ops::Fn).
///
/// The most useful part of the documentation for this is the implementations of [`From`]
/// on it, as this is what can be passed to
/// [`Server::add_route()`](crate::Server::add_route()) in order to create a new route.
/// Each implementation has bespoke docs that describe how the type is used, and what
/// response is produced.
pub enum Handler {
    FnHandler(HandlerInner),
    StaticHandler(Response),
    #[cfg(feature = "serve_dir")]
    FilesHandler(PathBuf),
}

/// Since we can't store train objects, we need to wrap fn handlers in a box
type HandlerInner = Box<dyn Fn(Request) -> HandlerResponse + Send + Sync>;
/// Same with dyn Futures
type HandlerResponse = Pin<Box<dyn Future<Output = Result<Response>> + Send>>;

impl Handler {
    /// Handle an incoming request
    ///
    /// This delegates to the request to the appropriate method of handling it, whether
    /// that's fetching a file or directory listing, cloning a static response, or handing
    /// the request to a wrapped handler function.
    ///
    /// Any unexpected errors that occur will be printed to the log and potentially
    /// reported to the user, depending on the handler type.
    pub async fn handle(&self, request: Request) -> Response {
        match self {
            Self::FnHandler(inner) => {
                let fut_handle = (inner)(request);
                let fut_handle = AssertUnwindSafe(fut_handle);

                HandlerCatchUnwind::new(fut_handle).await
                    .unwrap_or_else(|err| {
                        error!("Handler failed: {:?}", err);
                        Response::temporary_failure("")
                    })
            },
            Self::StaticHandler(response) => {
                match &response.body {
                    None => Response::new(response.status, &response.meta),
                    Some(Body::Bytes(bytes)) => Response::success(&response.meta, bytes.clone()),
                    _ => {
                        error!(concat!(
                            "Cannot construct a static handler with a reader-based body! ",
                            " We're sending a response so that the client doesn't crash, but",
                            " given that this is a release build you should really fix this."
                        ));
                        Response::permanent_failure(
                            "Very bad server error, go tell the sysadmin to look at the logs."
                        )
                    }
                }
            },
            #[cfg(feature = "serve_dir")]
            Self::FilesHandler(path) => {
                if path.is_dir() {
                    crate::util::serve_dir(path, request.trailing_segments()).await
                } else {
                    let mime = crate::util::guess_mime_from_path(&path);
                    crate::util::serve_file(path, &mime).await
                }
            },
        }
    }
}

impl<H, R> From<H> for Handler
where
    H: 'static + Fn(Request) -> R + Send + Sync,
    R: 'static + Future<Output = Result<Response>> + Send,
{
    /// Wrap an [`Fn`] in a [`Handler`] struct
    ///
    /// This automatically boxes both the [`Fn`] and the [`Fn`]'s response.
    ///
    /// Any requests passed to the handler will be directly handed down to the handler,
    /// with the request as the first argument.  The response provided will be sent to the
    /// requester.  If the handler panics or returns an [`Err`], this will be logged, and
    /// the requester will be sent a [`SERVER_ERROR`](Response::server_error()).
    fn from(handler: H) -> Self {
        Self::FnHandler(
            Box::new(move|req| Box::pin((handler)(req)) as HandlerResponse)
        )
    }
}

// We tolerate a fallible `impl From` because this is *really* not the kind of thing the
// user should be catching in runtime.
#[allow(clippy::fallible_impl_from)]
impl From<Response> for Handler {
    /// Serve an unchanging response
    ///
    /// Any and all requests to this handler will be responded to with the same response,
    /// no matter what.  This is good for static content that is provided by your app.
    /// For serving files & directories, try looking at creating a handler from a path
    ///
    /// ## Panics
    /// This response type **CANNOT** be created using Responses with [`Reader`] bodies.
    /// Attempting to do this will cause a panic.  Don't.
    ///
    /// [`Reader`]: Body::Reader
    fn from(response: Response) -> Self {
        #[cfg(debug_assertions)] {
            // We have another check once the handler is actually called that is not
            // disabled for release builds
            if let Some(Body::Reader(_)) = response.as_ref() {
                panic!("Cannot construct a static handler with a reader-based body");
            }
        }
        Self::StaticHandler(response)
    }
}

impl From<&Document> for Handler {
    /// Serve an unchanging response, shorthand for From<Response>
    ///
    /// This document will be sent in response to any requests that arrive at this
    /// handler.  As with all documents, this will be a successful response with a
    /// `text/gemini` MIME.
    fn from(doc: &Document) -> Self {
        Self::StaticHandler(doc.into())
    }
}

#[cfg(feature = "serve_dir")]
impl From<PathBuf> for Handler {
    /// Serve files from a directory
    ///
    /// Any requests directed to this handler will be served from this path.  For example,
    /// if a handler serving files from the path `./public/` and bound to `/serve`
    /// receives a request for `/serve/file.txt`, it will respond with the contents of the
    /// file at `./public/file.txt`.
    ///
    /// This is equivilent to serving files using [`util::serve_dir()`], and as such will
    /// include directory listings.
    ///
    /// The path to a single file can be passed in order to serve only a single file for
    /// any and all requests.
    ///
    /// [`util::serve_dir()`]: crate::util::serve_dir()
    fn from(path: PathBuf) -> Self {
        Self::FilesHandler(path)
    }
}

/// A utility for catching unwinds on Futures.
///
/// This is adapted from the futures-rs CatchUnwind, in an effort to reduce the large
/// amount of dependencies tied into the feature that provides this simple struct.
#[must_use = "futures do nothing unless polled"]
struct HandlerCatchUnwind {
    future: AssertUnwindSafe<HandlerResponse>,
}

impl HandlerCatchUnwind {
    fn new(future: AssertUnwindSafe<HandlerResponse>) -> Self {
        Self { future }
    }
}

impl Future for HandlerCatchUnwind {
    type Output = Result<Response>;

    fn poll(
        mut self: std::pin::Pin<&mut Self>,
        cx: &mut std::task::Context
    ) -> Poll<Self::Output> {
        match catch_unwind(AssertUnwindSafe(|| self.future.as_mut().poll(cx))) {
            Ok(res) => res,
            Err(e) => {
                error!("Handler panic! {:?}", e);
                Poll::Ready(Ok(Response::temporary_failure("")))
            }
        }
    }
}