ActuallyUsingIron

Preface

If you want a less-ranty version, check out ActuallyUsingIronAgain which is more a proper tutorial. It also has a few minor updates that I haven’t bothered backporting here.

If you want something like Flask, use the pencil crate. It’s probably what you want unless you actually want a big professional full-power web application stack.

Actually using Iron: A grumpy introduction to web development in Rust

So I started playing around with the Iron web framework because writing Rust is awesome, and I wanted to write a wiki. This comes from the perspective of someone who’s done a bit of of backend web development in Django and Flask but really isn’t part of the Crazy Web Ecosystem. This chronicles my experiences as of November 2016. Note that this is a tutorial on web dev written by someone who doesn’t like web dev very much, so the tone will reflect that.

To start with, why Iron, instead of Nickel or Conduit or anything else on http://www.arewewebyet.org/? Because Iron actually has documentation. That’s literally it.

First up, you will notice that Iron has a modest amount of documentation which doesn’t actually tell you how to do anything useful, with poorly-explained terms and obvious things that obviously work until you try to do anything more complicated with them. It also tends to assume you already know what the hell is going on, since you’re obviously a web guru who lives and breathes this stuff and has spent years using the four different Javascript web frameworks that inspired this sort of architecture, instead of some random person who’s good at programming but doesn’t feel like keeping up the vast, ever-shifting and mostly-dysfunctional ecosystem of internet software. This is typical of web framework junk. Here’s how things actually work:

All you’re really doing is defining a pipeline. HTTP requests go in one side, responses come out the other. “Middleware” is a stupid and amazingly vague term for all the various stages in this pipeline. A “middleware” is a step in this pipeline. It’s actually explained moderately well in the iron::middleware module docs, so go check that out. I’ll wait for you to come back.

Back? Good. Each step in this pipeline is implemented as a trait on an object; this trait just defines a function that is called to actually execute the stage in the pipeline. There are four types: BeforeMiddleware, which takes a Request and modifies it, AfterMiddleware which takes a Response and modifies it, AroundMiddleware which takes a request and modifies it on the way in, then takes the response it produces and modifies it on the way out… and finally Handler which actually does the real work of taking a Request and turning it into a Response. Nevermind that you could easily call them RequestProcessor, ResponseProcessor, and HandlerWrapper, given that that’s what they actually do, but I digress…

Now you run your server by doing Iron::new(something).http(address).unwrap(), as per Iron’s trivial docs. What in the world is something? something is a Handler, of course, it takes a single request and spits out a single response. That’s all you need! Let’s do this:

extern crate iron;

use iron::prelude::*;

fn hello_world(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with((iron::status::Ok, "Hello World")))
}

fn main() {
    Iron::new(hello_world).http("localhost:3000").unwrap();
}

(Don’t ask me about Response::with. We’ll get to that rant in a bit.)

Great! That was easy. Does it work? Test it:

$ curl localhost:3000
Hello World

Great.

Well this is a little weird, you might say. I was expecting something like Django or Flask, where you define routes that match a particular URL pattern, and then functions that are called when a request hits those routes. Flask gives me that instantly. How the flying fuck do you do that in Iron? Ah, but this is GREAT WEB STUFF, so obviously you can’t have anything that simple. You need more layers of indirection in the mix, young padawan!

The functionality you actually want is provided by the router crate. It’s just called router, not iron_router or anything useful like that. What if someone wants to actually make a router, as in, a device that routes packets, and is searching crates.io? Well screw those guys. What if someone’s searching crates.io for all packages related to iron? Screw those guys too, they won’t find router in the top couple dozen hits. Sure, crates.io has a tag system, but who the hell uses that? Nevermind that router a core part of the iron software that essentially anyone will want to use, even if only in the early stages of their project.

Ahem. ANYWAY! The router crate provides an object named, naturally, Router, which is a Handler. You will be forgiven for not immediately realizing this as the term Handler is mentioned nowhere in its documentation until you dig down through the trait sludge of the Router object itself. So the Router object will take a path and another Handler, suck in requests, match them based on their path, and shunt them to the appropriate Handler.

