iron / staticfile

Static file-serving middleware for the Iron web framework.
MIT License
63 stars 56 forks source link

Static with fallback #78

Open remko opened 8 years ago

remko commented 8 years ago

Hi,

I have several web apps where I have a chain where I first try to serve a static file, and if it doesn't exist, fallback to another handler. What's the recommended way to use such a fallback mechanism with staticfile? Right now, i defined my own custom handler that implements this cascading by delegating to static and catching 404, but I was wondering whether this is something that can be done without a custom handler; is this something that 'static' could implement? (i.e. a constructor that takes a path and a handler to call when the file isn't on the filesystem?

thanks, Remko

shepmaster commented 8 years ago

Yep, this seems like it will be a common pattern for frontend-heavy web applications. If a static file exists, serve it, otherwise serve the HTML & JS for the application, which will handle the routing.

One complication I see is that we may want Iron to be able to check the URL against a whitelist to know if it is a valid path. If it isn't, then Iron should return a 404. After sketching some pseudocode, I thought of this:

let static_files = Static::new("assets");

let remap = Remap::new("assets/index.html")
    .get("/users")
    .get("/users/:id")
    .get("/posts")
    .get("/posts/:id");

let fallback = Fallback::new(static_files, remap);

This seemingly allows the composition currently favored by the Iron project. Perhaps the Fallback may not even be needed, if Remap is a Modifier which can then simply allow Static to do the normal flow...

shepmaster commented 8 years ago

Something similar to this appears to work:

remap.rs

use std::collections::HashSet;

use iron::prelude::*;
use iron::{BeforeMiddleware, Url};

pub struct Remap {
    new_path: String,
    routes: HashSet<&'static str>,
}

impl Remap {
    pub fn new(path: &str) -> Remap {
        Remap {
            new_path: path.into(),
            routes: HashSet::new(),
        }
    }

    pub fn get(&mut self, path: &'static str) {
        self.routes.insert(path);
    }
}

impl BeforeMiddleware for Remap {
    fn before(&self, req: &mut Request) -> IronResult<()> {
        // TODO: do we really need to clone?
        let original_url = req.url.clone().into_generic_url();

        if self.routes.contains(original_url.path()) {
            let mut changed_url = original_url;
            changed_url.set_path(&self.new_path);
            let changed_url = Url::from_generic_url(changed_url).expect("Unable to rebuild URL");
            req.url = changed_url;
        }

        Ok(())
    }
}

main.rs

mod remap;
use remap::Remap;

fn main() {
    let mut mount = Mount::new();
    mount.mount("/", Static::new(&root));

    // Routes that are handled client-side
    let mut remap = Remap::new("/");
    remap.get("/about");

    let mut remapped = Chain::new(mount);
    remapped.link_before(remap);

    info!("Starting the server on {}:{}", address, port);
    Iron::new(remapped).http((&*address, port)).expect("Unable to start server");
}

To be more complete, it should use the same underlying glob and route recognizer libraries as Iron to allow for the syntax to match.

untitaker commented 8 years ago

You can write a handler which tries two handlers and returns the first one that doesn't 404.

shepmaster commented 6 years ago

You can write a handler which tries two handlers and returns the first one that doesn't 404.

And here's another solution, a year later

struct Fallback;

impl AroundMiddleware for Fallback {
    fn around(self, handler: Box<Handler>) -> Box<Handler> {
        Box::new(FallbackHandler(handler))
    }
}

struct FallbackHandler(Box<Handler>);

impl Handler for FallbackHandler {
    fn handle(&self, req: &mut Request) -> IronResult<Response> {
        let resp = self.0.handle(req);

        match resp {
            Err(err) => {
                match err.response.status {
                    Some(status::NotFound) => {
                        let file = File::open("/tmp/example").unwrap();
                        Ok(Response::with((status::Ok, file)))
                    }
                    _ => Err(err),
                }
            }
            other => other
        }
    }
}
ernestas-poskus commented 6 years ago

maybe it's worth to create middleware's or alike repository under /iron organization to stock these treasures?