AnOpinionatedGuideToRustWebServers

Disclaimer

This was written in October 2017. Information on Rust tends to rot fairly quickly at the moment, so it’s unlikely to stay up to date TOO long.

This is written from the perspective of someone who doesn’t REALLY care about webdev and doesn’t do it at large scales, but just wants to get something small and relatively simple working without putting huge amounts of work into it. Your requirements might not be the same as mine. Also the actual code mutates a little bit throughout the course of the endeavor, so I can’t promise everything will compile perfectly when you copy-paste it.

An opinionated guide to Rust web servers

I want to make a tiny HTTP REST service. Instead of using Flask, let’s try Rust. According to www.arewewebyet.org (as of Sep 28 2017), “The web frameworks of choice in the community are”:

  • Conduit
  • Gotham
  • Iron
  • Nickel
  • Rocket

In web development frameworks it also lists:

  • conduit
  • edge
  • pencil
  • rouille
  • rustful
  • rustless

Let’s look at them.

Things not covered, because there’s only so much damn time in the world:

  • shio

All the code is at https://github.com/icefoxen/rustyweb/

First pass

In which we throw out stuff we don’t have time for.

  • conduit: No readme, barely any API docs. Out it goes.
  • edge: Released version is 0.0.1. Out it goes.
  • gotham: Relatively young, but actively developed and seems like the devs are taking it seriously. Worth looking at more.
  • iron: Documented in ActuallyUsingIron; it works but I’m not eager to repeat the experience. I’ll pass for now.
  • nickel: “Inspired by javascript” does not inspire faith, but I’ll give it a go.
  • pencil: Used it before and didn’t hate it, I’ll give it another go even though it hasn’t been updated in over a year.
  • rocket: Apparently super trendy, but it requires experimental version of rustc, out it goes. I prefer my compiler bugs well-cooked, thank you. …okay, okay, fine, I’ll try it. But only ’cause rustup makes using nightly for a particular build trivial.
  • rouille: Tomaka made a web server? Of course he did. Only thing in this list at version 1.0. I’m a little scared ’cause tomaka’s code, while high quality, tends to be a bit hard to wrap one’s head around. But the docs and examples look good, I’ll give it a try.
  • rustful: No obvious red flags; last release was >1 year ago but latest git commit was earlier this summer.
  • rustless: No obvious red flags; last release was 10 months ago and there’s some reasonable github activity.

So that narrows it down to:

  • gotham
  • nickel
  • pencil
  • rouille
  • rustful
  • rustless
  • rocket

Second pass

In which we try to make more informed judgements. I’m going to try to implement three basic little programs for each framework: First, hello-world, second, serving a static file from a route (which is ALWAYS annoying unless the framework dev specifically gave you tools to make it easy, and the path handling is also horrifically touchy to hand-roll so you really want the framework to provide it), and third, a tiny JSON API that just updates a little bit of internal state in the server and exposes it to the client.

Each program is basically self-contained and includes unit tests.

rouille 1.0.3

We’ll start with rouille because if something breaks I know can bug tomaka on IRC.

Hello world

extern crate rouille;
use rouille::Response;

fn main() {
    rouille::start_server("0.0.0.0:8888", move |_request| {
        Response::text("hello world")
    });
}


#[cfg(test)]
mod tests {
    extern crate reqwest;
    use std::thread;
    use std::io::Read;
    use super::main;

    #[test]
    fn test_basic() {
        let _t = thread::spawn(main);
        let mut resp = reqwest::get("http://localhost:8888").unwrap();
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "hello world");
    }
}

Serve static file with route

Combining the static-file-server and route handling tripped me up for a while, until I found the Request::remove_prefix() method which does exactly what I needed.


#[macro_use]
extern crate rouille;
#[macro_use]                                     
extern crate lazy_static;                   
                                        
use rouille::Request;                              
use rouille::Response;                     
     
fn main() {
    rouille::start_server("0.0.0.0:8888", move |request| {
        router!(    
            request,                                    
            (GET) (/id/{_foo:String}) => {  
                if let Some(request) = request.remove_prefix("/id") {
                    rouille::match_assets(&request, "id")
                } else {                        
                    Response::text("Should never happen!").with_status_code(500)
                }
            },
            _ => Response::text("hello world")                                                                                  
        )
    });
}

#[cfg(test)]             
mod tests {             
    extern crate reqwest;
    use lazy_static;                           
    use std::thread;
    use std::io::Read;                                                                
                                                      
    lazy_static! {
        static ref SERVER_THREAD: thread::JoinHandle<()> = thread::spawn(super::main);
    }                                                         
                                                
    fn spawn_server_and_get(path: &str) -> reqwest::Response {            
        lazy_static::initialize(&SERVER_THREAD);
        let new_path = String::from("http://localhost:8888") + path;                                                            
        reqwest::get(&new_path).unwrap()
    }                
                                                 
    #[test]                                 
    fn test_basic() {                   
        let mut resp = spawn_server_and_get("/");  
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "hello world");               
    }               
                                                        
    #[test]                                 
    fn test_file() {                                                 
        let mut resp = spawn_server_and_get("/id/test"); 
        assert!(resp.status().is_success());    
        let mut content = String::new();                                        
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "Test contents!\n");
    }
}

Accept post requests

This involves a lot of framework and infrastructure to make the server actually do stuff, I’ve pulled it all out into its own module. This module will be identical for all projects; it’s the rustyweb crate in the git repo.

Also note that there’s some crypto stuff in here; I do NOT know what I’m doing and am just playing around, so don’t take it as something you should actually use. :-P It’s vulnerable to replay attacks, at the very least.

Here’s the shared code:

//! rustyweb lib.rs
extern crate ring;
extern crate untrusted;
extern crate chrono;
extern crate base64;
extern crate rustc_serialize;
extern crate serde;
#[macro_use]
extern crate serde_derive;

use ring::{signature, rand};
use chrono::prelude::*;

use std::collections::HashMap;

#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ValidationError {
    UnknownUser(String),
    MalformedSignature,
    InvalidSignature,
}


#[derive(Debug, Clone, PartialEq, Eq, RustcDecodable, RustcEncodable, Serialize, Deserialize)]
pub struct UpdateMessage {
    pub user: String,
    pub utc: DateTime<Utc>,
    pub signature: String,
    pub new_contents: String,
}
impl UpdateMessage {
    pub fn signed_message(keypair: &signature::Ed25519KeyPair, user: &str, msg: &str) -> UpdateMessage {
        let aggregated_message = String::from(user) + " " + msg;
        let message_bytes = aggregated_message.as_bytes();
        let sig = keypair.sign(message_bytes);
        let base64_sig = base64::encode(sig.as_ref());
        UpdateMessage {
            user: user.to_string(),
            utc: Utc::now(),
            signature: base64_sig,
            new_contents: msg.to_string(),
        }
    }

    pub fn verify_signature(&self, pubkey_bytes: &[u8]) -> Result<(), ValidationError> {
        let aggregated_message = String::from(self.user.as_str()) + " " + self.new_contents.as_ref();
        let message_bytes = aggregated_message.as_bytes();
        let sig_bytes = base64::decode(&self.signature)
            .map_err(|_decode_error| ValidationError::MalformedSignature)?;
        let pubkey = untrusted::Input::from(pubkey_bytes);
        let msg = untrusted::Input::from(message_bytes);
        let sig = untrusted::Input::from(&sig_bytes);
        signature::verify(&signature::ED25519, pubkey, msg, sig)
            .map_err(|_err| ValidationError::InvalidSignature)
    }
}


#[derive(Debug, Default, Clone)]
pub struct ServerData {
    names: HashMap<String, UpdateMessage>,
    keys: HashMap<String, Vec<u8>>,
}

