Repository: phoenixframework/phoenix
Pull request #2900 (2018-05-29 16:11:55)

Show a better router error message when a non-Enumerable is passed as params
Currently calling a path such as:

  page_page(conn, :show, :id, "extra-params")

Will raise a `Protocol.UndefinedError` as "extra-params" is not enumerable. A
custom error message is now raised in this scenario to prompt the user what to
do next.


Patch:
diff --git a/assets/js/phoenix.js b/assets/js/phoenix.js
index 7e54782f3..4145e8dea 100644
--- a/assets/js/phoenix.js
+++ b/assets/js/phoenix.js
@@ -726,7 +726,7 @@ export class Socket {
     this.heartbeatTimer       = null
     this.pendingHeartbeatRef  = null
     this.reconnectTimer       = new Timer(() => {
-      this.disconnect(() => this.connect())
+      this.teardown(() => this.connect())
     }, this.reconnectAfterMs)
   }
 
@@ -757,12 +757,8 @@ export class Socket {
    * @param {string} reason
    */
   disconnect(callback, code, reason){
-    if(this.conn){
-      this.conn.onclose = function(){} // noop
-      if(code){ this.conn.close(code, reason || "") } else { this.conn.close() }
-      this.conn = null
-    }
-    callback && callback()
+    this.reconnectTimer.reset()
+    this.teardown(callback, code, reason)
   }
 
   /**
@@ -839,11 +835,23 @@ export class Socket {
   /**
    * @private
    */
+
+  teardown(callback, code, reason){
+    if(this.conn){
+      this.conn.onclose = function(){} // noop
+      if(code){ this.conn.close(code, reason || "") } else { this.conn.close() }
+      this.conn = null
+    }
+    callback && callback()
+  }
+
   onConnClose(event){
     this.log("transport", "close", event)
     this.triggerChanError()
     clearInterval(this.heartbeatTimer)
-    this.reconnectTimer.scheduleTimeout()
+    if(event && event.code !== WS_CLOSE_NORMAL) {
+      this.reconnectTimer.scheduleTimeout()
+    }
     this.stateChangeCallbacks.close.forEach( callback => callback(event) )
   }
 
@@ -1333,4 +1341,4 @@ class Timer {
       this.callback()
     }, this.timerCalc(this.tries + 1))
   }
-}
\ No newline at end of file
+}
diff --git a/guides/deployment/deployment.md b/guides/deployment/deployment.md
index 5a78df1b9..c3183d2cc 100644
--- a/guides/deployment/deployment.md
+++ b/guides/deployment/deployment.md
@@ -37,7 +37,6 @@ config :foo, Foo.Endpoint,
 
 # Configure your database
 config :foo, Foo.Repo,
-  adapter: Ecto.Adapters.Postgres,
   username: "postgres",
   password: "postgres",
   database: "foo_prod",
diff --git a/guides/deployment/heroku.md b/guides/deployment/heroku.md
index 924166566..1c57adda4 100644
--- a/guides/deployment/heroku.md
+++ b/guides/deployment/heroku.md
@@ -134,7 +134,6 @@ First, let's make sure our secret key is loaded from Heroku's environment variab
 
 ```elixir
 config :hello, HelloWeb.Endpoint,
-  load_from_system_env: true,
   url: [host: "example.com", port: 80],
   cache_static_manifest: "priv/static/cache_manifest.json",
   secret_key_base: Map.fetch!(System.get_env(), "SECRET_KEY_BASE")
@@ -145,7 +144,6 @@ Then, we'll add the production database configuration to `config/prod.exs`:
 ```elixir
 # Configure your database
 config :hello, Hello.Repo,
-  adapter: Ecto.Adapters.Postgres,
   url: System.get_env("DATABASE_URL"),
   pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
   ssl: true
@@ -184,7 +182,6 @@ use Mix.Config
 ...
 
 config :hello, HelloWeb.Endpoint,
-  load_from_system_env: true,
   url: [scheme: "https", host: "mysterious-meadow-6277.herokuapp.com", port: 443],
   force_ssl: [rewrite_on: [:x_forwarded_proto]],
   cache_static_manifest: "priv/static/cache_manifest.json",
@@ -195,7 +192,6 @@ config :logger, level: :info
 
 # Configure your database
 config :hello, Hello.Repo,
-  adapter: Ecto.Adapters.Postgres,
   url: System.get_env("DATABASE_URL"),
   pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10"),
   ssl: true
diff --git a/guides/ecto.md b/guides/ecto.md
index 8a8011b32..4590a0a5f 100644
--- a/guides/ecto.md
+++ b/guides/ecto.md
@@ -122,19 +122,13 @@ Our `Hello.Repo` module is the foundation we need to work with databases in a Ph
 
 ```elixir
 defmodule Hello.Repo do
-  use Ecto.Repo, otp_app: :hello
-
-  @doc """
-  Dynamically loads the repository url from the
-  DATABASE_URL environment variable.
-  """
-  def init(_, opts) do
-    {:ok, Keyword.put(opts, :url, System.get_env("DATABASE_URL"))}
-  end
+  use Ecto.Repo,
+    otp_app: :hello,
+    adapter: Ecto.Adapter.Postgres
 end
 ```
 
-Our repo has three main tasks - to bring in all the common query functions from `Ecto.Repo`, to set the `otp_app` name equal to our application name, and to initialize the options passed to the database adapter in `init/2`. We'll talk more about how to use the Repo in a bit.
+Our repo has three main tasks - to bring in all the common query functions from `Ecto.Repo`, to set the `otp_app` name equal to our application name, and to configure our database adapter. We'll talk more about how to use the Repo in a bit.
 
 When `phx.new` generated our application, it included some basic repo configuration as well. Let's look at `config/dev.exs`.
 
@@ -142,7 +136,6 @@ When `phx.new` generated our application, it included some basic repo configurat
 ...
 # Configure your database
 config :hello, Hello.Repo,
-  adapter: Ecto.Adapters.Postgres,
   username: "postgres",
   password: "postgres",
   database: "hello_dev",
@@ -493,15 +486,14 @@ Next, we need to configure our new adapter. Let's open up our `config/dev.exs` f
 
 ```
 config :hello_phoenix, HelloPhoenix.Repo,
-adapter: Ecto.Adapters.MySQL,
 username: "root",
 password: "",
 database: "hello_phoenix_dev"
 ```
 
-If we have an existing configuration block for our `HelloPhoenix.Repo`, we can simply change the values to match our new ones. The most important thing is to make sure we are using the MySQL adapter `adapter: Ecto.Adapters.MySQL,`.
+If we have an existing configuration block for our `HelloPhoenix.Repo`, we can simply change the values to match our new ones. We also need to configure the correct values in the `config/test.exs` and `config/prod.secret.exs` files as well.
 
-We also need to configure the correct values in the `config/test.exs` and `config/prod.secret.exs` files as well.
+The last change is to open up `lib/hello_phoenix/repo.ex` and make sure to set the `:adapter` to `Ecto.Adapters.MySQL`.
 
 Now all we need to do is fetch our new dependency, and we'll be ready to go.
 
diff --git a/guides/endpoint.md b/guides/endpoint.md
index c65af5951..24583446b 100644
--- a/guides/endpoint.md
+++ b/guides/endpoint.md
@@ -85,19 +85,6 @@ By default the last plug in the endpoint is the router. The router matches a pat
 
 The endpoint can be customized to add additional plugs, to allow HTTP basic authentication, CORS, subdomain routing and more.
 
