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.

Sources

Related topics