Lately I've been experimenting with Elixir and Phoenix and have started to ask myself "what's the difference between Phoenix's Plugs vs middleware in other server-side paradigms"
To start I'm going to talk about Phoenix's Plugs vs synchronous middleware (like what's found in Laravel). For the most part, this will also apply to asyc middleware, but there are a few caveats that I'll cover.
As a comparison, we will have a hypothetical system with a few operations to respond to HTTP requests:
- Authorize via JWT or some token auth
- Parse JSON body as an object
- Normalize and Serialize API payloads to match DB schemas
In a more traditional middleware, the request flows into the system then flows back out through each layer of middleware.
In Laravel this is achieved by running the next
callback and passing in the request (which is usually modified), and finally returning some response result.
For instance the Authorize JWT middleware could be something like this:
public function handle($req, Closure $next)
{
if (isValidToken($request->header('Authorization'))) {
return $next($req);
}
return response()->error(401, 'Unauthorized access');
}
This works fine, except that this allows for abuse since Middleware can modify both the request and the response before AND after the next piece of middleware. Take for instance a possible implementation of JSON normalization and serialization:
public function handle($req, Closure $next)
{
$normalized_request = normalizeJSONRequestBody($req);
$result = $next($normalized_request);
return response()->json(serializeJSONResponse($result));
}
In some ways this flexibility can be powerful. But, it also leads to the possibility of strange side effects. There have been times where I've had to completely replace third-party middleware because it modified both the request and response in a single place. This boils down to an issue of usability vs single responsibility.
Compare this to plugs in Phoenix. One thing to note is that plugs pass the connection which is a set of both the request and response. Plugs modify the connection then pass things off to the next plug. The connection doesn't come back. This enforces single responsibility for plugs.
That's not to say that you couldn't modify both the request and the response in one plug. But instead, it means you aren't able to modify the connection in response to something further down the pipeline. This means our JSON normalization and serialization would have to be broken into two separate plugs:
def normalize_json(conn, params) do
conn |> normalize_json_request_body
end
def serialize_json(conn, params) do
conn |> serialize_json_response
end
Then in a pipeline or controller, these plugs would have to be put in such that normalize_json
is plugged in before the controller action and serialize_json
is plugged in after the controller is done.