extern crate iron;
extern crate router;

use iron::prelude::*;
use router::Router;

fn get_page(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with((iron::status::Ok, "Got page")))
}

fn post_page(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with((iron::status::Ok, "Posted page")))
}

fn main() {

    let mut router = Router::new();
    router.get("/:page", get_page, "page");
    router.post("/:page", post_page, "page");

    Iron::new(router).http("localhost:3000").unwrap();
}

router.get() and router.post() match HTTP GET and POST requests, naturally; there’s also methods for PUT, DELETE, etc. There’s even a nice router!() macro that lets you build this up with… well, a macro, but since the information you put into it is exactly the same as with the function calls and it’s not actually any less verbose, you might as well just use the function calls and save yourself the trouble when you make a typo and the macro spews an incomprehensible match error. The “page” part in the route definition is just a label that lets you refer to that particular route later, if you need to. Now, does it work?

$ curl http://localhost:3000/test
Got page
$ curl -d dummy=data http://localhost:3000/test
Posted page
$ curl -i http://localhost:3000/path/that/produces/no/match
HTTP/1.1 404 Not Found
Date: Mon, 07 Nov 2016 18:56:41 GMT
Content-Length: 0

Great, so there’s your dispatch of requests to various functions to do stuff with them. Oh, you want to know what the matching syntax is? UNDOCUMENTED, THAT’S WHAT IT IS! Get over it punk! Anyway, you can take the source apart and figure that out, it’s not too tricky, so… problem solved, right? But wait, you want to actually use some fiddleware? Like, perhaps, the inscrutable and infathomable handlebars_iron crate to do templating, or even (gasp) the logger crate which will forever pollute the namespace for everyone who’s ever looking for logging functionality that doesn’t involve Iron? You poor bastard!

Okay, this is what the Chain struct is for. A Chain is a Handler that takes another Handler and actually does the piddleware song-and-dance, letting you define pipeline stages for your request to shuffle through until it hits the Handler you give it, and shunting the Response through the same process. So you create a Chain with the Handler that does your actual work, then use the link_before(), link_after(), etc. methods to slot diddleware objects together to build your pipeline.

Oooooh, suddenly this all makes sense. The magical implicit web crap that everyone loves because it hides the complexity and makes complicated things look simple until you try to do anything with them has been made explicit and sensible. Well, apart from the fact that a Handler or a Middleware can be either a function or an object implementing the appropriate trait. That’s honestly kind of nice, but nobody tells you that anywhere so you’re just confused by what the hell type everything actually is until you say “Oh, I bet the right skittleware trait is implemented for a function of the appropriate type” and check the docs to figure out what the type signature should be.

Let’s make a simple logger that prints our responses out on the server console.

extern crate iron;
extern crate router;

use iron::prelude::*;
use router::Router;

fn get_page(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with((iron::status::Ok, "Got page")))
}

fn post_page(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with((iron::status::Ok, "Posted page")))
}

fn response_printer(_req: &mut Request, res: Response) -> IronResult<Response> {
    println!("Response produced: {}", res);
    Ok(res)
}

fn main() {
    let mut router = Router::new();
    router.get("/:page", get_page, "page");
    router.post("/:page", post_page, "page");

    let mut chain = Chain::new(router);
    chain.link_after(response_printer);

    Iron::new(chain).http("localhost:3000").unwrap();
}

Okay, that works, great. Enough jibber-jabber. Let’s do something real. Say you want to serve some static files. WOAH HOLD THE PHONE, YOU CAN’T JUST DO THAT! YOU NEED SOME MORE CRATES! staticfile specifically, which at least doesn’t conflict too egregiously with anything anyone else would want to name a crate. But the sole example the staticfile crate gives uses another crate, mount, which lets you compose handlers together much like router does. The main difference is that while router will do some sort of matching and pass the full path on to its Handler, mount will only pass a portion of the path, letting you build up your paths out of multiple relative paths. I’d really rather not use mount and KISS, but it honestly does seem the best way to use staticfile, ’cause otherwise it’s rather hard to tell router that “file foo.png should come from images/foo.png.”

