Repository: phoenixframework/phoenix
Pull request #5620 (2023-11-01 16:18:10)

Add `c:Phoenix.Endpoint.server_info/1`
Solves https://github.com/phoenixframework/phoenix/pull/5553

Paired with https://github.com/mtrudel/bandit/pull/258 over in Bandit-land

@mtrudel will follow with a change to refactor https://github.com/livebook-dev/livebook/pull/2316 once this is merged/released (@josevalim relevant to your recent interests)



josevalim: Thank @mtrudel. I am thinking this should be a single function, called `server_info(:http | :https)`. WDYT?
josevalim: Perhaps it should be `server_info(:http | :https) :: {:ok, address, port} | {:local, path} | {:error, reason}`
mtrudel: > I am thinking this should be a single function, called server_info(:http | :https)

Yep; I'll work that up; won't take a few minutes.
mtrudel: Revised and ready to go!

Patch:
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 0c7490f93..b76ebe7f6 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -251,6 +251,9 @@ defmodule Phoenix.Endpoint do
     * for handling paths and URLs: `c:struct_url/0`, `c:url/0`, `c:path/1`,
       `c:static_url/0`,`c:static_path/1`, and `c:static_integrity/1`
 
+    * for gethering runtime information about the address and port the
+      endpoint is running on: `c:server_info/1`
+
     * for broadcasting to channels: `c:broadcast/3`, `c:broadcast!/3`,
       `c:broadcast_from/4`, `c:broadcast_from!/4`, `c:local_broadcast/3`,
       and `c:local_broadcast_from/4`
@@ -342,6 +345,15 @@ defmodule Phoenix.Endpoint do
   """
   @callback host() :: String.t()
 
+  # Server information
+
+  @doc """
+  Returns the address and port that the server is running on
+  """
+  @callback server_info(Plug.Conn.scheme()) ::
+              {:ok, {:inet.ip_address(), :inet.port_number()} | :inet.returned_non_ip_address()}
+              | {:error, term()}
+
   # Channels
 
   @doc """
@@ -605,6 +617,11 @@ defmodule Phoenix.Endpoint do
           &Phoenix.Endpoint.Supervisor.static_lookup(&1, path)
         )
       end
+
+      @doc """
+      Returns the address and port that the server is running on
+      """
+      def server_info(scheme), do: config(:adapter).server_info(__MODULE__, scheme)
     end
   end
 
diff --git a/lib/phoenix/endpoint/cowboy2_adapter.ex b/lib/phoenix/endpoint/cowboy2_adapter.ex
index 84117a9c7..2356e26bd 100644
--- a/lib/phoenix/endpoint/cowboy2_adapter.ex
+++ b/lib/phoenix/endpoint/cowboy2_adapter.ex
@@ -80,7 +80,7 @@ defmodule Phoenix.Endpoint.Cowboy2Adapter do
       Application.ensure_all_started(:ssl)
     end
 
-    ref = Module.concat(endpoint, scheme |> Atom.to_string() |> String.upcase())
+    ref = make_ref(endpoint, scheme)
 
     plug =
       if code_reloader? do
@@ -137,4 +137,16 @@ defmodule Phoenix.Endpoint.Cowboy2Adapter do
   defp port_to_integer({:system, env_var}), do: port_to_integer(System.get_env(env_var))
   defp port_to_integer(port) when is_binary(port), do: String.to_integer(port)
   defp port_to_integer(port) when is_integer(port), do: port
+
+  def server_info(endpoint, scheme) do
+    make_ref(endpoint, scheme)
+    |> :ranch.get_addr()
+    |> then(&{:ok, &1})
+  rescue
+    e -> {:error, e.message}
+  end
+
+  defp make_ref(endpoint, scheme) do
+    Module.concat(endpoint, scheme |> Atom.to_string() |> String.upcase())
+  end
 end
diff --git a/lib/phoenix/endpoint/supervisor.ex b/lib/phoenix/endpoint/supervisor.ex
index 36bd2ef4f..98d4f55c7 100644
--- a/lib/phoenix/endpoint/supervisor.ex
+++ b/lib/phoenix/endpoint/supervisor.ex
@@ -137,7 +137,7 @@ defmodule Phoenix.Endpoint.Supervisor do
   defp server_children(mod, config, server?) do
     cond do
       server? ->
-        adapter = config[:adapter] || Phoenix.Endpoint.Cowboy2Adapter
+        adapter = config[:adapter]
         adapter.child_specs(mod, config)
 
       config[:http] || config[:https] ->
@@ -196,6 +196,7 @@ defmodule Phoenix.Endpoint.Supervisor do
       render_errors: [view: render_errors(module), accepts: ~w(html), layout: false],
 
       # Runtime config
+      adapter: Phoenix.Endpoint.Cowboy2Adapter,
       cache_static_manifest: nil,
       check_origin: true,
       http: false,


Test patch:
diff --git a/test/phoenix/endpoint/endpoint_test.exs b/test/phoenix/endpoint/endpoint_test.exs
index beb946a99..720fb6467 100644
--- a/test/phoenix/endpoint/endpoint_test.exs
+++ b/test/phoenix/endpoint/endpoint_test.exs
@@ -227,6 +227,22 @@ defmodule Phoenix.Endpoint.EndpointTest do
     assert StaticEndpoint.static_path("/phoenix.png") =~ "/static/phoenix.png"
   end
 
+  @tag :capture_log
+  test "can find the running address and port for an endpoint" do
+    Application.put_env(:phoenix, __MODULE__.AddressEndpoint,
+      http: [ip: {127, 0, 0, 1}, port: 0],
+      server: true
+    )
+
+    defmodule AddressEndpoint do
+      use Phoenix.Endpoint, otp_app: :phoenix
+    end
+
+    AddressEndpoint.start_link()
+    assert {:ok, {{127, 0, 0, 1}, port}} = AddressEndpoint.server_info(:http)
+    assert is_integer(port)
+  end
+
   test "injects pubsub broadcast with configured server" do
     Endpoint.subscribe("sometopic")
     some = spawn fn -> :ok end