Repository: phoenixframework/phoenix
Pull request #3166 (2018-11-14 19:17:40)

Allow custom paths to be provided to :websocket and :longpoll
Previously, it was not possible to use a socket with a path that differed from
the key name. For example `:websocket` would always end with `/websocket` and
`:longpoll` would always end with `/longpoll`.

This commit allows a `:path` to be passed into the keyword options, the path
will be used instead of the default if available.


Patch:
diff --git a/guides/adding_pages.md b/guides/adding_pages.md
index 4ac4f68a2..1157a1e03 100644
--- a/guides/adding_pages.md
+++ b/guides/adding_pages.md
@@ -109,7 +109,7 @@ defmodule HelloWeb.Router do
   end
 
   scope "/", HelloWeb do
-    pipe_through :browser # Use the default browser stack
+    pipe_through :browser
 
     get "/", PageController, :index
   end
@@ -134,7 +134,7 @@ The `scope "/"` block of our `router.ex` file should now look like this:
 
 ```elixir
 scope "/", HelloWeb do
-  pipe_through :browser # Use the default browser stack
+  pipe_through :browser
 
   get "/", PageController, :index
   get "/hello", HelloController, :index
@@ -189,7 +189,7 @@ Templates are scoped to a view, which are scoped to controller. Phoenix creates
 Let's do that now. Create `lib/hello_web/templates/hello/index.html.eex` and make it look like this:
 
 ```html
-<div class="jumbotron">
+<div class="phx-hero">
   <h2>Hello World, from Phoenix!</h2>
 </div>
 ```
@@ -218,7 +218,7 @@ For this exercise, we're going to re-use the `HelloController` we just created a
 
 ```elixir
 scope "/", HelloWeb do
-  pipe_through :browser # Use the default browser stack.
+  pipe_through :browser
 
   get "/", PageController, :index
   get "/hello", HelloController, :index
@@ -262,7 +262,7 @@ To do that, we'll use the special EEx tags for executing Elixir expressions - `<
 And this is what the template should look like:
 
 ```html
-<div class="jumbotron">
+<div class="phx-hero">
   <h2>Hello World, from <%= @messenger %>!</h2>
 </div>
 ```
diff --git a/guides/contexts.md b/guides/contexts.md
index 1ef674e47..996bc7312 100644
--- a/guides/contexts.md
+++ b/guides/contexts.md
@@ -79,7 +79,7 @@ Phoenix generated the web files as expected in `lib/hello_web/`. We can also see
 
 ```elixir
   scope "/", HelloWeb do
-    pipe_through :browser # Use the default browser stack
+    pipe_through :browser
 
     get "/", PageController, :index
 +   resources "/users", UserController
@@ -497,7 +497,7 @@ Next, let's wire up our session routes in `lib/hello_web/router.ex`:
 
 ```elixir
   scope "/", HelloWeb do
-    pipe_through :browser # Use the default browser stack
+    pipe_through :browser
 
     get "/", PageController, :index
     resources "/users", UserController