extern crate iron;
extern crate staticfile;
extern crate mount;

use iron::prelude::*;
use staticfile::Static;
use mount::Mount;

fn main() {
    let mut mount = Mount::new();
    mount.mount("/", Static::new("static/"));
    Iron::new(mount).http("127.0.0.1:3000").unwrap();
}

Notice that mount is a Handler, not a BeforeMiddleware, even though you could consider it to be rewriting a request and passing it on to a Handler. In fact, it’s not even an AroundMiddleware. What IS an AroundMiddleware, anyway? Let’s look at the trait’s signature:

pub trait AroundMiddleware {
    fn around(self, handler: Box<Handler>) -> Box<Handler>;
}

So… it takes a Handler, presumably does whatever you want to wrap it in other code, and returns another Handler. But, between router and mount we’ve seen two instances where you create a Handler by passing them another Handler anyway. Am… am I misunderstanding this? Because it seems like an AroundMiddleware is literally just a Handler. It’s a Handler that gets called with another Handler when a request hits it, but that Handler is going to always be the same thing because you have no real way to swap them out at runtime, and we see tons of instances where a Handler is constructed from another Handler anyway, so, so… augh, I need to go lie down for a while.

Anyway Back on topic! Make a static/index.html file and give it a spin:

$ curl http://localhost:3000/index.html
Static index page!

Well that was easy. Now let’s try integrating it into our previous router-based thingy, so we serve static files from one subdirectory and generate responses for others. Let’s see, we want something like this, right?

fn main() {

    let staticfiles = Static::new("static/");

    let mut router = Router::new();
    router.get("/:page", get_page, "page");
    router.post("/:page", post_page, "page");
    router.get("/ourdata/*", staticfiles, "static");

    let mut chain = Chain::new(router);
    chain.link_after(response_printer);

    Iron::new(chain).http("localhost:3000").unwrap();
}
$ curl -i http://localhost:3000/ourdata/index.html
HTTP/1.1 404 Not Found
Date: Mon, 07 Nov 2016 19:55:02 GMT
Content-Length: 0

$ curl -i http://localhost:3000/index.html
HTTP/1.1 200 OK
Content-Length: 8
Content-Type: text/plain
Date: Mon, 07 Nov 2016 19:54:48 GMT

Got page

No? Uh… let’s try the simplest case, like this…

fn main() {
    let s = Static::new("static/");

    let mut router = Router::new();
    // router.get("/:page", get_page, "page");
    // router.post("/:page", post_page, "page");
    router.get("/*", s, "mount");

    let mut chain = Chain::new(router);
    chain.link_after(response_printer);

    Iron::new(chain).http("localhost:3000").unwrap();
}
$ curl -i http://localhost:3000/
HTTP/1.1 404 Not Found
Content-Length: 0

$ curl -i http://localhost:3000/index.html
HTTP/1.1 200 OK
Content-Length: 7
Content-Type: text/html
Date: Mon, 07 Nov 2016 19:53:02 GMT

Static index page!

$ curl -i http://localhost:3000/ourdata/index.html
HTTP/1.1 404 Not Found
Date: Mon, 07 Nov 2016 19:54:53 GMT
Content-Length: 0

Okay, that lets us get our static pages, but not our generated responses. In fact, our response_printer() function isn’t getting called on the 404 errors either. What the hell is going on? This seems like a good time to talk about error handling!

Part 2: Erronous Errors

Summary of error handling: Oh gods. Handlers and shittleware return a Result, as is only sane and just in the world. But BeforeMiddleware and AfterMiddleware defines an extra method called catch(). When something returns an Err() value, instead of the pipeline aborting as try!() would do, or the next tiddleware’s normal handler method getting called with an error Response, a Response with some sort of error type is generated and passed to the next giggleware’s catch() method instead. So you have two parallel paths a Request and Response can travel down, the normal-ass one or the there-was-an-error-path. If you don’t define a catch() method, for instance if you’re using a single function as a handler/biddleware as we’ve been doing, the default catch() method does nothing but pass the error on to the next middleware’s catch() method. But you can return an Ok(()) from the catch() method which puts your train back on its normal tracks, calling the normal-ass handler methods in the subsequent hiddleware. Except, I suppose, with a Response that has an error value in it, which seems like a potentially perilous thing to do, unless you’ve changed it.

