Phoenix (Elixir)JuniorTechnical
Что такое Plugs в Phoenix и как работает plug-конвейер?
Plug — поведение (behaviour) с двумя функциями: init/1 для компиляции опций и call/2 для обработки conn. Plug-конвейер в Phoenix — это последовательный pipeline plug-ов, определённых в Router через pipeline/pipe_through.
Что такое Plug
Plug — спецификация для создания компонентов веб-приложений в Elixir. Каждый plug получает структуру %Plug.Conn{} и возвращает изменённый conn. Это аналог middleware в Express.js или Django middleware.
Существует два вида plug:
- Function plug: обычная функция с сигнатурой
def plug_name(conn, opts) - Module plug: модуль с функциями
init/1иcall/2
Function plug
# Простой function plug
def add_request_id(conn, _opts) do
request_id = :crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower)
conn
|> put_resp_header("x-request-id", request_id)
|> assign(:request_id, request_id)
end
# Использование в pipeline
pipeline :api do
plug :accepts, ["json"]
plug :add_request_id
end
Module plug
# lib/myapp_web/plugs/authenticate.ex
defmodule MyappWeb.Plugs.Authenticate do
import Plug.Conn
import Phoenix.Controller
def init(opts), do: opts # Вызывается при компиляции, opts становятся аргументом call/2
def call(conn, _opts) do
token = get_req_header(conn, "authorization")
|> List.first()
|> extract_bearer_token()
case Myapp.Auth.verify_token(token) do
{:ok, user_id} ->
assign(conn, :current_user_id, user_id)
{:error, reason} ->
conn
|> put_status(:unauthorized)
|> json(%{error: "Unauthorized: #{reason}"})
|> halt() # Прерывает выполнение последующих plug-ов
end
end
defp extract_bearer_token(nil), do: nil
defp extract_bearer_token("Bearer " <> token), do: token
defp extract_bearer_token(_), do: nil
end
Plug-конвейер в Router
# lib/myapp_web/router.ex
defmodule MyappWeb.Router do
use MyappWeb, :router
# Определение pipeline
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {MyappWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
plug MyappWeb.Plugs.Authenticate
end
pipeline :rate_limit do
plug MyappWeb.Plugs.RateLimit, max_requests: 100, window_seconds: 60
end
# Применение pipeline к группе маршрутов
scope "/api", MyappWeb do
pipe_through [:api, :rate_limit]
resources "/users", UserController, only: [:index, :show]
post "/orders", OrderController, :create
end
scope "/", MyappWeb do
pipe_through :browser
get "/", PageController, :home
end
end
Plug с конфигурируемыми опциями
defmodule MyappWeb.Plugs.RateLimit do
import Plug.Conn
# init/1 — compile-time, возвращает конфиг для call/2
def init(opts) do
%{
max_requests: Keyword.get(opts, :max_requests, 60),
window_seconds: Keyword.get(opts, :window_seconds, 60),
}
end
def call(conn, %{max_requests: max, window_seconds: window}) do
ip = conn.remote_ip |> :inet.ntoa() |> to_string()
key = "rate_limit:#{ip}"
case Myapp.Cache.incr(key, window) do
count when count > max ->
conn
|> put_status(429)
|> json(%{error: "Too many requests"})
|> halt()
_ ->
conn
end
end
end
Подводные камни
- halt() обязателен при прерывании: без
halt(conn)последующие plug-и продолжат выполняться даже послеsend_resp. Это приводит к ошибке "already sent". - init/1 вызывается при компиляции: не делайте в
init/1обращений к базе данных или внешним сервисам — они выполнятся один раз при компиляции, не при каждом запросе. - Порядок plug в pipeline имеет значение:
fetch_sessionдолжен идти до любого plug, читающего сессию.protect_from_forgery— до обработки тела запроса. - plug :function_name вне модуля: function plug вызывается как
MyappWeb.Router.function_name(conn, opts). Убедитесь, что функция определена в том же модуле или импортирована. - conn неизменяемый: все функции возвращают новый
conn. Если забыть вернуть результат (например написатьput_resp_header(conn, ...)без присваивания), изменения потеряются. - Plug.Conn.halt vs early return:
haltтолько помечает conn флагомhalted: true. Сам Plug.Builder проверяет этот флаг перед вызовом следующего plug. В кастомных конвейерах нужно проверятьconn.haltedвручную.
Common mistakes
- Сводить plugs pipeline к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Phoenix 1.8 работает поверх Plug, Endpoint, Router, Controllers/LiveViews, PubSub и OTP supervision.
- Не отделять validation, authorization, transaction boundary и business logic.
What the interviewer is testing
- Объясняет plugs pipeline через конкретную точку lifecycle в Phoenix (Elixir).
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.