Repository: phoenixframework/phoenix (elixir)

Issue:
Support subdomains in the Router Helpers
### Environment

* Elixir version (elixir -v): Elixir 1.5.1
* Phoenix version (mix deps):  phoenix 1.2.3
### Expected behavior

Url generated on the subdomain should point to the subdomain

### Actual behavior
Url generated on the subdomain points to the host specified in the endpoint configuration


This further development of a host-based routing discussed in https://github.com/phoenixframework/phoenix/issues/489:
while  router itself supports subdomains, generating correct urls still requires manual work - user has to:
-  deduct current host via `conn.host`
- construct `%URI{}` structure with that host 
- pass URI to the `_url` helper

```
uri = %URI{host: conn.host}
MyAppWeb.Router.Helpers.page_url(uri, :show, "hello")
"https://other.example.com/pages/hello"
```

On the surface adding this piece of functionality is as simple as:

```
  def url(_router, %Conn{private: private, host: host}) do

    private.phoenix_endpoint.url
    |> URI.parse()
    |> Map.put(:host, host)
    |> URI.to_string()
  end
```

But it feels more like plumbing than a proper solution and I want to ask whether team has any strategy/vision/plans for the subdomains support in the Phoenix?

Thanks.

Pull request:
Support subdomains in the Router Helpers. Closes #2462
@josevalim we have some overlap here and need to reconcile the `current_url` docs and behavior with this new feature. Let's chat tomorrow about it.

Patch:
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index 99639b0ac..6c18d5b83 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -13,11 +13,11 @@ defmodule Phoenix.Controller do
 
   For example, the route:
 
-      get "/users/:id", MyApp.UserController, :show
+      get "/users/:id", MyAppWeb.UserController, :show
 
-  will invoke the `show/2` action in the `MyApp.UserController`:
+  will invoke the `show/2` action in the `MyAppWeb.UserController`:
 
-      defmodule MyApp.UserController do
+      defmodule MyAppWeb.UserController do
         use MyAppWeb, :controller
 
         def show(conn, %{"id" => id}) do
@@ -63,7 +63,7 @@ defmodule Phoenix.Controller do
   As with routers, controllers also have their own plug pipeline.
   However, different from routers, controllers have a single pipeline:
 
-      defmodule MyApp.UserController do
+      defmodule MyAppWeb.UserController do
         use MyAppWeb, :controller
 
         plug :authenticate, usernames: ["jose", "eric", "sonny"]
@@ -602,7 +602,7 @@ defmodule Phoenix.Controller do
 
   ## Examples
 
-      defmodule MyApp.UserController do
+      defmodule MyAppWeb.UserController do
         use Phoenix.Controller
 
         def show(conn, _params) do
@@ -610,7 +610,7 @@ defmodule Phoenix.Controller do
         end
       end
 
-  The example above renders a template "show.html" from the `MyApp.UserView`
+  The example above renders a template "show.html" from the `MyAppWeb.UserView`
   and sets the response content type to "text/html".
 
   In many cases, you may want the template format to be set dynamically based
@@ -630,22 +630,22 @@ defmodule Phoenix.Controller do
   ## Views
 
   By default, Controllers render templates in a view with a similar name to the
-  controller. For example, `MyApp.UserController` will render templates inside
-  the `MyApp.UserView`. This information can be changed any time by using the
+  controller. For example, `MyAppWeb.UserController` will render templates inside
+  the `MyAppWeb.UserView`. This information can be changed any time by using the
   `put_view/2` function:
 
       def show(conn, _params) do
         conn
-        |> put_view(MyApp.SpecialView)
+        |> put_view(MyAppWeb.SpecialView)
         |> render(:show, message: "Hello")
       end
 
   `put_view/2` can also be used as a plug:
 
-      defmodule MyApp.UserController do
+      defmodule MyAppWeb.UserController do
         use Phoenix.Controller
 