Also note that Handler doesn’t get a catch() method, so I guess it’s not allowed to recover from upstream errors… The docs justify this as saying that the Handler’s job is to create a Response, and when you return an IronError in a Result it already has an (error) response in it. Fair enough. Also AroundMiddleware doesn’t have catch() either, because it’s really just a fucking Handler.

I’m of mixed feelings about this two-track error handling system. On the one hand, this separates error handling code out into its own little special case, which it generally is. On the other hand, this is very effective at making errors magically disappear, which is pretty un-Rust-y. But it lets you completely ignore error handling in the ten lines of shiny example code on your framework’s web page that you use to show how easy it is, and that’s what web dev is all about, isn’t it? It also makes the data flow potentailly completely batshit because it turns a very simple pure-functional “take a value and return a new value” type flow into something weird and different, that might potentially involve a Request and Response getting shuffled back and forth between main-track and error-track control flow multiple times. But Iron already doesn’t use a “take a value and return a new value” type flow, we’ll get to that…

Also, if this two-parallel-error-paths type of programming sounds at all familiar, it should. Now if only Rust had something designed to do exactly that without any kind of funny business…

Okay, so. Let’s modify our response_printer() function to be a jiggleware object that implements a catch() method…

extern crate iron;
extern crate router;
extern crate staticfile;
extern crate mount;

use iron::prelude::*;
use iron::middleware::AfterMiddleware;
use router::Router;
use staticfile::Static;
use mount::Mount;

fn get_page(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with((iron::status::Ok, "Got page")))
}

fn post_page(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with((iron::status::Ok, "Posted page")))
}

struct ResponsePrinter;

impl AfterMiddleware for ResponsePrinter {
    fn after(&self, _req: &mut Request, res: Response) -> IronResult<Response> {
        println!("Response produced: {}", res);
        Ok(res)
    }

    fn catch(&self, request: &mut Request, err: IronError) -> IronResult<Response> {
        println!("Error happened: {}", err);
        println!("Request was: {:?}", request);
        Err(err)
    }
}

fn main() {
    let s = Static::new("static/");

    let mut router = Router::new();
    // router.get("/:page", get_page, "page");
    // router.post("/:page", post_page, "page");
    router.get("/*", s, "static");

    let mut chain = Chain::new(router);
    let printer = ResponsePrinter;
    chain.link_after(printer);

    Iron::new(chain).http("localhost:3000").unwrap();
}

Run it and see what gets printed out when we ask for something that produces an error:

Error happened: No such file or directory (os error 2)
Request was: Request {
    url: Url { generic_url: "http://localhost:3000/static/index.html" }
    method: Get
    remote_addr: V6([::1]:58348)
    local_addr: V6([::1]:3000)
}

Well that was less informative than I had hoped, honestly. Still, it proves that our understanding of error handling is correct! Though I can’t figure out any way of making Static or Router print out what it expected to get versus what it actually got, just that an error happened. You’d think that router::url_for() might be useful for that purpose, but in reality it just panics when I ask it for the “static” route in the error handler. So it’s back to trial and error.

So it looks like the mount crate is really what we want after all. Check this out:

extern crate iron;
extern crate router;
extern crate staticfile;
extern crate mount;

use iron::prelude::*;
use router::Router;
use staticfile::Static;
use mount::Mount;

fn get_page(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with((iron::status::Ok, "Got page")))
}

fn post_page(_: &mut Request) -> IronResult<Response> {
    Ok(Response::with((iron::status::Ok, "Posted page")))
}

fn response_printer(_req: &mut Request, res: Response) -> IronResult<Response> {
    println!("Response produced: {}", res);
    Ok(res)
}

fn main() {
    let mut router = Router::new();
    router.get("/:page", get_page, "page");
    router.post("/:page", post_page, "page");

    let mut mount = Mount::new();
    mount.mount("/ourdata", Static::new("static/"));
    mount.mount("/", router);

    let mut chain = Chain::new(mount);
    chain.link_after(response_printer);

    Iron::new(chain).http("localhost:3000").unwrap();
}