impl ServerData {
    pub fn get_name(&self, name: &str) -> Option<&UpdateMessage> {
        self.names.get(name)
    }

    pub fn get_id_key(&self, id: &str) -> Option<&[u8]> {
        self.keys.get(id).map(|x| x.as_ref())
    }

    pub fn add_id(&mut self, id: &str, key: &[u8]) {
        self.keys.insert(id.into(), key.into());
    }

    pub fn validate_update(&self, msg: &UpdateMessage) -> Result<(), ValidationError> {
        match self.keys.get(&msg.user) {
            Some(key) => msg.verify_signature(key),
            None => Err(ValidationError::UnknownUser(msg.user.clone()))
        }
    }

    pub fn update_name(&mut self, name: &str, contents: &UpdateMessage) {
        self.names.insert(name.to_string(), contents.clone());
    }

    pub fn apply_update_if_valid(&mut self, dest: &str, msg: &UpdateMessage) -> Result<(), ValidationError> {
        let _ = self.validate_update(msg)?;
        self.update_name(dest, &msg);
        Ok(())
    }

    pub fn add_user(&mut self, username: &str) {
        let rng = rand::SystemRandom::new();
        let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
        let keypair = signature::Ed25519KeyPair::from_pkcs8(
            untrusted::Input::from(&pkcs8_bytes)
        ).unwrap();

        let encoded_privkey = base64::encode(&pkcs8_bytes[..]);
        println!("Private key for {} is: {}", username, encoded_privkey);

        let pubkey_bytes = keypair.public_key_bytes();
        self.add_id(username, pubkey_bytes);

    }
}

Now here’s the actual server code:


#[macro_use]
extern crate rouille;
extern crate lazy_static;
extern crate serde;
extern crate rustc_serialize;
extern crate ring;
extern crate untrusted;
extern crate base64;
extern crate chrono;


use std::collections::HashMap;
use std::sync::RwLock;

use rouille::Response;

use ring::{signature, rand};

use infra::*;

fn run(server: ServerData, addr: &str) {
    let server = RwLock::new(server);
    server.write().unwrap().add_user("icefox");
    rouille::start_server(addr, move |request| {
        router!(
            request,
            (GET) (/id/{name:String}) => {
                if let Some(n) = server.read().unwrap().get_id_key(&name) {
                    Response::text(base64::encode(n))
                } else {
                    Response::empty_404()
                }
            },
            (GET) (/name/{name:String}) => {
                println!("Got get to {}", &name);
                if let Some(n) = server.read().unwrap().get_name(&name) {
                    Response::json(n)
                } else {
                    Response::empty_404()
                }
            },
            (POST) (/name/{name:String}) => {
                println!("Got post to {}", &name);
                let rename_request: UpdateMessage = try_or_400!(rouille::input::json_input(request));
                println!("Got post to {}: {:?}", &name, rename_request);
                match server.write().unwrap().apply_update_if_valid(&name, &rename_request) {
                    Ok(_) => Response::text("ok"),
                    Err(v) => Response::text(format!("{:?}", v)).with_status_code(403),
                }
            },
            _ => Response::text("hello world")
        )
    });
}

fn main() {
    let s = ServerData::default();
    run(s, "127.0.0.1:8888");
}

#[cfg(test)]
mod tests {
    extern crate reqwest;
    use lazy_static;
    use std::thread;
    use std::io::Read;
    use serde::Serialize;
    use ring::{rand, signature};
    use untrusted;
    use base64;

    const UNITTEST_USER: &str = "unittest_user";
    const UNITTEST_NAME: &str = "unittest_name";
    const UNITTEST_NAME_VALUE: &str = "unittest_name_value";

    fn start_test_server() {
        use super::ServerData;
        let mut s = ServerData::default();
        let pubkey_bytes = KEYPAIR.public_key_bytes();
        s.add_id(UNITTEST_USER, pubkey_bytes);
        s.update_name(UNITTEST_NAME, UNITTEST_NAME_VALUE);
        ServerData::run(s, "127.0.0.1:8888");

    }

    fn generate_keypair() -> signature::Ed25519KeyPair {
        let rng = rand::SystemRandom::new();
        let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
        let keypair = signature::Ed25519KeyPair::from_pkcs8(
            untrusted::Input::from(&pkcs8_bytes)
        ).unwrap();
        keypair
    }

    lazy_static! {
        static ref SERVER_THREAD: thread::JoinHandle<()> = thread::spawn(start_test_server);
        static ref KEYPAIR: signature::Ed25519KeyPair = generate_keypair();
    }


    fn spawn_server_and_get(path: &str) -> reqwest::Response {
        lazy_static::initialize(&SERVER_THREAD);
        let new_path = String::from("http://localhost:8888") + path;
        reqwest::get(&new_path).unwrap()
    }

    fn spawn_server_and_post<T: Serialize>(path: &str, json: &T) -> reqwest::Response {
        lazy_static::initialize(&SERVER_THREAD);
        let client = reqwest::Client::new().unwrap();
        let new_path = String::from("http://localhost:8888") + path;
        client.post(&new_path).unwrap()
            .json(json).unwrap()
            .send().unwrap()
    }

    #[test]
    fn test_basic() {
        let mut resp = spawn_server_and_get("/");
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "hello world");
    }

    #[test]
    fn test_id() {
        let mut resp = spawn_server_and_get((String::from("/id/") + UNITTEST_USER).as_str());
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        let pubkey_bytes = KEYPAIR.public_key_bytes();
        let pubkey_string = base64::encode(pubkey_bytes);
        assert_eq!(content, pubkey_string);
    }

    #[test]
    fn test_get_name() {
        // Test unset name default
        let resp = spawn_server_and_get("/name/test_no_name");
        assert_eq!(resp.status(), reqwest::StatusCode::NotFound);

        // Test set name
        let mut resp = spawn_server_and_get((String::from("/name/") + UNITTEST_NAME).as_str());
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, UNITTEST_NAME_VALUE);
    }

    #[test]
    fn test_post_name() {
        const NEWNAME: &str = "/name/test_post_name";
        // See that name DNE
        let resp = spawn_server_and_get(NEWNAME);
        assert!(!resp.status().is_success());

        let changed_name = "foo!";
        let data = super::UpdateMessage::signed_message(&KEYPAIR, UNITTEST_USER, changed_name);

        // Change name
        let mut resp = spawn_server_and_post(NEWNAME, &data);
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "ok");

        // Test name now that it's been changed
        let mut resp = spawn_server_and_get(NEWNAME);
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, changed_name);

        // Try changing it again with unsigned request
        let baddata = super::UpdateMessage {
            user: UNITTEST_USER.into(),
            signature: "".into(),
            new_contents: "aieeee!".into(),
        };
        let resp = spawn_server_and_post(NEWNAME, &baddata);
        assert!(!resp.status().is_success());

        // Ensure it hasn't changed.
        let mut resp = spawn_server_and_get(NEWNAME);
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, changed_name);

    }
}

I had some issues wrapping up the server’s state into a single object; I thought that rouille just started a single thread and I could just move the ServerData into the closure. That is NOT the case, I have to wrap the ServerData in some synchronization (I chose RwLock over Mutex since reads will probably be much more common than writes, though Mutex might still end up faster). This was annoying until I read the documentation which actually told me exactly what was going on and how to handle it.

Summary

  • Low friction: 4/5
  • Documentation: 4.5/5 (there are some gotchas in how the threading is set up and the documentation specifically points them out and tells you how to get around them)
  • Examples: 3/5 (could use one or two bigger things that compose more features)
  • Signal-to-noise ratio: 5/5
  • Maintenance: 4/5, will probably keep working forever but probably won’t get many fancy new features. Doesn’t necessarily use the shiniest things (rustc_deserialize instead of serde for instance). Looks like issue tracker response is generally prompt.
  • Other notes: Multithreaded but synchronous, which will put some people off because they think they need to be able to run Facebook from their laptop. Using rustc-deserialize is fine but redundant, because I need serde anyway to send my structs into reqwest sanely. If rustc-deserialize ever expects structs to be in a slightly different form than what serde serializes them into, you’re going to be in a world of hurt.

