ActuallyUsingIronAgain

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, using Iron 0.4.0.

This is essentially same as the rantier ActuallyUsingIron but I’ve learned a little bit from doing that and it seemed people might want a version with a higher signal to noise ratio.

To start with, why Iron, instead of Nickel or Conduit or anything else on http://www.arewewebyet.org/? Because Iron actually has documentation. However, Iron’s documentation is fairly decent in some places and sorely lacking in others, but completely absent is any high-level overview of how things actually work and how to get stuff done. So that’s what I’m going to try to focus on. Iron’s design goal seems to be a very flexible and relatively low-level web application server, so you’re squarely in the territory of being able to choose how to do anything, and having a pile of useful bits and pieces cast at your feet for you to choose from. Because of this it can be tricky to figure out how things work without a high-level overview. This is opposed to things like Flask, which define how things work at a higher level for the most common use cases, but require you to delve a bit deeper into taking things apart and putting them back together to do the fancy stuff that Iron exposes to you directly.

In the end though, all you’re really doing is defining a pipeline. HTTP requests go in one side, responses come out the other. You build this pipeline out of objects which modify a Request on its way in, one or more objects which take a Request and generate a Response, and then more objects which can modify a Response on the way out. Each of the objects in this chain is called “middleware”, but I think that’s an amazingly dumb and meaningless term so I’ll call it “skittlewear”: small, colorful, tasty, and probably bad for you if you have too much of it. A skittleware pipeline is a fairly standard and common way of building a web application server though, and 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.

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();
}

Well! 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. How do I do this? Well, this functionality is provided by the router crate, which is provided and maintained by the Iron team but isn’t included in the core. To see my full feelings on this, see figure 1 But this is typical of Iron; really it’s just a framework for putting this pipeline together, and dictates nothing about what actually goes on inside it.

Anyway, the router crate provides an object named, naturally, Router, which is a Handler. Note that a Handler can be anything that defines the iron::middleware::Handler trait; this trait is defined on a function that takes a Request and returns an IronResult<Response> by default, as in our hello world example, but you can also define it on a struct or some other type to do more sophisticated stuff.

So let’s actually use the Router to route get and post requests to different functions:

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, "pageroute");
    router.post("/:page", post_page, "pageroute");

    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 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, I don’t see the point. The “/:page” is a pattern that matches a URL path and stores it in the variable “page” in the request; we’ll get to that in a little bit. The matching syntax is unfortunately undocumented, so I can’t comment much on that, but this pattern will match “/foo” but won’t match “/foo/bar”. The “pageroute” string 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

All right, but this isn’t actually using any skittleware, this is just a single Handler. Which happens to take other Handlers as arguments. Where’s the skittleware and how do we put things together? Well you might have noticed that Iron doesn’t by default have any logging facilities, so let’s add a skittleware that just notices when a response happens and prints it out to the console as it goes by. (You could use the logger crate for that, but it’s more useful to write it ourself.)

Okay, this is what the Chain struct is for. A Chain is a Handler that takes another Handler and actually does the skittleware 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 skittleware objects together to build your pipeline. Like a Handler, BeforeMiddleware, AfterMiddleware, etc is just a trait that you can define for an object, but which is already defined for functions with the right type signatures. So we can just write a function to log our responses:

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, "pageroute");
    router.post("/:page", post_page, "pageroute");

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

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

Okay, Hey, great, it works! Now why in the world does our AfterMiddleware function, which takes a Response and returns a new Response, also take a &mut Request?

(A slight diversion because this is the one part of Iron that actually seems architecturally bad. A Request isn’t just a HTTP request, it’s a HTTP request and some metadata, like the page variable our Router adds (which we still haven’t done anything with just yet, we’re getting there). So skittleware’s can communicate with each other down this pipeline by adding data to a Request. A BeforeMiddleware’s function type is fn before(req: &mut Request) -> IronResult<()>, so it can mutate a request and that request gets passed on to the next stage of the pipeline. However, for some reason an AfterMiddleware’s function type is fn response_printer(req: &mut Request, res: Response) -> IronResult<Response>. It takes a Request and a Response, which is honestly useful because the Request might still have data you want in it, like whether your Response should be reformatted into JSON or XML or whatever. Then if necessary modifies the Request so it can pass data on to subsequent skittleware, while also creating a new Response from the old one and passing that along in the IronResult.

I’d imagine it might be nicer or more consistent to have something like this:

fn before<S>(request: Request, state: &mut S) -> IronResult<Request>
fn after<S>(request: Request, res: Response, state: &mut S) -> IronResult<Response>

So each skittleware would explicitly take a mutable state that gets passed down the chain, and just create its Request and Response based on the old one. There may be reasons Iron does it the way it does, it just seems weird to me. Still, as warts to it’s a pretty trivial one that doesn’t actually affect much of anything.)