Try it out:

$ curl -i http://localhost:3000/index.html
HTTP/1.1 200 OK
Content-Type: text/plain
Date: Mon, 07 Nov 2016 20:46:15 GMT
Content-Length: 8

Got page

$ curl -i http://localhost:3000/ourdata/index.html
HTTP/1.1 200 OK
Date: Mon, 07 Nov 2016 20:46:19 GMT
Content-Type: text/html
Content-Length: 7

Static index!

This does exactly what we want. Remember, mount basically matches a path, and then snips off the part it matches and hands the rest to the Handler. So if we request /ourdata/index.html then it hits mount, matches /ourdata, and the Static gets passed /index.html. It looks that up in the static/ directory and finds it. Meanwhile, if we just request /whatever it matches /, and our router gets passed whatever, which apparently it’s smart enough to match against /:page. It makes perfect sense!

Why just using the router doesn’t work? I dunno; I THINK it’s because if we passed /ourdata/index.html to the Static it would look for it in static/ourdata/index.html, which isn’t where we put it. But there’s no real way to get diagnostics out of the damn thing. Where do the slashes go, precisely, and does Mount preserve or nuke directory separators, and how does Router handle prefixed slashes and such? Good bloody question, but this seems to work. Oh, and whatever you do, make sure the Mount contains the Router and not vice versa by accident, or else who knows what might happen?

Well, the same thing that’s already happened: your requests will go somewhere and you’ll have no way of figuring out where they’re going.

So congratulations, we have braved the wilds of Rust web development and made a fully functioning static file server using Iron!

Part 3, in which things get worse

Great. Can we do something a little less pathetic? Let’s serve up Markdown files converted to HTML. That should be easy, right?

First off, let’s use the hoedown crate. For once we’re actually using a crate that shouldn’t be part of the core infrastructure and isn’t treasonously poorly named! hoedown apparently wraps some Markdown converter library I’ve never heard of, but it has a funny name and is the Markdown parser that has the most hits on crates.io, so it’s gotta be good. Now I’m gonna slap a heap of code on to you and we’ll go through it in a bit more detail…

extern crate iron;
extern crate router;
extern crate staticfile;
extern crate mount;
extern crate hoedown;

use std::fs;
use std::io;

use iron::prelude::*;
use iron::status;
use router::Router;
use hoedown::Render;


static PAGE_PATH: &'static str = "pages/";

fn get_page(req: &mut Request) -> IronResult<Response> {
    let ref pagename = req.extensions
                          .get::<Router>()
                          .unwrap()
                          .find("page")
                          .unwrap_or("no query");

    let mut pagepath = PAGE_PATH.to_owned();
    pagepath += pagename;
    pagepath += ".md";

    match fs::File::open(pagepath) {
        Ok(file) => {
            let md = hoedown::Markdown::read_from(file);
            let mut html = hoedown::Html::new(hoedown::renderer::html::Flags::empty(), 0);
            let buffer = html.render(&md);
            let rendered_markdown = buffer.to_str().unwrap();

            Ok(Response::with((status::Ok, rendered_markdown)))
        }
        Err(e) => {
            let status = match e.kind() {
                io::ErrorKind::NotFound => status::NotFound,
                io::ErrorKind::PermissionDenied => status::Forbidden,
                _ => status::InternalServerError,
            };

            Err(IronError::new(e, status))
        }
    }
}

fn main() {
    let mut router = Router::new();
    router.get("/:page", get_page, "page");

    Iron::new(router).http("localhost:3000").unwrap();
}

Okay, I’ve taken out everything except the get_page() function ’cause that’s all we’re concerned with and our signal-to-noise ratio is low enough as it is. So, first off:

    let ref pagename = req.extensions
                          .get::<Router>()
                          .unwrap()
                          .find("page")
                          .unwrap_or("no query");