Conclusion: Yes.

rustful 0.9.0

It’s next on the list so let’s give it a spin.

Hello world

Requires openssl to build. Even when you exclude the ssl feature, apparently. …Okay, it looks like the git version is rather ahead of the released version so let’s ditch the git readme and follow what’s in the released version’s docs instead.

I do the same thing in keeping ggez’s git master far ahead of the released version, so I can’t whine TOO much, but I also make sure to have a branch for each release version so you can always at least SEE what repo state a particular release relates to. No such luck here. Also I’ve been convinced this is a bad idea and will be making sure that the git master == the latest stable version in the future.

Okay it looks like you MUST use a router in the Server specification. I tried using just a function that takes a Request and gives a Response but that does not work and gives me trait errors in Server::default() of all things somehow.

The example in the docs doesn’t show how to just deal with plain text, I’m going to have to suck it and see.

The example in the docs on how to build a router using the insert_routes! macro causes mysterious type errors, let’s do it by hand.

OKAY! It works now.

extern crate rustful;

use rustful::{Server, Context, Response, Router, TreeRouter};

fn say_hello(context: Context, response: Response) {
    response.send(format!("Hello, world!"));
}


fn main() {
    let mut router = TreeRouter::new();

    router.insert(rustful::Method::Get, "/", say_hello);

    //Build and run the server.
    let server = Server {
        handlers: router,

        //Turn a port number into an IPV4 host address (0.0.0.0:8080 in this case).
        host: 8888.into(),

        //Use default values for everything else.
        ..Server::default()
    };
    
    server.run().expect("Could not run server?");
}

Ok I got the insert_routes! macro to work, here’s what it looks like with that:

#[macro_use]
extern crate rustful;

use rustful::{Server, Context, Response, TreeRouter};

fn say_hello(context: Context, response: Response) {
    response.send(format!("Hello, world!"));
}


fn main() {
    let router = insert_routes! {
        TreeRouter::new() => {
            Get: say_hello,
        }
    };

    //Build and run the server.
    let server = Server {
        handlers: router,

        //Turn a port number into an IPV4 host address (0.0.0.0:8080 in this case).
        host: 8888.into(),

        //Use default values for everything else.
        ..Server::default()
    };
    
    server.run().expect("Could not run server?");
}

Using reqwest for tests runs into problems because reqwest needs openssl-sys 0.9 and rustful needs openssl-sys 0.7. I tried rolling back to earlier versions of reqwest and it didn’t make any difference. Hmmmmm. This is not going well.

Serve static file with route

#[macro_use]
extern crate rustful;
use std::path::Path;

use rustful::{Server, Context, Response, TreeRouter};

use rustful::StatusCode;
use rustful::file::check_path;


fn say_hello(context: Context, mut response: Response) {
    response.send(format!("Hello, world!"));
}

fn send_file(context: Context, mut response: Response) {
    if let Some(file) = context.variables.get("id") {
        let file_path = Path::new(file.as_ref());

        //Check if the path is valid
        if check_path(file_path).is_ok() {
            //Make a full path from the filename
            println!("File path is {:?}", file_path);
            let path = Path::new("src").join(file_path);

            //Send the file
            let res = response.send_file(&path)
                .or_else(|e| e.send_not_found("the file was not found"))
                .or_else(|e| e.ignore_send_error());

            //Check if a more fatal file error than "not found" occurred
            if let Err((e, mut response)) = res {
                //Something went horribly wrong
                response.set_status(StatusCode::InternalServerError);
            }
        } else {
            //Accessing parent directories is forbidden
            response.set_status(StatusCode::Forbidden);
        }
    } else {
        //No filename was specified
        response.set_status(StatusCode::Forbidden);
    }
}


fn main() {
    let router = insert_routes! {
        TreeRouter::new() => {
            // Get: say_hello,
            "id" => {
                ":id" => Get: send_file
            },
        }
    };

    //Build and run the server.
    let server = Server {
        handlers: router,

        //Turn a port number into an IPV4 host address (0.0.0.0:8080 in this case).
        host: 8888.into(),

        //Use default values for everything else.
        ..Server::default()
    };
    
    server.run().expect("Could not run server?");
}

The sharp-eyed will note that the original say_hello route is commented out. When I uncomment it I get the following helpful compiler error:

error[E0308]: mismatched types
  --> src/main.rs:51:33
   |
51 |                 ":file" => Get: send_file
   |                                 ^^^^^^^^^ expected fn item, found a different fn item
   |
   = note: expected type `fn(rustful::Context<'_, '_, '_>, rustful::Response<'_, '_>) {say_hello}`
              found type `fn(rustful::Context<'_, '_, '_>, rustful::Response<'_, '_>) {send_file}`

The docs don’t seem super helpful in this case, and fiddling with the mutability of the Response doesn’t make any difference.

The culprit in this case is Rust’s abysmal handling of function and closure types. The TreeRouter is generic on something that implements the trait Handler, which includes implementations for Fn(Context, Response) + Send + Sync + 'static. However, rustc decides send_file is a closure and say_hello is another closure and they are obviously entirely incompatible and you can’t use one in place of another, which is problematic since the TreeRouter is generic on the type the exact type of Handler; it doesn’t appear that you can put multiple different types of Handler into the same router, which seems a little ingenuous.

The SOLUTION is that the Handler trait is also implemented for Box<Handler> (or Arc), so you can insert as many different Box<Handler>’s into the TreeRouter as you want, like so:

   let router = insert_routes! {
        TreeRouter::new() => {
            Get: Box::new(say_hello) as Box<rustful::handler::Handler>,
            "id" => {
                ":id" => Get: Box::new(send_file)
            },
        }
    };

Still. In light of all the problems I’ve encountered, I think I’m stopping here; I don’t think going forward would be worth the effort.

Summary

  • Low friction: 2/5 yeah nothing but friction here
  • Documentation: 3/5 Pretty ok once you realize the git version != the released version
  • Examples: 3/5 Same
  • Signal-to-noise ratio: 2.5/5 Kiiiinda noisy, though that can be nice when it gives you more control over error-handling and such.
  • Maintenance: 2.5/5 Released version is like a year old and last update on github was 3 months ago; that said, the author DOES appear to be quite responsive to issues and PR’s when they occur.
  • Other notes: Really the main problem is the last release was a year ago, and the Rust ecosystem has moved a lot since then. I bet the experience is a lot better on the git version, but I’m not going to go there. I also feel like it tries to be broad instead of deep in terms of feature-set; I don’t need or want SSL in my application server, I mostly don’t want to define my own Handler’s unless really necessary, and so on. That said there’s really nothing BAD here; the architecture is a bit clunky and noisy, but if I had to use rustful then I’d suck it up, fix up whatever old dependencies were in my way, hack my way around the occasional problem, and get on with life without sweating it too much.

Conclusion: Not until 0.10 is released.

rustless 0.10.0

Built upon Iron, interesting. Readme is good but looks very API-centric (which might be great if that’s what you want). Last release, 10 months ago (Jan 2017).

Has a website, rustless.org. However, it does not appear to show anything useful about rustless, but instead just ads. Has it been pwned? That’s encouraging.

First off the bat, I run into the same openssl problem as with rustful. The culprit appears to be hyper 0.9, which depends on the cookie crate version 0.2 or 0.3. THIS requires the openssl crate if the secure feature is enabled, which it is by default until version 0.4 or so. What the fruit? Is it possible to disable this feature for this crate even though I don’t depend directly on it? Doesn’t look like it. Can I disable reqwest from using it? Nope, doesn’t have a feature flag for it.

