From The Forge — Rebuilding Iron
New and improved!
Iron was originally designed, implemented, documented and released in three weeks, the first week of which was mostly me and my teammates learning Rust in the first place! We have come a long way since then, and so has Iron.
Today, I’m re-releasing Iron, version 0.0.1. I know, exciting, isn’t it. This is the first time I’m really comfortable putting a version mark down, because it’s the first time I think we’ve really gotten it right.
The new Iron is a much more flexible framework that has had much more thought go into it than 0.0.0. It has an all-new middleware system, allocates much less, is better documented, and makes significantly more sense.
Without further ado, the major changes:
#1: enter/exit => before/after
enter/exit middleware are gone, replaced by the more familiar and more semantically meaningful before/after middleware system. In the new Iron, the old Middleware trait is replaced by:
pub trait BeforeMiddleware {
fn before(&self, &mut Request) -> IronResult<()>;
// some methods omitted
}pub trait AfterMiddleware {
fn after(&self, &mut Request, &mut Response) -> IronResult<()>;
// some methods omitted
}pub trait AroundMiddleware: Handler {
fn around<H: Handler>(&mut self, H);
}
That’s right, not one but three traits. One of the things we learned this time is the importance of making Traits composable (+) rather than inheritable (:).
This refactor allows Middleware to be both Before and After if they want, but does not force them to implement both in cases where it is meaningless. It also allows Before and After middleware to be kept separately, and have their semantics be altered in important ways.
The third trait, AroundMiddleware, introduces around-middleware to Iron. AroundMiddleware must themselves be Handlers, and they are used to wrap around an existing handler and add functionality to it. What is a handler? Well, this brings us to our second change — the introduction of the Handler trait.
#2 Handler
This change introduces a new trait, Handler, and removes special treatment of Chain from the framework.
pub trait Handler {
fn call(&self, &mut Request) -> IronResult<Response>;
}
Each instance of Iron has a single Handler, and that Handler is responsible for doing one thing, handling a request by producing either a Response or an Error, which is translated into a 500 response for the client.
Handlers can be elegantly nested by using a Chain, since Chain now requires Handler as a bound. You can then wrap Handlers in other Handlers using AroundMiddleware, and the whole thing allows Chains to be easily used in any place that expects a Handler, such as in the controllers of a Router.
This also, as a side-effect, takes care of the problem of generating defaults for a Response object. On the other hand, it also necessitates moving some of the constructors previously relegated to iron-test into core, so that Handler’s can conveniently create Response structs.
#3 Clone => Send + Sync
You may have noticed in the last few traits that most methods receive &self rather than &mut self. This is a result of another major change in Iron — Middleware and Handlers are stored behind an Arc so they are not copied for each Request. This is a huge win performance-wise, as copying large Middleware with complex clone semantics such as Router was a performance bottleneck.
Middleware which need to have data associated with them for every request can instead store that data in Request::extensions. With a private type as key this will also keep the data accessible to only that Middleware.
This is a huge ergonomic win as well, as it means that Middleware no longer need to be Clone, which was sometimes difficult or impossible to implement for interesting types. In the worst case, Middleware can put themselves behind RWLocks so they become Send and Sync if they are not already.
#4 Alloy => Plugins
In most web frameworks, including the micro-frameworks in dynamic languages that early Iron was heavily inspired by, request and response extensions are just added as new fields and methods dynamically and in a specific order. This means that if you link your body-parsing middleware after your authentication, you will get repeated runtime errors.
Iron has a new approach for Request and Response plugins which do not modify control flow that uses rust-plugin to provide typesafe, lazily evaluated extensions with a guaranteed interface that are automatically cached and order-independent.
let body = req.alloy.find::<ParseBody>().unwrap().unwrap()
has become:
let body = req.get::<Body, Json>().expect("No Body");
Which then parses the body by-need and only once, providing automatic caching. This makes Iron applications significantly more robust than before, as they will no longer implode dramatically if you forgot to link a middleware — in fact, under the new system, bringing the Body type into scope would be enough to enable the above behavior, you don’t even need to link it to your chain.
#5 AnyMap => TypeMap
AnyMap served us extremely well for the first iteration of Iron, but a better abstraction that allowed values to be a different type than their keys was needed. In the newest Iron, AnyMap has been replaced by TypeMap, an actual key-value store which is keyed by Types and can have many values of the same type.
TypeMap allows:
struct Key;let num = req.extensions.get::<Key, uint>();
whereas with AnyMap you had to do:
struct Value(uint);let Value(num) = req.extensions.get::<Value>();
which becomes tricky with common types like uint, String, or Url.
#6 Request and Response
In the earliest versions of Iron, Request and Response were just aliases for the equivalent types provided by rust-http. They were relatively low-level implementations and were not pleasant to work with on an application level.
Iron now has its own Request and Response representations that expose a high-level, extensible interface. Both Request and Response contain an extensions TypeMap, meaning they can be used for automatic Plugins, and they are full of helpful and useful fields, such as a parsed Url struct, or a Reader for the Response body as opposed to a String.
This is a massive ergonomics win makes Iron significantly more usable as an application framework.
#7 Errors and Handling Errors
Iron’s initial release had no error-handling at all — the only thing a middleware could do to signal something had gone wrong was to abort and unwind the stack.
To remedy this, I introduced a new variant to the now-removed Status enum: Error(Box<Show>). This had slightly different behavior than Unwind and allowed other handlers to at least know that an error had been thrown and perhaps, well, show it.
However, that is not a production ready approach. It does not allow deep introspection of Errors or allow for the possibility of recovery. Iron now uses the error type from rust-error, which will be at least convertible to and from stdlib errors. In addition to a new Error type, Iron now allows errors to occur at any time during the handling of a Request and has a full system for propagating, handling, and recovering from errors.
The hidden methods from the BeforeMiddleware, AfterMiddleware, and Handler traits all have to do with creating, propagating, and recovering from errors. I will defer to the documentation to explain the full system.
Trivia: There is not a single call to fail!, {Option, Result}::unwrap, or a single unsafe block in the entirety of iron core.
#8–100 Various things about the Place
Many other small fixes have been made to Iron. It no longer clones Chain twice and it no longer blocks on listen, to take two two tiny examples.
Generally, Iron is a much more hospitable place now. Many of the middleware still need refactoring and updating to bring them in line with Iron’s new approach, but the core framework has matured and evolved enormously into something we can proudly call version 0.0.1.
I hope you’ll take a second look. Thanks for listening.
Discuss on Reddit
Discuss on Hacker News