-The final thing generated in the endpoint by default is the `init` function. This callback is used for dynamic configuration. The specifics of the dynamic configuration are covered in the `Phoenix.Endpoint` module documentation.
-
-```elixir
-def init(_key, config) do
-  if config[:load_from_system_env] do
-    port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
-    {:ok, Keyword.put(config, :http, [:inet6, port: port])}
-  else
-    {:ok, config}
-  end
-end
-```
-
 Faults in the different parts of the supervision tree, such as the Ecto Repo, will not immediately impact the main application. The supervisor is therefore able to restart those processes separately after unexpected faults. It is also possible for an application to have multiple endpoints, each with its own supervision tree.
 
 There are many functions defined in the endpoint module for path helpers, channel subscriptions and broadcasts, instrumentation, and endpoint configuration. These are all covered in the [Endpoint API docs](Phoenix.Endpoint.html#module-endpoint-api) for `Phoenix.Endpoint`.
diff --git a/guides/phoenix_mix_tasks.md b/guides/phoenix_mix_tasks.md
index 8c59c2551..71af03d98 100644
--- a/guides/phoenix_mix_tasks.md
+++ b/guides/phoenix_mix_tasks.md
@@ -711,7 +711,6 @@ Notice that this task has updated `config/config.exs`. If we take a look, we'll
 ```elixir
 . . .
 config :hello, OurCustom.Repo,
-adapter: Ecto.Adapters.Postgres,
 database: "hello_repo",
 username: "user",
 password: "pass",
diff --git a/guides/presence.md b/guides/presence.md
index d3ebb620c..0f852eb02 100644
--- a/guides/presence.md
+++ b/guides/presence.md
@@ -15,7 +15,7 @@ in lib/hello/application.ex:
 
     children = [
       ...
-      supervisor(HelloWeb.Presence, []),
+      HelloWeb.Presence,
     ]
 
 You're all set! See the Phoenix.Presence docs for more details:
@@ -35,7 +35,7 @@ This sets up the module for presence, defining the functions we require for trac
 ```elixir
 children = [
   # ...
-  supervisor(HelloWeb.Presence, [])
+  HelloWeb.Presence,
 ]
 ```
 
@@ -95,11 +95,11 @@ defmodule HelloWeb.RoomChannel do
 end
 ```
 
-Finally we can use the client-side Presence library provided by Phoenix in order to manage the state and presence diffs that come down the socket. We listen for the initial `presence_state` event fired after joining to get the initial state and the later `presence_diff` events that contain joins and leaves.
+Finally we can use the client-side Presence library included in `phoenix.js` to manage the state and presence diffs that come down the socket. It listens for the `"presence_state"` and `"presence_diff"` events and provides a simple callback for you to handle the events as they happen, with the `onSync` callback.
 
-We can use the included `Presence.syncState()` and `Presence.syncDiff()` methods to easily handle these events and sync our `presences` variable with the latest state. When we want to use the current presence state we can pass it through `Presence.list()` in order to get each presence individually.
+The `onSync` callback allows you to easily react to presence state changes, which most often results in re-rendering an updated list of active users. You can use the `list` method is to format and return each individual presence based on the needs of your application.
 
-When we want to iterate the users, we use the `Presences.list()` function which takes a presences object and a callback function. The callback will be called for each presence item with 2 arguments, the presence id and a list of metas (one for each presence for that presence id). We use this to display the users and the number of devices they are online with.
+To iterate users, we use the `presences.list()` function which accepts a callback. The callback will be called for each presence item with 2 arguments, the presence id and a list of metas (one for each presence for that presence id). We use this to display the users and the number of devices they are online with.
 
 We can see presence working by adding the following to `assets/js/app.js`:
 
@@ -107,13 +107,16 @@ We can see presence working by adding the following to `assets/js/app.js`:
 import {Socket, Presence} from "phoenix"
 
 let socket = new Socket("/socket", {
-  params: { user_id: window.location.search.split("=")[1] }
+  params: {user_id: window.location.search.split("=")[1]}
 })
 
-function renderOnlineUsers(presences) {
+let channel = socket.channel("room:lobby", {})
+let presence = new Presence(channel)
+
+function renderOnlineUsers(presence) {
   let response = ""
 
-  Presence.list(presences, (id, {metas: [first, ...rest]}) => {
+  presence.list((id, {metas: [first, ...rest]}) => {
     let count = rest.length + 1
     response += `<br>${id} (count: ${count})</br>`
   })
@@ -123,19 +126,7 @@ function renderOnlineUsers(presences) {
 
 socket.connect()
 
-let presences = {}
-
-let channel = socket.channel("room:lobby", {})
-
-channel.on("presence_state", state => {
-  presences = Presence.syncState(presences, state)
-  renderOnlineUsers(presences)
-})
-
-channel.on("presence_diff", diff => {
-  presences = Presence.syncDiff(presences, diff)
-  renderOnlineUsers(presences)
-})
+presence.onSync(() => renderOnlineUsers(presence))
 
 channel.join()
 ```
diff --git a/installer/lib/phx_new/generator.ex b/installer/lib/phx_new/generator.ex
index 28240da85..539f2135e 100644
--- a/installer/lib/phx_new/generator.ex
+++ b/installer/lib/phx_new/generator.ex
@@ -153,23 +153,20 @@ defmodule Phx.New.Generator do
     append_to app_path, "config/dev.exs", """
 
     # Configure your database
-    config :#{binding[:app_name]}, #{binding[:app_module]}.Repo,
-      adapter: #{inspect binding[:adapter_module]}#{kw_to_config adapter_config[:dev]},
+    config :#{binding[:app_name]}, #{binding[:app_module]}.Repo#{kw_to_config adapter_config[:dev]},
       pool_size: 10
     """
 
     append_to app_path, "config/test.exs", """
 
     # Configure your database
-    config :#{binding[:app_name]}, #{binding[:app_module]}.Repo,
-      adapter: #{inspect binding[:adapter_module]}#{kw_to_config adapter_config[:test]}
+    config :#{binding[:app_name]}, #{binding[:app_module]}.Repo#{kw_to_config adapter_config[:test]}
     """
 
     append_to app_path, "config/prod.secret.exs", """
 
     # Configure your database
-    config :#{binding[:app_name]}, #{binding[:app_module]}.Repo,
-      adapter: #{inspect binding[:adapter_module]}#{kw_to_config adapter_config[:prod]},
+    config :#{binding[:app_name]}, #{binding[:app_module]}.Repo#{kw_to_config adapter_config[:prod]},
       pool_size: 15
     """
   end
@@ -180,6 +177,7 @@ defmodule Phx.New.Generator do
     |> hd()
     |> Module.concat(PubSub)
   end
+
   defp get_ecto_adapter("mysql", app, module) do
     {:mariaex, Ecto.Adapters.MySQL, db_config(app, module, "root", "")}
   end
diff --git a/installer/templates/phx_ecto/repo.ex b/installer/templates/phx_ecto/repo.ex
index 1e81c5bf1..ccfbb2f45 100644
--- a/installer/templates/phx_ecto/repo.ex
+++ b/installer/templates/phx_ecto/repo.ex
@@ -1,15 +1,5 @@
 defmodule <%= app_module %>.Repo do
-  use Ecto.Repo, otp_app: :<%= app_name %>
-
-  @doc """
-  Dynamically loads the repository url from the
-  DATABASE_URL environment variable.
-  """
-  def init(_, opts) do
-    if url = System.get_env("DATABASE_URL") do
-      {:ok, Keyword.put(opts, :url, url)}
-    else
-      {:ok, opts}
-    end
-  end
+  use Ecto.Repo,
+    otp_app: :<%= app_name %>,
+    adapter: <%= inspect adapter_module %>
 end
diff --git a/installer/templates/phx_single/config/prod.exs b/installer/templates/phx_single/config/prod.exs
index 9a2381b44..7d34284b9 100644
--- a/installer/templates/phx_single/config/prod.exs
+++ b/installer/templates/phx_single/config/prod.exs
@@ -1,20 +1,16 @@
 use Mix.Config
 
-# For production, we often load configuration from external
-# sources, such as your system environment. For this reason,
-# you won't find the :http configuration below, but set inside
-# <%= endpoint_module %>.init/2 when load_from_system_env is
-# true. Any dynamic configuration should be done there.
+# For production, don't forget to configure the url host
+# to something meaningful, Phoenix uses this information
+# when generating URLs.
 #
-# Don't forget to configure the url host to something meaningful,
-# Phoenix uses this information when generating URLs.
-#
-# Finally, we also include the path to a cache manifest
+# Note we also include the path to a cache manifest
 # containing the digested version of static files. This
-# manifest is generated by the mix phx.digest task
-# which you typically run after static files are built.
+# manifest is generated by the `mix phx.digest` task,
+# which you should run after static files are built and
+# before starting your production server.
 config :<%= web_app_name %>, <%= endpoint_module %>,
-  load_from_system_env: true,
+  http: [:inet6, port: System.get_env("PORT") || 4000]
   url: [host: "example.com", port: 80],
   cache_static_manifest: "priv/static/cache_manifest.json"
 
@@ -46,7 +42,7 @@ config :logger, level: :info
 #
 # Check `Plug.SSL` for all available options in `force_ssl`.
 
-# ## Using releases
+# ## Using releases (distillery)
 #
 # If you are doing OTP releases, you need to instruct Phoenix
 # to start the server for all endpoints:
@@ -58,7 +54,9 @@ config :logger, level: :info
 #
 #     config :<%= app_name %>, <%= endpoint_module %>, server: true
 #
+# Note you can't rely on `System.get_env/1` when using releases.
+# See the releases documentation accordingly.
 
-# Finally import the config/prod.secret.exs
-# which should be versioned separately.
+# Finally import the config/prod.secret.exs which should be versioned
+# separately.
 import_config "prod.secret.exs"
diff --git a/installer/templates/phx_umbrella/apps/app_name/config/config.exs b/installer/templates/phx_umbrella/apps/app_name/config/config.exs
index edbcd7074..ff118151a 100644
--- a/installer/templates/phx_umbrella/apps/app_name/config/config.exs
+++ b/installer/templates/phx_umbrella/apps/app_name/config/config.exs
@@ -1,6 +1,9 @@
+# Since configuration is shared in umbrella projects, this file
+# should only configure the :<%= app_name %> application itself
+# and only for organization purposes. All other config goes to
+# the umbrella root.
 use Mix.Config
 
-<%= if ecto do %>config :<%= app_name %>, ecto_repos: [<%= app_module %>.Repo]
-config :ecto, :json_library, Jason<% end %>
+<%= if ecto do %>config :<%= app_name %>, ecto_repos: [<%= app_module %>.Repo]<% end %>
 
 import_config "#{Mix.env}.exs"
diff --git a/installer/templates/phx_umbrella/apps/app_name/config/dev.exs b/installer/templates/phx_umbrella/apps/app_name/config/dev.exs
index d2d855e6d..8cd2bc327 100644
--- a/installer/templates/phx_umbrella/apps/app_name/config/dev.exs
+++ b/installer/templates/phx_umbrella/apps/app_name/config/dev.exs
@@ -1,1 +1,5 @@
+# Since configuration is shared in umbrella projects, this file
+# should only configure the :<%= app_name %> application itself
+# and only for organization purposes. All other config goes to
+# the umbrella root.
 use Mix.Config
diff --git a/installer/templates/phx_umbrella/apps/app_name/config/prod.exs b/installer/templates/phx_umbrella/apps/app_name/config/prod.exs
index c1ca5b903..8873b3767 100644
--- a/installer/templates/phx_umbrella/apps/app_name/config/prod.exs
+++ b/installer/templates/phx_umbrella/apps/app_name/config/prod.exs
@@ -1,3 +1,7 @@
+# Since configuration is shared in umbrella projects, this file
+# should only configure the :<%= app_name %> application itself
+# and only for organization purposes. All other config goes to
+# the umbrella root.
 use Mix.Config
 
 import_config "prod.secret.exs"
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs
index babb75792..5b9533200 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/config.exs
@@ -1,8 +1,7 @@
-# This file is responsible for configuring your application
-# and its dependencies with the aid of the Mix.Config module.
-#
-# This configuration file is loaded before any dependency and
-# is restricted to this project.
+# Since configuration is shared in umbrella projects, this file
+# should only configure the :<%= web_app_name %> application itself
+# and only for organization purposes. All other config goes to
+# the umbrella root.
 use Mix.Config
 
 <%= if namespaced? || ecto || generators do %># General application configuration
@@ -19,9 +18,6 @@ config :<%= web_app_name %>, <%= endpoint_module %>,
   pubsub: [name: <%= web_namespace %>.PubSub,
            adapter: Phoenix.PubSub.PG2]
 
-# Use Jason for JSON parsing in Phoenix
-config :phoenix, :json_library, Jason
-
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
 import_config "#{Mix.env}.exs"
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
index 1c3d31ba4..71ec3c935 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/dev.exs
@@ -1,3 +1,7 @@
+# Since configuration is shared in umbrella projects, this file
+# should only configure the :<%= web_app_name %> application itself
+# and only for organization purposes. All other config goes to
+# the umbrella root.
 use Mix.Config
 
 # For development, we disable any cache and enable
@@ -42,6 +46,3 @@ config :<%= web_app_name %>, <%= endpoint_module %>,
   ]
 
 <% end %>
-
-# Initialize plugs at runtime for faster development compilation
-config :phoenix, :plug_init_mode, :runtime
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
index 51994ecdc..25d2250f6 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/prod.exs
@@ -1,20 +1,20 @@
+# Since configuration is shared in umbrella projects, this file
+# should only configure the :<%= web_app_name %> application itself
+# and only for organization purposes. All other config goes to
+# the umbrella root.
 use Mix.Config
 
-# For production, we often load configuration from external
-# sources, such as your system environment. For this reason,
-# you won't find the :http configuration below, but set inside
-# <%= endpoint_module %>.init/2 when load_from_system_env is
-# true. Any dynamic configuration should be done there.
+# For production, don't forget to configure the url host
+# to something meaningful, Phoenix uses this information
+# when generating URLs.
 #
-# Don't forget to configure the url host to something meaningful,
-# Phoenix uses this information when generating URLs.
-#
-# Finally, we also include the path to a cache manifest
+# Note we also include the path to a cache manifest
 # containing the digested version of static files. This
-# manifest is generated by the mix phx.digest task
-# which you typically run after static files are built.
+# manifest is generated by the `mix phx.digest` task,
+# which you should run after static files are built and
+# before starting your production server.
 config :<%= web_app_name %>, <%= endpoint_module %>,
-  load_from_system_env: true,
+  http: [:inet6, port: System.get_env("PORT") || 4000]
   url: [host: "example.com", port: 80],
   cache_static_manifest: "priv/static/cache_manifest.json"
 
@@ -43,7 +43,7 @@ config :<%= web_app_name %>, <%= endpoint_module %>,
 #
 # Check `Plug.SSL` for all available options in `force_ssl`.
 
-# ## Using releases
+# ## Using releases (distillery)
 #
 # If you are doing OTP releases, you need to instruct Phoenix
 # to start the server for all endpoints:
@@ -53,9 +53,11 @@ config :<%= web_app_name %>, <%= endpoint_module %>,
 # Alternatively, you can configure exactly which server to
 # start per endpoint:
 #
-#     config :<%= web_app_name %>, <%= endpoint_module %>, server: true
+#     config :<%= app_name %>, <%= endpoint_module %>, server: true
 #
+# Note you can't rely on `System.get_env/1` when using releases.
+# See the releases documentation accordingly.
 
-# Finally import the config/prod.secret.exs
-# which should be versioned separately.
+# Finally import the config/prod.secret.exs which should be versioned
+# separately.
 import_config "prod.secret.exs"
diff --git a/installer/templates/phx_umbrella/config/config.exs b/installer/templates/phx_umbrella/config/config.exs
index 8a20addc7..b9bfb3a3e 100644
--- a/installer/templates/phx_umbrella/config/config.exs
+++ b/installer/templates/phx_umbrella/config/config.exs
@@ -3,10 +3,13 @@
 use Mix.Config
 
 # By default, the umbrella project as well as each child
-# application will require this configuration file, ensuring
-# they all use the same configuration. While one could
-# configure all applications here, we prefer to delegate
-# back to each application for organization purposes.
+# application will require this configuration file, as
+# configuration and dependencies are shared in an umbrella
+# project. While one could configure all applications here,
+# we prefer to keep the configuration of each individual
+# child application in their own app, but all other
+# dependencies, regardless if they belong to one or multiple
+# apps, should be configured in the umbrella to avoid confusion.
 import_config "../apps/*/config/config.exs"
 
 # Configures Elixir's Logger
@@ -14,6 +17,9 @@ config :logger, :console,
   format: "$time $metadata[$level] $message\n",
   metadata: [:request_id]
 
+# Use Jason for JSON parsing in Phoenix
+config :phoenix, :json_library, Jason
+
 # Import environment specific config. This must remain at the bottom
 # of this file so it overrides the configuration defined above.
 import_config "#{Mix.env}.exs"
diff --git a/installer/templates/phx_umbrella/config/dev.exs b/installer/templates/phx_umbrella/config/dev.exs
index 8c1edbbdd..b3471eeb1 100644
--- a/installer/templates/phx_umbrella/config/dev.exs
+++ b/installer/templates/phx_umbrella/config/dev.exs
@@ -3,6 +3,9 @@ use Mix.Config
 # Do not include metadata nor timestamps in development logs
 config :logger, :console, format: "[$level] $message\n"
 
+# Initialize plugs at runtime for faster development compilation
+config :phoenix, :plug_init_mode, :runtime
+
 # Set a higher stacktrace during development. Avoid configuring such
 # in production as building large stacktraces may be expensive.
 config :phoenix, :stacktrace_depth, 20
diff --git a/installer/templates/phx_web/endpoint.ex b/installer/templates/phx_web/endpoint.ex
index 836894ad9..7fbf42447 100644
--- a/installer/templates/phx_web/endpoint.ex
+++ b/installer/templates/phx_web/endpoint.ex
@@ -41,19 +41,4 @@ defmodule <%= endpoint_module %> do
     signing_salt: "<%= signing_salt %>"
 
   plug <%= web_namespace %>.Router
-
-  @doc """
-  Callback invoked for dynamically configuring the endpoint.
-
-  It receives the endpoint configuration and checks if
-  configuration should be loaded from the system environment.
-  """
-  def init(_key, config) do
-    if config[:load_from_system_env] do
-      port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
-      {:ok, Keyword.put(config, :http, [:inet6, port: port])}
-    else
-      {:ok, config}
-    end
-  end
 end
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index 63bb144e3..cd2a431ba 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -998,6 +998,8 @@ defmodule Phoenix.Controller do
         context of the application (like critical domain cookies)
       * x-permitted-cross-domain-policies - set to none to restrict
         Adobe Flash Player’s access to data
+      * cross-origin-window-policy - set to deny to avoid window
+        control attacks
 
   A custom headers map may also be given to be merged with defaults.
   """
@@ -1016,7 +1018,8 @@ defmodule Phoenix.Controller do
       {"x-xss-protection", "1; mode=block"},
       {"x-content-type-options", "nosniff"},
       {"x-download-options", "noopen"},
-      {"x-permitted-cross-domain-policies", "none"}
+      {"x-permitted-cross-domain-policies", "none"},
+      {"cross-origin-window-policy", "deny"}
     ])
   end
 
diff --git a/lib/phoenix/endpoint.ex b/lib/phoenix/endpoint.ex
index 528e0e27f..ea698ff63 100644
--- a/lib/phoenix/endpoint.ex
+++ b/lib/phoenix/endpoint.ex
@@ -702,17 +702,6 @@ defmodule Phoenix.Endpoint do
   @doc false
   def __force_ssl__(module, config) do
     if force_ssl = config[:force_ssl] do
-      host = force_ssl[:host] || config[:url][:host] || "localhost"
-
-      if host == "localhost" do
-        Logger.warn """
-        you have enabled :force_ssl but your host is currently set to localhost.
-        Please configure your endpoint url host properly:
-
-            config #{inspect module}, url: [host: "YOURHOST.com"]
-        """
-      end
-
       Keyword.put_new(force_ssl, :host, {module, :host, []})
     end
   end
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index 5de9745f8..d99275786 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -186,10 +186,7 @@ defmodule Phoenix.Router do
   No plug is invoked in case no matches were found.
   """
 
-  alias Phoenix.Router.Resource
-  alias Phoenix.Router.Scope
-  alias Phoenix.Router.Route
-  alias Phoenix.Router.Helpers
+  alias Phoenix.Router.{Resource, Scope, Route, Helpers}
 
   @http_methods [:get, :post, :put, :patch, :delete, :options, :connect, :trace, :head]
 
@@ -269,13 +266,13 @@ defmodule Phoenix.Router do
   def __call__({%Plug.Conn{private: %{phoenix_bypass: :all}} = conn, _pipeline, _dispatch}) do
     conn
   end
-  def __call__({conn, pipeline, dispatch}) do
+  def __call__({conn, pipeline, {plug, opts}}) do
     case pipeline.(conn) do
       %Plug.Conn{halted: true} = halted_conn ->
         halted_conn
       %Plug.Conn{} = piped_conn ->
         try do
-          dispatch.(piped_conn)
+          plug.call(piped_conn, plug.init(opts))
         rescue
           e in Plug.Conn.WrapperError ->
             Plug.Conn.WrapperError.reraise(e)
@@ -324,7 +321,7 @@ defmodule Phoenix.Router do
     routes_with_exprs = Enum.map(routes, &{&1, Route.exprs(&1)})
 
     Helpers.define(env, routes_with_exprs)
-    matches = Enum.map(routes_with_exprs, &build_match/1)
+    {matches, _} = Enum.map_reduce(routes_with_exprs, %{}, &build_match/2)
 
     # @anno is used here to avoid warnings if forwarding to root path
     match_404 =
@@ -343,9 +340,9 @@ defmodule Phoenix.Router do
 
       defp prepare(conn) do
         update_in conn.private,
-          &(&1 |> Map.put(:phoenix_pipelines, [])
-          |> Map.put(:phoenix_router, __MODULE__)
-          |> Map.put(__MODULE__, {conn.script_name, @phoenix_forwards}))
+          &(&1
+            |> Map.put(:phoenix_router, __MODULE__)
+            |> Map.put(__MODULE__, {conn.script_name, @phoenix_forwards}))
       end
 
       unquote(matches)
@@ -353,16 +350,57 @@ defmodule Phoenix.Router do
     end
   end
 
-  defp build_match({route, exprs}) do
-    {conn_block, pipelines, dispatch} = exprs.route_match
+  defp build_match({route, exprs}, known_pipelines) do
+    %{pipe_through: pipe_through} = route
 
-    quote line: route.line do
-      @doc false
-      def __match_route__(var!(conn), unquote(exprs.verb_match), unquote(exprs.path),
-                 unquote(exprs.host)) do
+    %{
+      prepare: prepare,
+      dispatch: dispatch,
+      verb_match: verb_match,
+      path: path,
+      host: host
+    } = exprs
+
+
+    {pipe_name, pipe_definition, known_pipelines} =
+      case known_pipelines do
+        %{^pipe_through => name} ->
+          {name, :ok, known_pipelines}
+
+        %{} ->
+          name = :"__pipe_through#{map_size(known_pipelines)}__"
+          {name, build_pipes(name, pipe_through), Map.put(known_pipelines, pipe_through, name)}
+      end
+
+    quoted =
+      quote line: route.line do
+        unquote(pipe_definition)
+
+        @doc false
+        def __match_route__(var!(conn), unquote(verb_match), unquote(path), unquote(host)) do
+          {unquote(prepare), &unquote(Macro.var(pipe_name, __MODULE__))/1, unquote(dispatch)}
+        end
+      end
 
-        unquote(conn_block)
-        {var!(conn), unquote(pipelines), unquote(dispatch)}
+    {quoted, known_pipelines}
+  end
+
+  defp build_pipes(name, []) do
+    quote do
+      defp unquote(name)(conn) do
+        Plug.Conn.put_private(conn, :phoenix_pipelines, [])
+      end
+    end
+  end
+
+  defp build_pipes(name, pipe_through) do
+    plugs = pipe_through |> Enum.reverse |> Enum.map(&{&1, [], true})
+    {conn, body} = Plug.Builder.compile(__ENV__, plugs, init_mode: Phoenix.plug_init_mode())
+
+    quote do
+      defp unquote(name)(unquote(conn)) do
+        unquote(conn) = Plug.Conn.put_private(unquote(conn), :phoenix_pipelines, unquote(pipe_through))
+        unquote(body)
       end
     end
   end
diff --git a/lib/phoenix/router/helpers.ex b/lib/phoenix/router/helpers.ex
index be789e866..ad0c9b031 100644
--- a/lib/phoenix/router/helpers.ex
+++ b/lib/phoenix/router/helpers.ex
@@ -97,14 +97,15 @@ defmodule Phoenix.Router.Helpers do
   Generates the helper module for the given environment and routes.
   """
   def define(env, routes) do
-    ast = for {route, exprs} <- routes, do: defhelper(route, exprs)
+    # Ignore any route without helper or forwards.
+    routes =
+      Enum.filter(routes, fn {route, _exprs} ->
+        (not is_nil(route.helper) and not (route.kind == :forward))
+      end)
 
-    catch_all =
-      routes
-      |> Enum.filter(fn {route, _exprs} ->
-        (not is_nil(route.helper) and not (route.kind == :forward)) end)
-      |> Enum.group_by(fn {route, _exprs} -> route.helper end)
-      |> Enum.map(&defhelper_catch_all/1)
+    impls = for {route, exprs} <- routes, do: defhelper(route, exprs)
+    groups = Enum.group_by(routes, fn {route, _exprs} -> route.helper end)
+    catch_all = Enum.map(groups, &defhelper_catch_all/1)
 
     # It is in general bad practice to generate large chunks of code
     # inside quoted expressions. However, we can get away with this
@@ -119,9 +120,8 @@ defmodule Phoenix.Router.Helpers do
       @moduledoc """
       Module with named helpers generated from #{inspect unquote(env.module)}.
       """
-      unquote(ast)
-
-      unquote(catch_all)
+      unquote_splicing(impls)
+      unquote_splicing(catch_all)
 
       @doc """
       Generates the connection/endpoint base URL without any path information.
@@ -176,20 +176,36 @@ defmodule Phoenix.Router.Helpers do
       defp to_param(true), do: "true"
       defp to_param(data), do: Phoenix.Param.to_param(data)
 
-      defp segments(segments, [], _reserved) do
+      defp segments!(segments, [], _reserved, _opts) do
         segments
       end
 
-      defp segments(segments, query, reserved) do
+      defp segments!(segments, query, reserved, _opts) when is_list(query) or is_map(query) do
         dict = for {k, v} <- query,
                not ((k = to_string(k)) in reserved),
                do: {k, v}
 
+
         case Conn.Query.encode dict, &to_param/1 do
           "" -> segments
           o  -> segments <> "?" <> o
         end
       end
+
+      defp segments!(segments, query, reserved, {helper, opts, call_vars}) do
+        arity = length(call_vars) + 3
+
+        raise ArgumentError, """
+        #{__MODULE__}.#{helper}_path/#{arity} called with invalid params.
+        The last argument to this function should be a keyword list or a map.
+        For example:
+
+            Router.#{helper}_path(#{Enum.join(["conn", ":#{opts}" | call_vars], ", ")}, page: 5, per_page: 10)
+
+        It is possible you have called this function without defining the proper
+        number of path segments in your router.
+        """
+      end
     end
 
     Module.create(Module.concat(env.module, Helpers), code,
@@ -207,8 +223,6 @@ defmodule Phoenix.Router.Helpers do
 
   In case a helper name was not given, or route is forwarded, returns nil.
   """
-  def defhelper(%Route{helper: nil}, _exprs), do: nil
-  def defhelper(%Route{kind: :forward}, _exprs), do: nil
   def defhelper(%Route{} = route, exprs) do
     helper = route.helper
     opts = route.opts
@@ -223,7 +237,8 @@ defmodule Phoenix.Router.Helpers do
       end
 
       def unquote(:"#{helper}_path")(conn_or_endpoint, unquote(opts), unquote_splicing(vars), params) do
-        path(conn_or_endpoint, segments(unquote(segs), params, unquote(bins)))
+        path(conn_or_endpoint, segments!(unquote(segs), params, unquote(bins),
+              {unquote(helper), unquote(opts), unquote(Enum.map(vars, &Macro.to_string/1))}))
       end
 
       def unquote(:"#{helper}_url")(conn_or_endpoint, unquote(opts), unquote_splicing(vars)) do
@@ -289,15 +304,15 @@ defmodule Phoenix.Router.Helpers do
   def raise_route_error(mod, fun, arity, action, routes) do
     prelude =
       if Keyword.has_key?(routes, action) do
-        "No action #{inspect action} for helper #{inspect mod}.#{fun}/#{arity}"
+        "no action #{inspect action} for helper #{inspect mod}.#{fun}/#{arity}"
       else
-        "No function clause for #{inspect mod}.#{fun}/#{arity} and action #{inspect action}"
+        "no function clause for #{inspect mod}.#{fun}/#{arity} and action #{inspect action}"
       end
 
     suggestions =
       for {action, bindings} <- routes do
-        bindings = Enum.join(bindings, ", ")
-        "\n    #{fun}(conn_or_endpoint, #{inspect action}, #{bindings}, opts \\\\ [])"
+        bindings = Enum.join([inspect(action) | bindings], ", ")
+        "\n    #{fun}(conn_or_endpoint, #{bindings}, params \\\\ [])"
       end
 
     raise ArgumentError, "#{prelude}. The following actions/clauses are supported:\n#{suggestions}"
@@ -309,8 +324,11 @@ defmodule Phoenix.Router.Helpers do
   def encode_param(str), do: URI.encode(str, &URI.char_unreserved?/1)
 
   defp expand_segments([]), do: "/"
-  defp expand_segments(segments) when is_list(segments),
-    do: expand_segments(segments, "")
+
+  defp expand_segments(segments) when is_list(segments) do
+    expand_segments(segments, "")
+  end
+
   defp expand_segments(segments) do
     quote(do: "/" <> Enum.map_join(unquote(segments), "/", &unquote(__MODULE__).encode_param/1))
   end
@@ -320,8 +338,10 @@ defmodule Phoenix.Router.Helpers do
 
   defp expand_segments([h|t], acc) when is_binary(h),
     do: expand_segments(t, quote(do: unquote(acc) <> unquote("/" <> h)))
+
   defp expand_segments([h|t], acc),
     do: expand_segments(t, quote(do: unquote(acc) <> "/" <> URI.encode(to_param(unquote(h)), &URI.char_unreserved?/1)))
+
   defp expand_segments([], acc),
     do: acc
 end
diff --git a/lib/phoenix/router/route.ex b/lib/phoenix/router/route.ex
index 8c5dab53b..d78636ee8 100644
--- a/lib/phoenix/router/route.ex
+++ b/lib/phoenix/router/route.ex
@@ -28,6 +28,18 @@ defmodule Phoenix.Router.Route do
 
   @type t :: %Route{}
 
+  @doc "Used as a plug on forwarding"
+  def init(opts), do: opts
+
+  @doc "Used as a plug on forwarding"
+  def call(%{path_info: path, script_name: script} = conn, {fwd_segments, plug, opts}) do
+    new_path = path -- fwd_segments
+    {base, ^new_path} = Enum.split(path, length(path) - length(new_path))
+    conn = %{conn | path_info: new_path, script_name: script ++ base}
+    conn = plug.call(conn, plug.init(opts))
+    %{conn | path_info: path, script_name: script}
+  end
+
   @doc """
   Receives the verb, path, plug, options and helper
   and returns a `Phoenix.Router.Route` struct.
@@ -45,16 +57,19 @@ defmodule Phoenix.Router.Route do
   end
 
   @doc """
-  Builds the expressions used by the route.
+  Builds the compiled expressions used by the route.
   """
   def exprs(route) do
     {path, binding} = build_path_and_binding(route)
 
-    %{path: path,
+    %{
+      path: path,
       host: build_host(route.host),
       verb_match: verb_match(route.verb),
       binding: binding,
-      route_match: build_route_match(route, binding)}
+      prepare: build_prepare(route, binding),
+      dispatch: build_dispatch(route)
+    }
   end
 
   defp verb_match(:*), do: Macro.var(:_verb, nil)
@@ -81,89 +96,58 @@ defmodule Phoenix.Router.Route do
     end
   end
 
-  defp build_route_match(route, binding) do
-    dispatch_block = build_dispatch(route)
-    pipes_block = build_pipes(route)
-    exprs =
-      [build_params(binding),
-       maybe_merge(:private, route.private),
-       maybe_merge(:assigns, route.assigns)]
+  defp build_prepare(route, binding) do
+    {static_data, match_params, merge_params} = build_params(binding)
+    {match_private, merge_private} = build_prepare_expr(:private, route.private)
+    {match_assigns, merge_assigns} = build_prepare_expr(:assigns, route.assigns)
 
-    conn_block = {:__block__, [], Enum.filter(exprs, & &1 != nil)}
+    match_all = match_params ++ match_private ++ match_assigns
+    merge_all = merge_params ++ merge_private ++ merge_assigns
 
-    {conn_block, pipes_block, dispatch_block}
+    if merge_all != [] do
+      quote do
+        unquote_splicing(static_data)
+        %{unquote_splicing(match_all)} = var!(conn)
+        %{var!(conn) | unquote_splicing(merge_all)}
+      end
+    else
+      quote do
+        var!(conn)
+      end
+    end
   end
 
   defp build_dispatch(%Route{kind: :forward} = route) do
     {_params, fwd_segments} = Plug.Router.Utils.build_path_match(route.path)
-    opts = route.opts |> route.plug.init() |> Macro.escape()
 
     quote do
-      fn conn ->
-        Phoenix.Router.Route.forward(conn, unquote(fwd_segments), unquote(route.plug), unquote(opts))
-      end
+      {Phoenix.Router.Route, {unquote(fwd_segments), unquote(route.plug), unquote(route.opts)}}
     end
   end
+
   defp build_dispatch(%Route{} = route) do
     quote do
-      fn conn ->
-        # We need to store this in a variable so the compiler
-        # does not see a call and then suddenly start tracking
-        # changes in the controller.
-        plug = unquote(route.plug)
-        opts = plug.init(unquote(route.opts))
-        plug.call(conn, opts)
-      end
+      {unquote(route.plug), unquote(route.opts)}
     end
   end
 
-  defp maybe_merge(key, data) do
-    if map_size(data) > 0 do
-      quote do
-        var!(conn) =
-          update_in var!(conn).unquote(key), &Map.merge(&1, unquote(Macro.escape(data)))
-      end
-    end
+  defp build_prepare_expr(_key, data) when data == %{}, do: {[], []}
+  defp build_prepare_expr(key, data) do
+    var = Macro.var(key, :conn)
+    merge = quote(do: Map.merge(unquote(var), unquote(Macro.escape(data))))
+    {[{key, var}], [{key, merge}]}
   end
 
-  defp build_params([]), do: nil
+  defp build_params([]), do: {[], [], []}
   defp build_params(binding) do
-    quote do
-      path_binding = unquote({:%{}, [], binding})
-      var!(conn) =
-        %Plug.Conn{var!(conn) |
-                   params: Map.merge(var!(conn).params, path_binding),
-                   path_params: path_binding}
-    end
-  end
-
-  defp build_pipes(%Route{pipe_through: []}) do
-    quote do: fn conn -> conn end
-  end
-  defp build_pipes(%Route{pipe_through: pipe_through}) do
-    plugs = pipe_through |> Enum.reverse |> Enum.map(&{&1, [], true})
-
-    {conn, body} = Plug.Builder.compile(__ENV__, plugs,
-      init_mode: Phoenix.plug_init_mode())
-
-    quote do
-      fn unquote(conn) ->
-        unquote(conn) = Plug.Conn.put_private(unquote(conn), :phoenix_pipelines, unquote(pipe_through))
-        unquote(body)
-      end
-    end
-  end
-
-
-  @doc """
-  Forwards requests to another Plug at a new path.
-  """
-  def forward(%Plug.Conn{path_info: path, script_name: script} = conn, fwd_segments, target, opts) do
-    new_path = path -- fwd_segments
-    {base, ^new_path} = Enum.split(path, length(path) - length(new_path))
-
-    conn = %Plug.Conn{conn | path_info: new_path, script_name: script ++ base} |> target.call(opts)
-    %Plug.Conn{conn | path_info: path, script_name: script}
+    params = Macro.var(:params, :conn)
+    path_params = Macro.var(:path_params, :conn)
+    merge_params = quote(do: Map.merge(unquote(params), unquote(path_params)))
+    {
+      [quote(do: unquote(path_params) = %{unquote_splicing(binding)})],
+      [{:params, params}],
+      [{:params, merge_params}, {:path_params, path_params}]
+    }
   end
 
   @doc """
diff --git a/lib/phoenix/socket/pool_supervisor.ex b/lib/phoenix/socket/pool_supervisor.ex
index 074fdec1d..8be5660df 100644
--- a/lib/phoenix/socket/pool_supervisor.ex
+++ b/lib/phoenix/socket/pool_supervisor.ex
@@ -17,7 +17,7 @@ defmodule Phoenix.Socket.PoolSupervisor do
     import Supervisor.Spec
 
     ref = :ets.new(name, [:named_table, :public, read_concurrency: true])
-    :ets.insert(ref, {:partitions, partitions - 1})
+    :ets.insert(ref, {:partitions, partitions})
 
     children =
       for i <- 0..(partitions - 1) do
diff --git a/priv/static/phoenix.js b/priv/static/phoenix.js
index dd40f3937..1b2805b14 100644
--- a/priv/static/phoenix.js
+++ b/priv/static/phoenix.js
@@ -1,1 +1,1 @@
-!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Phoenix=t():e.Phoenix=t()}(window,function(){return function(e){var t={};function s(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,s),i.l=!0,i.exports}return s.m=e,s.c=t,s.d=function(e,t,n){s.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:n})},s.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},s.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return s.d(t,"a",t),t},s.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},s.p="",s(s.s=2)}([function(e,t,s){"use strict";s.r(t),s.d(t,"Channel",function(){return d}),s.d(t,"Socket",function(){return g}),s.d(t,"LongPoll",function(){return v}),s.d(t,"Ajax",function(){return j}),s.d(t,"Presence",function(){return b});const n="undefined"!=typeof self?self:window,i="2.0.0",o={connecting:0,open:1,closing:2,closed:3},r=1e4,h=1e3,a={closed:"closed",errored:"errored",joined:"joined",joining:"joining",leaving:"leaving"},c={close:"phx_close",error:"phx_error",join:"phx_join",reply:"phx_reply",leave:"phx_leave"},l=[c.close,c.error,c.join,c.reply,c.leave],u={longpoll:"longpoll",websocket:"websocket"};let f=e=>{if("function"==typeof e)return e;return function(){return e}};class p{constructor(e,t,s,n){this.channel=e,this.event=t,this.payload=s||function(){return{}},this.receivedResp=null,this.timeout=n,this.timeoutTimer=null,this.recHooks=[],this.sent=!1}resend(e){this.timeout=e,this.reset(),this.send()}send(){this.hasReceived("timeout")||(this.startTimeout(),this.sent=!0,this.channel.socket.push({topic:this.channel.topic,event:this.event,payload:this.payload(),ref:this.ref,join_ref:this.channel.joinRef()}))}receive(e,t){return this.hasReceived(e)&&t(this.receivedResp.response),this.recHooks.push({status:e,callback:t}),this}reset(){this.cancelRefEvent(),this.ref=null,this.refEvent=null,this.receivedResp=null,this.sent=!1}matchReceive({status:e,response:t,ref:s}){this.recHooks.filter(t=>t.status===e).forEach(e=>e.callback(t))}cancelRefEvent(){this.refEvent&&this.channel.off(this.refEvent)}cancelTimeout(){clearTimeout(this.timeoutTimer),this.timeoutTimer=null}startTimeout(){this.timeoutTimer&&this.cancelTimeout(),this.ref=this.channel.socket.makeRef(),this.refEvent=this.channel.replyEventName(this.ref),this.channel.on(this.refEvent,e=>{this.cancelRefEvent(),this.cancelTimeout(),this.receivedResp=e,this.matchReceive(e)}),this.timeoutTimer=setTimeout(()=>{this.trigger("timeout",{})},this.timeout)}hasReceived(e){return this.receivedResp&&this.receivedResp.status===e}trigger(e,t){this.channel.trigger(this.refEvent,{status:e,response:t})}}class d{constructor(e,t,s){this.state=a.closed,this.topic=e,this.params=f(t||{}),this.socket=s,this.bindings=[],this.bindingRef=0,this.timeout=this.socket.timeout,this.joinedOnce=!1,this.joinPush=new p(this,c.join,this.params,this.timeout),this.pushBuffer=[],this.rejoinTimer=new y(()=>this.rejoinUntilConnected(),this.socket.reconnectAfterMs),this.joinPush.receive("ok",()=>{this.state=a.joined,this.rejoinTimer.reset(),this.pushBuffer.forEach(e=>e.send()),this.pushBuffer=[]}),this.onClose(()=>{this.rejoinTimer.reset(),this.socket.log("channel",`close ${this.topic} ${this.joinRef()}`),this.state=a.closed,this.socket.remove(this)}),this.onError(e=>{this.isLeaving()||this.isClosed()||(this.socket.log("channel",`error ${this.topic}`,e),this.state=a.errored,this.rejoinTimer.scheduleTimeout())}),this.joinPush.receive("timeout",()=>{if(!this.isJoining())return;this.socket.log("channel",`timeout ${this.topic} (${this.joinRef()})`,this.joinPush.timeout),new p(this,c.leave,f({}),this.timeout).send(),this.state=a.errored,this.joinPush.reset(),this.rejoinTimer.scheduleTimeout()}),this.on(c.reply,(e,t)=>{this.trigger(this.replyEventName(t),e)})}rejoinUntilConnected(){this.rejoinTimer.scheduleTimeout(),this.socket.isConnected()&&this.rejoin()}join(e=this.timeout){if(this.joinedOnce)throw"tried to join multiple times. 'join' can only be called a single time per channel instance";return this.joinedOnce=!0,this.rejoin(e),this.joinPush}onClose(e){this.on(c.close,e)}onError(e){return this.on(c.error,t=>e(t))}on(e,t){let s=this.bindingRef++;return this.bindings.push({event:e,ref:s,callback:t}),s}off(e,t){this.bindings=this.bindings.filter(s=>!(s.event===e&&(void 0===t||t===s.ref)))}canPush(){return this.socket.isConnected()&&this.isJoined()}push(e,t,s=this.timeout){if(!this.joinedOnce)throw`tried to push '${e}' to '${this.topic}' before joining. Use channel.join() before pushing events`;let n=new p(this,e,function(){return t},s);return this.canPush()?n.send():(n.startTimeout(),this.pushBuffer.push(n)),n}leave(e=this.timeout){this.state=a.leaving;let t=()=>{this.socket.log("channel",`leave ${this.topic}`),this.trigger(c.close,"leave")},s=new p(this,c.leave,f({}),e);return s.receive("ok",()=>t()).receive("timeout",()=>t()),s.send(),this.canPush()||s.trigger("ok",{}),s}onMessage(e,t,s){return t}isMember(e,t,s,n){if(this.topic!==e)return!1;let i=l.indexOf(t)>=0;return!n||!i||n===this.joinRef()||(this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:s,joinRef:n}),!1)}joinRef(){return this.joinPush.ref}sendJoin(e){this.state=a.joining,this.joinPush.resend(e)}rejoin(e=this.timeout){this.isLeaving()||this.sendJoin(e)}trigger(e,t,s,n){let i=this.onMessage(e,t,s,n);if(t&&!i)throw"channel onMessage callbacks must return the payload, modified or unmodified";this.bindings.filter(t=>t.event===e).map(e=>e.callback(i,s,n||this.joinRef()))}replyEventName(e){return`chan_reply_${e}`}isClosed(){return this.state===a.closed}isErrored(){return this.state===a.errored}isJoined(){return this.state===a.joined}isJoining(){return this.state===a.joining}isLeaving(){return this.state===a.leaving}}const m={encode(e,t){let s=[e.join_ref,e.ref,e.topic,e.event,e.payload];return t(JSON.stringify(s))},decode(e,t){let[s,n,i,o,r]=JSON.parse(e);return t({join_ref:s,ref:n,topic:i,event:o,payload:r})}};class g{constructor(e,t={}){this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.timeout=t.timeout||r,this.transport=t.transport||n.WebSocket||v,this.defaultEncoder=m.encode,this.defaultDecoder=m.decode,this.transport!==v?(this.encode=t.encode||this.defaultEncoder,this.decode=t.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder),this.heartbeatIntervalMs=t.heartbeatIntervalMs||3e4,this.reconnectAfterMs=t.reconnectAfterMs||function(e){return[1e3,2e3,5e3,1e4][e-1]||1e4},this.logger=t.logger||function(){},this.longpollerTimeout=t.longpollerTimeout||2e4,this.params=f(t.params||{}),this.endPoint=`${e}/${u.websocket}`,this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new y(()=>{this.disconnect(()=>this.connect())},this.reconnectAfterMs)}protocol(){return location.protocol.match(/^https/)?"wss":"ws"}endPointURL(){let e=j.appendParams(j.appendParams(this.endPoint,this.params()),{vsn:i});return"/"!==e.charAt(0)?e:"/"===e.charAt(1)?`${this.protocol()}:${e}`:`${this.protocol()}://${location.host}${e}`}disconnect(e,t,s){this.conn&&(this.conn.onclose=function(){},t?this.conn.close(t,s||""):this.conn.close(),this.conn=null),e&&e()}connect(e){e&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=f(e)),this.conn||(this.conn=new this.transport(this.endPointURL()),this.conn.timeout=this.longpollerTimeout,this.conn.onopen=(()=>this.onConnOpen()),this.conn.onerror=(e=>this.onConnError(e)),this.conn.onmessage=(e=>this.onConnMessage(e)),this.conn.onclose=(e=>this.onConnClose(e)))}log(e,t,s){this.logger(e,t,s)}onOpen(e){this.stateChangeCallbacks.open.push(e)}onClose(e){this.stateChangeCallbacks.close.push(e)}onError(e){this.stateChangeCallbacks.error.push(e)}onMessage(e){this.stateChangeCallbacks.message.push(e)}onConnOpen(){this.log("transport",`connected to ${this.endPointURL()}`),this.flushSendBuffer(),this.reconnectTimer.reset(),this.conn.skipHeartbeat||(clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval(()=>this.sendHeartbeat(),this.heartbeatIntervalMs)),this.stateChangeCallbacks.open.forEach(e=>e())}onConnClose(e){this.log("transport","close",e),this.triggerChanError(),clearInterval(this.heartbeatTimer),this.reconnectTimer.scheduleTimeout(),this.stateChangeCallbacks.close.forEach(t=>t(e))}onConnError(e){this.log("transport",e),this.triggerChanError(),this.stateChangeCallbacks.error.forEach(t=>t(e))}triggerChanError(){this.channels.forEach(e=>e.trigger(c.error))}connectionState(){switch(this.conn&&this.conn.readyState){case o.connecting:return"connecting";case o.open:return"open";case o.closing:return"closing";default:return"closed"}}isConnected(){return"open"===this.connectionState()}remove(e){this.channels=this.channels.filter(t=>t.joinRef()!==e.joinRef())}channel(e,t={}){let s=new d(e,t,this);return this.channels.push(s),s}push(e){let{topic:t,event:s,payload:n,ref:i,join_ref:o}=e,r=()=>{this.encode(e,e=>{this.conn.send(e)})};this.log("push",`${t} ${s} (${o}, ${i})`,n),this.isConnected()?r():this.sendBuffer.push(r)}makeRef(){let e=this.ref+1;return e===this.ref?this.ref=0:this.ref=e,this.ref.toString()}sendHeartbeat(){if(this.isConnected()){if(this.pendingHeartbeatRef)return this.pendingHeartbeatRef=null,this.log("transport","heartbeat timeout. Attempting to re-establish connection"),void this.conn.close(h,"hearbeat timeout");this.pendingHeartbeatRef=this.makeRef(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef})}}flushSendBuffer(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach(e=>e()),this.sendBuffer=[])}onConnMessage(e){this.decode(e.data,e=>{let{topic:t,event:s,payload:n,ref:i,join_ref:o}=e;i&&i===this.pendingHeartbeatRef&&(this.pendingHeartbeatRef=null),this.log("receive",`${n.status||""} ${t} ${s} ${i&&"("+i+")"||""}`,n),this.channels.filter(e=>e.isMember(t,s,n,o)).forEach(e=>e.trigger(s,n,i,o)),this.stateChangeCallbacks.message.forEach(t=>t(e))})}}class v{constructor(e){this.endPoint=null,this.token=null,this.skipHeartbeat=!0,this.onopen=function(){},this.onerror=function(){},this.onmessage=function(){},this.onclose=function(){},this.pollEndpoint=this.normalizeEndpoint(e),this.readyState=o.connecting,this.poll()}normalizeEndpoint(e){return e.replace("ws://","http://").replace("wss://","https://").replace(new RegExp("(.*)/"+u.websocket),"$1/"+u.longpoll)}endpointURL(){return j.appendParams(this.pollEndpoint,{token:this.token})}closeAndRetry(){this.close(),this.readyState=o.connecting}ontimeout(){this.onerror("timeout"),this.closeAndRetry()}poll(){this.readyState!==o.open&&this.readyState!==o.connecting||j.request("GET",this.endpointURL(),"application/json",null,this.timeout,this.ontimeout.bind(this),e=>{if(e){var{status:t,token:s,messages:n}=e;this.token=s}else var t=0;switch(t){case 200:n.forEach(e=>this.onmessage({data:e})),this.poll();break;case 204:this.poll();break;case 410:this.readyState=o.open,this.onopen(),this.poll();break;case 0:case 500:this.onerror(),this.closeAndRetry();break;default:throw`unhandled poll status ${t}`}})}send(e){j.request("POST",this.endpointURL(),"application/json",e,this.timeout,this.onerror.bind(this,"timeout"),e=>{e&&200===e.status||(this.onerror(e&&e.status),this.closeAndRetry())})}close(e,t){this.readyState=o.closed,this.onclose()}}class j{static request(e,t,s,i,o,r,h){if(n.XDomainRequest){let s=new XDomainRequest;this.xdomainRequest(s,e,t,i,o,r,h)}else{let a=n.XMLHttpRequest?new n.XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP");this.xhrRequest(a,e,t,s,i,o,r,h)}}static xdomainRequest(e,t,s,n,i,o,r){e.timeout=i,e.open(t,s),e.onload=(()=>{let t=this.parseJSON(e.responseText);r&&r(t)}),o&&(e.ontimeout=o),e.onprogress=(()=>{}),e.send(n)}static xhrRequest(e,t,s,n,i,o,r,h){e.open(t,s,!0),e.timeout=o,e.setRequestHeader("Content-Type",n),e.onerror=(()=>{h&&h(null)}),e.onreadystatechange=(()=>{if(e.readyState===this.states.complete&&h){let t=this.parseJSON(e.responseText);h(t)}}),r&&(e.ontimeout=r),e.send(i)}static parseJSON(e){if(!e||""===e)return null;try{return JSON.parse(e)}catch(t){return console&&console.log("failed to parse JSON response",e),null}}static serialize(e,t){let s=[];for(var n in e){if(!e.hasOwnProperty(n))continue;let i=t?`${t}[${n}]`:n,o=e[n];"object"==typeof o?s.push(this.serialize(o,i)):s.push(encodeURIComponent(i)+"="+encodeURIComponent(o))}return s.join("&")}static appendParams(e,t){if(0===Object.keys(t).length)return e;return`${e}${e.match(/\?/)?"&":"?"}${this.serialize(t)}`}}j.states={complete:4};class b{constructor(e,t={}){let s=t.events||{state:"presence_state",diff:"presence_diff"};this.state={},this.pendingDiffs=[],this.channel=e,this.joinRef=null,this.caller={onJoin:function(){},onLeave:function(){},onSync:function(){}},this.channel.on(s.state,e=>{let{onJoin:t,onLeave:s,onSync:n}=this.caller;this.joinRef=this.channel.joinRef(),this.state=b.syncState(this.state,e,t,s),this.pendingDiffs.forEach(e=>{this.state=b.syncDiff(this.state,e,t,s)}),this.pendingDiffs=[],n()}),this.channel.on(s.diff,e=>{let{onJoin:t,onLeave:s,onSync:n}=this.caller;this.inPendingSyncState()?this.pendingDiffs.push(e):(this.state=b.syncDiff(this.state,e,t,s),n())})}onJoin(e){this.caller.onJoin=e}onLeave(e){this.caller.onLeave=e}onSync(e){this.caller.onSync=e}list(e){return b.list(this.state,e)}inPendingSyncState(){return!this.joinRef||this.joinRef!==this.channel.joinRef()}static syncState(e,t,s,n){let i=this.clone(e),o={},r={};return this.map(i,(e,s)=>{t[e]||(r[e]=s)}),this.map(t,(e,t)=>{let s=i[e];if(s){let n=t.metas.map(e=>e.phx_ref),i=s.metas.map(e=>e.phx_ref),h=t.metas.filter(e=>i.indexOf(e.phx_ref)<0),a=s.metas.filter(e=>n.indexOf(e.phx_ref)<0);h.length>0&&(o[e]=t,o[e].metas=h),a.length>0&&(r[e]=this.clone(s),r[e].metas=a)}else o[e]=t}),this.syncDiff(i,{joins:o,leaves:r},s,n)}static syncDiff(e,{joins:t,leaves:s},n,i){let o=this.clone(e);return n||(n=function(){}),i||(i=function(){}),this.map(t,(e,t)=>{let s=o[e];if(o[e]=t,s){let t=o[e].metas.map(e=>e.phx_ref),n=s.metas.filter(e=>t.indexOf(e.phx_ref)<0);o[e].metas.unshift(...n)}n(e,s,t)}),this.map(s,(e,t)=>{let s=o[e];if(!s)return;let n=t.metas.map(e=>e.phx_ref);s.metas=s.metas.filter(e=>n.indexOf(e.phx_ref)<0),i(e,s,t),0===s.metas.length&&delete o[e]}),o}static list(e,t){return t||(t=function(e,t){return t}),this.map(e,(e,s)=>t(e,s))}static map(e,t){return Object.getOwnPropertyNames(e).map(s=>t(s,e[s]))}static clone(e){return JSON.parse(JSON.stringify(e))}}class y{constructor(e,t){this.callback=e,this.timerCalc=t,this.timer=null,this.tries=0}reset(){this.tries=0,clearTimeout(this.timer)}scheduleTimeout(){clearTimeout(this.timer),this.timer=setTimeout(()=>{this.tries=this.tries+1,this.callback()},this.timerCalc(this.tries+1))}}},function(e,t){var s;s=function(){return this}();try{s=s||Function("return this")()||(0,eval)("this")}catch(e){"object"==typeof window&&(s=window)}e.exports=s},function(e,t,s){"use strict";(function(t){e.exports=t.Phoenix=s(0)}).call(this,s(1))}])});
\ No newline at end of file
+!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.Phoenix=t():e.Phoenix=t()}(window,function(){return function(e){var t={};function s(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,s),i.l=!0,i.exports}return s.m=e,s.c=t,s.d=function(e,t,n){s.o(e,t)||Object.defineProperty(e,t,{configurable:!1,enumerable:!0,get:n})},s.r=function(e){Object.defineProperty(e,"__esModule",{value:!0})},s.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return s.d(t,"a",t),t},s.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},s.p="",s(s.s=2)}([function(e,t,s){"use strict";s.r(t),s.d(t,"Channel",function(){return d}),s.d(t,"Socket",function(){return g}),s.d(t,"LongPoll",function(){return v}),s.d(t,"Ajax",function(){return j}),s.d(t,"Presence",function(){return b});const n="undefined"!=typeof self?self:window,i="2.0.0",o={connecting:0,open:1,closing:2,closed:3},r=1e4,h=1e3,a={closed:"closed",errored:"errored",joined:"joined",joining:"joining",leaving:"leaving"},c={close:"phx_close",error:"phx_error",join:"phx_join",reply:"phx_reply",leave:"phx_leave"},l=[c.close,c.error,c.join,c.reply,c.leave],u={longpoll:"longpoll",websocket:"websocket"};let f=e=>{if("function"==typeof e)return e;return function(){return e}};class p{constructor(e,t,s,n){this.channel=e,this.event=t,this.payload=s||function(){return{}},this.receivedResp=null,this.timeout=n,this.timeoutTimer=null,this.recHooks=[],this.sent=!1}resend(e){this.timeout=e,this.reset(),this.send()}send(){this.hasReceived("timeout")||(this.startTimeout(),this.sent=!0,this.channel.socket.push({topic:this.channel.topic,event:this.event,payload:this.payload(),ref:this.ref,join_ref:this.channel.joinRef()}))}receive(e,t){return this.hasReceived(e)&&t(this.receivedResp.response),this.recHooks.push({status:e,callback:t}),this}reset(){this.cancelRefEvent(),this.ref=null,this.refEvent=null,this.receivedResp=null,this.sent=!1}matchReceive({status:e,response:t,ref:s}){this.recHooks.filter(t=>t.status===e).forEach(e=>e.callback(t))}cancelRefEvent(){this.refEvent&&this.channel.off(this.refEvent)}cancelTimeout(){clearTimeout(this.timeoutTimer),this.timeoutTimer=null}startTimeout(){this.timeoutTimer&&this.cancelTimeout(),this.ref=this.channel.socket.makeRef(),this.refEvent=this.channel.replyEventName(this.ref),this.channel.on(this.refEvent,e=>{this.cancelRefEvent(),this.cancelTimeout(),this.receivedResp=e,this.matchReceive(e)}),this.timeoutTimer=setTimeout(()=>{this.trigger("timeout",{})},this.timeout)}hasReceived(e){return this.receivedResp&&this.receivedResp.status===e}trigger(e,t){this.channel.trigger(this.refEvent,{status:e,response:t})}}class d{constructor(e,t,s){this.state=a.closed,this.topic=e,this.params=f(t||{}),this.socket=s,this.bindings=[],this.bindingRef=0,this.timeout=this.socket.timeout,this.joinedOnce=!1,this.joinPush=new p(this,c.join,this.params,this.timeout),this.pushBuffer=[],this.rejoinTimer=new y(()=>this.rejoinUntilConnected(),this.socket.reconnectAfterMs),this.joinPush.receive("ok",()=>{this.state=a.joined,this.rejoinTimer.reset(),this.pushBuffer.forEach(e=>e.send()),this.pushBuffer=[]}),this.onClose(()=>{this.rejoinTimer.reset(),this.socket.log("channel",`close ${this.topic} ${this.joinRef()}`),this.state=a.closed,this.socket.remove(this)}),this.onError(e=>{this.isLeaving()||this.isClosed()||(this.socket.log("channel",`error ${this.topic}`,e),this.state=a.errored,this.rejoinTimer.scheduleTimeout())}),this.joinPush.receive("timeout",()=>{if(!this.isJoining())return;this.socket.log("channel",`timeout ${this.topic} (${this.joinRef()})`,this.joinPush.timeout),new p(this,c.leave,f({}),this.timeout).send(),this.state=a.errored,this.joinPush.reset(),this.rejoinTimer.scheduleTimeout()}),this.on(c.reply,(e,t)=>{this.trigger(this.replyEventName(t),e)})}rejoinUntilConnected(){this.rejoinTimer.scheduleTimeout(),this.socket.isConnected()&&this.rejoin()}join(e=this.timeout){if(this.joinedOnce)throw"tried to join multiple times. 'join' can only be called a single time per channel instance";return this.joinedOnce=!0,this.rejoin(e),this.joinPush}onClose(e){this.on(c.close,e)}onError(e){return this.on(c.error,t=>e(t))}on(e,t){let s=this.bindingRef++;return this.bindings.push({event:e,ref:s,callback:t}),s}off(e,t){this.bindings=this.bindings.filter(s=>!(s.event===e&&(void 0===t||t===s.ref)))}canPush(){return this.socket.isConnected()&&this.isJoined()}push(e,t,s=this.timeout){if(!this.joinedOnce)throw`tried to push '${e}' to '${this.topic}' before joining. Use channel.join() before pushing events`;let n=new p(this,e,function(){return t},s);return this.canPush()?n.send():(n.startTimeout(),this.pushBuffer.push(n)),n}leave(e=this.timeout){this.state=a.leaving;let t=()=>{this.socket.log("channel",`leave ${this.topic}`),this.trigger(c.close,"leave")},s=new p(this,c.leave,f({}),e);return s.receive("ok",()=>t()).receive("timeout",()=>t()),s.send(),this.canPush()||s.trigger("ok",{}),s}onMessage(e,t,s){return t}isMember(e,t,s,n){if(this.topic!==e)return!1;let i=l.indexOf(t)>=0;return!n||!i||n===this.joinRef()||(this.socket.log("channel","dropping outdated message",{topic:e,event:t,payload:s,joinRef:n}),!1)}joinRef(){return this.joinPush.ref}sendJoin(e){this.state=a.joining,this.joinPush.resend(e)}rejoin(e=this.timeout){this.isLeaving()||this.sendJoin(e)}trigger(e,t,s,n){let i=this.onMessage(e,t,s,n);if(t&&!i)throw"channel onMessage callbacks must return the payload, modified or unmodified";this.bindings.filter(t=>t.event===e).map(e=>e.callback(i,s,n||this.joinRef()))}replyEventName(e){return`chan_reply_${e}`}isClosed(){return this.state===a.closed}isErrored(){return this.state===a.errored}isJoined(){return this.state===a.joined}isJoining(){return this.state===a.joining}isLeaving(){return this.state===a.leaving}}const m={encode(e,t){let s=[e.join_ref,e.ref,e.topic,e.event,e.payload];return t(JSON.stringify(s))},decode(e,t){let[s,n,i,o,r]=JSON.parse(e);return t({join_ref:s,ref:n,topic:i,event:o,payload:r})}};class g{constructor(e,t={}){this.stateChangeCallbacks={open:[],close:[],error:[],message:[]},this.channels=[],this.sendBuffer=[],this.ref=0,this.timeout=t.timeout||r,this.transport=t.transport||n.WebSocket||v,this.defaultEncoder=m.encode,this.defaultDecoder=m.decode,this.transport!==v?(this.encode=t.encode||this.defaultEncoder,this.decode=t.decode||this.defaultDecoder):(this.encode=this.defaultEncoder,this.decode=this.defaultDecoder),this.heartbeatIntervalMs=t.heartbeatIntervalMs||3e4,this.reconnectAfterMs=t.reconnectAfterMs||function(e){return[1e3,2e3,5e3,1e4][e-1]||1e4},this.logger=t.logger||function(){},this.longpollerTimeout=t.longpollerTimeout||2e4,this.params=f(t.params||{}),this.endPoint=`${e}/${u.websocket}`,this.heartbeatTimer=null,this.pendingHeartbeatRef=null,this.reconnectTimer=new y(()=>{this.teardown(()=>this.connect())},this.reconnectAfterMs)}protocol(){return location.protocol.match(/^https/)?"wss":"ws"}endPointURL(){let e=j.appendParams(j.appendParams(this.endPoint,this.params()),{vsn:i});return"/"!==e.charAt(0)?e:"/"===e.charAt(1)?`${this.protocol()}:${e}`:`${this.protocol()}://${location.host}${e}`}disconnect(e,t,s){this.reconnectTimer.reset(),this.teardown(e,t,s)}connect(e){e&&(console&&console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor"),this.params=f(e)),this.conn||(this.conn=new this.transport(this.endPointURL()),this.conn.timeout=this.longpollerTimeout,this.conn.onopen=(()=>this.onConnOpen()),this.conn.onerror=(e=>this.onConnError(e)),this.conn.onmessage=(e=>this.onConnMessage(e)),this.conn.onclose=(e=>this.onConnClose(e)))}log(e,t,s){this.logger(e,t,s)}onOpen(e){this.stateChangeCallbacks.open.push(e)}onClose(e){this.stateChangeCallbacks.close.push(e)}onError(e){this.stateChangeCallbacks.error.push(e)}onMessage(e){this.stateChangeCallbacks.message.push(e)}onConnOpen(){this.log("transport",`connected to ${this.endPointURL()}`),this.flushSendBuffer(),this.reconnectTimer.reset(),this.conn.skipHeartbeat||(clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval(()=>this.sendHeartbeat(),this.heartbeatIntervalMs)),this.stateChangeCallbacks.open.forEach(e=>e())}teardown(e,t,s){this.conn&&(this.conn.onclose=function(){},t?this.conn.close(t,s||""):this.conn.close(),this.conn=null),e&&e()}onConnClose(e){this.log("transport","close",e),this.triggerChanError(),clearInterval(this.heartbeatTimer),e&&e.code!==h&&this.reconnectTimer.scheduleTimeout(),this.stateChangeCallbacks.close.forEach(t=>t(e))}onConnError(e){this.log("transport",e),this.triggerChanError(),this.stateChangeCallbacks.error.forEach(t=>t(e))}triggerChanError(){this.channels.forEach(e=>e.trigger(c.error))}connectionState(){switch(this.conn&&this.conn.readyState){case o.connecting:return"connecting";case o.open:return"open";case o.closing:return"closing";default:return"closed"}}isConnected(){return"open"===this.connectionState()}remove(e){this.channels=this.channels.filter(t=>t.joinRef()!==e.joinRef())}channel(e,t={}){let s=new d(e,t,this);return this.channels.push(s),s}push(e){let{topic:t,event:s,payload:n,ref:i,join_ref:o}=e,r=()=>{this.encode(e,e=>{this.conn.send(e)})};this.log("push",`${t} ${s} (${o}, ${i})`,n),this.isConnected()?r():this.sendBuffer.push(r)}makeRef(){let e=this.ref+1;return e===this.ref?this.ref=0:this.ref=e,this.ref.toString()}sendHeartbeat(){if(this.isConnected()){if(this.pendingHeartbeatRef)return this.pendingHeartbeatRef=null,this.log("transport","heartbeat timeout. Attempting to re-establish connection"),void this.conn.close(h,"hearbeat timeout");this.pendingHeartbeatRef=this.makeRef(),this.push({topic:"phoenix",event:"heartbeat",payload:{},ref:this.pendingHeartbeatRef})}}flushSendBuffer(){this.isConnected()&&this.sendBuffer.length>0&&(this.sendBuffer.forEach(e=>e()),this.sendBuffer=[])}onConnMessage(e){this.decode(e.data,e=>{let{topic:t,event:s,payload:n,ref:i,join_ref:o}=e;i&&i===this.pendingHeartbeatRef&&(this.pendingHeartbeatRef=null),this.log("receive",`${n.status||""} ${t} ${s} ${i&&"("+i+")"||""}`,n),this.channels.filter(e=>e.isMember(t,s,n,o)).forEach(e=>e.trigger(s,n,i,o)),this.stateChangeCallbacks.message.forEach(t=>t(e))})}}class v{constructor(e){this.endPoint=null,this.token=null,this.skipHeartbeat=!0,this.onopen=function(){},this.onerror=function(){},this.onmessage=function(){},this.onclose=function(){},this.pollEndpoint=this.normalizeEndpoint(e),this.readyState=o.connecting,this.poll()}normalizeEndpoint(e){return e.replace("ws://","http://").replace("wss://","https://").replace(new RegExp("(.*)/"+u.websocket),"$1/"+u.longpoll)}endpointURL(){return j.appendParams(this.pollEndpoint,{token:this.token})}closeAndRetry(){this.close(),this.readyState=o.connecting}ontimeout(){this.onerror("timeout"),this.closeAndRetry()}poll(){this.readyState!==o.open&&this.readyState!==o.connecting||j.request("GET",this.endpointURL(),"application/json",null,this.timeout,this.ontimeout.bind(this),e=>{if(e){var{status:t,token:s,messages:n}=e;this.token=s}else var t=0;switch(t){case 200:n.forEach(e=>this.onmessage({data:e})),this.poll();break;case 204:this.poll();break;case 410:this.readyState=o.open,this.onopen(),this.poll();break;case 0:case 500:this.onerror(),this.closeAndRetry();break;default:throw`unhandled poll status ${t}`}})}send(e){j.request("POST",this.endpointURL(),"application/json",e,this.timeout,this.onerror.bind(this,"timeout"),e=>{e&&200===e.status||(this.onerror(e&&e.status),this.closeAndRetry())})}close(e,t){this.readyState=o.closed,this.onclose()}}class j{static request(e,t,s,i,o,r,h){if(n.XDomainRequest){let s=new XDomainRequest;this.xdomainRequest(s,e,t,i,o,r,h)}else{let a=n.XMLHttpRequest?new n.XMLHttpRequest:new ActiveXObject("Microsoft.XMLHTTP");this.xhrRequest(a,e,t,s,i,o,r,h)}}static xdomainRequest(e,t,s,n,i,o,r){e.timeout=i,e.open(t,s),e.onload=(()=>{let t=this.parseJSON(e.responseText);r&&r(t)}),o&&(e.ontimeout=o),e.onprogress=(()=>{}),e.send(n)}static xhrRequest(e,t,s,n,i,o,r,h){e.open(t,s,!0),e.timeout=o,e.setRequestHeader("Content-Type",n),e.onerror=(()=>{h&&h(null)}),e.onreadystatechange=(()=>{if(e.readyState===this.states.complete&&h){let t=this.parseJSON(e.responseText);h(t)}}),r&&(e.ontimeout=r),e.send(i)}static parseJSON(e){if(!e||""===e)return null;try{return JSON.parse(e)}catch(t){return console&&console.log("failed to parse JSON response",e),null}}static serialize(e,t){let s=[];for(var n in e){if(!e.hasOwnProperty(n))continue;let i=t?`${t}[${n}]`:n,o=e[n];"object"==typeof o?s.push(this.serialize(o,i)):s.push(encodeURIComponent(i)+"="+encodeURIComponent(o))}return s.join("&")}static appendParams(e,t){if(0===Object.keys(t).length)return e;return`${e}${e.match(/\?/)?"&":"?"}${this.serialize(t)}`}}j.states={complete:4};class b{constructor(e,t={}){let s=t.events||{state:"presence_state",diff:"presence_diff"};this.state={},this.pendingDiffs=[],this.channel=e,this.joinRef=null,this.caller={onJoin:function(){},onLeave:function(){},onSync:function(){}},this.channel.on(s.state,e=>{let{onJoin:t,onLeave:s,onSync:n}=this.caller;this.joinRef=this.channel.joinRef(),this.state=b.syncState(this.state,e,t,s),this.pendingDiffs.forEach(e=>{this.state=b.syncDiff(this.state,e,t,s)}),this.pendingDiffs=[],n()}),this.channel.on(s.diff,e=>{let{onJoin:t,onLeave:s,onSync:n}=this.caller;this.inPendingSyncState()?this.pendingDiffs.push(e):(this.state=b.syncDiff(this.state,e,t,s),n())})}onJoin(e){this.caller.onJoin=e}onLeave(e){this.caller.onLeave=e}onSync(e){this.caller.onSync=e}list(e){return b.list(this.state,e)}inPendingSyncState(){return!this.joinRef||this.joinRef!==this.channel.joinRef()}static syncState(e,t,s,n){let i=this.clone(e),o={},r={};return this.map(i,(e,s)=>{t[e]||(r[e]=s)}),this.map(t,(e,t)=>{let s=i[e];if(s){let n=t.metas.map(e=>e.phx_ref),i=s.metas.map(e=>e.phx_ref),h=t.metas.filter(e=>i.indexOf(e.phx_ref)<0),a=s.metas.filter(e=>n.indexOf(e.phx_ref)<0);h.length>0&&(o[e]=t,o[e].metas=h),a.length>0&&(r[e]=this.clone(s),r[e].metas=a)}else o[e]=t}),this.syncDiff(i,{joins:o,leaves:r},s,n)}static syncDiff(e,{joins:t,leaves:s},n,i){let o=this.clone(e);return n||(n=function(){}),i||(i=function(){}),this.map(t,(e,t)=>{let s=o[e];if(o[e]=t,s){let t=o[e].metas.map(e=>e.phx_ref),n=s.metas.filter(e=>t.indexOf(e.phx_ref)<0);o[e].metas.unshift(...n)}n(e,s,t)}),this.map(s,(e,t)=>{let s=o[e];if(!s)return;let n=t.metas.map(e=>e.phx_ref);s.metas=s.metas.filter(e=>n.indexOf(e.phx_ref)<0),i(e,s,t),0===s.metas.length&&delete o[e]}),o}static list(e,t){return t||(t=function(e,t){return t}),this.map(e,(e,s)=>t(e,s))}static map(e,t){return Object.getOwnPropertyNames(e).map(s=>t(s,e[s]))}static clone(e){return JSON.parse(JSON.stringify(e))}}class y{constructor(e,t){this.callback=e,this.timerCalc=t,this.timer=null,this.tries=0}reset(){this.tries=0,clearTimeout(this.timer)}scheduleTimeout(){clearTimeout(this.timer),this.timer=setTimeout(()=>{this.tries=this.tries+1,this.callback()},this.timerCalc(this.tries+1))}}},function(e,t){var s;s=function(){return this}();try{s=s||Function("return this")()||(0,eval)("this")}catch(e){"object"==typeof window&&(s=window)}e.exports=s},function(e,t,s){"use strict";(function(t){e.exports=t.Phoenix=s(0)}).call(this,s(1))}])});
\ No newline at end of file


Test patch:
diff --git a/assets/test/socket_test.js b/assets/test/socket_test.js
index 352c934a8..61e53bdf3 100644
--- a/assets/test/socket_test.js
+++ b/assets/test/socket_test.js
@@ -637,10 +637,22 @@ describe("onConnClose", () => {
     socket.connect()
   })
 
-  it("schedules reconnectTimer timeout", () => {
-    const spy = sinon.spy(socket.reconnectTimer, "scheduleTimeout")
+  it('does not schedules reconnectTimer timeout if normal close', () => {
+    const spy = sinon.spy(socket.reconnectTimer, 'scheduleTimeout')
 
-    socket.onConnClose()
+    const event = { code: 1000 }
+
+    socket.onConnClose(event)
+
+    assert.ok(spy.notCalled)
+  })
+
+  it('schedules reconnectTimer timeout if not normal close', () => {
+    const spy = sinon.spy(socket.reconnectTimer, 'scheduleTimeout')
+
+    const event = { code: 1001 }
+
+    socket.onConnClose(event)
 
     assert.ok(spy.calledOnce)
   })
diff --git a/installer/templates/phx_umbrella/apps/app_name/config/test.exs b/installer/templates/phx_umbrella/apps/app_name/config/test.exs
index d2d855e6d..8cd2bc327 100644
--- a/installer/templates/phx_umbrella/apps/app_name/config/test.exs
+++ b/installer/templates/phx_umbrella/apps/app_name/config/test.exs
@@ -1,1 +1,5 @@
+# Since configuration is shared in umbrella projects, this file
+# should only configure the :<%= app_name %> application itself
+# and only for organization purposes. All other config goes to
+# the umbrella root.
 use Mix.Config
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/config/test.exs b/installer/templates/phx_umbrella/apps/app_name_web/config/test.exs
index 38a1650fc..57e814f1c 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/config/test.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/config/test.exs
@@ -1,3 +1,7 @@
+# Since configuration is shared in umbrella projects, this file
+# should only configure the :<%= web_app_name %> application itself
+# and only for organization purposes. All other config goes to
+# the umbrella root.
 use Mix.Config
 
 # We don't run a server during test. If one is required,
diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs
index ac89329a4..0f163c446 100644
--- a/installer/test/phx_new_test.exs
+++ b/installer/test/phx_new_test.exs
@@ -283,31 +283,16 @@ defmodule Mix.Tasks.Phx.NewTest do
     end
   end
 
-  test "new with mysql adapter" do
-    in_tmp "new with mysql adapter", fn ->
-      project_path = Path.join(File.cwd!, "custom_path")
-      Mix.Tasks.Phx.New.run([project_path, "--database", "mysql"])
-
-      assert_file "custom_path/mix.exs", ~r/:mariaex/
-      assert_file "custom_path/config/dev.exs", [~r/Ecto.Adapters.MySQL/, ~r/username: "root"/, ~r/password: ""/]
-      assert_file "custom_path/config/test.exs", [~r/Ecto.Adapters.MySQL/, ~r/username: "root"/, ~r/password: ""/]
-      assert_file "custom_path/config/prod.secret.exs", [~r/Ecto.Adapters.MySQL/, ~r/username: "root"/, ~r/password: ""/]
-
-      assert_file "custom_path/test/support/conn_case.ex", "Ecto.Adapters.SQL.Sandbox.mode"
-      assert_file "custom_path/test/support/channel_case.ex", "Ecto.Adapters.SQL.Sandbox.mode"
-      assert_file "custom_path/test/support/data_case.ex", "Ecto.Adapters.SQL.Sandbox.mode"
-    end
-  end
-
   test "new defaults to pg adapter" do
     in_tmp "new defaults to pg adapter", fn ->
       project_path = Path.join(File.cwd!, "custom_path")
       Mix.Tasks.Phx.New.run([project_path])
 
-      assert_file "custom_path/mix.exs", ~r/:postgrex/
-      assert_file "custom_path/config/dev.exs", [~r/Ecto.Adapters.Postgres/, ~r/username: "postgres"/, ~r/password: "postgres"/, ~r/hostname: "localhost"/]
-      assert_file "custom_path/config/test.exs", [~r/Ecto.Adapters.Postgres/, ~r/username: "postgres"/, ~r/password: "postgres"/, ~r/hostname: "localhost"/]
-      assert_file "custom_path/config/prod.secret.exs", [~r/Ecto.Adapters.Postgres/, ~r/username: "postgres"/, ~r/password: "postgres"/]
+      assert_file "custom_path/mix.exs", ":postgrex"
+      assert_file "custom_path/config/dev.exs", [~r/username: "postgres"/, ~r/password: "postgres"/, ~r/hostname: "localhost"/]
+      assert_file "custom_path/config/test.exs", [~r/username: "postgres"/, ~r/password: "postgres"/, ~r/hostname: "localhost"/]
+      assert_file "custom_path/config/prod.secret.exs", [~r/username: "postgres"/, ~r/password: "postgres"/]
+      assert_file "custom_path/lib/custom_path/repo.ex", "Ecto.Adapters.Postgres"
 
       assert_file "custom_path/test/support/conn_case.ex", "Ecto.Adapters.SQL.Sandbox.checkout"
       assert_file "custom_path/test/support/channel_case.ex", "Ecto.Adapters.SQL.Sandbox.checkout"
@@ -315,6 +300,23 @@ defmodule Mix.Tasks.Phx.NewTest do
     end
   end
 
+  test "new with mysql adapter" do
+    in_tmp "new with mysql adapter", fn ->
+      project_path = Path.join(File.cwd!, "custom_path")
+      Mix.Tasks.Phx.New.run([project_path, "--database", "mysql"])
+
+      assert_file "custom_path/mix.exs", ":mariaex"
+      assert_file "custom_path/config/dev.exs", [~r/username: "root"/, ~r/password: ""/]
+      assert_file "custom_path/config/test.exs", [~r/username: "root"/, ~r/password: ""/]
+      assert_file "custom_path/config/prod.secret.exs", [~r/username: "root"/, ~r/password: ""/]
+      assert_file "custom_path/lib/custom_path/repo.ex", "Ecto.Adapters.MySQL"
+
+      assert_file "custom_path/test/support/conn_case.ex", "Ecto.Adapters.SQL.Sandbox.mode"
+      assert_file "custom_path/test/support/channel_case.ex", "Ecto.Adapters.SQL.Sandbox.mode"
+      assert_file "custom_path/test/support/data_case.ex", "Ecto.Adapters.SQL.Sandbox.mode"
+    end
+  end
+
   test "new with invalid database adapter" do
     in_tmp "new with invalid database adapter", fn ->
       project_path = Path.join(File.cwd!, "custom_path")
diff --git a/installer/test/phx_new_umbrella_test.exs b/installer/test/phx_new_umbrella_test.exs
index fc3c2f2e9..afb5b4241 100644
--- a/installer/test/phx_new_umbrella_test.exs
+++ b/installer/test/phx_new_umbrella_test.exs
@@ -35,16 +35,16 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
 
       assert_file root_path(@app, "README.md")
       assert_file root_path(@app, ".gitignore")
-      assert_file( root_path(@app, ".gitignore"), ~r/\n$/)
 
       assert_file app_path(@app, "README.md")
-      assert_file app_path(@app, ".gitignore")
       assert_file app_path(@app, ".gitignore"), "#{@app}-*.tar"
-      assert_file( app_path(@app, ".gitignore"), ~r/\n$/)
+
       assert_file web_path(@app, "README.md")
+
       assert_file root_path(@app, "mix.exs"), fn file ->
         assert file =~ "apps_path: \"apps\""
       end
+
       assert_file app_path(@app, "mix.exs"), fn file ->
         assert file =~ "app: :phx_umb"
         assert file =~ ~S{build_path: "../../_build"}
@@ -56,18 +56,19 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
       assert_file root_path(@app, "config/config.exs"), fn file ->
         assert file =~ ~S[import_config "../apps/*/config/config.exs"]
         assert file =~ ~S[import_config "#{Mix.env}.exs"]
+        assert file =~ "config :phoenix, :json_library, Jason"
       end
+
       assert_file app_path(@app, "config/config.exs"), fn file ->
         assert file =~ "ecto_repos: [PhxUmb.Repo]"
-        assert file =~ "config :ecto, :json_library, Jason"
         refute file =~ "namespace"
         refute file =~ "config :phx_blog_web, :generators"
       end
+
       assert_file web_path(@app, "config/config.exs"), fn file ->
         assert file =~ "ecto_repos: [PhxUmb.Repo]"
         assert file =~ ":phx_umb_web, PhxUmbWeb.Endpoint"
         assert file =~ "generators: [context_app: :phx_umb]\n"
-        assert file =~ "config :phoenix, :json_library, Jason"
       end
 
       assert_file web_path(@app, "config/prod.exs"), fn file ->
@@ -226,7 +227,6 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
 
       assert_file web_path(@app, "config/dev.exs"), fn file ->
         refute file =~ config
-        assert file =~ "config :phoenix, :plug_init_mode, :runtime"
       end
       assert_file web_path(@app, "config/test.exs"), &refute(&1 =~ config)
       assert_file web_path(@app, "config/prod.secret.exs"), &refute(&1 =~ config)
@@ -326,38 +326,34 @@ defmodule Mix.Tasks.Phx.New.UmbrellaTest do
     end
   end
 
-  test "new with mysql adapter" do
-    in_tmp "new with mysql adapter", fn ->
-      project_path = Path.join(File.cwd!, "custom_path")
-      Mix.Tasks.Phx.New.run([project_path, "--umbrella", "--database", "mysql"])
+  test "new defaults to pg adapter" do
+    in_tmp "new defaults to pg adapter", fn ->
+      app = "custom_path"
+      project_path = Path.join(File.cwd!, app)
+      Mix.Tasks.Phx.New.run([project_path, "--umbrella"])
 
-      assert_file "custom_path_umbrella/apps/custom_path/mix.exs", ~r/:mariaex/
-      assert_file "custom_path_umbrella/apps/custom_path/config/dev.exs",
-        [~r/Ecto.Adapters.MySQL/, ~r/username: "root"/, ~r/password: ""/]
-      assert_file "custom_path_umbrella/apps/custom_path/config/test.exs",
-        [~r/Ecto.Adapters.MySQL/, ~r/username: "root"/, ~r/password: ""/]
-      assert_file "custom_path_umbrella/apps/custom_path/config/prod.secret.exs",
-        [~r/Ecto.Adapters.MySQL/, ~r/username: "root"/, ~r/password: ""/]
-
-      assert_file "custom_path_umbrella/apps/custom_path/test/support/data_case.ex",
-        "Ecto.Adapters.SQL.Sandbox.mode"
-      assert_file "custom_path_umbrella/apps/custom_path_web/test/support/channel_case.ex",
-        "Ecto.Adapters.SQL.Sandbox.mode"
-      assert_file "custom_path_umbrella/apps/custom_path_web/test/support/conn_case.ex",
-        "Ecto.Adapters.SQL.Sandbox.mode"
+      assert_file app_path(app, "mix.exs"), ":postgrex"
+      assert_file app_path(app, "config/dev.exs"), [~r/username: "postgres"/, ~r/password: "postgres"/, ~r/hostname: "localhost"/]
+      assert_file app_path(app, "config/test.exs"), [~r/username: "postgres"/, ~r/password: "postgres"/, ~r/hostname: "localhost"/]
+      assert_file app_path(app, "config/prod.secret.exs"), [~r/username: "postgres"/, ~r/password: "postgres"/]
+      assert_file app_path(app, "lib/custom_path/repo.ex"), "Ecto.Adapters.Postgres"
+
+      assert_file web_path(app, "test/support/conn_case.ex"), "Ecto.Adapters.SQL.Sandbox.checkout"
+      assert_file web_path(app, "test/support/channel_case.ex"), "Ecto.Adapters.SQL.Sandbox.checkout"
     end
   end
 
-  test "new defaults to pg adapter" do
-    in_tmp "new defaults to pg adapter", fn ->
+  test "new with mysql adapter" do
+    in_tmp "new with mysql adapter", fn ->
       app = "custom_path"
       project_path = Path.join(File.cwd!, app)
-      Mix.Tasks.Phx.New.run([project_path, "--umbrella"])
+      Mix.Tasks.Phx.New.run([project_path, "--umbrella", "--database", "mysql"])
 
-      assert_file app_path(app, "mix.exs"), ~r/:postgrex/
-      assert_file app_path(app, "config/dev.exs"), [~r/Ecto.Adapters.Postgres/, ~r/username: "postgres"/, ~r/password: "postgres"/, ~r/hostname: "localhost"/]
-      assert_file app_path(app, "config/test.exs"), [~r/Ecto.Adapters.Postgres/, ~r/username: "postgres"/, ~r/password: "postgres"/, ~r/hostname: "localhost"/]
-      assert_file app_path(app, "config/prod.secret.exs"), [~r/Ecto.Adapters.Postgres/, ~r/username: "postgres"/, ~r/password: "postgres"/]
+      assert_file app_path(app, "mix.exs"), ":mariaex"
+      assert_file app_path(app, "config/dev.exs"), [~r/username: "root"/, ~r/password: ""/]
+      assert_file app_path(app, "config/test.exs"), [~r/username: "root"/, ~r/password: ""/]
+      assert_file app_path(app, "config/prod.secret.exs"), [~r/username: "root"/, ~r/password: ""/]
+      assert_file app_path(app, "lib/custom_path/repo.ex"), "Ecto.Adapters.MySQL"
 
       assert_file web_path(app, "test/support/conn_case.ex"), "Ecto.Adapters.SQL.Sandbox.checkout"
       assert_file web_path(app, "test/support/channel_case.ex"), "Ecto.Adapters.SQL.Sandbox.checkout"
diff --git a/test/phoenix/controller/controller_test.exs b/test/phoenix/controller/controller_test.exs
index 870187fcd..817dd6327 100644
--- a/test/phoenix/controller/controller_test.exs
+++ b/test/phoenix/controller/controller_test.exs
@@ -527,6 +527,7 @@ defmodule Phoenix.Controller.ControllerTest do
     assert get_resp_header(conn, "x-content-type-options") == ["nosniff"]
     assert get_resp_header(conn, "x-download-options") == ["noopen"]
     assert get_resp_header(conn, "x-permitted-cross-domain-policies") == ["none"]
+    assert get_resp_header(conn, "cross-origin-window-policy") == ["deny"]
 
     custom_headers = %{"x-frame-options" => "custom", "foo" => "bar"}
     conn = conn(:get, "/") |> put_secure_browser_headers(custom_headers)
@@ -534,6 +535,7 @@ defmodule Phoenix.Controller.ControllerTest do
     assert get_resp_header(conn, "x-xss-protection") == ["1; mode=block"]
     assert get_resp_header(conn, "x-download-options") == ["noopen"]
     assert get_resp_header(conn, "x-permitted-cross-domain-policies") == ["none"]
+    assert get_resp_header(conn, "cross-origin-window-policy") == ["deny"]
     assert get_resp_header(conn, "foo") == ["bar"]
   end
 
diff --git a/test/phoenix/router/helpers_test.exs b/test/phoenix/router/helpers_test.exs
index c3729d1ea..e11e3d248 100644
--- a/test/phoenix/router/helpers_test.exs
+++ b/test/phoenix/router/helpers_test.exs
@@ -17,7 +17,7 @@ defmodule Phoenix.Router.HelpersTest do
 
     assert extract_defhelper(route, 1) == String.trim """
     def(hello_world_path(conn_or_endpoint, :world, bar, params)) do
-      path(conn_or_endpoint, segments(("" <> "/foo") <> "/" <> URI.encode(to_param(bar), &URI.char_unreserved?/1), params, ["bar"]))
+      path(conn_or_endpoint, segments!((\"\" <> \"/foo\") <> \"/\" <> URI.encode(to_param(bar), &URI.char_unreserved?/1), params, [\"bar\"], {\"hello_world\", :world, [\"bar\"]}))
     end
     """
   end
@@ -33,7 +33,7 @@ defmodule Phoenix.Router.HelpersTest do
 
     assert extract_defhelper(route, 1) == String.trim """
     def(hello_world_path(conn_or_endpoint, :world, bar, params)) do
-      path(conn_or_endpoint, segments(("" <> "/foo") <> "/" <> Enum.map_join(bar, "/", fn s -> URI.encode(s, &URI.char_unreserved?/1) end), params, ["bar"]))
+      path(conn_or_endpoint, segments!((\"\" <> \"/foo\") <> \"/\" <> Enum.map_join(bar, \"/\", fn s -> URI.encode(s, &URI.char_unreserved?/1) end), params, [\"bar\"], {\"hello_world\", :world, [\"bar\"]}))
     end
     """
   end
@@ -53,6 +53,7 @@ defmodule Phoenix.Router.HelpersTest do
     use Phoenix.Router
 
     get "/posts/top", PostController, :top, as: :top
+    get "/posts/bottom/:order/:count", PostController, :bottom, as: :bottom
     get "/posts/:id", PostController, :show
     get "/posts/file/*file", PostController, :file
     get "/posts/skip", PostController, :skip, as: nil
@@ -136,6 +137,20 @@ defmodule Phoenix.Router.HelpersTest do
     end
   end
 
+  test "url helper shows an error if an id is accidentally passed" do
+    error_suggestion = ~r/Router.bottom_path\(conn, :bottom, order, count, page: 5, per_page: 10\)/
+
+    assert_raise ArgumentError, error_suggestion, fn ->
+      Helpers.bottom_path(__MODULE__, :bottom, :asc, 8, {:not, :enumerable})
+    end
+
+    error_suggestion = ~r/Router.top_path\(conn, :top, page: 5, per_page: 10\)/
+
+    assert_raise ArgumentError, error_suggestion, fn ->
+      Helpers.top_path(__MODULE__, :top, "invalid")
+    end
+  end
+
   test "top-level named route" do
     assert Helpers.post_path(__MODULE__, :show, 5) == "/posts/5"
     assert Helpers.post_path(__MODULE__, :show, 5, []) == "/posts/5"
@@ -161,10 +176,10 @@ defmodule Phoenix.Router.HelpersTest do
 
     error_message = fn helper, arity ->
       """
-      No function clause for #{inspect Helpers}.#{helper}/#{arity} and action :skip. The following actions/clauses are supported:
+      no function clause for #{inspect Helpers}.#{helper}/#{arity} and action :skip. The following actions/clauses are supported:
 
-          #{helper}(conn_or_endpoint, :file, file, opts \\\\ [])
-          #{helper}(conn_or_endpoint, :show, id, opts \\\\ [])
+          #{helper}(conn_or_endpoint, :file, file, params \\\\ [])
+          #{helper}(conn_or_endpoint, :show, id, params \\\\ [])
 
       """ |> String.trim
     end
@@ -267,15 +282,15 @@ defmodule Phoenix.Router.HelpersTest do
 
     error_message = fn helper, arity ->
       """
-      No function clause for #{inspect Helpers}.#{helper}/#{arity} and action :skip. The following actions/clauses are supported:
-
-          user_comment_file_path(conn_or_endpoint, :create, user_id, comment_id, opts \\\\ [])
-          user_comment_file_path(conn_or_endpoint, :delete, user_id, comment_id, id, opts \\\\ [])
-          user_comment_file_path(conn_or_endpoint, :edit, user_id, comment_id, id, opts \\\\ [])
-          user_comment_file_path(conn_or_endpoint, :index, user_id, comment_id, opts \\\\ [])
-          user_comment_file_path(conn_or_endpoint, :new, user_id, comment_id, opts \\\\ [])
-          user_comment_file_path(conn_or_endpoint, :show, user_id, comment_id, id, opts \\\\ [])
-          user_comment_file_path(conn_or_endpoint, :update, user_id, comment_id, id, opts \\\\ [])
+      no function clause for #{inspect Helpers}.#{helper}/#{arity} and action :skip. The following actions/clauses are supported:
+
+          user_comment_file_path(conn_or_endpoint, :create, user_id, comment_id, params \\\\ [])
+          user_comment_file_path(conn_or_endpoint, :delete, user_id, comment_id, id, params \\\\ [])
+          user_comment_file_path(conn_or_endpoint, :edit, user_id, comment_id, id, params \\\\ [])
+          user_comment_file_path(conn_or_endpoint, :index, user_id, comment_id, params \\\\ [])
+          user_comment_file_path(conn_or_endpoint, :new, user_id, comment_id, params \\\\ [])
+          user_comment_file_path(conn_or_endpoint, :show, user_id, comment_id, id, params \\\\ [])
+          user_comment_file_path(conn_or_endpoint, :update, user_id, comment_id, id, params \\\\ [])
       """ |> String.trim
     end
 
@@ -289,15 +304,15 @@ defmodule Phoenix.Router.HelpersTest do
 
     arity_error_message =
       """
-      No action :show for helper #{inspect Helpers}.user_comment_path/3. The following actions/clauses are supported:
-
-          user_comment_path(conn_or_endpoint, :create, user_id, opts \\\\ [])
-          user_comment_path(conn_or_endpoint, :delete, user_id, id, opts \\\\ [])
-          user_comment_path(conn_or_endpoint, :edit, user_id, id, opts \\\\ [])
-          user_comment_path(conn_or_endpoint, :index, user_id, opts \\\\ [])
-          user_comment_path(conn_or_endpoint, :new, user_id, opts \\\\ [])
-          user_comment_path(conn_or_endpoint, :show, user_id, id, opts \\\\ [])
-          user_comment_path(conn_or_endpoint, :update, user_id, id, opts \\\\ [])
+      no action :show for helper #{inspect Helpers}.user_comment_path/3. The following actions/clauses are supported:
+
+          user_comment_path(conn_or_endpoint, :create, user_id, params \\\\ [])
+          user_comment_path(conn_or_endpoint, :delete, user_id, id, params \\\\ [])
+          user_comment_path(conn_or_endpoint, :edit, user_id, id, params \\\\ [])
+          user_comment_path(conn_or_endpoint, :index, user_id, params \\\\ [])
+          user_comment_path(conn_or_endpoint, :new, user_id, params \\\\ [])
+          user_comment_path(conn_or_endpoint, :show, user_id, id, params \\\\ [])
+          user_comment_path(conn_or_endpoint, :update, user_id, id, params \\\\ [])
 
       """ |> String.trim
 
diff --git a/test/phoenix/router/route_test.exs b/test/phoenix/router/route_test.exs
index 66a305b90..d35d00896 100644
--- a/test/phoenix/router/route_test.exs
+++ b/test/phoenix/router/route_test.exs
@@ -6,6 +6,7 @@ defmodule Phoenix.Router.RouteTest do
   def init(opts), do: opts
 
   defmodule AdminRouter do
+    def init(opts), do: opts
     def call(conn, _), do: Plug.Conn.assign(conn, :fwd_conn, conn)
   end
 
@@ -53,9 +54,9 @@ defmodule Phoenix.Router.RouteTest do
     assert exprs(route).verb_match == {:_verb, [], nil}
   end
 
-  test "forward sets path_info and script_name for target, then resumes" do
+  test "as a plug, it forwards and sets path_info and script_name for target, then resumes" do
     conn = %Plug.Conn{path_info: ["admin", "stats"], script_name: ["phoenix"]}
-    conn = forward(conn, ["admin"], AdminRouter, [])
+    conn = call(conn, {["admin"], AdminRouter, []})
     fwd_conn = conn.assigns[:fwd_conn]
     assert fwd_conn.path_info == ["stats"]
     assert fwd_conn.script_name == ["phoenix", "admin"]