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 intoreqwest
sanely. Ifrustc-deserialize
ever expects structs to be in a slightly different form than whatserde
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.