jonnifyelixir
F6 / F6.02 / plugs

F6.02 · part 3 of 3

Writing a plug

Everything in the request path is a plug, so writing one is how you add behaviour anywhere along it. The contract is tiny: a module plug implements init/1, which runs once at compile time and returns the options, and call/2, which takes a conn and the options and returns a conn. A function plug is a plain function of (conn, opts) that returns a conn. The one extra move that matters is halt/1: a plug that halts the connection stops the rest of the pipeline from running, which is exactly how an auth plug refuses a request before any action sees it. Master this contract and pipelines, the router, and the endpoint stop being framework magic.

See the three parts of the contract, watch halt short-circuit the chain, then read a real RequireUser plug and a function plug.

The plug contract

A module plug is two callbacks and an optional early exit. init/1 prepares options at compile time; call/2 does the work on each request; halt/1 ends the pipeline when a plug has already decided the response. Select a part.

The contract · select a part

call/2 def call(conn, opts), do: conn — the work, per request conn → conn init/1 def init(opts), do: opts — runs once, at compile time compile time halt/1 conn |> halt() — stop; no later plug or action runs early exit

part: call/2

does: conn in, conn out

A plug is a function from a connection to a connection. Once that clicks, the endpoint, every pipeline step, and the router are all the same thing — and you can add your own step anywhere in the line.

How halt short-circuits

Plugs run in order, each handed the connection the previous one returned. If a plug calls halt/1, Phoenix stops walking the pipeline — no later plug and no controller action runs, and whatever response that plug set is sent. That is how RequireUser refuses an unauthenticated request without the action ever knowing.

A conn through the pipeline · halt stops the chain

conn → :fetch_session RequireUser user? halt : assign controller action skipped when halted halt() + redirect response sent here no later plug or action runs after halt; the connection set by the halting plug is the response

halt is the difference between a plug that checks and a plug that enforces. Without it, returning a redirect still lets the action run; with it, the request stops where the decision was made.

A real plug

RequireUser is the plug the :require_auth pipeline runs: load the user id from the session, assign it on success, or halt with a redirect when it is missing. A function plug is the same contract without a module — handy for a one-line step.

defmodule PortalWeb.RequireUser do
  import Plug.Conn
  import Phoenix.Controller, only: [redirect: 2, put_flash: 3]

  # init/1 runs at compile time; its result is passed to call/2
  def init(opts), do: opts

  # call/2 takes the conn and returns a conn
  def call(conn, _opts) do
    case get_session(conn, :user_id) do
      nil ->
        conn
        |> put_flash(:error, "Please sign in")
        |> redirect(to: "/login")
        |> halt()                       # stop the pipeline — no action runs

      user_id ->
        assign(conn, :current_user_id, user_id)
    end
  end
end

# a function plug: a plain function of (conn, opts) that returns a conn
def put_request_time(conn, _opts), do: assign(conn, :req_at, DateTime.utc_now())

conn in, conn out

A plug transforms the connection — assign, set a header, or decide to stop.

halt to enforce

A halting plug sends its response and the action never runs — the basis of auth.

That completes F6.02. The match in the middle of the lifecycle is built — routes, pipelines, and your own plugs. The next module, F6.03 — Ecto, puts a database behind the engine’s port. Back to the module overview or the F6 chapter.

References

Primary sources for this lesson, and where it connects in the course.

Sources

Related in this course