We get the name of the page we’re looking for. I mean, that’s obvious, right? Why wouldn’t it be obvious? The Request has an extensions field, which is a TypeMap. TypeMap is a HashMap, more or less, except instead of using values for keys it uses types. However the hell it does that. So, first we get the value associated with the Router type, which is presumably some extra data our Router has squirreled away in the request rather than anything actually associated with a HTTP request. This is how omgiddleware passes data around, apparently!

(More specifically, in fact, we’re getting the value of the “page” variable matched by the “:page” pattern in our Router. If the pattern was “/:foobaz” we would be doing reg.extensions.get::<Router>().unwrap().find("foobaz")...)

What the hell are we actually getting out of it? What are the values in the Request.extensions map? Uh. Well, let’s look at the return type of iron::typemap::TypeMap::get():

impl<A> TypeMap<A> where A: UnsafeAnyExt + ?Sized
...
    fn get<K>(&self) -> Option<&K::Value> where K: Key, K::Value: Any, K::Value: Implements<A>

…uh. Well, it’s obvious, of course. You know just what’s going on here, right? I don’t need to explain any of this crap.

Oh! Look again and ignore the cruft: fn get<K>(&self) -> Option<&K::Value>. So it returns the Value type associated with whatever the key type is. The key here is the Router type, so we look at the docs for that, and lo and behold, down in the trait sludge:

impl Key for Router
   type Value = Params

Heyyyy, so it’s a Params object! Which is who knows what under the hood but it doesn’t matter, we can see it does indeed have a find() method that’s completely undocumented, so we’re golden. There that wasn’t so hard, was it? (Though arcane, this is actually secretly brilliant because it means that anything can store and access values in the TypeMap without accidentally stomping on anyone else’s data; if you index things by the type of what put it there then you always know what you’re getting.)

Okay, so we read the file (or return an error of some kind if we can’t read it), and convert the Markdown into HTML with some defaults:

let md = hoedown::Markdown::read_from(file);
let mut html = hoedown::Html::new(hoedown::renderer::html::Flags::empty(), 0);
let buffer = html.render(&md);
let rendered_markdown = buffer.to_str().unwrap();

Hey, that’s so sane it’s downright boring! Well we can’t be having that, let’s look at the next bit:

Ok(Response::with((status::Ok, rendered_markdown)))

What’s wrong with that? Response::with() takes a status and a &str and creates this Response object we’ve been talking about so much. Easy, right? Well let’s look at the type for Response::with() again…

fn with<M: Modifier<Response>>(m: M) -> Response

Modifier<Response>>? What the hell is that? Well it uses the crate called modifier, which provides a single trait, Modifier<F>. This trait provides a single method, modify(self, &mut F), with the clear and concise documentation modify F with self. This… took a little while to wrap my brain around. Response::with() obviously creates a response from some values, but it requires those values to implement the modify() method which… does something… to the response???? So… how is it different from a hypothetical Response::new() function that takes those types as parameters, or even a Response::from() function? And… uh, what types can it actually take? In the process of figuring this out I realized that if rendered_markdown is a Read instead of a &str it still works, which is generally not what happens in super-duper-static Rust! What are they actually doing? What’s going on?!?!

Okay, okay. Deep breath. Here’s what’s going on in the above function, sort of pseudo-expanded for illustrative purposes:

let mut r = Response::default();
let status_code = status::Ok;
let response_body = rendered_markdown;
r.set_status(status_code);
r.set_body(response_body)
Ok(r)

Okay, how is that happening? Well, take a look at the modifier::Modifier documentation in the Iron docs, and you will see that the trait is implemented for a bunch of types:

impl<'a> Modifier<Response> for &'a str
impl Modifier<Response> for Status
impl Modifier<Response> for Vec<u8>
...

This is really just regular-ass dynamic dispatch, except backwards, so it’s the variant type that determines what the hell is going on, not the type being affected. So we can rewrite the above code as:

let mut r = Response::default();
let status_code = status::Ok;
let response_body = rendered_markdown;
status_code.modify(r);
response_body.modify(r)
Ok(r)

