raxx v1.1.0 Raxx.Middleware behaviour

A "middleware" is a component that sits between the HTTP server such as as Ace and a Raxx.Server controller. The middleware can modify requests request before giving it to the controller and modify the controllers response before it's given to the server.

Oftentimes multiple middlewaress might be attached to a controller and function as a single Raxx.Server.t/0 - see Raxx.Stack for details.

The Raxx.Middleware provides a behaviour to be implemented by middlewares.

Example

Traditionally, middlewares are used for a variety of purposes: managing CORS, CSRF protection, logging, error handling, and many more. This example shows a middleware that given a HEAD request "translates" it to a GET one, hands it over to the controller and strips the response body transforms the response according to RFC 2616

This way the controller doesn't heed to handle the HEAD case at all.

defmodule Raxx.Middleware.Head do
  alias Raxx.Server
  alias Raxx.Middleware

  @behaviour Middleware

  @impl Middleware
  def process_head(request = %{method: :HEAD}, _config, inner_server) do
    request = %{request | method: :GET}
    state = :engage
    {parts, inner_server} = Server.handle_head(inner_server, request)

    parts = modify_response_parts(parts, state)
    {parts, state, inner_server}
  end

  def process_head(request = %{method: _}, _config, inner_server) do
    {parts, inner_server} = Server.handle_head(inner_server, request)
    {parts, :disengage, inner_server}
  end

  @impl Middleware
  def process_data(data, state, inner_server) do
    {parts, inner_server} = Server.handle_data(inner_server, data)
    parts = modify_response_parts(parts, state)
    {parts, state, inner_server}
  end

  @impl Middleware
  def process_tail(tail, state, inner_server) do
    {parts, inner_server} = Server.handle_tail(inner_server, tail)
    parts = modify_response_parts(parts, state)
    {parts, state, inner_server}
  end

  @impl Middleware
  def process_info(info, state, inner_server) do
    {parts, inner_server} = Server.handle_info(inner_server, info)
    parts = modify_response_parts(parts, state)
    {parts, state, inner_server}
  end

  defp modify_response_parts(parts, :disengage) do
    parts
  end

  defp modify_response_parts(parts, :engage) do
    Enum.flat_map(parts, &do_handle_response_part(&1))
  end

  defp do_handle_response_part(response = %Raxx.Response{}) do
    # the content-length will remain the same
    [%Raxx.Response{response | body: false}]
  end

  defp do_handle_response_part(%Raxx.Data{}) do
    []
  end

  defp do_handle_response_part(%Raxx.Tail{}) do
    []
  end
end

Within the callback implementations the middleware should call through to the "inner" server and make sure to return its updated state as part of the Raxx.Middleware.next/0 tuple.

In certain situations the middleware might want to short-circuit processing of the incoming messages, bypassing the server. In that case, it should not call through using Raxx.Server's handle_* helper functions and return the inner_server unmodified.

Gotchas

Info messages forwarding

As you can see in the above example, the middleware can even modify the info messages sent to the server and is responsible for forwarding them to the inner servers.

Iodata contents

While much of the time the request body, response body and data chunks will be represented with binaries, they can be represented as iodata.

A robust middleware should handle that.

Link to this section Summary

Types

Values returned from the process_* callbacks

State of middleware.

t()

The behaviour module and state/config of a raxx middleware

Callbacks

Called every time data from the request body is received.

Called once when a client starts a stream,

Called for all other messages the middleware may recieve.

Called once when a request finishes.

Link to this section Types

Link to this type

next()

next() :: {[Raxx.part()], state(), Raxx.Server.t()}

Values returned from the process_* callbacks

Link to this type

state()

state() :: any()

State of middleware.

Link to this type

t()

t() :: {module(), state()}

The behaviour module and state/config of a raxx middleware

Link to this section Callbacks

Link to this callback

process_data(binary, state, inner_server)

process_data(binary(), state(), inner_server :: Raxx.Server.t()) :: next()

Called every time data from the request body is received.

Link to this callback

process_head(request, state, inner_server)

process_head(
  request :: Raxx.Request.t(),
  state(),
  inner_server :: Raxx.Server.t()
) :: next()

Called once when a client starts a stream,

The arguments a Raxx.Request, the middleware configuration and the "inner" server for the middleware to call through to.

This callback can be relied upon to execute before any other callbacks

Link to this callback

process_info(any, state, inner_server)

process_info(any(), state(), inner_server :: Raxx.Server.t()) :: next()

Called for all other messages the middleware may recieve.

The middleware is responsible for forwarding them to the inner server.

Link to this callback

process_tail(trailers, state, inner_server)

process_tail(
  trailers :: [{binary(), binary()}],
  state(),
  inner_server :: Raxx.Server.t()
) :: next()

Called once when a request finishes.

This will be called with an empty list of headers is request is completed without trailers.

Will not be called at all if the Raxx.Request.t/0 passed to process_head/3 had body: false.