Well, bah. I guess my “unit tests” will be run by hand using curl again!

Hello world

The example code is far bigger and more complex than this, and builds a JSON API. It also does not have any other examples. Let’s look at the docs. WELL THAT WAS QUICK, there aren’t any.

Okay, after some dissection to cut all the API-specific STURM UND DRANG out of the example, hello world is really just this:

extern crate rustless;
extern crate iron;

use rustless::{Application, Api, Nesting};

fn main() {

    let api = Api::build(|api| {
        api.get("", |endpoint| {
            endpoint.handle(|client, _params| client.text("Hello world!".to_owned()))
        })
    });

    let app = Application::new(api);

    iron::Iron::new(app).http("0.0.0.0:8888").unwrap();
    println!("Server started on port 8888");
}

Hey, that’s honestly reasonable.

Serve static file with route

Okay, this was far more painful than it should have been:

extern crate rustless;
extern crate iron;

use rustless::{Application, Api, Nesting};

use std::path::PathBuf;

fn main() {

    let api = Api::build(|api| {
        api.get("", |endpoint| {
            endpoint.handle(|client, _params| client.text("Hello world!".to_owned()))
        });

        api.namespace("id/:filename", |file_ns| {

            file_ns.get("", |file_endpoint| {
                file_endpoint.handle(|mut client, params| {
                    // Params is a JSON object, which is a little weird
                    // when it basically encodes the router path parameter...
                    if let Some(jsonval) = params.find("filename") {
                        // Yes we should be able to use and_then() or such
                        // to extract this but ownership makes it tricksy.
                        if let Some(s) = jsonval.as_str() {
                            println!("Returning file {:?}", params);
                            // Can't find any SAFE way of doing this so
                            // we're just gonna suck it.
                            let mut root = PathBuf::new();
                            root.push("src");
                            root.push(s);
                            client.file(&root)
                        } else {
                            client.set_status(rustless::server::status::StatusCode::BadRequest);
                            client.text("bad file type".to_owned())
                        }
                    } else {
                        client.not_found();
                        client.text("File not found".to_owned())
                    }
                })
            })
        });
    });

    let app = Application::new(api);

    iron::Iron::new(app).http("0.0.0.0:8888").unwrap();
    println!("Server started on port 8888");
}

First off, no shortcut for safely serving static files that I can find… maybe client.file() does that automatically? curl -v localhost:8888/id/main.rs serves the right file and curl -v localhost:8888/id/../Cargo.toml gives a 404, and I have to build the path explicitly otherwise it won’t accept it if I, say, ask for curl -v localhost:8888/id/src/main.rs, so maybe there’s some protection built in. But without any documentation, you can’t tell without source-diving! Are there any gotchas? I dunno!

Second off… I REALLY WANT to like this. But building the routes and handlers piece by piece, imperatively, by hand, is utterly awful. Let’s see… we have an Api object, that implements get/post/etc methods. Then we use that to create a Namespace object, which implements get/post/etc methods. In either of those you create an Endpoint object, which actually handles the thing with the callback you give it in Endpoint::handle(). THEN AND ONLY THEN can you get the client object, which lets you talk to the actual client, and a params object, which SOMEHOW encodes some STUFF that’s part of the request string. As a JSON object. Okay.

Overall it feels very Iron-y, with lots of manual piecing together of loosely-coupled bits and pieces, no documentation, no shortcuts and a really cool and ambitious goal. That might be what you want but it’s not what I want. I wish it was better, because I really like its goals.

Accept post requests

Yeah not happening

Summary

  • Low friction: 2/5 Easy to get started but once you do then UGH
  • Documentation: 1/5 Hope you like the README ’cause that’s all there is.
  • Examples: 1/5 Yes there is an example
  • Signal-to-noise ratio: 1/5 You are stuck in a twisty maze of request-ish objects, all alike
  • Maintenance: 1/5 There is a post on the issue tracker from April 2017 saying “the website is redirecting to a placeholder domain”, with no response from any Rustless developer.
  • Other notes: I really really want a good API-centric rust web server, which Rustless looks like it provides. But given no developer has so much as glanced at the issue tracker in six months, I doubt it’s worth the trouble.

Conclusion: Someone needs to fork this, dust it off and keep working on it!

pencil 0.3.0

Aha, something whose name doesn’t start with “R”! I’ve actually used this once or twice back in the halcyon days of mid 2016 and remember it being vaguely reasonable, but don’t recall details. With the last release also a year ago, we will see how it’s aged.

The same stupid toxic openssl version mismatch exists here; all these things that haven’t been touched in a year rely on hyper 0.9. Alas.

Hello world

extern crate pencil;

use pencil::{Pencil, Request, Response, PencilResult};

fn hello(_: &mut Request) -> PencilResult {
    Ok(Response::from("Hello World!"))
}

fn main() {
    let mut app = Pencil::new("/web/hello");
    app.get("/", "hello", hello);
    app.run("127.0.0.1:8888");
}

There, that was easy.

Serve static file with route

OH MY GODS IT’S NOT AN UTTER GONG SHOW

extern crate pencil;

use pencil::{Pencil, Request, Response, PencilResult};

fn hello(_: &mut Request) -> PencilResult {
    Ok(Response::from("Hello World!"))
}

fn main() {
    let mut app = Pencil::new("/web/hello");
    app.static_folder = env!("CARGO_MANIFEST_DIR").to_owned();
    app.static_folder += "/src/";
    app.static_url_path = "/id".to_owned();
    println!("Static folder is {}", &app.static_folder);
    app.get("/", "hello", hello);
    app.enable_static_file_handling();
    app.run("127.0.0.1:8888");
}

I’m a little grumpy that the static_folder thing is a String and not a Path though.

Accept post requests

This really doesn’t seem like the sort of thing that SHOULD be hard but it seems to be where the landmines occur…

extern crate pencil;
#[macro_use]
extern crate lazy_static;
extern crate rustc_serialize;

extern crate rustyweb;
use rustyweb::*;


use pencil::{Pencil, Request, Response, PencilResult};
use std::sync::RwLock;


lazy_static! {
    static ref SERVER_STATE: RwLock<ServerData> = {
        let mut s = ServerData::default();
        s.add_user("testuser");
        RwLock::new(s)
    };
}

fn hello(_: &mut Request) -> PencilResult {
    Ok(Response::from("Hello World!"))
}


fn get_name(request: &mut Request) -> PencilResult {
    if let Some(name) = request.view_args.get("name") {
        println!("Got get to {}", &name);
        if let Some(n) = SERVER_STATE.read().unwrap().get_name(&name) {
            pencil::jsonify(n)
        } else {
            pencil::abort(404)
        }
    } else {
        pencil::abort(404)
    }
}

fn post_name(request: &mut Request) -> PencilResult {
    if let Some(name) = request.view_args.get("name").cloned() {
        println!("Got post to {}", &name);
        if let Some(ref json) = request.get_json().clone() {
            // This is awful but I can't find a way to get the JSON from a request and
            // parse it directly into an object.  Sooooo.
            let json_str = rustc_serialize::json::encode(&json).unwrap();
            if let Ok(rename_request) = rustc_serialize::json::decode::<UpdateMessage>(&json_str) {
                match SERVER_STATE.write().unwrap().apply_update_if_valid(&name, &rename_request) {
                    Ok(_) => Ok(Response::from("ok")),
                    Err(_v) => pencil::abort(403),
                }
            } else {
                pencil::abort(400)
            }
        } else {
            pencil::abort(400)
        }
    } else {
        pencil::abort(404)
    }
}

