From d8da7029a19cf3b0cae867fe25b313f203e9adfb Mon Sep 17 00:00:00 2001 From: Dalton Alexandre <166029845+dl-alexandre@users.noreply.github.com> Date: Sun, 22 Mar 2026 13:23:23 -0700 Subject: [PATCH 1/5] fix(phx.new): validate paths with colons before generating Adds early validation in Project.new/2 to detect colons in the project path that would produce invalid OTP application names. Raises a helpful error suggesting the --app flag when the path contains invalid characters. Fixes #6585 --- installer/lib/phx_new/project.ex | 10 ++++++++++ installer/test/phx_new_test.exs | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/installer/lib/phx_new/project.ex b/installer/lib/phx_new/project.ex index 36885b37d1..c62be32e30 100644 --- a/installer/lib/phx_new/project.ex +++ b/installer/lib/phx_new/project.ex @@ -22,6 +22,16 @@ defmodule Phx.New.Project do def new(project_path, opts) do project_path = Path.expand(project_path) app = opts[:app] || Path.basename(project_path) + + path_app = Path.basename(project_path) + + if is_nil(opts[:app]) and path_app =~ ~r/:/ do + Mix.raise( + "The project path contains characters not valid in OTP application names. " <> + "Use --app to specify a valid name: mix phx.new #{path_app} --app my_app" + ) + end + app_mod = Module.concat([opts[:module] || Macro.camelize(app)]) %Project{ diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs index 1e153da77b..e578246a55 100644 --- a/installer/test/phx_new_test.exs +++ b/installer/test/phx_new_test.exs @@ -832,6 +832,21 @@ defmodule Mix.Tasks.Phx.NewTest do end end + test "new with colon in path without --app flag raises error" do + assert_raise Mix.Error, + ~r/The project path contains characters not valid in OTP application names/, + fn -> + Mix.Tasks.Phx.New.run(["my:app"]) + end + end + + test "new with --app flag overrides invalid path app name" do + in_tmp("new with app flag override", fn -> + Mix.Tasks.Phx.New.run(["007_invalid", "--app", "valid_app"]) + assert_file("007_invalid/mix.exs", ~r/app: :valid_app/) + end) + end + test "new from inside docker machine (simulated)" do in_tmp("new without defaults", fn -> Mix.Tasks.Phx.New.run([@app_name, "--inside-docker-env"]) From 0758a5fd7a7a376460e50263ccfbba751b6d7279 Mon Sep 17 00:00:00 2001 From: Dalton Alexandre <166029845+dl-alexandre@users.noreply.github.com> Date: Mon, 23 Mar 2026 07:04:56 -0700 Subject: [PATCH 2/5] fix(installer): handle paths with colons defensively in expand_path_with_bindings Instead of crashing with KeyError when path contains colons (e.g., 0:0/test), defensively check if the key exists in the Project struct before expanding. If not found, leave the colon-syntax unchanged. Fixes #6585 --- installer/lib/phx_new/project.ex | 14 ++++---------- installer/test/phx_new_test.exs | 15 --------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/installer/lib/phx_new/project.ex b/installer/lib/phx_new/project.ex index c62be32e30..3480a93dc1 100644 --- a/installer/lib/phx_new/project.ex +++ b/installer/lib/phx_new/project.ex @@ -23,15 +23,6 @@ defmodule Phx.New.Project do project_path = Path.expand(project_path) app = opts[:app] || Path.basename(project_path) - path_app = Path.basename(project_path) - - if is_nil(opts[:app]) and path_app =~ ~r/:/ do - Mix.raise( - "The project path contains characters not valid in OTP application names. " <> - "Use --app to specify a valid name: mix phx.new #{path_app} --app my_app" - ) - end - app_mod = Module.concat([opts[:module] || Macro.camelize(app)]) %Project{ @@ -89,7 +80,10 @@ defmodule Phx.New.Project do defp expand_path_with_bindings(path, %Project{} = project) do Regex.replace(Regex.recompile!(~r/:[a-zA-Z0-9_]+/), path, fn ":" <> key, _ -> - project |> Map.fetch!(:"#{key}") |> to_string() + case Map.fetch(project, :"#{key}") do + {:ok, value} -> to_string(value) + :error -> ":#{key}" + end end) end end diff --git a/installer/test/phx_new_test.exs b/installer/test/phx_new_test.exs index e578246a55..1e153da77b 100644 --- a/installer/test/phx_new_test.exs +++ b/installer/test/phx_new_test.exs @@ -832,21 +832,6 @@ defmodule Mix.Tasks.Phx.NewTest do end end - test "new with colon in path without --app flag raises error" do - assert_raise Mix.Error, - ~r/The project path contains characters not valid in OTP application names/, - fn -> - Mix.Tasks.Phx.New.run(["my:app"]) - end - end - - test "new with --app flag overrides invalid path app name" do - in_tmp("new with app flag override", fn -> - Mix.Tasks.Phx.New.run(["007_invalid", "--app", "valid_app"]) - assert_file("007_invalid/mix.exs", ~r/app: :valid_app/) - end) - end - test "new from inside docker machine (simulated)" do in_tmp("new without defaults", fn -> Mix.Tasks.Phx.New.run([@app_name, "--inside-docker-env"]) From bca3a4fe9328c3a47819c20ee8b4d4534eb1bf27 Mon Sep 17 00:00:00 2001 From: Dalton Alexandre <166029845+dl-alexandre@users.noreply.github.com> Date: Tue, 24 Mar 2026 12:40:13 -0700 Subject: [PATCH 3/5] fix: provide clear error when importing VerifiedRoutes instead of using Closes #6550 When a user mistakenly uses Version: ImageMagick 7.1.2-18 Q16-HDRI aarch64 23822 https://imagemagick.org Copyright: (C) 1999 ImageMagick Studio LLC License: https://imagemagick.org/license/ Features: Cipher DPC HDRI Modules Delegates (built-in): bzlib freetype heic jng jpeg lcms ltdl lzma png tiff webp xml zlib zstd Compiler: clang (17.0.0) Usage: import [options ...] [ file ] Image Settings: -adjoin join images into a single multi-image file -border include window border in the output image -channel type apply option to select image channels -colorspace type alternate image colorspace -comment string annotate image with comment -compress type type of pixel compression when writing the image -define format:option define one or more image format options -density geometry horizontal and vertical density of the image -depth value image depth -descend obtain image by descending window hierarchy -display server X server to contact -dispose method layer disposal method -dither method apply error diffusion to image -delay value display the next image after pausing -encipher filename convert plain pixels to cipher pixels -endian type endianness (MSB or LSB) of the image -encoding type text encoding type -filter type use this filter when resizing an image -format "string" output formatted image characteristics -frame include window manager frame -gravity direction which direction to gravitate towards -identify identify the format and characteristics of the image -interlace type None, Line, Plane, or Partition -interpolate method pixel color interpolation method -label string assign a label to an image -limit type value Area, Disk, Map, or Memory resource limit -monitor monitor progress -page geometry size and location of an image canvas -pause seconds seconds delay between snapshots -pointsize value font point size -quality value JPEG/MIFF/PNG compression level -quiet suppress all warning messages -regard-warnings pay attention to warning messages -repage geometry size and location of an image canvas -respect-parentheses settings remain in effect until parenthesis boundary -sampling-factor geometry horizontal and vertical sampling factor -scene value image scene number -screen select image from root window -seed value seed a new sequence of pseudo-random numbers -set property value set an image property -silent operate silently, i.e. don't ring any bells -snaps value number of screen snapshots -support factor resize support: > 1.0 is blurry, < 1.0 is sharp -synchronize synchronize image to storage device -taint declare the image as modified -transparent-color color transparent color -treedepth value color tree depth -verbose print detailed information about the image -virtual-pixel method Constant, Edge, Mirror, or Tile -window id select window with this id or name root selects whole screen Image Operators: -annotate geometry text annotate the image with text -colors value preferred number of colors in the image -crop geometry preferred size and location of the cropped image -encipher filename convert plain pixels to cipher pixels -extent geometry set the image size -geometry geometry preferred size or location of the image -help print program options -monochrome transform image to black and white -negate replace every pixel with its complementary color -quantize colorspace reduce colors in this colorspace -resize geometry resize the image -rotate degrees apply Paeth rotation to the image -strip strip image of all profiles and comments -thumbnail geometry create a thumbnail of the image -transparent color make this color transparent within the image -trim trim image edges -type type image type Miscellaneous Options: -debug events display copious debugging information -help print program options -list type print a list of supported option arguments -log format format of debugging information -version print version information By default, 'file' is written in the MIFF image format. To specify a particular image format, precede the filename with an image format name and a colon (i.e. ps:image) or specify the image type as the filename suffix (i.e. image.ps). Specify 'file' as '-' for standard input or output. instead of , the module attribute is not set. This caused a cryptic BadMapError when the rewrite_path function tried to access config.path_prefixes on an empty list. This change adds a validation check at the start of build_route/5 that detects when the config attribute is missing (empty list) and raises a clear, actionable error message: attempted to use Phoenix.VerifiedRoutes without calling first. You must use with the appropriate options instead of importing it: use Phoenix.VerifiedRoutes, endpoint: MyAppWeb.Endpoint, router: MyAppWeb.Router This significantly improves the developer experience by guiding users to the correct usage pattern instead of showing a confusing implementation detail error. --- lib/phoenix/verified_routes.ex | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/phoenix/verified_routes.ex b/lib/phoenix/verified_routes.ex index 0585bde717..5e53302561 100644 --- a/lib/phoenix/verified_routes.ex +++ b/lib/phoenix/verified_routes.ex @@ -927,6 +927,18 @@ defmodule Phoenix.VerifiedRoutes do defp build_route(route_ast, sigil_p, env, endpoint_ctx, router) do config = Module.get_attribute(env.module, :phoenix_verified_config, []) + if config == [] do + raise ArgumentError, """ + attempted to use Phoenix.VerifiedRoutes without calling `use Phoenix.VerifiedRoutes` first. + + You must use `use Phoenix.VerifiedRoutes` with the appropriate options instead of importing it: + + use Phoenix.VerifiedRoutes, endpoint: MyAppWeb.Endpoint, router: MyAppWeb.Router + + See the documentation for more details on configuration options. + """ + end + router = case Macro.expand(router, env) do mod when is_atom(mod) -> From a07e2204729e39e356e0f9dd73a07e20c9e64c46 Mon Sep 17 00:00:00 2001 From: Dalton Alexandre <166029845+dl-alexandre@users.noreply.github.com> Date: Tue, 24 Mar 2026 13:59:46 -0700 Subject: [PATCH 4/5] fix: prevent unnecessary reconnection after intentional disconnect Closes #6578 Previously, calling socket.disconnect() and then returning to the tab would cause an automatic reconnection due to the visibilitychange handler not respecting the intentional disconnect. This change adds an intentionallyDisconnected flag that: 1. Gets set to true when disconnect() is called 2. Gets reset to false when connect() is called 3. Is checked in the visibilitychange handler before reconnecting Now, if a user intentionally disconnects the socket, switching tabs and returning will NOT automatically reconnect. The user must explicitly call socket.connect() to reconnect. Changes: - Add intentionallyDisconnected flag in constructor - Set flag in disconnect() method - Reset flag in connect() method - Check flag in visibilitychange handler --- assets/js/phoenix/socket.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/assets/js/phoenix/socket.js b/assets/js/phoenix/socket.js index 41e9760547..639e7a8aeb 100644 --- a/assets/js/phoenix/socket.js +++ b/assets/js/phoenix/socket.js @@ -131,6 +131,7 @@ export default class Socket { // transportConnect sets it to false on open. this.closeWasClean = true this.disconnecting = false + this.intentionallyDisconnected = false this.binaryType = opts.binaryType || "arraybuffer" this.connectClock = 1 this.pageHidden = false @@ -160,8 +161,8 @@ export default class Socket { this.pageHidden = true } else { this.pageHidden = false - // reconnect immediately - if(!this.isConnected() && !this.closeWasClean){ + // reconnect immediately if not intentionally disconnected + if(!this.isConnected() && !this.closeWasClean && !this.intentionallyDisconnected){ this.teardown(() => this.connect()) } } @@ -260,6 +261,7 @@ export default class Socket { disconnect(callback, code, reason){ this.connectClock++ this.disconnecting = true + this.intentionallyDisconnected = true this.closeWasClean = true clearTimeout(this.fallbackTimer) this.reconnectTimer.reset() @@ -277,6 +279,7 @@ export default class Socket { * `new Socket("/socket", {params: {user_id: userToken}})`. */ connect(params){ + this.intentionallyDisconnected = false if(params){ console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor") this.params = closure(params) From 358276124f859bcfe259c555e2fdc41f78865967 Mon Sep 17 00:00:00 2001 From: Dalton Alexandre <166029845+dl-alexandre@users.noreply.github.com> Date: Tue, 24 Mar 2026 14:53:40 -0700 Subject: [PATCH 5/5] feat: display host constraints in mix phx.routes output Closes #6589 When routes have host constraints (e.g., host: api.example.com), these are now displayed in the mix phx.routes output to help developers distinguish between routes that share the same path but are matched based on different hosts. Example: Before: GET /users MyAppWeb.UserController :index GET /users MyAppWeb.AdminUserController :index After: [api.example.com] GET /users MyAppWeb.UserController :index [admin.example.com] GET /users MyAppWeb.AdminUserController :index Changes: - Added hosts field to formatted route in __formatted_routes__ - Updated ConsoleFormatter to calculate and display host column - Hosts are shown as [host1, host2] when present, empty otherwise --- lib/phoenix/router.ex | 31 +++++++++++---------- lib/phoenix/router/console_formatter.ex | 37 +++++++++++++++++++------ 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/lib/phoenix/router.ex b/lib/phoenix/router.ex index 763d45da64..d138d24850 100644 --- a/lib/phoenix/router.ex +++ b/lib/phoenix/router.ex @@ -328,49 +328,49 @@ defmodule Phoenix.Router do if resource.singleton do Enum.each(resource.actions, fn :show -> - get path, ctrl, :show, opts + get(path, ctrl, :show, opts) :new -> - get path <> "/new", ctrl, :new, opts + get(path <> "/new", ctrl, :new, opts) :edit -> - get path <> "/edit", ctrl, :edit, opts + get(path <> "/edit", ctrl, :edit, opts) :create -> - post path, ctrl, :create, opts + post(path, ctrl, :create, opts) :delete -> - delete path, ctrl, :delete, opts + delete(path, ctrl, :delete, opts) :update -> - patch path, ctrl, :update, opts - put path, ctrl, :update, Keyword.put(opts, :as, nil) + patch(path, ctrl, :update, opts) + put(path, ctrl, :update, Keyword.put(opts, :as, nil)) end) else param = resource.param Enum.each(resource.actions, fn :index -> - get path, ctrl, :index, opts + get(path, ctrl, :index, opts) :show -> - get path <> "/:" <> param, ctrl, :show, opts + get(path <> "/:" <> param, ctrl, :show, opts) :new -> - get path <> "/new", ctrl, :new, opts + get(path <> "/new", ctrl, :new, opts) :edit -> - get path <> "/:" <> param <> "/edit", ctrl, :edit, opts + get(path <> "/:" <> param <> "/edit", ctrl, :edit, opts) :create -> - post path, ctrl, :create, opts + post(path, ctrl, :create, opts) :delete -> - delete path <> "/:" <> param, ctrl, :delete, opts + delete(path <> "/:" <> param, ctrl, :delete, opts) :update -> - patch path <> "/:" <> param, ctrl, :update, opts - put path <> "/:" <> param, ctrl, :update, Keyword.put(opts, :as, nil) + patch(path <> "/:" <> param, ctrl, :update, opts) + put(path <> "/:" <> param, ctrl, :update, Keyword.put(opts, :as, nil)) end) end end @@ -1302,6 +1302,7 @@ defmodule Phoenix.Router do helper: route.helper, verb: route.verb, path: route.path, + hosts: route.hosts, label: label } ] diff --git a/lib/phoenix/router/console_formatter.ex b/lib/phoenix/router/console_formatter.ex index f8085094c8..9ef92114be 100644 --- a/lib/phoenix/router/console_formatter.ex +++ b/lib/phoenix/router/console_formatter.ex @@ -78,29 +78,39 @@ defmodule Phoenix.Router.ConsoleFormatter do sockets = (endpoint && endpoint.__sockets__()) || [] widths = - Enum.reduce(routes, {0, 0, 0}, fn route, acc -> + Enum.reduce(routes, {0, 0, 0, 0}, fn route, acc -> %{verb: verb, path: path, helper: helper} = route verb = verb_name(verb) - {verb_len, path_len, route_name_len} = acc + {hosts_len, verb_len, path_len, route_name_len} = acc route_name = route_name(router, helper) + hosts_width = hosts_column_width(Map.get(route, :hosts, [])) - {max(verb_len, String.length(verb)), max(path_len, String.length(path)), - max(route_name_len, String.length(route_name))} + {max(hosts_len, hosts_width), max(verb_len, String.length(verb)), + max(path_len, String.length(path)), max(route_name_len, String.length(route_name))} end) Enum.reduce(sockets, widths, fn {path, _mod, opts}, acc -> - {verb_len, path_len, route_name_len} = acc + {hosts_len, verb_len, path_len, route_name_len} = acc verb_length = socket_verbs(opts) |> Enum.map(&String.length/1) |> Enum.max(&>=/2, fn -> 0 end) - {max(verb_len, verb_length), max(path_len, String.length(path <> "/websocket")), + {hosts_len, max(verb_len, verb_length), max(path_len, String.length(path <> "/websocket")), route_name_len} end) end + defp hosts_column_width([]), do: 0 + + defp hosts_column_width(hosts) when is_list(hosts) do + host_str = Enum.join(hosts, ", ") + String.length("[#{host_str}]") + 2 + end + + defp hosts_column_width(_), do: 0 + defp format_route(route, router, column_widths) do %{ verb: verb, @@ -110,9 +120,11 @@ defmodule Phoenix.Router.ConsoleFormatter do verb = verb_name(verb) route_name = route_name(router, Map.get(route, :helper)) - {verb_len, path_len, route_name_len} = column_widths + hosts = format_hosts(Map.get(route, :hosts, [])) + {hosts_len, verb_len, path_len, route_name_len} = column_widths - String.pad_leading(route_name, route_name_len) <> + String.pad_trailing(hosts, hosts_len) <> + String.pad_leading(route_name, route_name_len) <> " " <> String.pad_trailing(verb, verb_len) <> " " <> @@ -121,6 +133,15 @@ defmodule Phoenix.Router.ConsoleFormatter do label <> "\n" end + defp format_hosts([]), do: "" + + defp format_hosts(hosts) when is_list(hosts) do + host_str = Enum.join(hosts, ", ") + "[#{host_str}]" + end + + defp format_hosts(_), do: "" + defp route_name(_router, nil), do: "" defp route_name(router, name) do