Repository: phoenixframework/phoenix (elixir)

Issue:
Error when using send_download/3 with a filename that contains spaces and commas
### Environment

* Elixir version (elixir -v):
Erlang/OTP 26 [erts-14.2.5] [source] [64-bit] [smp:6:6] [ds:6:6:10] [async-threads:1]
Elixir 1.16.3 (compiled with Erlang/OTP 26)
* Phoenix version (mix deps):
phoenix 1.7.12 (Hex package) (mix)
* Operating system:
MacOS

### Actual behaviour
Chrome shows an error page stating that the server sent an invalid response with the error `ERR_RESPONSE_HEADERS_MULTIPLE_CONTENT_DISPOSITION`. Sending a file with filename `datei, mit beistrich.pdf` will send the following header.
```
content-disposition: attachment; filename="datei,%20mit%20beistrich.pdf"; filename*=utf-8''datei,%20mit%20beistrich.pdf
```
Notably, the comma has not been encoded. 

### Expected behaviour
For context, the encoding has been adjusted in as part of #5478. Looking at [RFC5987](https://www.rfc-editor.org/rfc/rfc5987), and it's definition of a token in [RTC2616](https://www.rfc-editor.org/rfc/rfc2616#section-2.2), I believe the comma and other characters should have been encoded, since they are excluded. A possible fix would be to change the function call in [Phoenix.Controller.encode_filename/2](https://github.com/phoenixframework/phoenix/blob/v1.7.14/lib/phoenix/controller.ex#L1277) to `URI.encode(filename, &URI.char_unreserved?/1)`


Pull request:
Send download encodes reserved chars
- Closes #5926

Patch:
diff --git a/guides/asset_management.md b/guides/asset_management.md
index b789ee177..c56cc7115 100644
--- a/guides/asset_management.md
+++ b/guides/asset_management.md
@@ -20,7 +20,7 @@ If you want to import JavaScript dependencies, you have at least three options t
    import topbar from "../vendor/topbar"
    ```
 
-2. Call `npm install topbar --save` inside your assets directory and `esbuild` will be able to automatically pick them up:
+2. Call `npm install topbar --prefix assets` will create `package.json` and `package-lock.json` inside your assets directory and `esbuild` will be able to automatically pick them up:
 
    ```js
    import topbar from "topbar"
diff --git a/guides/cheatsheets/router.cheatmd b/guides/cheatsheets/router.cheatmd
index 1f0af13e7..65d976ace 100644
--- a/guides/cheatsheets/router.cheatmd
+++ b/guides/cheatsheets/router.cheatmd
@@ -49,7 +49,7 @@ end
 ~p"/users/3/posts" # user_id is 3
 ~p"/users/3/posts/17" # user_id is 3 and post_id = 17
 ```
-For more info check the [resources docs.](routing-1.html#resources)
+For more info check the [resources docs.](routing.html#resources)
 
 ### Scopes
 
diff --git a/guides/components.md b/guides/components.md
index c75ccbef4..f78aa38a3 100644
--- a/guides/components.md
+++ b/guides/components.md
@@ -62,11 +62,7 @@ Next we need to update `show.html.heex`:
 </section>
 ```
 
-When we reload `http://localhost:4000/hello/Frank`, we should see the same content as before.
-
-Since templates are embedded inside the `HelloHTML` module, we were able to invoke the view function simply as `<.greet messenger="..." />`.
-
-If the component was defined elsewhere, we can also type `<HelloWeb.HelloHTML.greet messenger="..." />`.
+When we reload `http://localhost:4000/hello/Frank`, we should see the same content as before. Since the `show.html.heex` template is embedded within the `HelloHTML` module, we were able to invoke the function component directly as `<.greet messenger="..." />`. If the component was defined elsewhere, we would need to give its full name: `<HelloWeb.HelloHTML.greet messenger="..." />`.
 
 By declaring attributes as required, Phoenix will warn at compile time if we call the `<.greet />` component without passing attributes. If an attribute is optional, you can specify the `:default` option with a value:
 
@@ -74,19 +70,22 @@ By declaring attributes as required, Phoenix will warn at compile time if we cal
 attr :messenger, :string, default: nil
 ```
 
-Although this is a quick example, it shows the different roles function components play in Phoenix:
-
-* Function components can be defined as functions that receive `assigns` as argument and call the `~H` sigil, as we did in `greet/1`
+Overall, function components are the essential building block of Phoenix rendering stack. The majority of the times, they are functions that receive a single argument called `assigns` and call the `~H` sigil, as we did in `greet/1`. They can also be invoked from templates, with compile-time validation of its attributes declared via `attr`.
 
-* Function components can be embedded from template files, that's how we load `show.html.heex` into `HelloWeb.HelloHTML`
+In fact, every template embedded into `HelloHTML` is a function component in itself. `show.html.heex` simply becomes a function component named `show`. This also means you can directly render function components directly from the controller, skipping the `show.html.heex` template:
 
-* Function components can declare which attributes are expected, which are validated at compilation time
-
-* Function components can be directly rendered from controllers
+```elixir
+def HelloWeb.HelloController do
+  use HelloWeb, :controller
 