-        plug :put_view, MyApp.SpecialView
+        plug :put_view, MyAppWeb.SpecialView
 
         def show(conn, _params) do
           render conn, :show, message: "Hello"
@@ -657,7 +657,7 @@ defmodule Phoenix.Controller do
   Templates are often rendered inside layouts. By default, Phoenix
   will render layouts for html requests. For example:
 
-      defmodule MyApp.UserController do
+      defmodule MyAppWeb.UserController do
         use Phoenix.Controller
 
         def show(conn, _params) do
@@ -666,7 +666,7 @@ defmodule Phoenix.Controller do
       end
 
   will render the  "show.html" template inside an "app.html"
-  template specified in `MyApp.LayoutView`. `put_layout/2` can be used
+  template specified in `MyAppWeb.LayoutView`. `put_layout/2` can be used
   to change the layout, similar to how `put_view/2` can be used to change
   the view.
 
@@ -785,6 +785,35 @@ defmodule Phoenix.Controller do
     end
   end
 
+  @doc """
+  Puts the URL or `%URI{}` to be used for route generation.
+
+  This function overrides the default URL generation pulled
+  from the `%Plug.Conn{}`'s endpoint configuration.
+
+  ## Examples
+
+  Imagine your application is configured to run on "example.com"
+  but after the user signs in, you want all links to use
+  "some_user.example.com". You can do so by setting the proper
+  router url configuration:
+
+      def put_router_url_by_user(conn) do
+        put_router_url(conn, get_user_from_conn(conn).account_name <> ".example.com")
+      end
+
+  Now when you call `Routes.some_route_url(conn, ...)`, it will use
+  the router url set above. Keep in mind that, if you want to generate
+  routes to the current domain, it is preferred to use `Routes.some_route_path`
+  helpers, as those are always relative.
+  """
+  def put_router_url(conn, %URI{} = uri) do
+    put_private(conn, :phoenix_router_url, uri)
+  end
+  def put_router_url(conn, url) when is_binary(url) do
+    put_private(conn, :phoenix_router_url, url)
+  end
+
   @doc """
   Puts the format in the connection.
 
@@ -1323,8 +1352,14 @@ defmodule Phoenix.Controller do
   @doc ~S"""
   Returns the current request URL, with and without query params.
 
-  The connection's endpoint will be used for URL generation.
-  See `current_path/1` for details on how the request path is generated.
+  The path will be retrieved from the currently requested path via
+  `current_path/1`. The scheme, host and others will be received from
+  the URL configuration in your Phoenix endpoint. The reason we don't
+  use the host and scheme information in the request is because most
+  applications are behind proxies and the host and scheme may not
+  actually reflect the host and scheme accessed by the client. If you
+  want to access the url precisely as requested by the client, see
+  `Plug.Conn.request_url/1`.
 
   ## Examples
 
@@ -1339,37 +1374,29 @@ defmodule Phoenix.Controller do
 
   ## Custom URL Generation
 
-  In some cases, you'll need to generate a request's URL, but
-  using a different scheme, different host, etc. This can be
-  accomplished by concatentating the request path with a
-  custom built URL from your Router helpers, another Endpoint, mix
-  config, or a hand-built string.
+  In some cases, you'll need to generate a request's URL, but using a
+  different scheme, different host, etc. This can be accomplished in
+  two ways.
 
-  For example, you may way to generate an https URL from an http request.
-  You could define a function like the following:
+  If you want to do so in a case-by-case basis, you can define a custom
+  function that gets the endpoint URI configuration and changes it accordingly.
+  For example, to get the current URL always in HTTPS format:
 
       def current_secure_url(conn, params \\ %{}) do