So, for each of these types there’s a modify() method that takes a Response, and does something to it. Presumably, modifies it to include whatever data it represents. But wait, we’re actually passing Response::with() a tuple? Well Modifier is also implemented on tuples where the objects of a tuple implement Modifier, up to tuples of length six! Presumably it just executes the modifiers on each in some kind of sequence, and presumably these modifiers won’t step on each other in any stupid and terrible way, right? Right? Because you’ve documented what they do to the Response, right?

Ahahahahhaha I kill me.

Seriously, people. We’ve progressed to the point where we layer our metaprogramming three layers deep to modify an object. That’s all you’re doing! You have a crate, a trait and two layers of genericity to accomplish “response.body = rendered_markdown”, and it could break at any moment with nobody knowing why. No wonder the Javascript ecosystem sucks so much. And I can think of at least three ways off the top of my head to do it better! And here they are!

  1. Use the builder pattern. I don’t like it, I think it’s laborious and dumb, but it’s undeniably clear and effective, and anyone who has programmed Rust for more than twenty minutes is going to know exactly how it works and what it’s doing. PLUS you document all the things you can do to a Response in the process.
  2. Use a functional style where each “modifier” takes a Response and returns a new Response. It’s more clear what the hell is going on, you can see it just in the type signatures of the functions without going two traits deep. Again, you have a place to document what’s actually going on. And it’s a pattern, again, that people have probably seen before. And you can even compose it quite easily if you want, without doing sneaky things to tuples. (Okay that’s a lie, 3/4 of functional programming is doing sneaky things to tuples.)
  3. Just do it the dumb and explicit way and have a Response have a number of methods on it that, you know, modify stuff, taking different types of arguments. If you try hard enough you can even make them sneakily generic, similar to how the TypeMap works, so you don’t need modify_with_str or modify_with_status and you can prevent people from being sure what the hell is going on unless they really try, because that’s obviously your goal.

There’s one reason I can think of to do it this way, and that’s that anyone can implement the Modifier trait on their own object types and build Response’s out of them. But you can do that for the builder pattern too, you just have to define your own trait for the response builder, which is a bit of a pain but no big deal. You can do that with the functional style by defining functions. Or you can do it the dumb way and… write functions.

Why is everybody so against writing functions? I could have spent all day writing functions instead of figuring out how the fuck this works, and then writing this rant about it. I didn’t even intend this to be a rant, I swear.

Concluding remarks

Iron is pretty darn good. Really! It seems generally solidly written, very functional, and reasonably simple. All of the batshit stuff I struggle with here is either a matter of documentation, which will get smoothed out in time, or architectural quibbles that, honestly, are pretty minor. If all this madness is just how web app servers get written these days, Iron is a quite solid example of such, and I learned a lot from digging into it in this much detail. Of course I feel like this program is utterly loco and I could do way better… I’m a programmer, I always feel that way. But at the end of the day I’ve been coding this long enough to know that I probably won’t actually do it better, there may well be good reasons for it being the way it is that I don’t understand, and I’m really glad that I don’t have to write it myself.

Now let’s go see if Nickel is any saner. Signs point to no.

Errata

Function signature mysteries

For an encore, let’s check out the signatures for some of the stuff we’ve written:

pub trait BeforeMiddleware: Send + Sync + 'static {
    fn before(&self, _: &mut Request) -> IronResult<()> { ... }
    fn catch(&self, _: &mut Request, err: IronError) -> IronResult<()> { ... }
}
pub trait AfterMiddleware: Send + Sync + 'static {
    fn after(&self, _: &mut Request, res: Response) -> IronResult<Response> { ... }
    fn catch(&self, _: &mut Request, err: IronError) -> IronResult<Response> { ... }
}

There’s a couple of rusty nails in the pudding here. First, I would expect BeforeMiddleware to take a Request and produce a Request, and AfterMiddleware to take a Response and produce a Response. Instead, BeforeMiddleware mutates a Request that gets passed to it and returns (). That’s fine, again, as long as it can make sure it doesn’t step on stuff. Now let’s look at AfterMiddleware, which presumably does the exact same thing… Second, why does AfterMiddleware need a Request? Okay, you might need to pull random data out of it helpfully, but why is it &mut in that case? I don’t know, but I suspect that what’s going on is it’s using the Request’s TypeMap as a conduit to sneakily pass data around between shizzleware objects. Or something. I don’t know! …Okay, mea culpa, the Iron docs actually say that AfterMiddleware is allowed to modify the Request to do things like append tracking data or such. That’s fine. (Even if it comes under the heading of sneakily passing data around etc.)

