Phoenix is Not Magic
Note: This series is using Phoenix 1.4.1
A common criticism leveled against Phoenix is that it’s just another monolithic framework like Rails. Or that it’s too magic, or too opinionated. Generally, these complaints are unfounded, and seem to stem from superficial similarities (Elixir does look a little Ruby-ish if you squint) rather than experience. This blog post is intended to be the first of several posts that can serve as a response — a light overview of core Phoenix internals to demystify your application.
The rough outline of these posts will look something like this:
- Part 1 - Endpoint
- Part 2 - Router through Controller
- Part 3 - Rendering views
- Part 4+ - Socket, Repo, etc…
This first part will cover the “Endpoint”. If you are new to Phoenix, you may only recognize the Endpoint as “the thing you sometimes tweak configs in”, but it is essential to the function of your application.
High Level Overview
If we generate a new Phoenix project with
mix phx.new phoenix_internals
, we’ll find a module calledPhoenixInternalsWeb.Endpoint
that looks something like this:# lib/phoenix_internals_web/endpoint.ex defmodule PhoenixInternalsWeb.Endpoint do use Phoenix.Endpoint, otp_app: :phoenix_internals socket "/socket", PhoenixInternalsWeb.UserSocket, websocket: true, longpoll: false # Serve at "/" the static files from "priv/static" directory. # # You should set gzip to true if you are running phx.digest # when deploying your static files in production. plug Plug.Static, at: "/", from: :phoenix_internals, gzip: false, only: ~w(css fonts images js favicon.ico robots.txt) # Code reloading can be explicitly enabled under the # :code_reloader configuration of your endpoint. if code_reloading? do socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket plug Phoenix.LiveReloader plug Phoenix.CodeReloader end plug Plug.RequestId plug Plug.Logger plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], json_decoder: Phoenix.json_library() plug Plug.MethodOverride plug Plug.Head # The session will be stored in the cookie and signed, # this means its contents can be read but not tampered with. # Set :encryption_salt if you would also like to encrypt it. plug Plug.Session, store: :cookie, key: "_phoenix_internals_key", signing_salt: "iiFYGc1q" plug PhoenixInternalsWeb.Router end
This module is the heart of our Phoenix application. It’s also little more than a standard Plug1 configuration. In reality, a lot of what looks like “Phoenix functionality” is really just Plug underneath. In this case, we’re looking at the work of
Plug.Builder
, which allows us to createplug
pipelines.Plugs in the pipeline are defined with the
plug
macro, then are called in the order that they are defined. Modules thatuse
Plug.Builder
are plugs themselves, and arecall
ed accordingly.Here is a
Plug.Builder
example modified from the Plug documentation:defmodule MyApp do use Plug.Builder plug Plug.Logger plug :hello, upper: true def hello(conn, opts) do body = if opts[:upper], do: "WORLD", else: "world" send_resp(conn, 200, body) end end
The structure and intention of this example and our Phoenix Endpoint is quite similar. In fact, we can see that if we just started moving plug definitions from our Endpoint to this pipeline, we’d very quickly wind up with the same functionality. The only piece the Phoenix Endpoint is missing is
use Plug.Builder
, which is ultimately included in the Endpoint viause Phoenix.Endpoint
.We will get back to this momentarily, but first as an exercise, let’s remove all of the Phoenix-specific macros from the Endpoint - that is the socket, router, and code reloading blocks. Then let’s add the following to the end of our module:
# lib/phoenix_internals_web/endpoint.ex plug :hello def hello(conn, _) do send_resp(conn, 200, "Hello, world!") end
If we start up our Phoenix application and go to http://localhost:4000, we’ll see “Hello, world!”. We can even take this a step further with a naive router.
# lib/phoenix_internals_web/endpoint.ex plug :hello def hello(conn, _) do body = case conn.request_path do "/" -> "Hello, world!" "/bye" -> "Goodbye, world!" _ -> "What?" end send_resp(conn, 200, body) end
Try going to http://localhost:4000/bye or http://localhost:4000/nothing to see our custom router in action. This is a fully functioning (if very basic) Phoenix web application, bootstrapped off of simple Plug elements.
We can see that the Endpoint is just Plug, and, from our understanding of Plug, we know that somewhere in the codebase the following steps must occur:
- Something calls
PhoenixInternalsWeb.Endpoint.call(conn, opts)
- The
conn
andopts
find their way toPlug.Builder
.Plug.Builder
does its thing and calls all of the plugs in the Endpoint in order.This is already a pretty solid understanding of how the Endpoint works, but we are still missing a few details. How is the application actually started? How do we get from
mix phx.server
to a running application?The Internals
If you’ve built a web application with Phoenix, you know your application starts with a call to
mix phx.server
. This is a very basic use of Mix, Elixir’s application build tool. If a custom Mix task is called “phx.server,” you can expect to find a “phx.server.ex” file somewhere in your project directory2. In this file, you’ll find a very simple task definition.# deps/phoenix/lib/mix/tasks/phx.server.ex def run(args) do Application.put_env(:phoenix, :serve_endpoints, true, persistent: true) Mix.Tasks.Run.run run_args() ++ args end defp run_args do if iex_running?(), do: [], else: ["--no-halt"] end
This definition sets up some environment variables, but essentially just runs
Mix.Tasks.Run
, a built-in task that runs the current application. Let’s take a look at our application configuration in “mix.exs.”When we make an OTP application with Elixir, we can define our application parameters through the
application/0
function in “mix.exs.” If we’ve used the Phoenix generators, that function will look like this:# mix.exs def application do [ mod: {PhoenixInternals.Application, []}, extra_applications: [:logger, :runtime_tools] ] end
The part that is most interesting to us is the
mod
key, which specifies the Application callback module. When the application is started, this is the module that will be invoked. From our application definition, we can expect the call flow to look something like…
- You run
mix phx.server
, which in turn starts the application.- Some internal Mix/Elixir thing calls
Application.start(:phoenix_internals)
.- Our callback configuration results in a call to
PhoenixInternals.Application.start(_type, _args)
.Let’s a look at
PhoenixInternals.Application
. As expected, this is your Application callback module, and it defines the necessarystart/2
function.# lib/phoenix_internals/application.ex def start(_type, _args) do # List all child processes to be supervised children = [ # Start the Ecto repository PhoenixInternals.Repo, # Start the endpoint when the application starts PhoenixInternalsWeb.Endpoint # Starts a worker by calling: PhoenixInternals.Worker.start_link(arg) # {PhoenixInternals.Worker, arg}, ] # See https://hexdocs.pm/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: PhoenixInternals.Supervisor] Supervisor.start_link(children, opts) end
The Application spawns a supervisor,
PhoenixInternals.Supervisor
, which starts and monitors the Ecto repository,PhoenixInternals.Repo
, and the Phoenix Endpoint,PhoenixInternalsWeb.Endpoint
. This is all standard Elixir/OTP stuff - if you’re lost, the documentation in Elixir tends to be stellar, and the Phoenix source is well commented with links back to the docs.For the moment, we are going to ignore Ecto, and focus on
PhoenixInternalsWeb.Endpoint
. Again, if we have some knowledge about the way Elixir and OTP work, we can have some pretty strong assumptions about what is happening in the Endpoint. In particular, we know that, as a child of a Supervisor, theEndpoint
module will have to define achild_spec
which will dictate the way the process is started.If we take a look at
PhoenixInternalsWeb.Endpoint
again, we won’t see the definition we are looking for, but we will see ause Phoenix.Endpoint, otp_app: :phoenix_internals
. Now, callinguse
just runs theuse
d module’s__using__
macro at compile time. In turn, the__using__
macro includes the quoted source in the calling module.Knowing this, we can expect
Phoenix.Endpoint
to work (if not look) like the following:defmodule Phoenix.Endpoint do defmacro __using__(opts) do quote do def child_spec(opts) do child_spec_map end end end end
Let’s take a look at the module now, and see how our expectations match up. A quick search in the
Phoenix.Endpoint
module will reveal the__using__
macro.# deps/phoenix/lib/phoenix/endpoint.ex defmacro __using__(opts) do quote do @behaviour Phoenix.Endpoint unquote(config(opts)) unquote(pubsub()) unquote(plug()) unquote(server()) end end
This looks a bit different from what we expected, but presumably the
child_spec
function is defined in one of these included function calls. Perusing each function in turn, or searching forchild_spec
, shows that we are correct.child_spec
is defined in theserver
function.The
server
function defines a number of other functions that will be included in our own Endpoint, but there are only two that we really care about right now:child_spec
andstart_link
.# deps/phoenix/lib/phoenix/endpoint.ex defp server() do quote location: :keep, unquote: false do def child_spec(opts) do %{ id: __MODULE__, start: {__MODULE__, :start_link, [opts]}, type: :supervisor } end def start_link(_opts \\ []) do Phoenix.Endpoint.Supervisor.start_link(@otp_app, __MODULE__) end ...snip... end
If you aren’t familiar with the
quote
/unquote
macros, I’d suggest taking a look at the documentation, as it is beyond the scope of this post. Althoughquote
/unquote
is the core of Elixir’s metaprogramming, you only need a superficial understanding to follow along with the source.Anyway, back to the code. We were looking for the
child_spec
function, and we’ve found it. We can see that it’s defining the callback function as{__MODULE__, :start_link, [opts]}
. Because of metaprogramming,__MODULE__
will expand to our ownEndpoint
module, meaning thatPhoenixInternalsWeb.Endpoint.start_link/1
is our callback. Luckily for us,start_link
is defined just belowchild_spec
.This function is very simple, and appears to be kicking off an
Endpoint
supervisor. The__MODULE__
parameter will expand to the current module, and, in this case,@otp_app
will be:phoenix_internals
. The “how” on the@otp_app
definition is something we will cover a little later. For now, let’s take a look at the Endpoint Supervisor.At the very top of the Supervisor module, you’ll find the
start_link
definition.# deps/phoenix/lib/phoenix/endpoint/supervisor.ex def start_link(otp_app, mod) do case Supervisor.start_link(__MODULE__, {otp_app, mod}, name: mod) do {:ok, _} = ok -> warmup(mod) ok {:error, _} = error -> error end end
This function calls the built-in supervisor
start_link
, which triggers theinit
callback, which will return the appropriate specifications. Theinit
function is quite large, but the interesting/important bit is at the bottom of the function definition.# deps/phoenix/lib/phoenix/endpoint/supervisor.ex def init({otp_app, mod}) do ...snip... children = config_children(mod, secret_conf, otp_app) ++ pubsub_children(mod, conf) ++ socket_children(mod) ++ server_children(mod, conf, otp_app, server?) ++ watcher_children(mod, conf, server?) # Supervisor.init(children, strategy: :one_for_one) {:ok, {{:one_for_one, 3, 5}, children}} end
A quick scan of this code, and the intention is quite clear. Specs are being created for
config
,pubsub
,socket
,server
, andwatcher
child processes, which will be supervised by our supervisor. Since, at the moment, we are trying to figure out how Phoenix is starting and running a web server,server_children
is where we’ll look next.# deps/phoenix/lib/phoenix/endpoint/supervisor.ex defp server_children(mod, config, otp_app, server?) do if server? do user_adapter = user_adapter(mod, config) autodetected_adapter = cowboy_version_adapter() warn_on_different_adapter_version(user_adapter, autodetected_adapter, mod) adapter = user_adapter || autodetected_adapter for {scheme, port} <- [http: 4000, https: 4040], opts = config[scheme] do port = :proplists.get_value(:port, opts, port) unless port do raise "server can't start because :port in #{scheme} config is nil, " <> "please use a valid port number" end opts = [port: port_to_integer(port), otp_app: otp_app] ++ :proplists.delete(:port, opts) adapter.child_spec(scheme, mod, opts) end else [] end end
We are now several layers deep into function calls, so this function may start to feel a bit confusing. If it helps, in our generated application, the parameters will resolve to…
server_children(PhoenixInternalsWeb.Endpoint, config, :phoenix_internals, true)
All this function is doing is fetching the server adapter from the configs, and then fetching the child spec from the adapter for HTTP and HTTPS configurations. In a brand new project, unless you specifically define an adapter, it’s going to default to
Cowboy2Adapter
.The
child_spec
function in theCowboy2Adapter
module is one of the roughest pieces of code in the Phoenix codebase. It is a collection of configuration options, and, because it is purely internal, it isn’t well documented. Fortunately, the function is quite small.# deps/phoenix/lib/phoenix/endpoint/cowboy2_adapter.ex def child_spec(scheme, endpoint, config) do if scheme == :https do Application.ensure_all_started(:ssl) end dispatches = [{:_, Phoenix.Endpoint.Cowboy2Handler, {endpoint, endpoint.init([])}}] config = Keyword.put_new(config, :dispatch, [{:_, dispatches}]) spec = Plug.Cowboy.child_spec(scheme: scheme, plug: {endpoint, []}, options: config) update_in spec.start, &{__MODULE__, :start_link, [scheme, endpoint, &1]} end
The initial
child_spec
creation is deferred to Plug, with its built-in Cowboy adapter. Of particular interest to us, in the config options, is thedispatch
key, which is set toPhoenix.Endpoint.Cowboy2Handler
. This is a Cowboy/Ranch configuration, and means that theCowboy2Handler
will be acting as Cowboy handler middleware.The final statement in the function, overrides the default Plug
start
key and setsstart_link
in our current module as the mechanism for starting the child process. In turn, ourstart_link
will call:ranch_listener_sup.start_link()
to kick off a Ranch supervisor.How all of this actually works is Cowboy and Plug internal, and beyond the scope of this post. However, a nice way to get a picture of all of this is by running Observer.
$ iex -S mix phx.server (iex)> :observer.start()
Using Observer, you can see that our supervision tree looks something like this:
┌────────────────────────────────────────┐ │ │ │ │ │ │ │ │ │ Elixir.PhoenixInternals.Supervisor │ │ │ │ │ │ │ │ │ └───┬───────────────────────────────┬────┘ │ │ │ │ ┌───┘ └─┐ │ │ │ │ ┌────────────────┴──────────────────────┐ ┌─────┴─────┐ │ │ │ │ │ │ │ │ │ Elixir.PhoenixInternalsWeb.Endpoint │ │ │ │ │ │ Stuff We │ │ │ │ Aren't │ └────────────────┬──────────────────────┘ │ Worried │ │ │About Right│ │ │ Now │ ┌────────────────┴────────────────┐ │ │ │ │ │ │ │ │ │ │ │ ranch_listener_sup │ └───────────┘ │ │ │ │ └────────────────┬────────────────┘ │ │ ┌────────────────┴────────────────┐ │ │ │ │ │ ranch_acceptors_sup │ │ │ │ │ └────────────────┬────────────────┘ │ ┌─────────┬────┴────┬────────┐ │ │ │ │ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ ┌─┴─┐ │ │ │ │ │ │ │ │ └───┘ └───┘ └───┘ └───┘ acceptors
Back to the
Cowboy2Handler
. What does it mean that it will be acting as Cowboy middleware? Well, basically, it means that every request that passes through Cowboy will result in a call toCowboy2Handler.init(req, {endpoint, opts})
.# deps/phoenix/lib/phoenix/endpoint/cowboy2_handler.ex def init(req, {endpoint, opts}) do conn = @connection.conn(req) try do case endpoint.__handler__(conn, opts) do {:websocket, conn, handler, opts} -> ...snip... {:plug, conn, handler, opts} -> %{adapter: {@connection, req}} = conn |> handler.call(opts) |> maybe_send(handler) {:ok, req, {handler, opts}} end catch ...snip... after receive do @already_sent -> :ok after 0 -> :ok end end end
The first thing Phoenix does is turn Cowboy’s request map into a
Plug.Conn
structure. If you want to see this for yourself, you can addIO.inspect(req)
andIO.inspect(conn)
just after the conn assignment, and recompile your dependencies withmix deps.compile
. When you rerun your application, you will see each request come through.The next thing this
init
function does is callendpoint.__handler__
. Again, the referenced endpoint variable is our own Endpoint, whichuse
sPhoenix.Endpoint
, so we can take a look at “endpoint.ex” to find the__handler__
function. You’ll find__handler__
grouped with several other functions.# deps/phoenix/lib/phoenix/endpoint.ex def __handler__(%{path_info: path} = conn, opts), do: do_handler(path, conn, opts) unquote(instrumentation) unquote(dispatches) defp do_handler(_path, conn, opts), do: {:plug, conn, __MODULE__, opts}
unquote(dispatches)
will metaprogrammatically includedo_handler
functions relating to sockets. Since we are not interested in sockets at the moment, we can consider the call to__handler__
to very simply return{:plug, conn, __MODULE__, opts}
.Looking back at the
Cowboy2Handler
, we can see that this response maps well to theendpoint.__handler__
case of{:plug, conn, handler, opts}
.# deps/phoenix/lib/phoenix/endpoint/cowboy2_handler.ex {:plug, conn, handler, opts} -> %{adapter: {@connection, req}} = conn |> handler.call(opts) |> maybe_send(handler) {:ok, req, {handler, opts}}
The
handler
is our own Endpoint, sohandler.call
is actually calling a function defined in “phoenix/endpoint.ex”, just above thedo_handler
definitions.# deps/phoenix/lib/phoenix/endpoint.ex def call(conn, opts) do conn = put_in conn.secret_key_base, config(:secret_key_base) conn = put_in conn.script_name, script_name() conn = Plug.Conn.put_private(conn, :phoenix_endpoint, __MODULE__) try do super(conn, opts) rescue e in Plug.Conn.WrapperError -> %{conn: conn, kind: kind, reason: reason, stack: stack} = e Phoenix.Endpoint.RenderErrors.__catch__(conn, kind, reason, stack, @phoenix_render_errors) catch kind, reason -> stack = System.stacktrace() Phoenix.Endpoint.RenderErrors.__catch__(conn, kind, reason, stack, @phoenix_render_errors) end end
This function is pretty simple. It updates the conn structure with the secret key and some Phoenix-specific information, then calls
super(conn, opts)
, delegating the rest of the work to the “parent” module,Plug.Builder
. If you recall from the beginning of this post, this is exactly what we expected to happen.
Cowboy2Handler
callsPhoenixInternalsWeb.Endpoint.call(conn, opts)
- The Endpoint adds some data to the conn, then calls
super(conn, opts)
, which passes control toPlug.Builder
.Plug.Builder
does its thing and calls all of the plugs in the Endpoint in order.Now, the last thing that happens in
Cowboy2Handler
is a call tomaybe_send
.# deps/phoenix/lib/phoenix/endpoint/cowboy2_handler.ex defp maybe_send(%Plug.Conn{state: :unset}, _plug), do: raise(Plug.Conn.NotSentError) defp maybe_send(%Plug.Conn{state: :set} = conn, _plug), do: Plug.Conn.send_resp(conn) defp maybe_send(%Plug.Conn{} = conn, _plug), do: conn
This will result in the response actually being sent, or, if the response was sent earlier in the pipeline, it will do nothing.
And that’s it; that’s how a Phoenix app goes from
mix phx.server
to actually serving responses. Of course, there’s a lot that goes on between thehandler.call
andmaybe_send
functions, but we will cover that in a later post. For now, let’s finish up by taking a step back to the beginning.In the original
__using__
macro, we had a few things going on, but we only covered theserver
aspect.# deps/phoenix/lib/phoenix/endpoint.ex defmacro __using__(opts) do quote do @behaviour Phoenix.Endpoint unquote(config(opts)) unquote(pubsub()) unquote(plug()) unquote(server()) end end
We will save
pubsub
for another time, but there are a few things worth mentioning inconfig
andplug
.# deps/phoenix/lib/phoenix/endpoint.ex defp config(opts) do quote do @otp_app unquote(opts)[:otp_app] || raise "endpoint expects :otp_app to be given" var!(config) = Phoenix.Endpoint.Supervisor.config(@otp_app, __MODULE__) var!(code_reloading?) = var!(config)[:code_reloader] # Avoid unused variable warnings _ = var!(code_reloading?) @doc false def init(_key, config) do {:ok, config} end defoverridable init: 2 end end
The
config
function sets the@otp_app
module attribute using the value passed in from our own endpoint. In this case, it’s:phoenix_internals
. Then it sets a handful of variables that will be available from our endpoint at compile time. It does this with Elixir’svar!
function, which is more metaprogramming.The most interesting variable that gets set is
config
, whose data is fetched from theEndpoint.Supervisor
. In Phoenix, your configs are actually a supervised process of their own, defined in a similar way to your server. If you recall the supervision tree diagram from above, the config worker falls on the righthand side, under “stuff we didn’t care about at the moment.” Try going back and tracing that functionality now that you have a clear idea of how it works.The final bit of the Phoenix startup puzzle is the Endpoint’s
plug
function.# deps/phoenix/lib/phoenix/endpoint.ex defp plug() do quote location: :keep do use Plug.Builder ...snip... end end
This function does a few things based on configs, but, most importantly, this is where
use Plug.Builder
is actuallyuse
d, allowing the whole Endpoint to actually function.End Notes
Thanks for reading my Phoenix internals post! I decided to split this into multiple parts for a few reasons. One, certainly, is because this post was getting close to 2500 words on its own. But another reason is that I wanted to provide room for feedback. I hope for this series to be helpful, and if I end up writing 10k+ words that go down avenues no one cares about, then that’s a wasted effort. So, if you’ve read through this post (or even if you just scrolled straight to the bottom), then shoot me a message with what you liked or what you want to see improved in future posts.
From the documentation, Plug is a “specification and conveniences for composable modules between web applications.” Plug internals are beyond the scope of this post, but more information can be found at https://github.com/elixir-plug/plug. ↩
For third-party Mix tasks, you’ll only find them after you’ve fetched your dependencies. ↩