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:
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:
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:
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
:
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…
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.