-        cur_uri  = Phoenix.Controller.endpoint_module(conn).struct_url()
+        cur_uri  = MyAppWeb.Endpoint.struct_url()
         cur_path = Phoenix.Controller.current_path(conn, params)
 
         MyAppWeb.Router.Helpers.url(%URI{cur_uri | scheme: "https}) <> cur_path
       end
 
-  Or maybe you have a subdomain based URL for different organizations:
-
-      def organization_url(conn, org, params \\ %{}) do
-        cur_uri  = Phoenix.Controller.endpoint_module(conn).struct_url()
-        cur_path = Phoenix.Controller.current_path(conn, params)
-        org_host = "#{org.slug}.#{cur_uri.host}"
-
-        MyAppWeb.Router.Helpers.url(%URI{cur_uri | host: org_host}) <> cur_path
-      end
+  However, if you want all generated URLs to always have a certain schema,
+  host, etc, you may invoke `put_router_url/2`.
   """
   def current_url(%Plug.Conn{} = conn) do
-    endpoint_module(conn).url() <> current_path(conn)
+    Phoenix.Router.Helpers.url(router_module(conn), conn) <> current_path(conn)
   end
   def current_url(%Plug.Conn{} = conn, %{} = params) do
-    endpoint_module(conn).url() <> current_path(conn, params)
+    Phoenix.Router.Helpers.url(router_module(conn), conn) <> current_path(conn, params)
   end
 
   @doc false
diff --git a/lib/phoenix/router/helpers.ex b/lib/phoenix/router/helpers.ex
index 84cfa0875..abcdbc027 100644
--- a/lib/phoenix/router/helpers.ex
+++ b/lib/phoenix/router/helpers.ex
@@ -9,12 +9,16 @@ defmodule Phoenix.Router.Helpers do
   @doc """
   Callback invoked by url generated in each helper module.
   """
-  def url(_router, %Conn{private: private}) do
-    private.phoenix_endpoint.url
+  def url(router, %Conn{private: private}) do
+    case private do
+      %{phoenix_router_url: %URI{} = uri} -> url(router, uri)
+      %{phoenix_router_url: url} when is_binary(url) -> url
+      %{phoenix_endpoint: endpoint} -> endpoint.url()
+    end
   end
 
   def url(_router, %Socket{endpoint: endpoint}) do
-    endpoint.url
+    endpoint.url()
   end
 
   def url(_router, %URI{} = uri) do
@@ -22,7 +26,7 @@ defmodule Phoenix.Router.Helpers do
   end
 
   def url(_router, endpoint) when is_atom(endpoint) do
-    endpoint.url
+    endpoint.url()
   end
 
   def url(router, other) do


Test patch:
diff --git a/lib/phoenix/test/conn_test.ex b/lib/phoenix/test/conn_test.ex
index feec54fe7..636e1f250 100644
--- a/lib/phoenix/test/conn_test.ex
+++ b/lib/phoenix/test/conn_test.ex
@@ -461,7 +461,7 @@ defmodule Phoenix.ConnTest do
     build_conn()
     |> Map.put(:host, conn.host)
     |> Plug.Test.recycle_cookies(conn)
-    |> copy_headers(conn.req_headers, ~w(accept))
+    |> copy_headers(conn.req_headers, ~w(accept authorization))
   end
 
   defp copy_headers(conn, headers, copy) do
diff --git a/test/phoenix/controller/controller_test.exs b/test/phoenix/controller/controller_test.exs
index c65a7af7f..870187fcd 100644
--- a/test/phoenix/controller/controller_test.exs
+++ b/test/phoenix/controller/controller_test.exs
@@ -564,6 +564,7 @@ defmodule Phoenix.Controller.ControllerTest do
       conn(:get, path)
       |> fetch_query_params()
       |> put_private(:phoenix_endpoint, __MODULE__)
+      |> put_private(:phoenix_router, __MODULE__)
     end
 
     test "current_path/1 uses the conn's query params" do
diff --git a/test/phoenix/router/helpers_test.exs b/test/phoenix/router/helpers_test.exs
index f73e9e0e7..c3729d1ea 100644
--- a/test/phoenix/router/helpers_test.exs
+++ b/test/phoenix/router/helpers_test.exs
@@ -429,6 +429,24 @@ defmodule Phoenix.Router.HelpersTest do
     assert Helpers.url(uri()) == "https://example.com"
   end
 
+  test "phoenix_router_url with string takes precedence over endpoint" do
+    url = "https://phoenixframework.org"
+    conn = Phoenix.Controller.put_router_url(conn_with_endpoint(), url)
+
+    assert Helpers.url(conn) == url
+    assert Helpers.admin_message_url(conn, :show, 1) ==
+      url <> "/admin/new/messages/1"
+  end
+
+  test "phoenix_router_url with URI takes precedence over endpoint" do
+    uri = %URI{scheme: "https", host: "phoenixframework.org", port: 123, path: "/path"}
+    conn = Phoenix.Controller.put_router_url(conn_with_endpoint(), uri)
+
+    assert Helpers.url(conn) == "https://phoenixframework.org:123"
+    assert Helpers.admin_message_url(conn, :show, 1) ==
+      "https://phoenixframework.org:123/admin/new/messages/1"
+  end
+
   test "helpers module generates a path helper" do
     assert Helpers.path(__MODULE__, "/") == "/"
     assert Helpers.path(conn_with_endpoint(), "/") == "/"
diff --git a/test/phoenix/test/conn_test.exs b/test/phoenix/test/conn_test.exs
index 1c3dce245..74d17eacc 100644
--- a/test/phoenix/test/conn_test.exs
+++ b/test/phoenix/test/conn_test.exs
@@ -143,28 +143,44 @@ defmodule Phoenix.Test.ConnTest do
     refute conn.private.phoenix_recycled
   end
 
-  test "recycle/1" do
-    conn =
-      build_conn()
-      |> get("/")
-      |> put_req_header("hello", "world")
-      |> put_req_cookie("req_cookie", "req_cookie")
-      |> put_req_cookie("del_cookie", "del_cookie")
-      |> put_req_cookie("over_cookie", "pre_cookie")
-      |> put_resp_cookie("over_cookie", "pos_cookie")
-      |> put_resp_cookie("resp_cookie", "resp_cookie")
-      |> delete_resp_cookie("del_cookie")
-
-    conn = conn |> recycle() |> fetch_cookies()
-    assert get_req_header(conn, "hello") == []
-    assert conn.cookies == %{"req_cookie"  => "req_cookie",
-                             "over_cookie" => "pos_cookie",
-                             "resp_cookie" => "resp_cookie"}
+  describe "recycle/1" do
+    test "relevant request headers are persisted" do
+      conn =
+        build_conn()
+        |> get("/")
+        |> put_req_header("accept", "text/html")
+        |> put_req_header("authorization", "Bearer mytoken")
+        |> put_req_header("hello", "world")
+
+      conn = conn |> recycle()
+      assert get_req_header(conn, "accept") == ["text/html"]
+      assert get_req_header(conn, "authorization") == ["Bearer mytoken"]
+      assert get_req_header(conn, "hello") == []
+    end
 
-    conn =
-      build_conn(:get, "http://localhost/", nil)
-      |> recycle()
-    assert conn.host == "localhost"
+    test "host is persisted" do
+      conn =
+        build_conn(:get, "http://localhost/", nil)
+        |> recycle()
+      assert conn.host == "localhost"
+    end
+
+    test "cookies are persisted" do
+      conn =
+        build_conn()
+        |> get("/")
+        |> put_req_cookie("req_cookie", "req_cookie")
+        |> put_req_cookie("del_cookie", "del_cookie")
+        |> put_req_cookie("over_cookie", "pre_cookie")
+        |> put_resp_cookie("over_cookie", "pos_cookie")
+        |> put_resp_cookie("resp_cookie", "resp_cookie")
+        |> delete_resp_cookie("del_cookie")
+
+      conn = conn |> recycle() |> fetch_cookies()
+      assert conn.cookies == %{"req_cookie"  => "req_cookie",
+                               "over_cookie" => "pos_cookie",
+                               "resp_cookie" => "resp_cookie"}
+    end
   end
 
   test "ensure_recycled/1" do