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
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
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
- Plug — the composable middleware spec — the
init/1+call/2contract andhalt/1. - Phoenix — Routing — verbs, paths, scopes, and the pipelines plugs run in.
- Phoenix — Controllers — actions and rendering, the stage a plug guards.