A presentation at ElixirConf 2024 in in Orlando, FL, USA by Divya
Phoenix Unplugged; uncovering the magic of plugs
Request Response Cycle request Client (browser) Server (API, DB etc) response
Request Response Cycle request Client (browser) Server (API, DB etc) response a a a tcp p rsing routing get d t response send
Request Client Response Client Server
Request <R nch Process> <Thous nd Isl nd Process> Cowboy B ndit Client <Stre m> Phoenix router Phoenix Router controllers Phoenix Controller etc Phoenix Helpers a a a a a Server Response Client
Request <R nch Process> <Thous nd Isl nd Process> Cowboy B ndit Client <Stre m> Phoenix router Phoenix Router Fr mework (middlew re + plugs) controllers Phoenix Controller etc Phoenix Helpers a a a a a a a Server Response Client
Divya Sasidharan @shortdiv
Request <R nch Process> <Thous nd Isl nd Process> Cowboy B ndit Client <Stre m> Phoenix router Phoenix Router Fr mework (middlew re + plugs) controllers Phoenix Controller etc Phoenix Helpers a a a a a a a Server Response Client
Request <R nch Process> <Thous nd Isl nd Process> Cowboy B ndit Client <Stre m> Phoenix router Phoenix Router Fr mework (middlew re + plugs) controllers Phoenix Controller How does this work? etc Phoenix Helpers a a a a a a a Server Response Client
Wh t is cowboy/b ndit? a a a Cowboy/Bandit is a small, HTTP server for Erlang/OTP, used in the elixir ecosystem.
Wh t is r nch/thous nd isl nd? a a a a a Ranch/Thousand Island is a socket acceptor pool library for Erlang/OTP, commonly used in the Elixir ecosystem to manage TCP connections.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule HowdyElixirConf.Application do use Application defmodule Handler do def init(req, _opts) do resp = :cowboy_req.reply( _status = 200, _body = “<!doctype html><h1>Hello, Cowboy!</h1>”, _request = req ) {:ok, resp, []} end end def start(type, args) do routes = :cowboy_router.compile([ {:, [ {:, Handler, []} ]} ]) :cowboy.start_clear( :hello_http, [port: 4001], %{env: %{dispatch: routes}} ) application.ex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule HowdyElixirConf.Application do use Application defmodule Handler do def init(req, _opts) do resp = :cowboy_req.reply( _status = 200, _body = “<!doctype html><h1>Hello, Cowboy!</h1>”, _request = req ) {:ok, resp, []} end end def start(type, args) do routes = :cowboy_router.compile([ {:, [ {:, Handler, []} ]} ]) :cowboy.start_clear( :hello_http, [port: 4001], %{env: %{dispatch: routes}} ) application.ex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule HowdyElixirConf.Application do use Application application.ex def start(type, args) do routes = :cowboy_router.compile([ {:, [ {:, HowdyElixirConf.Web.Handler, []} ]} ]) :cowboy.start_clear( :hello_http, [port: 4001], %{env: %{dispatch: routes}} ) children = [] opts = [strategy: :one_for_one, name: Campsite.Supervisor] Supervisor.start_link(children, opts) end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule HowdyElixirConf.Application do use Application application.ex def start(_type, args) do routes = :cowboy_router.compile([ {:, [ {“/”, HowdyElixirConf.Web.RootHandler, []} ]} ]) :cowboy.start_clear( :hello_http, [port: 4001], %{env: %{dispatch: routes}} ) children = [] opts = [strategy: :one_for_one, name: Campsite.Supervisor] Supervisor.start_link(children, opts) end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 web/root_handler.ex defmodule HowdyElixirConf.Web.RootHandler do def init(req, _opts) do resp = :cowboy_req.reply( 200, %{“content-type” => “text/html”}, “<!doctype html><h1>Howdy Elixir Conf!</h1>”, req ) {:ok, resp, []} end def terminate(_reason, _req, _state) do :ok end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 web/root_handler.ex defmodule HowdyElixirConf.Web.RootHandler do def init(req, _opts) do # do some auth {AuthHeader, Req1} = cowboy_req:header(<<”authorization”>>, Req0) resp = if is_authorized(AuthHeader) do :cowboy_req.reply( 200, %{“content-type” => “text/html”}, “<!doctype html><h1>Howdy Elixir Conf!</h1>”, req ) else :cowboy_req.reply( 404, %{“content-type” => “text/html”}, “<!doctype html><h1>Route not found</h1>”, req ) end {:ok, resp, []} end def terminate(_reason, _req, _state) do :ok end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 web/root_handler.ex defmodule HowdyElixirConf.Web.RootHandler do def init(req, _opts) do # do some auth {AuthHeader, Req1} = cowboy_req:header(<<”authorization”>>, Req0) resp = if is_authorized(AuthHeader) do :cowboy_req.reply( 200, %{“content-type” => “text/html”}, “<!doctype html><h1>Howdy Elixir Conf!</h1>”, req ) else :cowboy_req.reply( 404, %{“content-type” => “text/html”}, “<!doctype html><h1>Route not found</h1>”, req ) end {:ok, resp, []} end def terminate(_reason, _req, _state) do :ok end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 web/root_handler.ex defmodule HowdyElixirConf.Web.RootHandler do def init(req, _opts) do Req2 = :cowboy_req.stream_reply( 200, %{“content-type” => “text/html”}, req ) Req3 = :cowboy_req.stream_body( “<!doctype html><html><head><title>Hi</title></head><body><h1>”, Req2 ) Req4 = :cowboy_req.stream_body( “Howdy Elixir Conf!</h1><p>Streaming with Cowboy!</p>”, Req3 ) Req5 = :cowboy_req.stream_body( “</body></html>”, Req4 ) {:ok, Req5, []} end def terminate(_reason, _req, _state) do :ok end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 web/root_handler.ex defmodule HowdyElixirConf.Web.RootHandler do def init(req, _opts) do Req2 = :cowboy_req.stream_reply( 200, %{“content-type” => “text/html”}, req ) Req3 = :cowboy_req.stream_body( “<!doctype html><html><head><title>Hi</title></head><body><h1>”, Req2 ) Req4 = :cowboy_req.stream_body( “Howdy Elixir Conf!</h1><p>Streaming with Cowboy!</p>”, Req3 ) Req5 = :cowboy_req.stream_body( “</body></html>”, Req4 ) {:ok, Req5, []} end def terminate(_reason, _req, _state) do :ok end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 web/root_handler.ex defmodule HowdyElixirConf.Web.RootHandler do def init( req, _opts) do Req2 = :cowboy_req.stream_reply( 200, %{“content-type” => “text/html”}, req ) Req3 = :cowboy_req.stream_body( “<!doctype html><html><head><title>Hi</title></head><body><h1>”, Req2 ) Req4 = :cowboy_req.stream_body( “Howdy Elixir Conf!</h1><p>Streaming with Cowboy!</p>”, Req3 ) Req5 = :cowboy_req.stream_body( “</body></html>”, Req4 ) {:ok, Req5, []} end def terminate(_reason, _req, _state) do :ok end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule HowdyElixirConf.Web.RootHandler do import Plug.Conn web/root_handler.ex def init(conn, _opts) do conn = send_chunked(conn, 200) {:ok, conn} = chunk( conn, “<!doctype html><html><head><title>Hi</title></head><body><h1>” ) {:ok, conn} = chunk( conn, “Howdy Elixir Conf!</h1><p>Streaming with Cowboy!</p>” ) {:ok, conn} = chunk( conn, “</body></html>” ) conn end def terminate(_reason, _req, _state) do :ok end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule HowdyElixirConf.Application do use Application application.ex def start(_type, args) do routes = :cowboy_router.compile([ {:, [ {“/”, HowdyElixirConf.Web.RootHandler, []} ]} ]) :cowboy.start_clear( :hello_http, [port: 4001], %{env: %{dispatch: routes}} ) children = [] opts = [strategy: :one_for_one, name: Campsite.Supervisor] Supervisor.start_link(children, opts) end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule HowdyElixirConf.Application do use Application application.ex def start(_type, args) do routes = :cowboy_router.compile([ {:, [ {“/”, HowdyElixirConf.Web.RootHandler, []} {“/another”, HowdyElixirConf.Web.AnotherHandler, []} {“/one”, HowdyElixirConf.Web.OneHandler, []} … ]} ]) :cowboy.start_clear( :hello_http, [port: 4001], %{env: %{dispatch: routes}} ) children = [] opts = [strategy: :one_for_one, name: Campsite.Supervisor] Supervisor.start_link(children, opts) end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule HowdyElixirConf.Application do use Application application.ex def start(_type, args) do routes = :cowboy_router.compile([ {:, [ {“/”, HowdyElixirConf.Web.RootHandler, []} {“/another”, HowdyElixirConf.Web.AnotherHandler, []} {“/one”, HowdyElixirConf.Web.OneHandler, []} … ]} ]) :cowboy.start_clear( :hello_http, [port: 4001], %{env: %{dispatch: routes}} ) children = [] opts = [strategy: :one_for_one, name: Campsite.Supervisor] Supervisor.start_link(children, opts) end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule HowdyElixirConf.Application do use Application application.ex def start(type, args) do routes = :cowboy_router.compile([ {:, [ {:, HowdyElixirConf.Web.PageHandler, HowdyElixirConf.Web.Router } … ]} ]) :cowboy.start_clear( :hello_http, [port: 4001], %{env: %{dispatch: routes}} ) children = [] opts = [strategy: :one_for_one, name: Campsite.Supervisor] Supervisor.start_link(children, opts) end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 1 15 2 16 3 17 4 18 5 19 6 20 7 21 8 22 9 23 10 24 11 25 12 26 13 27 14 28 15 29 16 30 17 web/router.ex defmodule HowdyElixirConf.Web.Router do use Plug.Router alias HowdyElixirConf.Web.PageController get “/”, PageController, :home get “/two”, PageController, :two get not_matched, PageController, :not_matched, %{path: not_matched} end defmodule HowdyElixirConf.Web.PageHandler do def init(req, router) do path = :cowboy_req.path(req) conn = %Plug.Conn{req_path: path} conn = router.call(conn) web/page_handler.ex resp = :cowboy_req.reply(conn.status, conn.resp_body, req) {:ok, resp, router} end def terminate(_reason, _req, _state) do :ok end end
1 2 3 4 5 6 7 8 9 10 11 12 13 14 1 15 2 16 3 17 4 18 5 19 6 20 7 21 8 22 9 23 10 24 11 25 12 26 13 27 14 28 15 29 16 30 17 web/router.ex defmodule HowdyElixirConf.Web.Router do use Plug.Router alias HowdyElixirConf.Web.PageController get “/”, PageController, :home get “/two”, PageController, :two get not_matched, PageController, :not_matched, %{path: not_matched} end defmodule HowdyElixirConf.Web.PageHandler do def init(req, router) do path = :cowboy_req.path(req) conn = %Plug.Conn{req_path: path} conn = router.call(conn) web/page_handler.ex resp = :cowboy_req.reply(conn.status, conn.resp_body, req) {:ok, resp, router} end def terminate(_reason, _req, _state) do :ok end end
Wh t is plug? a a Plugs are modules in Elixir used to build composable web applications. Each plug can intercept, modify, or respond to an HTTP request or response.
Function Plug Any function that receives a conn and a set of options and returns a conn. It has this type signature: (Plug.Conn.t, Plug.opts) :: Plug.Conn.t Module Plug An extension of the function plug. But must export 2 functions: (Plug.Conn.t(), any()) :: Plug.Conn.t() def call/2 (opts :: any()) :: any() def init/1
Function Plug (Plug.Conn.t, Plug.opts) :: Plug.Conn.t defmodule HowdyElixirConf.Web.Router do use Plug.Router plug :set_custom_header get “/”, HowdyElixirConf.Web.PageController, :home # Define the function plug defp set_custom_header(conn, _opts) do conn |> Plug.Conn.put_resp_header(“x-custom-header”, “HowdyHeader”) end end
Function Plug (Plug.Conn.t, Plug.opts) :: Plug.Conn.t defmodule HowdyElixirConf.Web.Router do use Plug.Router plug :set_custom_header get “/”, HowdyElixirConf.Web.PageController, :home # Define the function plug defp set_custom_header(conn, _opts) do conn |> Plug.Conn.put_resp_header(“x-custom-header”, “HowdyHeader”) end end
Module Plug (Plug.Conn.t(), any()) :: Plug.Conn.t() def call/2 (opts :: any()) :: any() def init/1 defmodule Plug.CustomHeader do import Plug.Conn @spec init(opts :: any()) :: any() def init(opts), do: opts end @spec call(conn :: Plug.Conn.t(), opts :: any()) :: Plug.Conn.t() def call(conn, _opts) do conn |> put_resp_header(“x-custom-header”, “MyAppHeader”) end end
Module Plug (Plug.Conn.t(), any()) :: Plug.Conn.t() def call/2 (opts :: any()) :: any() def init/1 defmodule HowdyElixirConf.Web.Router do use Plug.Router plug Plug.CustomHeader get “/”, HowdyElixirConf.Web.PageController, :home end
Module Plug (Plug.Conn.t(), any()) :: Plug.Conn.t() def call/2 (opts :: any()) :: any() def init/1 defmodule HowdyElixirConf.Web.Router do use Plug.Router plug Plug.CustomHeader get “/”, HowdyElixirConf.Web.PageController, :home end
The mighty Plug.Conn
defstruct adapter: {Plug.MissingAdapter, nil}, assigns: %{}, body_params: %Unfetched{aspect: :body_params}, cookies: %Unfetched{aspect: :cookies}, params: %Unfetched{aspect: :params}, path_info: [], query_params: %Unfetched{aspect: :query_params}, query_string: “”, req_cookies: %Unfetched{aspect: :cookies}, req_headers: [], request_path: “”, resp_body: nil, resp_cookies: %{}, scheme: :http, state: :unset, status: nil …
Plug.Router
defmodule HowdyElixirConf.Web.Router do use Plug.Router plug :match plug :dispatch get “/hello” do send_resp(conn, 200, “world”) end match _ do send_resp(conn, 404, “oops”) end end
Plug.Router
a
a
spoiler lert A router is
plug
Plug.Router
defmodule Plug.Router do defmacro using(opts) do quote location: :keep do import Plug.Router use Plug.Builder, unquote(opts) def match(conn, _opts) do … end … end … end end
Plug.Router
defmodule Plug.Router do defmacro using(opts) do quote location: :keep do import Plug.Router use Plug.Builder, unquote(opts) def match(conn, _opts) do … end … end … end end
Plug.Router
defmodule Plug.Router do defmacro using(opts) do quote location: :keep do import Plug.Router use Plug.Builder, unquote(opts) def match(conn, _opts) do … end defmacro using(opts) do quote do … @behaviour Plug end … def init(opts), do: opts end end end def call(conn, opts) do … end … end end
Let’s m ke router a a a a a bew re the met progr mming
Let’s m ke router a a a a a bew re the met progr mming
a a f But irst: A met progr mming primer
defmodule Greetable do defmacro using(opts) do greeting = Keyword.get(opts, :greeting, “Hello”) llows Greetable to inject functions into other modules. de ine the code th t will be injected quote do def greet(name) do IO.puts(unquote(greeting) <> “, ” <> name <> “!”) end def farewell(name) do IO.puts(“Goodbye, ” <> name <> “!”) end end end end defmodule EnglishGreeter do use Greetable end defmodule SpanishGreeter do use Greetable, greeting: “Hola” end implements the greet nd farewell func p ssed to the m cro s opts a a a EnglishGreeter.greet(“Alice”) # Outputs: Hello, Alice! SpanishGreeter.greet(“Bob”) # Outputs: Hola, Bob! EnglishGreeter.farewell(“Charlie”) # Outputs: Goodbye, Charlie! a f a a 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
B ck to plugs a a let’s m ke this router go brrrr
The Rules of
Plug
1.Must implement init/1
and call/2
a
2.Must use Plug.Conn
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule Spaghetti.Router do defmacro using(_opts) do quote do def init(opts), do: opts def call(conn, _opts) do end end end end spaghetti/router.ex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule Spaghetti.Router do defmacro using(_opts) do quote do import Spaghetti.Router @behaviour Plug def init(opts), do: opts def call(conn, _opts) do end end end end spaghetti/router.ex
defmodule Spaghetti.Router do defmacro using(_opts) do quote do import Spaghetti.Router @behaviour Plug def init(opts), do: opts def call(conn, _opts) do a end end end end a 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 spaghetti/router.ex modules th t use this module, import this Implements plug beh viour
defmodule Spaghetti.Router do defmacro using(_opts) do quote do import Spaghetti.Router @behaviour Plug
spaghetti/router.ex
modules th t use this module, import this Implements plug beh viour
def init(opts), do: opts def call(conn, _opts) do end end end end defmodule HowdyElixirConf.Web.Router do use Spaghetti.Router # The use
macro above expands to include: # import Spaghetti.Router # def call(conn) do … end
a
end
a
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 1 17 2 18 3 19 4 20 5 21 6 22 7 23 8 24 9 25 10 26 11 27 12 28 13 29 14 30 15
web/router.ex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule Spaghetti.Router do defmacro using(_opts) do quote do import Spaghetti.Router @behaviour Plug def init(opts), do: opts def call(conn, _opts) do end end end end spaghetti/router.ex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule Spaghetti.Router do defmacro using(_opts) do quote do import Spaghetti.Router @behaviour Plug def init(opts), do: opts def call(conn, _opts) do end end end defmacro get(path, controller, action) do quote do end end end spaghetti/router.ex
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule Spaghetti.Router do defmacro using(_opts) do quote do import Spaghetti.Router @behaviour Plug spaghetti/router.ex def init(opts), do: opts def call(conn, _opts) do content_for(conn.request_path, conn) end end end defmacro get(path, controller, action) do quote do defp content_for(unquote(path), conn) do apply(unquote(controller), :call, [conn, unquote(action)]) end end end end
defmodule Spaghetti.Router do defmacro using(_opts) do quote do import Spaghetti.Router @behaviour Plug spaghetti/router.ex def init(opts), do: opts def call(conn, _opts) do content_for(conn.request_path, conn) end end end defmacro get(path, controller, action) do quote do defp content_for(unquote(path), conn) do apply(unquote(controller), :call, [conn, unquote(action)]) end end end end dyn mic lly invokes the c ll/2 function on the controller module, p ssing in the conn nd ction s rguments. a a a a Kernel.apply/3 a a a a 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
spaghetti/router.ex defmodule Spaghetti.Router do defmacro using(_opts) do quote do import Spaghetti.Router @behaviour Plug def init(opts), do: opts def call(conn, _opts) do content_for(conn.request_path, conn) end end end defmacro get(path, controller, action) do quote do defp content_for(unquote(path), conn) do apply(unquote(controller), :call, [conn, unquote(action)]) end end end end dyn mic lly invokes the c ll/2 function on the controller module, p ssing in the conn nd ction s rguments. a a a a a a Kernel.apply/3 a a a a 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 (module, function n me, rgs)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 defmodule Spaghetti.Router do defmacro using(_opts) do quote do import Spaghetti.Router @behaviour Plug spaghetti/router.ex def init(opts), do: opts def call(conn, _opts) do content_for(conn.request_path, conn) end end end defmacro get(path, controller, action) do quote do defp content_for(unquote(path), conn) do apply(unquote(controller), :call, [conn, unquote(action)]) end end end end
14 15 16 17 18 19 20 21 22 23 24 1 25 2 26 3 27 4 28 5 29 6 30 7 31 8 32 9 33 10 1 34 11 2 35 12 3 36 13 4 37 14 5 38 15 6 39 16 7 40 17 8 41 18 9 42 19 10 43 20 11 44 spaghetti/router.ex defmacro get(path, controller, action) do quote do defp content_for(unquote(path), conn) do apply(unquote(controller), :call, [conn, unquote(action)]) end end end end defmodule HowdyElixirConf.Web.Router do use Spaghetti.Router web/router.ex alias HowdyElixirConf.Web.PageController get “/”, PageController, :home end defmodule HowdyElixirConf.Web.PageController do import Plugs.Conn def call(conn, action) do apply(MODULE, action, [conn]) end def home(conn, _) do put_resp_body(conn, “<h1>Howdy Elixir!</h1>”) end … web/page_controller.ex
Pony up! Let’s see some code.
At the heart of Phoenix, lies a key concept encapsulating power and flexibility, plugs. These small but mighty modules play a key role in connecting requests in a typical request-response cycle alongside other core concerns like authentication, session management, and error handling. With their modular approach that contains a single unit of functionality, plugs work perfectly when combined to support the needs of a fully fledged web application like Phoenix. In this talk, we’ll explore the inner workings of plugs to better understand their versatility in the context of an app. We’ll specifically work on building a phoenix like framework from the ground up using just plugs to see just how big and magical plugs can be in making applications like Phoenix work!