But it simultaniously warns against modifying your Response, saying it should only add things instead of overwriting things. Okay… And… it uses the functional-y idiom of taking an immutable Response and returning a new one. So why is this different from BeforeMiddleware? It is a mystery.

Interesting comments from reddit

’Cause I posted this to https://www.reddit.com/r/rust/comments/5bph98/actually_using_iron_a_rant/ and got far more lulz and support than I ever expected. Notable comments include (duplicated totally without permission, hah!):

nostrademons

TBH, as someone with only a little Rust knowledge but a lot of webdev experience across many different frameworks, this makes me pretty excited to try out Iron.

The middleware shit turns out to be how every complex webapp eventually ends up architected, eventually. At first you just want routes that go to functions that do something. Then someone decides that they want to support user logins, and now you need AuthenticationMiddleware. But before then, you need to support cookies, so better write some CookieMiddleware to parse the request headers and inject a cookie map. Both of these need to add arbitrary data to the request, but luckily Iron supports extensions in the form of a TypeMap on the Request. (Back in Django-land, this was very awkward, with most middleware just writing arbitrary attributes back onto the request and no provision for name-collisions.)

Then your security guys get ahold of the app, and dictate that your cookies need to be signed, so you better stick some middleware between CookieMiddleware and AuthenticationMiddleware that throws out forged cookies and logs the attempt. And then on the response end, you better have CSRF protection, so add CSRF middleware - but wait, CSRF protection can be done much more efficiently if it operates on raw forms & templates rather than HTML, so if you have a transparent template object representation, you could operate on that and then write it out to HTML later. Iron makes all of this very easy - it even has responses work in terms of a WriteBody trait instead of raw strings so you can do the last case.

The catch() shit comes in really handy here too: usually if any of your middleware fails, you want to log an error and return a prettified 500 server error to the user. You'd do this with the last layer of AfterMiddleware - it's great that you don't have to worry about the error until then.

The Modifier shit is great too. One big problem with other web framework ecosystems is they have no way of packaging up a series of modifications to the response in a single re-usable item that you can just include. Think of widgets like django-comments that add data to the response, or Babel which compiles your JS, or various inline-images packages that convert your images to data: URLs. They may do complex things to the response, like including additional script tags, changing the content type, parsing links, etc, but Modifier gives them a drop-in hook they can use to do anything necessary, transparently to the programmer.

It looks like all of Iron was designed for the creation of a stable a la carte ecosystem around it. It'll be pretty confusing for beginning webdevs until that ecosystem exists, but as someone who's been through the design & evolution of at least 5 different web frameworks, I'm glad to see them not making the mistakes of the past.
icefoxen

Man, thank you so much for explaining all of this, let alone taking the time to read through my profane ramblings. I'm legitimately happy to hear that all of this wackiness has a reason, and half of it is just a case of me cutting myself on too-sharp tools. I sort of got the feeling that might be the case, and I'm glad it all makes sense to someone!

...so can you explain how an AroundMiddleware is different from a Handler that is just constructed from another Handler?
nostrademons

It also gives a place & a type signature for the framework to hang metadata about each level in the middleware stack. Common reasons you would want this include:

    Many production webapps collect timing stats for each part of the request handling pipeline, so that if your users complain "The website is slow", you can diagnose the problem immediately.
    If a middleware layer is consistently throwing errors, this gives the framework a chance to collect stats on the errors and possibly shut off the middleware entirely if it's threatening the stability of the server. This is even more important in a static language like Rust than a dynamic one like Python.
    This lets you conditionally enable features via experiment or A/B test. For example, if you were to do middleware to inline images, you might A/B test it against omitting that middleware from the stack and then compare latency & conversion numbers against straight HTTP.