-* Function components can be directly rendered from other function components, as we called `<.greet messenger={@messenger} />` from `show.html.heex`
+  def show(conn, %{"messenger" => messenger}) do
+    # Render the HelloWeb.HelloHTML.greet/1 component
+    render(conn, :greet, messenger: messenger)
+  end
+end
+```
 
-And there's more. Before we go deeper, let's fully understand the expressive power behind the HEEx template language.
+Next, let's fully understand the expressive power behind the HEEx template language.
 
 ## HEEx
 
@@ -240,3 +239,5 @@ CoreComponents also play an important role in Phoenix code generators, as the co
   * Read the official documentation for [`Phoenix.Component`](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html)
 
   * Read the official documentation for [HEEx and the ~H sigils](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#sigil_H/2)
+
+  * If you are looking for higher level components beyond the minimal ones included by Phoenix, [the LiveView project keeps a list of component systems](https://github.com/phoenixframework/phoenix_live_view#component-systems)
diff --git a/guides/controllers.md b/guides/controllers.md
index 6ca334642..3bb93589e 100644
--- a/guides/controllers.md
+++ b/guides/controllers.md
@@ -47,7 +47,7 @@ defmodule HelloWeb.PageController do
   ...
 
   def index(conn, _params) do
-    render(conn, :index)
+    render(conn, :home)
   end
 end
 ```