Whew, back on topic. Let’s do something real. Say you want to serve some static files. Well, that’s something outside Iron’s core scope of “connect HTTP pipeline objects together”, so there’s an external crate for it. The staticfile crate in fact, 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.”

So let’s try a trivial case.

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. So AroundMiddleware doesn’t actually seem too useful to me, and I don’t seem to see it actually getting used anywhere I would expect it to, but I am assured it serves a purpose. But I’d recommend, if you think you need an AroundMiddleware, see if it can really just be a Handler instead.

Anyway Back on topic! Make a static/index.html file in your server’s directory 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, "pageroute");
    router.post("/:page", post_page, "pageroute");
    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, "pageroute");
    // router.post("/:page", post_page, "pageroute");
    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’s going on? This seems like a good time to talk about error handling!

Part 2: Erronous Errors

Okay, this is part where Iron’s pipeline model takes some zigs and zags. Handlers and skittleware return a Result, as is only sane and just in the world. But the BeforeMiddleware and AfterMiddleware traits define an extra method called catch(). When something returns an Err() value, instead of the pipeline aborting as try!() would do, or the next skittelware’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 skittleware’s catch() method instead. So you have two parallel paths a Request and Response can travel down, the successful 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/skittleware 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 successful-path handler methods in the subsequent skittleware. (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.)

This is all explained in the iron::middleware module docs, but of course you read that already so it’s nothing new to you, I’m sure!

Also note that Handler doesn’t get a catch() method, so I guess it’s not allowed to recover from upstream errors… The docs explain this by saying 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, presumably because it’s pretty much just like a 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. It also makes the data flow potentially completely confusing 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. So in the end, all one can really do is be aware of it and be judicious in using it. 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

Okay, so. Let’s modify our response_printer() function to be a skittleware 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, "pageroute");
    // router.post("/:page", post_page, "pageroute");
    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 alas, it’s back to trial and error, and thinking very analytically about what in the world is actually happening in the web server since we can’t ask the Router what it thinks it’s doing.

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, "pageroute");
    router.post("/:page", post_page, "pageroute");

    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 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? Sadly, undocumented.

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 trivial? Let’s serve up Markdown files converted to HTML.

First off, let’s find a Markdown convertor: I’m going to use the hoedown crate. 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 afterwards…

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, let’s focus in on 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, which the Router has squirrelled away for in the Request as we referred to earlier. Unfortunately, Magic Happens here, fortunately, it’s pretty cool magic. 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. I don’t know how it manages it, but when I figure it out I’ll post it here. So, we get the value associated with the Router type. (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")...)

Now what are we actually getting out of it? What are the values in the Request.extensions map? 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>

Okay, now 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

So it’s a router::Params object! Check the docs and we can see it does indeed have a find() method that returns Option<&str>, so we’re golden. Though arcane, this is actually secretly brilliant because it means that any skittleware 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, and you’ll never see anything that is irrelevant to your purposes.

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();

That’s so sane it’s downright boring! So now let’s see how we actually create a Response:

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

Seems reasonable. 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 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 what types can it actually take? In the process of figuring this out I realized that if rendered_markdown is a Read object instead of a &str it still works, which is generally not what happens in super-duper-static Rust! What are they actually doing?

Okay, okay. 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 type polymorphism, except backwards, so it’s the variant type that determines what 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. So if we pass our Response::with() method a tuple of things that implement the Modify trait, it will apply them all to the Response.

This honestly seems excessively labyrinthine again, but it also means that anyone can implement the Modifier trait on their own object types and build Response’s out of them. And other skittleware’s can use these objects and apply them to the Response without caring about how it gets done.

But however it it ends up getting there…

$ cat static/page.md
# Hello there!

This is some markdown!

$ curl http://localhost:3000/page.md
<h1>Hello there!</h1>

<p>This is some markdown!</p>

It does work.

Concluding remarks

Iron is pretty darn good. Really! It seems generally solidly written, very functional, and reasonably simple. The main problems I ran into it are 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.

Appendix: Interesting comments from reddit

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.

Appendix 2: How TypeMap works

Actually pretty simple: Rust does have facilities for dynamic typing! This is the Any trait which is implemented by most things. You can call get_type_id() on a value to get a TypeId, which, as it sounds, is a unique identifier for a particular type. And you can call foo.is<SomeType>() on Any value foo to see if it is SomeType, or TypeId::of::<FooType>() to get the TypeId of a particular type, and a few other methods to attempt conversions. So a TypeMap is really just a HashMap<TypeId, Any>. When you do my_typemap.get<Foo>() it does, more or less, hashmap.get(TypeId::of::<Foo>()), and when you do my_typemap.insert(something) it does hashmap.insert(something.get_type_id(), something).

The real implementation is far fancier than this, and thinking about it makes my brain hurt, but that’s the gist. TypeId lets you translate between compile-time type info and runtime type info and back.