diff --git a/guides/ecto.md b/guides/ecto.md
index 46b0f461a..08b5a1387 100644
--- a/guides/ecto.md
+++ b/guides/ecto.md
@@ -415,7 +415,7 @@ SELECT u0."email" FROM "users" AS u0 []
 ["user1@example.com", "user2@example.com"]
 ```
 
-First, we imported `Ecto.Query`, which imports the `from` macro of Ecto's Query DSL. Next, we built a query which selects all the the email addresses in our user's table. Let's try another example.
+First, we imported `Ecto.Query`, which imports the `from` macro of Ecto's Query DSL. Next, we built a query which selects all the email addresses in our user's table. Let's try another example.
 
 ```console
 iex)> Repo.one(from u in User, where: ilike(u.email, "%1%"),
diff --git a/guides/routing.md b/guides/routing.md
index eb4195963..8ca73f303 100644
--- a/guides/routing.md
+++ b/guides/routing.md
@@ -21,7 +21,7 @@ defmodule HelloWeb.Router do
   end
 
   scope "/", HelloWeb do
-    pipe_through :browser # Use the default browser stack
+    pipe_through :browser
 
     get "/", PageController, :index
   end
@@ -92,7 +92,7 @@ Let's add a resource to our `lib/hello_web/router.ex` file like this:
 
 ```elixir
 scope "/", HelloWeb do
-  pipe_through :browser # Use the default browser stack
+  pipe_through :browser
 
   get "/", PageController, :index
   resources "/users", UserController
@@ -797,7 +797,6 @@ defmodule HelloWeb.Router do
   ...
 
   scope "/reviews" do
-    # Use the default browser stack.
     pipe_through [:browser, :review_checks, :other_great_stuff]
 
     resources "/", HelloWeb.ReviewController
@@ -873,13 +872,17 @@ We mount socket handlers in our endpoint at `lib/hello_web/endpoint.ex`. Socket
 
 ```elixir
 defmodule HelloWeb.Endpoint do
-  use Phoenix.Endpoint
+  use Phoenix.Endpoint, otp_app: :hello
 
-  socket "/socket", HelloWeb.UserSocket
+  socket "/socket", HelloWeb.UserSocket,
+    websocket: true,
+    longpoll: false
   ...
 end
 ```
 
+By default, Phoenix supports both websockets and longpoll when invoking Phoenix.Endpoint.socket/3 in your endpoint. Here we're specifying that incoming socket connections can be made via a WebSocket connection.
+
 Next, we need to open our `lib/hello_web/channels/user_socket.ex` file and use the `channel/3` macro to define our channel routes. The routes will match a topic pattern to a channel to handle events. If we have a channel module called `RoomChannel` and a topic called `"rooms:*"`, the code to do this is straightforward.
 
 ```elixir
@@ -893,16 +896,10 @@ end
 
 Topics are just string identifiers. The form we are using here is a convention which allows us to define topics and subtopics in the same string - "topic:subtopic". The `*` is a wildcard character which allows us to match on any subtopic, so `"rooms:lobby"` and `"rooms:kitchen"` would both match this route.
 
-Phoenix abstracts the socket transport layer and includes two transport mechanisms out of the box - WebSockets and Long-Polling. If we wanted to make sure that our channel is handled by only one type of transport, we could specify that using the `via` option, like this.
-
-```elixir
-channel "rooms:*", HelloWeb.RoomChannel, via: [Phoenix.Transports.WebSocket]
-```
-
 Each socket can handle requests for multiple channels.
 
 ```elixir
-channel "rooms:*", HelloWeb.RoomChannel, via: [Phoenix.Transports.WebSocket]
+channel "rooms:*", HelloWeb.RoomChannel
 channel "foods:*", HelloWeb.FoodChannel
 ```
 
diff --git a/guides/templates.md b/guides/templates.md
index b8380bb5b..437f03a58 100644
--- a/guides/templates.md
+++ b/guides/templates.md
@@ -23,7 +23,7 @@ defmodule HelloWeb.Router do
   ...
 
   scope "/", HelloWeb do
-    pipe_through :browser # Use the default browser stack
+    pipe_through :browser
 
     get "/", PageController, :index
     get "/test", PageController, :test
@@ -88,7 +88,7 @@ end
 We have a route. We created a new controller action. We have made modifications to the main application view. Now all we need is a new template to display the string we get from `handler_info/1`. Let's create a new one at `lib/hello_web/templates/page/test.html.eex`.
 
 ```html
-<div class="jumbotron">
+<div class="phx-hero">
   <p><%= handler_info(@conn) %></p>
 </div>
 ```
@@ -110,7 +110,7 @@ Now that we have a function, visible to our template, that returns a list of key
 We can add a header and a list comprehension like this.
 
 ```html
-<div class="jumbotron">
+<div class="phx-hero">
   <p><%= handler_info(@conn) %></p>
 
   <h3>Keys for the conn Struct</h3>
@@ -147,7 +147,7 @@ We need to change `key` to `@key` here because this is a new template, not part
 Now that we have a template, we simply render it within our list comprehension in the `test.html.eex` template.
 
 ```html
-<div class="jumbotron">
+<div class="phx-hero">
   <p><%= handler_info(@conn) %></p>
 
   <h3>Keys for the conn Struct</h3>
@@ -169,7 +169,7 @@ Let's move our template into a shared view.
 `key.html.eex` is currently rendered by the `HelloWeb.PageView` module, but we use a render call which assumes that the current schema is what we want to render with. We could make that explicit, and re-write it like this:
 
 ```html
-<div class="jumbotron">
+<div class="phx-hero">
   ...
 
   <%= for key <- connection_keys(@conn) do %>
diff --git a/guides/up_and_running.md b/guides/up_and_running.md
index c010a5679..82d703507 100644
--- a/guides/up_and_running.md
+++ b/guides/up_and_running.md
@@ -26,8 +26,8 @@ mix phx.new hello
 * creating hello/config/dev.exs
 * creating hello/config/prod.exs
 ...
-* creating hello/lib/hello_web/views/layout_view.ex
-* creating hello/lib/hello_web/views/page_view.ex
+* creating hello/assets/static/images/phoenix.png
+* creating hello/assets/static/favicon.ico
 
 Fetch and install dependencies? [Yn]
 ```
@@ -40,7 +40,7 @@ Fetch and install dependencies? [Yn] Y
 * running mix deps.compile
 * running cd assets && npm install && node node_modules/webpack/bin/webpack.js --mode development
 
-We are all set! Go into your application by running:
+We are almost there! The following steps are missing:
 
     $ cd hello
 
@@ -69,6 +69,8 @@ Now we'll create our database:
 
 ```
 $ mix ecto.create
+Compiling 13 files (.ex)
+Generated hello app
 The database for Hello.Repo has been created
 ```
 
@@ -78,8 +80,10 @@ And finally, we'll start the Phoenix server:
 
 ```console
 $ mix phx.server
-[info] Running HelloWeb.Endpoint with Cowboy using http://0.0.0.0:4000
-19:30:43 - info: compiled 6 files into 2 files, copied 3 in 2.1 sec
+[info] Running HelloWeb.Endpoint with cowboy 2.5.0 at http://localhost:4000
+
+Webpack is watching the files…
+...
 ```
 
 If we choose not to have Phoenix install our dependencies when we generate a new application, the `phx.new` task will prompt us to take the necessary steps when we do want to install them.
diff --git a/guides/views.md b/guides/views.md
index ae2911e3d..0d7291432 100644
--- a/guides/views.md
+++ b/guides/views.md
@@ -73,7 +73,7 @@ When we `use HelloWeb, :view`, we get other conveniences as well. Since `view/0`
 Let's open up the `lib/hello_web/templates/page/index.html.eex` and locate this stanza.
 
 ```html
-<div class="jumbotron">
+<div class="phx-hero">
   <h2><%= gettext("Welcome to %{name}!", name: "Phoenix") %></h2>
   <p class="lead">A productive web framework that<br>does not compromise speed and maintainability.</p>
 </div>
@@ -82,7 +82,7 @@ Let's open up the `lib/hello_web/templates/page/index.html.eex` and locate this
 Then let's add a line with a link back to the same page. (The objective is to see how path helpers respond in a template, not to add any functionality.)
 
 ```html
-<div class="jumbotron">
+<div class="phx-hero">
   <h2><%= gettext("Welcome to %{name}!", name: "Phoenix") %></h2>
   <p class="lead">A productive web framework that<br>does not compromise speed and maintainability.</p>
   <p><a href="<%= Routes.page_path(@conn, :index) %>">Link back to this page</a></p>
@@ -250,7 +250,7 @@ Great, so we have a `render/2` function that takes a template and an `assigns` m
         <span class="logo"></span>
       </div>
 
-      <div class="jumbotron">
+      <div class="phx-hero">
         <p>Sorry, the page you are looking for does not exist.</p>
       </div>
 
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 5322a9542..f84907d9a 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -777,16 +777,18 @@ defmodule Phoenix.Endpoint do
 
     paths =
       if websocket do
-        triplet = {:websocket, socket, socket_config(websocket, Phoenix.Transports.WebSocket)}
-        [{socket_path(path, :websocket), triplet} | paths]
+        config = socket_config(websocket, Phoenix.Transports.WebSocket)
+        triplet = {:websocket, socket, config}
+        [{socket_path(path, config), triplet} | paths]
       else
         paths
       end
 
     paths =
       if longpoll do
-        plug_init = {endpoint, socket, socket_config(longpoll, Phoenix.Transports.LongPoll)}
-        [{socket_path(path, :longpoll), {:plug, Phoenix.Transports.LongPoll, plug_init}} | paths]
+        config = socket_config(longpoll, Phoenix.Transports.LongPoll)
+        plug_init = {endpoint, socket, config}
+        [{socket_path(path, config), {:plug, Phoenix.Transports.LongPoll, plug_init}} | paths]
       else
         paths
       end
@@ -794,8 +796,9 @@ defmodule Phoenix.Endpoint do
     paths
   end
 
-  defp socket_path(path, key) do
-    String.split(path, "/", trim: true) ++ [Atom.to_string(key)]
+  defp socket_path(path, config) do
+    end_path_fragment = Keyword.fetch!(config, :path)
+    String.split(path <> "/" <> end_path_fragment, "/", trim: true)
   end
 
   defp socket_config(true, module), do: module.default_config()
@@ -839,6 +842,9 @@ defmodule Phoenix.Endpoint do
   The configuration below can be given to both `:websocket` and
   `:longpoll` keys:
 
+    * `:path` - the path to use for the transport. Will default
+       to the transport name ("/websocket" or "/longpoll")
+
     * `:serializer` - a list of serializers for messages. See
       `Phoenix.Socket` for more information
 
diff --git a/lib/phoenix/endpoint/cowboy_adapter.ex b/lib/phoenix/endpoint/cowboy_adapter.ex
index 117ab060e..0ce4fec10 100644
--- a/lib/phoenix/endpoint/cowboy_adapter.ex
+++ b/lib/phoenix/endpoint/cowboy_adapter.ex
@@ -104,10 +104,11 @@ defmodule Phoenix.Endpoint.CowboyAdapter do
 
     paths =
       if websocket do
-        init = {endpoint, socket, socket_config(websocket, Phoenix.Transports.WebSocket)}
+        config = socket_config(websocket, Phoenix.Transports.WebSocket)
+        init = {endpoint, socket, config}
 
         [
-          {socket_path(path, :websocket), Phoenix.Endpoint.CowboyWebSocket,
+          {socket_path(path, config), Phoenix.Endpoint.CowboyWebSocket,
            {Phoenix.Transports.WebSocket, init}}
           | paths
         ]
@@ -117,10 +118,11 @@ defmodule Phoenix.Endpoint.CowboyAdapter do
 
     paths =
       if longpoll do
-        init = {endpoint, socket, socket_config(longpoll, Phoenix.Transports.LongPoll)}
+        config = socket_config(longpoll, Phoenix.Transports.LongPoll)
+        init = {endpoint, socket, config}
 
         [
-          {socket_path(path, :longpoll), Plug.Adapters.Cowboy.Handler,
+          {socket_path(path, config), Plug.Adapters.Cowboy.Handler,
            {Phoenix.Transports.LongPoll, init}}
           | paths
         ]
@@ -130,9 +132,9 @@ defmodule Phoenix.Endpoint.CowboyAdapter do
 
     paths
   end
-
-  defp socket_path(path, key) do
-    parts = String.split(path, "/", trim: true) ++ [Atom.to_string(key)]
+  defp socket_path(path, config) do
+    end_path_fragment = Keyword.fetch!(config, :path)
+    parts = String.split(path <> "/" <> end_path_fragment, "/", trim: true)
     "/" <> Path.join(parts)
   end
 
diff --git a/lib/phoenix/router/route.ex b/lib/phoenix/router/route.ex
index fcda6bba5..0a8ad59c9 100644
--- a/lib/phoenix/router/route.ex
+++ b/lib/phoenix/router/route.ex
@@ -77,8 +77,8 @@ defmodule Phoenix.Router.Route do
 
   defp build_path_and_binding(%Route{path: path} = route) do
     {params, segments} = case route.kind do
-      :forward -> build_path_match(path <> "/*_forward_path_info")
-      :match   -> build_path_match(path)
+      :forward -> Plug.Router.Utils.build_path_match(path <> "/*_forward_path_info")
+      :match   -> Plug.Router.Utils.build_path_match(path)
     end
 
     binding = for var <- params, var != :_forward_path_info do
@@ -118,7 +118,7 @@ defmodule Phoenix.Router.Route do
   end
 
   defp build_dispatch(%Route{kind: :forward} = route) do
-    {_params, fwd_segments} = build_path_match(route.path)
+    {_params, fwd_segments} = Plug.Router.Utils.build_path_match(route.path)
 
     quote do
       {
@@ -160,7 +160,7 @@ defmodule Phoenix.Router.Route do
   `path` contains a dynamic segment.
   """
   def forward_path_segments(path, plug, phoenix_forwards) do
-    case build_path_match(path) do
+    case Plug.Router.Utils.build_path_match(path) do
       {[], path_segments} ->
         if phoenix_forwards[plug] do
           raise ArgumentError, "#{inspect plug} has already been forwarded to. A module can only be forwarded a single time."
@@ -170,14 +170,4 @@ defmodule Phoenix.Router.Route do
         raise ArgumentError, "dynamic segment \"#{path}\" not allowed when forwarding. Use a static path instead."
     end
   end
-
-  if Code.ensure_loaded?(Plug.Router.Utils) do
-    defp build_path_match(path) do
-      Plug.Router.Utils.build_path_match(path)
-    end
-  else
-    defp build_path_match(path) do
-      Plug.Router.Compiler.build_path_match(path)
-    end
-  end
 end
diff --git a/lib/phoenix/transports/long_poll.ex b/lib/phoenix/transports/long_poll.ex
index 89f0c4769..a2436b0e8 100644
--- a/lib/phoenix/transports/long_poll.ex
+++ b/lib/phoenix/transports/long_poll.ex
@@ -8,6 +8,7 @@ defmodule Phoenix.Transports.LongPoll do
   def default_config() do
     [
       window_ms: 10_000,
+      path: "/longpoll",
       pubsub_timeout_ms: 2_000,
       serializer: [{V1.JSONSerializer, "~> 1.0.0"}, {V2.JSONSerializer, "~> 2.0.0"}],
       transport_log: false,
diff --git a/lib/phoenix/transports/websocket.ex b/lib/phoenix/transports/websocket.ex
index d86baaa04..0e5995259 100644
--- a/lib/phoenix/transports/websocket.ex
+++ b/lib/phoenix/transports/websocket.ex
@@ -4,6 +4,7 @@ defmodule Phoenix.Transports.WebSocket do
 
   def default_config() do
     [
+      path: "/websocket",
       serializer: [{V1.JSONSerializer, "~> 1.0.0"}, {V2.JSONSerializer, "~> 2.0.0"}],
       timeout: 60_000,
       transport_log: false,


Test patch:
diff --git a/guides/testing/testing.md b/guides/testing/testing.md
index 19f477cca..4041246b3 100644
--- a/guides/testing/testing.md
+++ b/guides/testing/testing.md
@@ -466,7 +466,7 @@ defmodule HelloWeb.Router do
   ...
 
   scope "/", Hello do
-    pipe_through :browser # Use the default browser stack
+    pipe_through :browser
 
     get "/", PageController, :index
     resources "/users", UserController
diff --git a/guides/testing/testing_controllers.md b/guides/testing/testing_controllers.md
index 0f5e5ec23..909bd674c 100644
--- a/guides/testing/testing_controllers.md
+++ b/guides/testing/testing_controllers.md
@@ -177,7 +177,7 @@ defmodule HelloWeb.Router do
   end
 
   scope "/", HelloWeb do
-    pipe_through :browser # Use the default browser stack
+    pipe_through :browser
 
     get "/", PageController, :index
   end
diff --git a/guides/testing/testing_schemas.md b/guides/testing/testing_schemas.md
index 2d0f4046e..5159297ba 100644
--- a/guides/testing/testing_schemas.md
+++ b/guides/testing/testing_schemas.md
@@ -497,7 +497,7 @@ defmodule HelloWeb.Router do
   ...
 
   scope "/", HelloWeb do
-    pipe_through :browser # Use the default browser stack
+    pipe_through :browser
 
     get "/", PageController, :index
     resources "/users", UserController
diff --git a/test/phoenix/integration/websocket_socket_test.exs b/test/phoenix/integration/websocket_socket_test.exs
index 7736f220c..1087e9584 100644
--- a/test/phoenix/integration/websocket_socket_test.exs
+++ b/test/phoenix/integration/websocket_socket_test.exs
@@ -63,6 +63,10 @@ defmodule Phoenix.Integration.WebSocketTest do
     socket "/ws", UserSocket,
       websocket: [check_origin: ["//example.com"], timeout: 200],
       custom: :value
+
+    socket "/custom/some_path", UserSocket,
+      websocket: [path: "nested/path", check_origin: ["//example.com"], timeout: 200],
+      custom: :value
   end
 
   setup_all do
@@ -91,4 +95,9 @@ defmodule Phoenix.Integration.WebSocketTest do
     WebsocketClient.send_message(client, "ping")
     assert_receive {:text, "pong"}
   end
+
+  test "allows a custom path" do
+    path = "ws://127.0.0.1:#{@port}/custom/some_path/nested/path"
+    assert {:ok, client} = WebsocketClient.start_link(self(), "#{path}?key=value", :noop)
+  end
 end