diff --git a/guides/deployment/fly.md b/guides/deployment/fly.md
index 1388a0a9b..b2513090c 100644
--- a/guides/deployment/fly.md
+++ b/guides/deployment/fly.md
@@ -390,11 +390,11 @@ $ fly scale count 2
 
 Refer to the [Fly.io Elixir documentation](https://fly.io/docs/getting-started/elixir) for additional information.
 
-[Working with Fly.io applications](https://fly.io/docs/getting-started/working-with-fly-apps/) covers things like:
+[The Fly.io docs](https://fly.io/docs/) covers things like:
 
-* Status and logs
-* Custom domains
-* Certificates
+* [Status](https://fly.io/docs/flyctl/status/) and [logs](https://fly.io/docs/monitoring/logging-overview/)
+* [Custom domains](https://fly.io/docs/networking/custom-domain/)
+* [Certificates](https://fly.io/docs/networking/custom-domain-api/)
 
 ## Troubleshooting
 
diff --git a/guides/introduction/up_and_running.md b/guides/introduction/up_and_running.md
index 8d07024b1..df1871d49 100644
--- a/guides/introduction/up_and_running.md
+++ b/guides/introduction/up_and_running.md
@@ -86,7 +86,7 @@ And finally, we'll start the Phoenix server:
 
 ```console
 $ mix phx.server
-[info] Running HelloWeb.Endpoint with cowboy 2.9.0 at 127.0.0.1:4000 (http)
+[info] Running HelloWeb.Endpoint with Bandit 1.5.7 at 127.0.0.1:4000 (http)
 [info] Access HelloWeb.Endpoint at http://localhost:4000
 [watch] build finished, watching for changes...
 ...
diff --git a/installer/lib/mix/tasks/phx.new.ex b/installer/lib/mix/tasks/phx.new.ex
index 899a9f84b..01882b9d2 100644
--- a/installer/lib/mix/tasks/phx.new.ex
+++ b/installer/lib/mix/tasks/phx.new.ex
@@ -266,8 +266,14 @@ defmodule Mix.Tasks.Phx.New do
     Code.ensure_loaded?(Hex)
   end
 
-  defp rebar_available? do
-    Mix.Rebar.rebar_cmd(:rebar3)
+  if Version.match?(System.version(), "~> 1.18") do
+    defp rebar_available? do
+      true
+    end
+  else
+    defp rebar_available? do
+      Mix.Rebar.rebar_cmd(:rebar3)
+    end
   end
 
   defp print_missing_steps(steps) do
diff --git a/installer/templates/phx_single/mix.exs b/installer/templates/phx_single/mix.exs
index 60fe5b267..828252311 100644
--- a/installer/templates/phx_single/mix.exs
+++ b/installer/templates/phx_single/mix.exs
@@ -13,7 +13,8 @@ defmodule <%= @app_module %>.MixProject do
       elixirc_paths: elixirc_paths(Mix.env()),
       start_permanent: Mix.env() == :prod,
       aliases: aliases(),
-      deps: deps()
+      deps: deps(),
+      listeners: [Phoenix.CodeReloader]
     ]
   end
 
diff --git a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
index 12f347b4d..620aa84ec 100644
--- a/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
+++ b/installer/templates/phx_umbrella/apps/app_name_web/mix.exs
@@ -13,7 +13,8 @@ defmodule <%= @web_namespace %>.MixProject do
       elixirc_paths: elixirc_paths(Mix.env()),
       start_permanent: Mix.env() == :prod,
       aliases: aliases(),
-      deps: deps()
+      deps: deps(),
+      listeners: [Phoenix.CodeReloader]
     ]
   end
 
diff --git a/installer/templates/phx_web/components/layouts/app.html.heex b/installer/templates/phx_web/components/layouts/app.html.heex
index c50aa21d2..d16409073 100644
--- a/installer/templates/phx_web/components/layouts/app.html.heex
+++ b/installer/templates/phx_web/components/layouts/app.html.heex
@@ -26,7 +26,7 @@
 </header>
 <main class="px-4 py-20 sm:px-6 lg:px-8">
   <div class="mx-auto max-w-2xl">
-    <.flash_group flash={@flash} />
     <%%= @inner_content %>
   </div>
 </main>
+<.flash_group flash={@flash} />
diff --git a/lib/mix/tasks/phx.gen.auth/hashing_library.ex b/lib/mix/tasks/phx.gen.auth/hashing_library.ex
index cd1d2b7c7..2d37afc26 100644
--- a/lib/mix/tasks/phx.gen.auth/hashing_library.ex
+++ b/lib/mix/tasks/phx.gen.auth/hashing_library.ex
@@ -40,7 +40,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.HashingLibrary do
     lib = %__MODULE__{
       name: :argon2,
       module: Argon2,
-      mix_dependency: ~s|{:argon2_elixir, "~> 3.0"}|,
+      mix_dependency: ~s|{:argon2_elixir, "~> 4.0"}|,
       test_config: """
       config :argon2_elixir, t_cost: 1, m_cost: 8
       """
diff --git a/lib/phoenix/code_reloader.ex b/lib/phoenix/code_reloader.ex
index 809d55525..433cc7547 100644
--- a/lib/phoenix/code_reloader.ex
+++ b/lib/phoenix/code_reloader.ex
@@ -31,6 +31,19 @@ defmodule Phoenix.CodeReloader do
 
   This function is a no-op and returns `:ok` if Mix is not available.
 
+  The reloader should also be configured as a Mix listener in project's
+  mix.exs file (since Elixir v1.18):
+
+      def project do
+        [
+          ...,
+          listeners: [Phoenix.CodeReloader]
+        ]
+      end
+
+  This way the reloader can notice whenever the project is compiled
+  concurrently.
+
   ## Options
 
     * `:reloadable_args` - additional CLI args to pass to the compiler tasks.
@@ -57,6 +70,10 @@ defmodule Phoenix.CodeReloader do
   @spec sync :: :ok
   defdelegate sync, to: Phoenix.CodeReloader.Server
 
+  @doc false
+  @spec child_spec(keyword) :: Supervisor.child_spec()
+  defdelegate child_spec(opts), to: Phoenix.CodeReloader.MixListener
+
   ## Plug
 
   @behaviour Plug
diff --git a/lib/phoenix/code_reloader/mix_listener.ex b/lib/phoenix/code_reloader/mix_listener.ex
new file mode 100644
index 000000000..f136314ee
--- /dev/null
+++ b/lib/phoenix/code_reloader/mix_listener.ex
@@ -0,0 +1,72 @@
+defmodule Phoenix.CodeReloader.MixListener do
+  @moduledoc false
+
+  use GenServer
+
+  @name __MODULE__
+
+  @spec start_link(keyword) :: GenServer.on_start()
+  def start_link(_opts) do
+    GenServer.start_link(__MODULE__, {}, name: @name)
+  end
+
+  @spec started? :: boolean()
+  def started? do
+    Process.whereis(Phoenix.CodeReloader.MixListener) != nil
+  end
+
+  @doc """
+  Unloads all modules invalidated by external compilations.
+
+  Only reloads modules from the given apps.
+  """
+  @spec purge([atom()]) :: :ok
+  def purge(apps) do
+    GenServer.call(@name, {:purge, apps}, :infinity)
+  end
+
+  @impl true
+  def init({}) do
+    {:ok, %{to_purge: %{}}}
+  end
+
+  @impl true
+  def handle_call({:purge, apps}, _from, state) do
+    for app <- apps, modules = state.to_purge[app] do
+      purge_modules(modules)
+    end
+
+    {:reply, :ok, %{state | to_purge: %{}}}
+  end
+
+  @impl true
+  def handle_info({:modules_compiled, info}, state) do
+    if info.os_pid == System.pid() do
+      # Ignore compilations from ourselves, because the modules are
+      # already updated in memory
+      {:noreply, state}
+    else
+      %{changed: changed, removed: removed} = info.modules_diff
+
+      state =
+        update_in(state.to_purge[info.app], fn to_purge ->
+          to_purge = to_purge || MapSet.new()
+          to_purge = Enum.into(changed, to_purge)
+          Enum.into(removed, to_purge)
+        end)
+
+      {:noreply, state}
+    end
+  end
+
+  def handle_info(_message, state) do
+    {:noreply, state}
+  end
+
+  defp purge_modules(modules) do
+    for module <- modules do
+      :code.purge(module)
+      :code.delete(module)
+    end
+  end
+end
diff --git a/lib/phoenix/code_reloader/server.ex b/lib/phoenix/code_reloader/server.ex
index 8f56f423c..16ff92bf1 100644
--- a/lib/phoenix/code_reloader/server.ex
+++ b/lib/phoenix/code_reloader/server.ex
@@ -63,23 +63,39 @@ defmodule Phoenix.CodeReloader.Server do
     apps = endpoint.config(:reloadable_apps) || default_reloadable_apps()
     args = Keyword.get(opts, :reloadable_args, ["--no-all-warnings"])
 
-    # We do a backup of the endpoint in case compilation fails.
-    # If so we can bring it back to finish the request handling.
-    backup = load_backup(endpoint)
     froms = all_waiting([from], endpoint)
 
-    {res, out} =
-      proxy_io(fn ->
-        try do
-          mix_compile(Code.ensure_loaded(Mix.Task), compilers, apps, args, state.timestamp)
-        catch
-          :exit, {:shutdown, 1} ->
-            :error
-
-          kind, reason ->
-            IO.puts(Exception.format(kind, reason, __STACKTRACE__))
-            :error
-        end
+    {backup, res, out} =
+      with_build_lock(fn ->
+        purge_fallback? =
+          if Phoenix.CodeReloader.MixListener.started?() do
+            Phoenix.CodeReloader.MixListener.purge(apps)
+            false
+          else
+            warn_missing_mix_listener()
+            true
+          end
+
+        # We do a backup of the endpoint in case compilation fails.
+        # If so we can bring it back to finish the request handling.
+        backup = load_backup(endpoint)
+
+        {res, out} =
+          proxy_io(fn ->
+            try do
+              task_loaded = Code.ensure_loaded(Mix.Task)
+              mix_compile(task_loaded, compilers, apps, args, state.timestamp, purge_fallback?)
+            catch
+              :exit, {:shutdown, 1} ->
+                :error
+
+              kind, reason ->
+                IO.puts(Exception.format(kind, reason, __STACKTRACE__))
+                :error
+            end
+          end)
+
+        {backup, res, out}
       end)
 
     reply =
@@ -175,7 +191,34 @@ defmodule Phoenix.CodeReloader.Server do
     defp purge_protocols(_path), do: :ok
   end
 
-  defp mix_compile({:module, Mix.Task}, compilers, apps_to_reload, compile_args, timestamp) do
+  defp warn_missing_mix_listener do
+    listeners_supported? = Version.match?(System.version(), ">= 1.18.0-dev")
+
+    if listeners_supported? do
+      IO.warn("""
+      a Mix listener expected by Phoenix.CodeReloader is missing.
+
+      Please add the listener to your mix.exs configuration, like so:
+
+          def project do
+            [
+              ...,
+              listeners: [Phoenix.CodeReloader]
+            ]
+          end
+
+      """)
+    end
+  end
+
+  defp mix_compile(
+         {:module, Mix.Task},
+         compilers,
+         apps_to_reload,
+         compile_args,
+         timestamp,
+         purge_fallback?
+       ) do
     config = Mix.Project.config()
     path = Mix.Project.consolidation_path(config)
 
@@ -184,8 +227,25 @@ defmodule Phoenix.CodeReloader.Server do
       purge_protocols(path)
     end
 
-    mix_compile_deps(Mix.Dep.cached(), apps_to_reload, compile_args, compilers, timestamp, path)
-    mix_compile_project(config[:app], apps_to_reload, compile_args, compilers, timestamp, path)
+    mix_compile_deps(
+      Mix.Dep.cached(),
+      apps_to_reload,
+      compile_args,
+      compilers,
+      timestamp,
+      path,
+      purge_fallback?
+    )
+
+    mix_compile_project(
+      config[:app],
+      apps_to_reload,
+      compile_args,
+      compilers,
+      timestamp,
+      path,
+      purge_fallback?
+    )
 
     if config[:consolidate_protocols] do
       # If we are consolidating protocols, we need to purge all of its modules
@@ -198,30 +258,46 @@ defmodule Phoenix.CodeReloader.Server do
     :ok
   end
 
-  defp mix_compile({:error, _reason}, _, _, _, _) do
+  defp mix_compile({:error, _reason}, _, _, _, _, _) do
     raise "the Code Reloader is enabled but Mix is not available. If you want to " <>
             "use the Code Reloader in production or inside an escript, you must add " <>
             ":mix to your applications list. Otherwise, you must disable code reloading " <>
             "in such environments"
   end
 
-  defp mix_compile_deps(deps, apps_to_reload, compile_args, compilers, timestamp, path) do
+  defp mix_compile_deps(
+         deps,
+         apps_to_reload,
+         compile_args,
+         compilers,
+         timestamp,
+         path,
+         purge_fallback?
+       ) do
     for dep <- deps, dep.app in apps_to_reload do
       Mix.Dep.in_dependency(dep, fn _ ->
-        mix_compile_unless_stale_config(compilers, compile_args, timestamp, path)
+        mix_compile_unless_stale_config(compilers, compile_args, timestamp, path, purge_fallback?)
       end)
     end
   end
 
-  defp mix_compile_project(nil, _, _, _, _, _), do: :ok
-
-  defp mix_compile_project(app, apps_to_reload, compile_args, compilers, timestamp, path) do
+  defp mix_compile_project(nil, _, _, _, _, _, _), do: :ok
+
+  defp mix_compile_project(
+         app,
+         apps_to_reload,
+         compile_args,
+         compilers,
+         timestamp,
+         path,
+         purge_fallback?
+       ) do
     if app in apps_to_reload do
-      mix_compile_unless_stale_config(compilers, compile_args, timestamp, path)
+      mix_compile_unless_stale_config(compilers, compile_args, timestamp, path, purge_fallback?)
     end
   end
 
-  defp mix_compile_unless_stale_config(compilers, compile_args, timestamp, path) do
+  defp mix_compile_unless_stale_config(compilers, compile_args, timestamp, path, purge_fallback?) do
     manifests = Mix.Tasks.Compile.Elixir.manifests()
     configs = Mix.Project.config_files()
     config = Mix.Project.config()
@@ -230,7 +306,8 @@ defmodule Phoenix.CodeReloader.Server do
       [] ->
         # If the manifests are more recent than the timestamp,
         # someone updated this app behind the scenes, so purge all beams.
-        if Mix.Utils.stale?(manifests, [timestamp]) do
+        # TODO: remove once we depend on Elixir 1.18
+        if purge_fallback? and Mix.Utils.stale?(manifests, [timestamp]) do
           purge_modules(Path.join(Mix.Project.app_path(config), "ebin"))
         end
 
@@ -351,4 +428,11 @@ defmodule Phoenix.CodeReloader.Server do
       Logger.configure(compile_time_application: logger_config_app)
     end
   end
+
+  # TODO: remove once we depend on Elixir 1.18
+  if Code.ensure_loaded?(Mix.Project) and function_exported?(Mix.Project, :with_build_lock, 1) do
+    defp with_build_lock(fun), do: Mix.Project.with_build_lock(fun)
+  else
+    defp with_build_lock(fun), do: fun.()
+  end
 end
diff --git a/lib/phoenix/controller.ex b/lib/phoenix/controller.ex
index 95cb139e5..74f1d5bd4 100644
--- a/lib/phoenix/controller.ex
+++ b/lib/phoenix/controller.ex
@@ -1274,7 +1274,7 @@ defmodule Phoenix.Controller do
   end
 
   defp encode_filename(filename, false), do: filename
-  defp encode_filename(filename, true), do: URI.encode(filename)
+  defp encode_filename(filename, true), do: URI.encode(filename, &URI.char_unreserved?/1)
 
   defp get_disposition_type(:attachment), do: "attachment"
   defp get_disposition_type(:inline), do: "inline"
diff --git a/lib/phoenix/param.ex b/lib/phoenix/param.ex
index 7ccd2b767..34a5d70d5 100644
--- a/lib/phoenix/param.ex
+++ b/lib/phoenix/param.ex
@@ -1,25 +1,34 @@
 defprotocol Phoenix.Param do
-  @moduledoc """
+  @moduledoc ~S"""
   A protocol that converts data structures into URL parameters.
 
-  This protocol is used by URL helpers and other parts of the
+  This protocol is used by `Phoenix.VerifiedRoutes` and other parts of the
   Phoenix stack. For example, when you write:
 
-      user_path(conn, :edit, @user)
+      ~p"/user/#{@user}/edit"
 
   Phoenix knows how to extract the `:id` from `@user` thanks
   to this protocol.
 
+  (Deprecated URL helpers, e.g. `user_path(conn, :edit, @user)`, work the
+  same way.)
+
   By default, Phoenix implements this protocol for integers, binaries, atoms,
   and structs. For structs, a key `:id` is assumed, but you may provide a
   specific implementation.
 
-  Nil values cannot be converted to param.
+  The term `nil` cannot be converted to param.
 
   ## Custom parameters
 
   In order to customize the parameter for any struct,
-  one can simply implement this protocol.
+  one can simply implement this protocol. For example for a `Date` struct:
+
+      defimpl Phoenix.Param, for: Date do
+        def to_param(date) do
+          Date.to_string(date)
+        end
+      end
 
   However, for convenience, this protocol can also be
   derivable. For example:
@@ -50,7 +59,7 @@ defprotocol Phoenix.Param do
 
   @fallback_to_any true
 
-  @spec to_param(term) :: String.t
+  @spec to_param(term) :: String.t()
   def to_param(term)
 end
 
@@ -79,7 +88,7 @@ end
 defimpl Phoenix.Param, for: Map do
   def to_param(map) do
     raise ArgumentError,
-      "maps cannot be converted to_param. A struct was expected, got: #{inspect map}"
+          "maps cannot be converted to_param. A struct was expected, got: #{inspect(map)}"
   end
 end
 
@@ -88,16 +97,18 @@ defimpl Phoenix.Param, for: Any do
     key = Keyword.get(options, :key, :id)
 
     unless Map.has_key?(struct, key) do
-      raise ArgumentError, "cannot derive Phoenix.Param for struct #{inspect module} " <>
-                           "because it does not have key #{inspect key}. Please pass " <>
-                           "the :key option when deriving"
+      raise ArgumentError,
+            "cannot derive Phoenix.Param for struct #{inspect(module)} " <>
+              "because it does not have key #{inspect(key)}. Please pass " <>
+              "the :key option when deriving"
     end
 
     quote do
       defimpl Phoenix.Param, for: unquote(module) do
         def to_param(%{unquote(key) => nil}) do
-          raise ArgumentError, "cannot convert #{inspect unquote(module)} to param, " <>
-                               "key #{inspect unquote(key)} contains a nil value"
+          raise ArgumentError,
+                "cannot convert #{inspect(unquote(module))} to param, " <>
+                  "key #{inspect(unquote(key))} contains a nil value"
         end
 
         def to_param(%{unquote(key) => key}) when is_integer(key), do: Integer.to_string(key)
@@ -110,15 +121,16 @@ defimpl Phoenix.Param, for: Any do
   def to_param(%{id: nil}) do
     raise ArgumentError, "cannot convert struct to param, key :id contains a nil value"
   end
+
   def to_param(%{id: id}) when is_integer(id), do: Integer.to_string(id)
   def to_param(%{id: id}) when is_binary(id), do: id
   def to_param(%{id: id}), do: Phoenix.Param.to_param(id)
 
   def to_param(map) when is_map(map) do
     raise ArgumentError,
-      "structs expect an :id key when converting to_param or a custom implementation " <>
-      "of the Phoenix.Param protocol (read Phoenix.Param docs for more information), " <>
-      "got: #{inspect map}"
+          "structs expect an :id key when converting to_param or a custom implementation " <>
+            "of the Phoenix.Param protocol (read Phoenix.Param docs for more information), " <>
+            "got: #{inspect(map)}"
   end
 
   def to_param(data) do
diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex
index 9725b1a21..ad4683ddf 100644
--- a/lib/phoenix/router.ex
+++ b/lib/phoenix/router.ex
@@ -210,15 +210,15 @@ defmodule Phoenix.Router do
 
   Scopes allow us to scope on any path or even on the helper name:
 
-      scope "/v1", MyAppWeb, host: "api." do
+      scope "/api/v1", MyAppWeb, as: :api_v1 do
         get "/pages/:id", PageController, :show
       end
 
   For example, the route above will match on the path `"/api/v1/pages/1"`
-  and the named route will be `api_v1_page_path`, as expected from the
-  values given to `scope/2` option.
+  and the named helper will be `api_v1_page_path`, as expected from the
+  values given to `scope/4` option.
 
-  Like all paths you can define dynamic segments that will be applied as
+  Like all paths, you can define dynamic segments that will be applied as
   parameters in the controller:
 
       scope "/api/:version", MyAppWeb do
diff --git a/lib/phoenix/socket.ex b/lib/phoenix/socket.ex
index 77286a8db..ce7920c31 100644
--- a/lib/phoenix/socket.ex
+++ b/lib/phoenix/socket.ex
@@ -509,7 +509,7 @@ defmodule Phoenix.Socket do
   defp result({:error, _}), do: :error
 
   def __init__({state, %{id: id, endpoint: endpoint} = socket}) do
-    _ = id && endpoint.subscribe(id, link: true)
+    _ = id && endpoint.subscribe(id)
     {:ok, {state, %{socket | transport_pid: self()}}}
   end
 
diff --git a/lib/phoenix/transports/long_poll.ex b/lib/phoenix/transports/long_poll.ex
index de3365cee..97cb107a1 100644
--- a/lib/phoenix/transports/long_poll.ex
+++ b/lib/phoenix/transports/long_poll.ex
@@ -226,7 +226,7 @@ defmodule Phoenix.Transports.LongPoll do
   defp client_ref(pid) when is_pid(pid), do: self()
 
   defp subscribe(endpoint, topic) when is_binary(topic),
-    do: Phoenix.PubSub.subscribe(endpoint.config(:pubsub_server), topic, link: true)
+    do: Phoenix.PubSub.subscribe(endpoint.config(:pubsub_server), topic)
 
   defp subscribe(_endpoint, pid) when is_pid(pid),
     do: :ok
diff --git a/lib/phoenix/transports/long_poll_server.ex b/lib/phoenix/transports/long_poll_server.ex
index 37e8c90bd..e3a5f23d8 100644
--- a/lib/phoenix/transports/long_poll_server.ex
+++ b/lib/phoenix/transports/long_poll_server.ex
@@ -33,7 +33,7 @@ defmodule Phoenix.Transports.LongPoll.Server do
           client_ref: nil
         }
 
-        :ok = PubSub.subscribe(state.pubsub_server, priv_topic, link: true)
+        :ok = PubSub.subscribe(state.pubsub_server, priv_topic)
         schedule_inactive_shutdown(state.window_ms)
         {:ok, state}
 


Test patch:
diff --git a/installer/templates/phx_test/live/page_live_test.exs b/installer/templates/phx_test/live/page_live_test.exs
deleted file mode 100644
index 98693942f..000000000
--- a/installer/templates/phx_test/live/page_live_test.exs
+++ /dev/null
@@ -1,14 +0,0 @@
-defmodule <%= @web_namespace %>.PageLiveTest do
-  use <%= @web_namespace %>.ConnCase
-
-  import Phoenix.LiveViewTest
-
-  test "disconnected and connected render", %{conn: conn} do
-    conn = get(conn, ~p"/")
-    disconnected_html = html_response(conn, 200)
-    {:ok, _live, connected_html} = live(conn)
-
-    assert disconnected_html =~ "Welcome to Phoenix!"
-    assert connected_html =~ "Welcome to Phoenix!"
-  end
-end
diff --git a/integration_test/mix.exs b/integration_test/mix.exs
index 81012c0c8..4c45c7fcf 100644
--- a/integration_test/mix.exs
+++ b/integration_test/mix.exs
@@ -54,7 +54,7 @@ defmodule Phoenix.Integration.MixProject do
       {:swoosh, "~> 1.16"},
       {:bandit, "~> 1.0"},
       {:bcrypt_elixir, "~> 3.0"},
-      {:argon2_elixir, "~> 3.0"},
+      {:argon2_elixir, "~> 4.0"},
       {:pbkdf2_elixir, "~> 2.0"},
       {:tailwind, "~> 0.2"},
       {:heroicons,
diff --git a/integration_test/mix.lock b/integration_test/mix.lock
index 59445ecf5..59d00655d 100644
--- a/integration_test/mix.lock
+++ b/integration_test/mix.lock
@@ -1,10 +1,10 @@
 %{
-  "argon2_elixir": {:hex, :argon2_elixir, "3.2.1", "f47740bf9f2a39ffef79ba48eb25dea2ee37bcc7eadf91d49615591d1a6fce1a", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "a813b78217394530b5fcf4c8070feee43df03ffef938d044019169c766315690"},
+  "argon2_elixir": {:hex, :argon2_elixir, "4.1.0", "2f242afe47c373663cb404eb75e792f749507075ed737b49685a9f2edcb401df", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ecb6f2ca2cca34b28e546224661bf2a85714516d2713c7313c5ffe8bdade7cf"},
   "bandit": {:hex, :bandit, "1.5.4", "8e56e7cfc06f3c57995be0d9bf4e45b972d8732f5c7e96ef8ec0735f52079527", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "04c2b38874769af67fe7f10034f606ad6dda1d8f80c4d7a0c616b347584d5aff"},
   "bcrypt_elixir": {:hex, :bcrypt_elixir, "3.1.0", "0b110a9a6c619b19a7f73fa3004aa11d6e719a67e672d1633dc36b6b2290a0f7", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "2ad2acb5a8bc049e8d5aa267802631912bb80d5f4110a178ae7999e69dca1bf7"},
-  "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
+  "castore": {:hex, :castore, "1.0.9", "5cc77474afadf02c7c017823f460a17daa7908e991b0cc917febc90e466a375c", [:mix], [], "hexpm", "5ea956504f1ba6f2b4eb707061d8e17870de2bee95fb59d512872c2ef06925e7"},
   "cc_precompiler": {:hex, :cc_precompiler, "0.1.10", "47c9c08d8869cf09b41da36538f62bc1abd3e19e41701c2cea2675b53c704258", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f6e046254e53cd6b41c6bacd70ae728011aa82b2742a80d6e2214855c6e06b22"},
-  "comeonin": {:hex, :comeonin, "5.4.0", "246a56ca3f41d404380fc6465650ddaa532c7f98be4bda1b4656b3a37cc13abe", [:mix], [], "hexpm", "796393a9e50d01999d56b7b8420ab0481a7538d0caf80919da493b4a6e51faf1"},
+  "comeonin": {:hex, :comeonin, "5.5.0", "364d00df52545c44a139bad919d7eacb55abf39e86565878e17cebb787977368", [:mix], [], "hexpm", "6287fc3ba0aad34883cbe3f7949fc1d1e738e5ccdce77165bc99490aa69f47fb"},
   "db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
   "decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
   "dns_cluster": {:hex, :dns_cluster, "0.1.3", "0bc20a2c88ed6cc494f2964075c359f8c2d00e1bf25518a6a6c7fd277c9b0c66", [:mix], [], "hexpm", "46cb7c4a1b3e52c7ad4cbe33ca5079fbde4840dedeafca2baf77996c2da1bc33"},
@@ -13,12 +13,12 @@
   "ecto_sqlite3": {:hex, :ecto_sqlite3, "0.16.0", "1cdc8ea6319e7cb1bc273a36db0ecde69ad56b4dea3037689ad8c0afc6a91e16", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.11", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:exqlite, "~> 0.22", [hex: :exqlite, repo: "hexpm", optional: false]}], "hexpm", "73c9dd56830d67c951bc254c082cb0a7f9fa139d44866bc3186c8859d1b4d787"},
   "elixir_make": {:hex, :elixir_make, "0.8.4", "4960a03ce79081dee8fe119d80ad372c4e7badb84c493cc75983f9d3bc8bde0f", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.0", [hex: :certifi, repo: "hexpm", optional: true]}], "hexpm", "6e7f1d619b5f61dfabd0a20aa268e575572b542ac31723293a4c1a567d5ef040"},
   "esbuild": {:hex, :esbuild, "0.8.1", "0cbf919f0eccb136d2eeef0df49c4acf55336de864e63594adcea3814f3edf41", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "25fc876a67c13cb0a776e7b5d7974851556baeda2085296c14ab48555ea7560f"},
-  "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"},
+  "expo": {:hex, :expo, "1.1.0", "f7b9ed7fb5745ebe1eeedf3d6f29226c5dd52897ac67c0f8af62a07e661e5c75", [:mix], [], "hexpm", "fbadf93f4700fb44c331362177bdca9eeb8097e8b0ef525c9cc501cb9917c960"},
   "exqlite": {:hex, :exqlite, "0.23.0", "6e851c937a033299d0784994c66da24845415072adbc455a337e20087bce9033", [:make, :mix], [{:cc_precompiler, "~> 0.1", [hex: :cc_precompiler, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.8", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "404341cceec5e6466aaed160cf0b58be2019b60af82588c215e1224ebd3ec831"},
   "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
   "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"},
   "floki": {:hex, :floki, "0.36.2", "a7da0193538c93f937714a6704369711998a51a6164a222d710ebd54020aa7a3", [:mix], [], "hexpm", "a8766c0bc92f074e5cb36c4f9961982eda84c5d2b8e979ca67f5c268ec8ed580"},
-  "gettext": {:hex, :gettext, "0.24.0", "6f4d90ac5f3111673cbefc4ebee96fe5f37a114861ab8c7b7d5b30a1108ce6d8", [:mix], [{:expo, "~> 0.5.1", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "bdf75cdfcbe9e4622dd18e034b227d77dd17f0f133853a1c73b97b3d6c770e8b"},
+  "gettext": {:hex, :gettext, "0.26.1", "38e14ea5dcf962d1fc9f361b63ea07c0ce715a8ef1f9e82d3dfb8e67e0416715", [:mix], [{:expo, "~> 0.5.1 or ~> 1.0", [hex: :expo, repo: "hexpm", optional: false]}], "hexpm", "01ce56f188b9dc28780a52783d6529ad2bc7124f9744e571e1ee4ea88bf08734"},
   "heroicons": {:git, "https://github.com/tailwindlabs/heroicons.git", "88ab3a0d790e6a47404cba02800a6b25d2afae50", [tag: "v2.1.1", sparse: "optimized", depth: 1]},
   "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"},
   "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"},
diff --git a/test/mix/tasks/phx.gen.auth_test.exs b/test/mix/tasks/phx.gen.auth_test.exs
index d0944d8f7..e252d1167 100644
--- a/test/mix/tasks/phx.gen.auth_test.exs
+++ b/test/mix/tasks/phx.gen.auth_test.exs
@@ -1050,7 +1050,7 @@ defmodule Mix.Tasks.Phx.Gen.AuthTest do
         assert_received {:mix_shell, :yes?, [@liveview_option_message]}
 
         assert_file("mix.exs", fn file ->
-          assert file =~ ~s|{:argon2_elixir, "~> 3.0"}|
+          assert file =~ ~s|{:argon2_elixir, "~> 4.0"}|
         end)
 
         assert_file("config/test.exs", fn file ->
diff --git a/test/phoenix/controller/controller_test.exs b/test/phoenix/controller/controller_test.exs
index 279715fa7..4b2057f09 100644
--- a/test/phoenix/controller/controller_test.exs
+++ b/test/phoenix/controller/controller_test.exs
@@ -537,6 +537,17 @@ defmodule Phoenix.Controller.ControllerTest do
              "world"
     end
 
+    test "sends file for download for filename with unreserved characters" do
+      conn = send_download(conn(:get, "/"), {:file, @hello_txt}, filename: "hello, world.json")
+      assert conn.status == 200
+      assert get_resp_header(conn, "content-disposition") ==
+             ["attachment; filename=\"hello%2C%20world.json\"; filename*=utf-8''hello%2C%20world.json"]
+      assert get_resp_header(conn, "content-type") ==
+             ["application/json"]
+      assert conn.resp_body ==
+             "world"
+    end
+
     test "sends file supports UTF-8" do
       conn = send_download(conn(:get, "/"), {:file, @hello_txt}, filename: "测 试.txt")
       assert conn.status == 200