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:
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:
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:
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:
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…
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!
- 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.
- 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.)
- 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 needmodify_with_str
ormodify_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.