fn main() {
    let mut app = Pencil::new("/web/hello");

    app.static_folder = env!("CARGO_MANIFEST_DIR").to_owned();
    app.static_folder += "/src/";
    app.static_url_path = "/id".to_owned();
    println!("Static folder is {}", &app.static_folder);

    app.get("/", "hello", hello);
    // You CAN'T pass a closure here, which I am somewhat annoyed about.
    // It makes it so you have to either put your state data in a global
    // or shove it into `Request.extensions_data` somehow.
    // The demo uses a `before_request()` hook to do the shoving but that
    // doesn't seem to let you actually PRESERVE state either.
    // So I'm just making it a global with `lazy_static`.
    app.get("/name/<name:String>", "get_name", get_name);
    app.post("/name/<name:String>", "post_name", post_name);
    app.enable_static_file_handling();

    app.run("127.0.0.1:8888");
}

Lots of little things. First, sharing state in the server handlers requires lazy_static! or possibly some other method to make things global, even though it shouldn’t. Second, you can grab JSON out of a request but you can’t try to parse the Json object you retrieve to an actual type, as far as I can figure out. So I literally re-serialize it to a string and then decode that. And THAT process creates some ooky borrowing interactions that requires a strategic clone or two.

But, on the whole, not TOO agonizing.

Summary

  • Low friction: 4/5 That’s what it’s about and it does it well, though there’s a few warts in the API.
  • Documentation: 4/5 Generated documentation was pretty easy to deal with
  • Examples: 4.5/5 The example in the git is fairly simplistic and doesn’t show off a lot, but https://fengsp.github.io/blog/2016/3/introducing-pencil/ is excellent.
  • Signal-to-noise ratio: 4/5 This is what it’s all about, and it does it quite well. Could be better with more refinement to the API though.
  • Maintenance: 1/5 It’s dead, dead, dead, and has been for a year
  • Other notes: It definitely takes the approach of “here are your buttons to push on the machine” instead of “here are your tools to build the machine” in some parts, but that can honestly be really nice when those buttons do exactly what you want.

Conclusion: Someone needs to take over and update this one too. It’s a tool to do some very common things with minimum fuss, which is the sort of tool I like as long as they can grow with me. I don’t think Pencil in its current state can do that, but that feels mainly like the API is unpolished rather than it’s fundamentally bad. There’s a built-in templating system that I haven’t even touched here, which I think would be better served as some sort of pluggable module, but… still. Makes me sad to see it go to seed.

nickel 0.10.0

Ok let’s try the 600-lb gorilla of Rust web app servers and see how it stacks up to Iron’s 800-lb gorilla.

Hello world

Oh yessssss we’re off of the ancient junk using hyper 0.9, we can use reqwest for unit tests again.

#[macro_use]
extern crate nickel;

use nickel::{Nickel, HttpRouter, Request, Response, MiddlewareResult};

fn hello_world<'mw>(_req: &mut Request, res: Response<'mw>) -> MiddlewareResult<'mw> {
    res.send("Hello world")
}

fn main() {
    let mut server = Nickel::new();
    server.get("**", hello_world);
    server.listen("127.0.0.1:8888").unwrap();
}



#[cfg(test)]
mod tests {
    extern crate reqwest;
    use std::thread;
    use std::io::Read;
    use super::main;

    #[test]
    fn test_basic() {
        let _t = thread::spawn(main);
        let mut resp = reqwest::get("http://localhost:8888").unwrap();
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "Hello world");
    }
}

Seems pretty reasonable, though I see some mentions of skittleware middleware in there that send chills down my spine.

Serve static file with route

#[macro_use] 
extern crate nickel;
#[macro_use]
extern crate lazy_static;

use nickel::{Nickel, Mountable, StaticFilesHandler, MiddlewareResult, Request, Response};

fn hello_world<'mw>(_req: &mut Request, res: Response<'mw>) -> MiddlewareResult<'mw> {
    res.send("Hello world")
}

fn main() {
    let mut server = Nickel::new();
    // Apparently this falls-through if we don't math the
    // first mount.
    server.mount("/id/", StaticFilesHandler::new("src/"));
    server.mount("/id/", middleware! { |req|
        let path = req.path_without_query().unwrap();
        format!("No static file with path '{}'!", path)
    });

    // The ordering is apparently important here; if I put this first then
    // it seems to match everything.
    server.mount("/",  hello_world);
    
    server.listen("127.0.0.1:8888").unwrap();
}


#[cfg(test)]             
mod tests {             
    extern crate reqwest;
    use lazy_static;                           
    use std::thread;
    use std::io::Read;                                                                
                                                      
    lazy_static! {
        static ref SERVER_THREAD: thread::JoinHandle<()> = thread::spawn(super::main);
    }                                                         
                                                
    fn spawn_server_and_get(path: &str) -> reqwest::Response {            
        lazy_static::initialize(&SERVER_THREAD);
        let new_path = String::from("http://localhost:8888") + path;                                                            
        reqwest::get(&new_path).unwrap()
    }                
                                                 
    #[test]                                 
    fn test_basic() {                   
        let mut resp = spawn_server_and_get("/");  
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "Hello world");               
    }               
                                                        
    #[test]                                 
    fn test_file() {                                                 
        let mut resp = spawn_server_and_get("/id/main.rs"); 
        assert!(resp.status().is_success());    
        let mut content = String::new();                                        
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, include_str!("main.rs"));
    }
}

Not bad, though the ordering in specifying mounts is weird; I expect it to match the most specific mount point, not the first one that it gets that matches at all.

It ALSO appears to mean that if you have a mount point for “/” at the end of the chain… that will match EVERY request that ever hits it, which is basically never what you want. How braindead.

There’s apparently a macro for building a router as well, but it seems to REQUIRE you to build a closure instead of just giving it a value for the handler object, and results in wacky type errors when you give it something even a little atypical, so it’s really not clear how you’re supposed to use it with StaticFileHandler. Still, some fiddling could probably make it work.

Accept post requests

#[macro_use] 
extern crate nickel;
#[macro_use]
extern crate lazy_static;
extern crate chrono;
extern crate serde;
extern crate rustyweb;
extern crate base64;
extern crate untrusted;
extern crate ring;
extern crate rustc_serialize;
use rustc_serialize::json;

use rustyweb::*;
use std::sync::RwLock;
use std::sync::Arc;

use nickel::{Nickel, Mountable, MiddlewareResult, Request, Response, HttpRouter, JsonBody};
use nickel::status::StatusCode;


fn hello_world<'mw>(_req: &mut Request, res: Response<'mw>) -> MiddlewareResult<'mw> {
    res.send("Hello world")
}

fn run(server_data: ServerData, addr: &str) {
    let mut server = Nickel::new();
    let server_data = Arc::new(RwLock::new(server_data));
    let server_data2 = server_data.clone();
    let server_data3 = server_data.clone();

    server.get("/id/:name", middleware! { |req, res|
        if let Some(name) = req.param("name") {
            if let Some(n) = server_data2.read().unwrap().get_id_key(name) {
                let encoded = base64::encode(n);
                (StatusCode::Ok, encoded)
            } else {
                (StatusCode::NotFound, "Not found".into())
            }
        } else {
            (StatusCode::InternalServerError, "Should never happen".into())
        }
    });

    server.get("/name/:name", middleware! { |req, mut res|
        if let Some(name) = req.param("name") {
            if let Some(n) = server_data.read().unwrap().get_name(name) {
                let encoded = json::encode(n).expect("Should never happen!");
                res.set(nickel::MediaType::Json);
                (StatusCode::Ok, encoded)
            } else {
                (StatusCode::NotFound, "Not found".into())
            }
        } else {
            (StatusCode::InternalServerError, "Should never happen".into())
        }
    });

    server.post("/name/:name", middleware! { |req, res|
        if let Some(name) = req.param("name").map(|s| s.to_owned()) {
            let message = try_with!(res, {
                req.json_as::<UpdateMessage>().map_err(|e| (StatusCode::BadRequest, e))
            });
            match server_data3.write().unwrap().apply_update_if_valid(&name, &message) {
                Ok(_) => (StatusCode::Ok, "ok".into()),
                Err(v) => (StatusCode::Forbidden, format!("{:?}", v))
            }
        } else {
            (StatusCode::InternalServerError, "Should never happen".into())
        }
    });

    server.mount("/",  hello_world);
    server.listen(addr).unwrap();
}

