Phoenix Unplugged; uncovering the magic of plugs
A presentation at ElixirConf 2024 in August 2024 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.