Middleware
Middleware is a piece of code that mediates the request and response to the application server.
Middleware is a piece of code that mediates the request and response to the application server. In Koa, a middleware looks like this:
app.use((ctx, next) => {
// do some stuff
next();
// do some other stuff
});
Rack middleware in Ruby is very similar:
class MyMiddleware
def initialize(app)
@app = app
end
def call(env)
# do some stuff
status, headers, response = @app.call(env)
# do some other stuff
[status, headers, response]
end
end
Both of these middleware examples have three parts.
- Preprocessing: before we yield control to the next middleware (either with
next();
or@app.call(env)
, we can do some pre-processing on the request. This is a great place to validate requests and reject them if you know your app can't handle them. - Yielding control: this is where we pass the request to the next middleware. The next middleware could have additional processing, or it could be the final middleware that resolves the requests.
- Postprocessing: control comes back to our middleware here and we can run our code here. This is often used for things like compressing the response data or collecting metrics.
Middleware is a Trampoline
I often hear the metaphor of a "middleware chain" this only sort of works. In my mind a chain has a start and a stop. But middleware is actually nested method calls that receive control, yield control, and receive it again before returning.
I like to visualize the request as if its first falling through hoops (each a middleware), hitting a trampoline (the application that resolves the request), and bouncing back up through the same hoops. Sometimes, one of those middleware hoops might intercept the request and bounce it back to the user early.
Common Middleware Mistakes
Not yielding control when you should
Middleware that is not designed to respond the a request should yield control to the next middleware. In Koa, you do this by calling await next()
and in Rails you do this by calling @app.call(env)
. These two methods yield control to the next middleware so that the application can continue to process the request.
At times, though, you might want to write a middleware that responds early under certain conditions. Here is an example from the Koa README:
app.use(async (ctx, next) => {
ctx.assert(ctx.request.accepts('xml'), 406);
// equivalent to:
// if (!ctx.request.accepts('xml')) ctx.throw(406);
await next();
});
If we forgot to call await next();
in this middleware, we would not yield control to the next middleware. However, the assert
will reply early with a 406
error code and will not call next();
.
Putting timing in the wrong place
Let's say you want to instrument your requests to log timing data to know how long your application takes to respond. You could do this by adding a startTime
to the context
and then comparing the start time to the end time when the request comes back to you.
app.use(async (ctx, next) => {
// Set the start time
const start = Date.now();
ctx.state.start = start;
// Yield control
await next();
// Lot the elapsed time
const ms = Date.now() - start;
console.log(`Time: ${ms}ms`);
});
An easy mistake to make is to put this middleware close to the final application. You will miss some data if requests are rejected before this middleware and you will have inaccurate timing because there are middleware calls before and after this middleware.
app.use(async (ctx, next) => {
// some other middleware like authentication
await next();
});
app.use(async (ctx, next) => {
// Set the start time
const start = Date.now();
ctx.state.start = start;
// Yield control
await next();
// Lot the elapsed time
const ms = Date.now() - start;
console.log(`Time: ${ms}ms`);
});
In the example above, we are not timing the authentication middleware.
Bad Timers is a small GitHub repo I put together demonstrating a few ways of making mistakes with timing in a Koa app. Always start and stop your timers in the same middleware and always put your timing middleware at the top.
Expensive synchronous processing
In the Bad Timers repo I simulated a slow function call. This function is called when control is yielded to one of the middleware calls but after we log the "unreliable" timer. The unreliable timer will be off my up to a second due to slowFunction
!
If you have to do expensive pre or post processing, consider do it asynchronously. Instrumentation to collect metrics should be sent off to its own process so that we don't interrupt responses to users.
const Koa = require('koa');
const app = new Koa();
async function slowFunction(ctx, next) {
return new Promise(resolve => {
setTimeout(resolve, Math.random() * 1000);
});
}
app.use(async (ctx, next) => {
const start = Date.now();
ctx.state.start = start;
await next();
const ms = Date.now() - start;
console.log(`Ended the accurate timer. Time: ${ms}ms`);
});
app.use(async (ctx, next) => {
await next();
await slowFunction(ctx, next);
});
app.use(async (ctx, next) => {
await next();
const ms = Date.now() - ctx.state.start;
console.log(`Ended the unreliable timer: ${ms}ms`);
});
app.use(async ctx => {
await new Promise(resolve => {
setTimeout(resolve, Math.random() * 1000);
});
ctx.body = 'Hello World';
});
app.listen(3000);