fn main() {
    let server_data = ServerData::default();
    run(server_data, "127.0.0.1:8888");
}

#[cfg(test)]
mod tests {
    extern crate reqwest;
    use lazy_static;
    use std::thread;
    use std::io::Read;
    use serde::Serialize;
    use ring::{rand, signature};
    use untrusted;
    use base64;
    use chrono::prelude::*;
    use super::UpdateMessage;

    const UNITTEST_USER: &str = "unittest_user";
    const UNITTEST_NAME: &str = "unittest_name";

    fn start_test_server() {
        use super::ServerData;
        let mut s = ServerData::default();
        let pubkey_bytes = KEYPAIR.public_key_bytes();
        s.add_id(UNITTEST_USER, pubkey_bytes);
        s.update_name(UNITTEST_NAME, &UNITTEST_NAME_VALUE);
        super::run(s, "127.0.0.1:8888");
    }

    fn generate_keypair() -> signature::Ed25519KeyPair {
        let rng = rand::SystemRandom::new();
        let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
        let keypair = signature::Ed25519KeyPair::from_pkcs8(
            untrusted::Input::from(&pkcs8_bytes)
        ).unwrap();
        keypair
    }

    lazy_static! {
        static ref SERVER_THREAD: thread::JoinHandle<()> = thread::spawn(start_test_server);
        static ref KEYPAIR: signature::Ed25519KeyPair = generate_keypair();
        static ref UNITTEST_NAME_VALUE: UpdateMessage = UpdateMessage::signed_message(&KEYPAIR, UNITTEST_USER, "unittest_value");
    }


    fn spawn_server_and_get(path: &str) -> reqwest::Response {
        lazy_static::initialize(&SERVER_THREAD);
        let new_path = String::from("http://localhost:8888") + path;
        reqwest::get(&new_path).unwrap()
    }

    fn spawn_server_and_post<T: Serialize>(path: &str, json: &T) -> reqwest::Response {
        lazy_static::initialize(&SERVER_THREAD);
        let client = reqwest::Client::new().unwrap();
        let new_path = String::from("http://localhost:8888") + path;
        client.post(&new_path).unwrap()
            .json(json).unwrap()
            .send().unwrap()
    }

    #[test]
    fn test_basic() {
        let mut resp = spawn_server_and_get("/");
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "Hello world");
    }

    #[test]
    fn test_id() {
        let mut resp = spawn_server_and_get((String::from("/id/") + UNITTEST_USER).as_str());
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        let pubkey_bytes = KEYPAIR.public_key_bytes();
        let pubkey_string = base64::encode(pubkey_bytes);
        assert_eq!(content, pubkey_string);
    }

    #[test]
    fn test_get_name() {
        // Test unset name default
        let resp = spawn_server_and_get("/name/test_no_name");
        assert_eq!(resp.status(), reqwest::StatusCode::NotFound);

        // Test set name
        let mut resp = spawn_server_and_get((String::from("/name/") + UNITTEST_NAME).as_str());
        assert!(resp.status().is_success());
        let resp_msg: UpdateMessage = resp.json().unwrap();
        assert_eq!(resp_msg, *UNITTEST_NAME_VALUE);
    }

    #[test]
    fn test_post_name() {
        const NEWNAME: &str = "/name/test_post_name";
        // See that name DNE
        let resp = spawn_server_and_get(NEWNAME);
        assert!(!resp.status().is_success());

        let changed_name = "foo!";
        let data = super::UpdateMessage::signed_message(&KEYPAIR, UNITTEST_USER, changed_name);

        // Change name
        let mut resp = spawn_server_and_post(NEWNAME, &data);
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "ok");

        // Test name now that it's been changed
        let mut resp = spawn_server_and_get(NEWNAME);
        assert!(resp.status().is_success());
        let resp_msg: UpdateMessage = resp.json().unwrap();
        assert_eq!(resp_msg, data);

        // Try changing it again with unsigned request
        let baddata = super::UpdateMessage {
            user: UNITTEST_USER.into(),
            utc: Utc::now(),
            signature: "".into(),
            new_contents: "aieeee!".into(),
        };
        let resp = spawn_server_and_post(NEWNAME, &baddata);
        assert!(!resp.status().is_success());

        // Ensure it hasn't changed.
        let mut resp = spawn_server_and_get(NEWNAME);
        assert!(resp.status().is_success());
        let resp_msg: UpdateMessage = resp.json().unwrap();
        assert_eq!(resp_msg, data);

    }
}

Ahhh, Arc<RwLock<T>>, the great hammer for pushing all ownership and sync issues off to runtime where you don’t have to worry about them. But apparently the middleware! macro makes takes a closure, and makes that closure move whether you tell it to or not, as far as I can tell. Which leaves no way to clone an Arc when you access it instead of making a bunch of copies up-front and moving one into each closure you define? That’s just… weird. I mean I know that keeping in-memory state in a web server is generally not something people actually do, but still.

This sort of ownership kerfuffle is something I often find in Rust libraries that don’t really get a strong workout; it’s easy to write something and say “that looks good”, and it takes someone else with a different use case to try doing something else with it and say “wait, this part of the API makes what I want to do actually impossible”. Another example of that sort of thing is in the middleware! macro; if I try to return different types from the two sides of my if or match branches, it complains and I have to manually make them match up. It SHOULD be possible, one way or another, to return anything that can be turned into a Response, even if it’s just by forcing the user to explicitly call Response::from() or something the way rouille does it; at least then it’s always consistent and obvious what’s going on. It’s easy to make magic happen with Rust’s generics and traits, but it takes a lot more finesse to make it easy for the Right Thing to happen in an understandable and composable way, which is really better than magic. It’s not good magic unless I can roll my sleeves up and get elbows-deep in it when necessary.

Also I don’t understand the difference between mount() vs get() and post(), really… mount() takes a Middleware and wires it into the matcher, while get() and post() take a Middleware and wire it into the matcher. There’s also a router! macro that takes a closure, turns it into a Middleware and builds a matcher out of it. Sooooo. I think there might be some slight differences in how they handle the request paths maybe?. I dunno. Seems like they have trouble deciding how they want it to work.

One more thing to note is that occasionally the web server doesn’t finish starting up and actually open the connection before the rest of my tests try to run. Derp! This is a problem with my tests, not Nickel, as it’s a fraction of a second difference either way.

Summary

  • Low friction: 2.5/5 There’s too many ways of doing the same thing but overall there’s some nice shortcuts for making the easy stuff easy; I like that my handler’s can return String, (StatusCode, String), or probably a few other things. It could be MORE ergonomic, but not bad. There’s a lot of sort of rough edges, but overall you can kinda see where things are going.
  • Documentation: 4/5 It exists, it’s reasonably complete and pretty good overall.
  • Examples: 4/5 There’s a lot of examples covering a fair variety of topics, though they don’t often show how the different moving parts of the framework can be composed.
  • Signal-to-noise ratio: 3.5/5 Again, rough edges, but once you get those papered over it’s fine.
  • Maintenance: 3.5/5 Development is active but doesn’t seem super vigorous. Last release was June 2017 or so. Issues and PR’s seem to get swift responses.
  • Other notes: Don’t REALLY see what makes it at all different from Iron.

Conclusion: All in all, it seems pretty reasonable. The main problems I had were ergonomic; the FEEL I get is that those problems are all sort of in the process of being worked out and the developers are deciding how they want to do things, but haven’t gotten there yet. It feels incomplete. There’s also a bit of a gap in design principles between things like Rouille and Pencil, which are designed to be low-friction, and things like Iron and Nickel, which are designed to be extremely flexible.

gotham 0.1.2

Hello world

Hoo boy, there’s no actual hello world example. There’s a single example ominously named kitchen-sink. Let’s see if I can chop it down…

Okay after ten minutes of digging through docs and stuff I still can’t start an HTTP server, so I’m going to give up for now. It’s a 0.1 version, so I really can’t judge too harshly. My expectations weren’t high and I want to get on with this mess.

Summary

  • Low friction: ???/5 Maybe it’s super simple? But trying to dig through the example code, it looks like they aim more at features than ease of use, so. That’s fine if features are what you want
  • Documentation: 3/5 There’s a lot of it, it’s very much of a work in progress. That’s fine. They have an actual book framework, which is promising.
  • Examples: 1/5 There’s lots of examples. They’re pretty well commented. They’re all FRAGMENTS of examples. There are NO examples I can find that you can actually copy-paste and run, that aren’t 300 lines of bits an pieces that do async whatsit and middleware hooha and Eris knows what else. They show how things work, which is the EASY part, and is fine for marketing to people who want to see shiny 5-line snippets on your website. They do nothing to actually teach people the HARD part, which is how things fit together. If I know even vaguely what I’m looking for, I can find out how it works in the API docs. Nothing in the API docs will EVER tell me “oh to actually run the thing you have to create such-and-so type and feed it into hyper::server::http::new().bind()”.
  • Signal-to-noise ratio: ???/5
  • Maintenance: ???/5 Under active development
  • Other notes: Promising???

Conclusion: Can’t judge, it’s too early to tell. Come back in version 0.3 or so.

rocket 0.3.3

Okay I said I wasn’t going to do this, but I’m curious and I’d feel bad leaving it out, since it’s the new hotness and seems to take itself pretty seriously. Let’s see if it lives up to the hype.

Hello world

Well there’s a FULL hello world example front and center, so that’s a good start. Let’s chop it up a bit to fit our standards:

#![feature(plugin)]
#![plugin(rocket_codegen)]

extern crate rocket;

#[get("/")]
fn hello() -> String {
    format!("hello world")
}

fn main() {
    let config = rocket::config::Config::build(rocket::config::Environment::Staging)
        .port(8888)
        .finalize().expect("Could not create config");

    rocket::custom(config, false)
        .mount("/", routes![hello])
        .launch();
}

#[cfg(test)]
mod tests {
    extern crate reqwest;
    use std::thread;
    use std::io::Read;
    use super::main;

    #[test]
    fn test_basic() {
        let _t = thread::spawn(main);
        let mut resp = reqwest::get("http://localhost:8888").unwrap();
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "hello world");
    }
}

This is gonna be one of those frameworks that tries to do ALL the things for you, isn’t it. Still, not too bad.

Okay, one nitpick: there’s a function called rocket::ignite(). This tells me NOTHING about what it actually does… and since it doesn’t take any config parameters, I’m never actually going to USE it in any real application. It’s there purely to sound cool. It’s marketing. It would be far better to impl the Default trait. But the Rocket type doesn’t impl Default.

On the other hand I can’t pick on anyone who likes igniting rockets, so.

Oh, apparently ignite() also loads configuration options from a Rocket.toml file. That’s cool, but makes the name even MORE confusing. Maybe call it with_defaults() instead? And just where can I put this TOML file anyway? The docs are good but I can’t find the specifics of that.

Serve static file with route

Once again, the existing example does what I want with minimal modification.

#![feature(plugin)]
#![plugin(rocket_codegen)]

#[macro_use]
extern crate lazy_static;
extern crate rocket;
use std::path::{Path, PathBuf};
use rocket::response::NamedFile;

#[get("/")]
fn hello() -> String {
    format!("hello world")
}


#[get("/id/<file..>")]
fn files(file: PathBuf) -> Option<NamedFile> {
    NamedFile::open(Path::new("src/").join(file)).ok()
}

fn main() {
    let config = rocket::config::Config::build(rocket::config::Environment::Staging)
        .port(8888)
        .finalize().expect("Could not create config");

    rocket::custom(config, false)
        .mount("/", routes![hello, files])
        .launch();
}



#[cfg(test)]
mod tests {
    extern crate reqwest;
    use lazy_static;                           
    use std::thread;
    use std::io::Read;                                                                
                                                      
    lazy_static! {
        static ref SERVER_THREAD: thread::JoinHandle<()> = thread::spawn(super::main);
    }                                                         
                                                
    fn spawn_server_and_get(path: &str) -> reqwest::Response {            
        lazy_static::initialize(&SERVER_THREAD);
        let new_path = String::from("http://localhost:8888") + path;                                                            
        reqwest::get(&new_path).unwrap()
    }                
                                                 
    #[test]                                 
    fn test_basic() {                   
        let mut resp = spawn_server_and_get("/");  
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "hello world");               
    }               
                                                        
    #[test]                                 
    fn test_file() {                                                 
        let mut resp = spawn_server_and_get("/id/main.rs"); 
        assert!(resp.status().is_success());    
        let mut content = String::new();                                        
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, include_str!("main.rs"));
    }
}

Accept post requests

All right this is the smoke test. By now I realize this is one of those things that isn’t actually HARD or COMPLICATED, but ends up with lots of moving parts having to touch each other: You have to parse paths, extract JSON, modify shared state, serialize stuff and return it, all while handling errors on the way. Really amazing that what is basically a RPC function call takes so much work.

#![feature(plugin)]
#![plugin(rocket_codegen)]

extern crate rocket;
extern crate rocket_contrib;
extern crate rustyweb;
#[macro_use]
extern crate lazy_static;
extern crate base64;
extern crate serde;
extern crate untrusted;
extern crate ring;
extern crate chrono;

use std::sync::RwLock;

use rocket::State;
use rocket_contrib::{Json};
use rocket::http::Status;

use rustyweb::*;

#[get("/")]
fn hello() -> String {
    format!("Hello world")
}

#[get("/id/<id>")]
fn get_id(id: String, serverdata: State<RwLock<ServerData>>) -> Option<String> {
    if let Some(key) = serverdata.read().unwrap().get_id_key(&id) {
        Some(base64::encode(key))
    } else {
        None
    }
}

#[get("/name/<name>", format = "application/json")]
fn get_name(name: String, serverdata: State<RwLock<ServerData>>) -> Option<Json<UpdateMessage>> {
    if let Some(n) = serverdata.read().unwrap().get_name(&name) {
        Some(Json((*n).clone()))
    } else {
        None
    }
}

#[post("/name/<name>", format = "application/json", data = "<msg>")]
fn post_name(name: String, msg: Json<UpdateMessage>, serverdata: State<RwLock<ServerData>>) -> Result<String, Status> {
    // serverdata.write().unwrap().apply_update_if_valid(&name, &msg.0).map(|_| "ok".to_string())
    match serverdata.write().unwrap().apply_update_if_valid(&name, &msg.0) {
        Ok(_) => Ok("ok".into()),
        Err(v) => Err(Status::Forbidden)
    }
}

fn run(server_data: ServerData, port: u16) {
    let config = rocket::config::Config::build(rocket::config::Environment::Staging)
        .port(port)
        .finalize().expect("Could not create config");

    rocket::custom(config, false)
        .mount("/", routes![hello, get_name, get_id, post_name])
        .manage(RwLock::new(server_data))
        .launch();

}

fn main() {
    let server_data = ServerData::default();
    run(server_data, 8888)
}



#[cfg(test)]
mod tests {
    extern crate reqwest;
    use lazy_static;
    use std::thread;
    use std::io::Read;
    use serde::Serialize;
    use ring::{rand, signature};
    use untrusted;
    use base64;
    use chrono::prelude::*;
    use super::UpdateMessage;

    const UNITTEST_USER: &str = "unittest_user";
    const UNITTEST_NAME: &str = "unittest_name";

    fn start_test_server() {
        use super::ServerData;
        let mut s = ServerData::default();
        let pubkey_bytes = KEYPAIR.public_key_bytes();
        s.add_id(UNITTEST_USER, pubkey_bytes);
        s.update_name(UNITTEST_NAME, &UNITTEST_NAME_VALUE);
        super::run(s, 8888);
    }

    fn generate_keypair() -> signature::Ed25519KeyPair {
        let rng = rand::SystemRandom::new();
        let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
        let keypair = signature::Ed25519KeyPair::from_pkcs8(
            untrusted::Input::from(&pkcs8_bytes)
        ).unwrap();
        keypair
    }

    lazy_static! {
        static ref SERVER_THREAD: thread::JoinHandle<()> = thread::spawn(start_test_server);
        static ref KEYPAIR: signature::Ed25519KeyPair = generate_keypair();
        static ref UNITTEST_NAME_VALUE: UpdateMessage = UpdateMessage::signed_message(&KEYPAIR, UNITTEST_USER, "unittest_value");
    }


    fn spawn_server_and_get(path: &str) -> reqwest::Response {
        lazy_static::initialize(&SERVER_THREAD);
        let new_path = String::from("http://localhost:8888") + path;
        reqwest::get(&new_path).unwrap()
    }

    fn spawn_server_and_post<T: Serialize>(path: &str, json: &T) -> reqwest::Response {
        lazy_static::initialize(&SERVER_THREAD);
        let client = reqwest::Client::new().unwrap();
        let new_path = String::from("http://localhost:8888") + path;
        client.post(&new_path).unwrap()
            .json(json).unwrap()
            .send().unwrap()
    }

    #[test]
    fn test_basic() {
        let mut resp = spawn_server_and_get("/");
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "Hello world");
    }

    #[test]
    fn test_id() {
        let mut resp = spawn_server_and_get((String::from("/id/") + UNITTEST_USER).as_str());
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        let pubkey_bytes = KEYPAIR.public_key_bytes();
        let pubkey_string = base64::encode(pubkey_bytes);
        assert_eq!(content, pubkey_string);
    }

    #[test]
    fn test_get_name() {
        // Test unset name default
        let resp = spawn_server_and_get("/name/test_no_name");
        assert_eq!(resp.status(), reqwest::StatusCode::NotFound);

        // Test set name
        let mut resp = spawn_server_and_get((String::from("/name/") + UNITTEST_NAME).as_str());
        assert!(resp.status().is_success());
        let resp_msg: UpdateMessage = resp.json().unwrap();
        assert_eq!(resp_msg, *UNITTEST_NAME_VALUE);
    }

    #[test]
    fn test_post_name() {
        const NEWNAME: &str = "/name/test_post_name";
        // See that name DNE
        let resp = spawn_server_and_get(NEWNAME);
        assert!(!resp.status().is_success());

        let changed_name = "foo!";
        let data = super::UpdateMessage::signed_message(&KEYPAIR, UNITTEST_USER, changed_name);

        // Change name
        let mut resp = spawn_server_and_post(NEWNAME, &data);
        assert!(resp.status().is_success());
        let mut content = String::new();
        resp.read_to_string(&mut content).unwrap();
        assert_eq!(content, "ok");

        // Test name now that it's been changed
        let mut resp = spawn_server_and_get(NEWNAME);
        assert!(resp.status().is_success());
        let resp_msg: UpdateMessage = resp.json().unwrap();
        assert_eq!(resp_msg, data);

        // Try changing it again with unsigned request
        let baddata = super::UpdateMessage {
            user: UNITTEST_USER.into(),
            utc: Utc::now(),
            signature: "".into(),
            new_contents: "aieeee!".into(),
        };
        let resp = spawn_server_and_post(NEWNAME, &baddata);
        assert!(!resp.status().is_success());

        // Ensure it hasn't changed.
        let mut resp = spawn_server_and_get(NEWNAME);
        assert!(resp.status().is_success());
        let resp_msg: UpdateMessage = resp.json().unwrap();
        assert_eq!(resp_msg, data);
    }
}

Summary

  • Low friction: 5/5 Wow
  • Documentation: 4.5/5 Can’t figure out how to return a message along with a 403 status code, but I expect that’s in the domain of error catchers which I didn’t try to delve into. Still.
  • Examples: 4.5/5 There’s lots of them and they cover a broad field from simple to complex, though one or two of them are kind of too simple to be interesting; couldn’t find any nontrivial error-handling example for instance.
  • Signal-to-noise ratio: 5/5 Wow
  • Maintenance: 5/5 Actively developed
  • Other notes: I’m a little scared of what it will take to get things done if you try to do something that cuts “across the grain”, implementing new abstractions instead of using the ones it offers you. But from what I can see there’s nothing stopping you from going down a level or two and getting into the guts of the system, ignoring the higher bits that aren’t relevant to you. It’d take an actual expert to say for sure though.

Conclusion: I honestly did not expect it to live up to the hype but damn if it isn’t all that and more.

Final thoughts

I’ve come to see there’s really two generations of Rust web app servers. The first generation is Iron, Nickel, Pencil, Rustless and Rustful (maybe Rouille too?). Of those, only Iron, Nickel and Rouille have actually survived. The others are essentially unmaintained, and sadly are starting to bit-rot. They work, but they don’t work with the newest tools that everyone actually wants to use. New tools are being developed at a fairly fast clip these days, so bit-rot also sets in pretty quick. A Python project that hasn’t been touched in a year would still be totally usable. A C project that hasn’t been touched in 15 years would take a bit of hacking but still be basically fine.

We’re currently in the middle of second generation, of which the forefront is probably Rocket, but there’s also Gotham, Shio, and so on. Not all of those are going to survive either… and it’s not entirely clear what benefits those bring, except for Rocket which… well. I can’t call it anything except experimental, ’cause it relies on compiler features that may or may not ever stabilize. But damn if it doesn’t make them appealing. And when/if it does get there? It’s going to be killer.

Apart from that, the best software I used was Rouille, hands down. Something without the newest-and-shiniest features, that is just engineered really well, is going to beat the new hotness that hasn’t had all the kinks worked out of it yet. There’s also the matter of scope: you can make something that’s designed to do everything, like Iron/Nickel, you can make something that’s designed to do one kind of thing really well, like Rustless, or you can make something that’s designed to be simple and easy to get rolling with, like Rouille and Pencil. (And if you’re a mad genius, apparently, you can do all three in Rocket.) You see the same sort of division in Python web stuff (which is the only other web backend stuff I’ve ever touched, I confess): Zope, Django and Flask kinda fit into the same categories, if you use a big enough crowbar.

It’s also funny seeing the changes in how people approach the ecosystem. A year ago people were saying “man tokio is going to rock the world”. Now they’re saying “man tokio is a pain in the butt to work with, we need real async primitives”.

Also OpenSSL is the devil and we should either stop using it or get openssl-sys to 1.0 ASAP.

So yeah. To all backend web devs who want to use Rust, use Rouille or Rocket, I guess unless you actually need to use Iron or Nickel. To all people who want to make a web framework, contribute to Rouille or Rocket instead of rolling your own.

Future work

It would be nice to at least touch everything I left out, and also re-visit Iron in the context of everything else I used. But this has already taken like 14 hours to write and I’m sick of it.