diff --git a/changelog.d/debug-bar-reload-subpath.fixed.md b/changelog.d/debug-bar-reload-subpath.fixed.md
new file mode 100644
index 0000000000..bb6002d433
--- /dev/null
+++ b/changelog.d/debug-bar-reload-subpath.fixed.md
@@ -0,0 +1,6 @@
+- Debug bar reload link (and the CFML error page's displayed URL) now honors the `subpath`
+ setting: the base URL is composed from the resolved `webPath` plus the front-controller
+ filename — the same idiom as `urlFor()` — instead of raw `cgi.script_name`, so subfolder
+ deployments emit `/myapp/posts?reload=` instead of the unroutable
+ `/myapp/public/index.cfm/posts?reload=`. Root installs render byte-identical to before.
+ Extracted into the unit-tested `$buildDebugReloadUrl()` helper in `Global.cfc` ([#3344](https://github.com/wheels-dev/wheels/issues/3344))
diff --git a/vendor/wheels/Global.cfc b/vendor/wheels/Global.cfc
index 118a096c90..6ed06af54f 100644
--- a/vendor/wheels/Global.cfc
+++ b/vendor/wheels/Global.cfc
@@ -2699,6 +2699,80 @@ return local.$wheels;
return local.base & local.relative;
}
+ /**
+ * Internal function. Builds the debug bar's base reload URL (issue #3344).
+ * The base is composed from the resolved `webPath` plus the front-controller
+ * filename — the same idiom `urlFor()` uses — instead of raw
+ * `cgi.script_name`, so subfolder (subpath) installs emit links like
+ * `/myapp/posts?reload=` rather than `/myapp/public/index.cfm/posts?reload=`
+ * (which the user's rewrite rules don't route). The caller selects which
+ * path_info to pass (`request.cgi.path_info` when available, `cgi.path_info`
+ * otherwise — engines report it differently). `webPath` and `rewriteFile`
+ * default from application scope; tests pass them explicitly, and early
+ * boot/error paths where they're missing fall back to the raw script name
+ * (the pre-#3344 behavior). Pure string logic so it can be unit-tested in
+ * isolation.
+ */
+ public string function $buildDebugReloadUrl(
+ required string scriptName,
+ string pathInfo = "",
+ string queryString = "",
+ string webPath,
+ string rewriteFile
+ ) {
+ // Resolve webPath/rewriteFile from application scope unless overridden.
+ // No runtime default-arg expressions (some engines evaluate those
+ // eagerly) — same pattern as $resolveSubpathInclude.
+ if (StructKeyExists(arguments, "webPath")) {
+ local.resolvedWebPath = arguments.webPath;
+ } else if (IsDefined("application.wheels.webPath")) {
+ local.resolvedWebPath = application.wheels.webPath;
+ } else {
+ local.resolvedWebPath = "";
+ }
+ if (StructKeyExists(arguments, "rewriteFile")) {
+ local.resolvedRewriteFile = arguments.rewriteFile;
+ } else if (IsDefined("application.wheels.rewriteFile")) {
+ local.resolvedRewriteFile = application.wheels.rewriteFile;
+ } else {
+ local.resolvedRewriteFile = "";
+ }
+
+ // Base: webPath + front-controller filename (matches urlFor()); fall
+ // back to the raw script name when webPath isn't resolved yet.
+ if (Len(local.resolvedWebPath)) {
+ local.rv = local.resolvedWebPath & ListLast(arguments.scriptName, "/");
+ } else {
+ local.rv = arguments.scriptName;
+ }
+ if (arguments.pathInfo != arguments.scriptName) {
+ local.rv &= arguments.pathInfo;
+ }
+ if (Len(arguments.queryString)) {
+ local.rv &= "?" & arguments.queryString;
+ }
+ if (Len(local.resolvedRewriteFile)) {
+ local.rv = ReplaceNoCase(local.rv, "/" & local.resolvedRewriteFile, "");
+ }
+ local.reloadTokens = "development,testing,maintenance,production,true";
+ local.iEnd = ListLen(local.reloadTokens);
+ for (local.i = 1; local.i <= local.iEnd; local.i++) {
+ local.token = ListGetAt(local.reloadTokens, local.i);
+ local.rv = ReplaceNoCase(
+ ReplaceNoCase(local.rv, "?reload=" & local.token, ""),
+ "&reload=" & local.token,
+ ""
+ );
+ }
+ if (Find("?", local.rv)) {
+ local.rv &= "&";
+ } else {
+ local.rv &= "?";
+ }
+ local.rv &= "reload=";
+ return local.rv;
+ }
+
/**
* Internal function.
*/
diff --git a/vendor/wheels/events/onerror/cfmlerror.cfm b/vendor/wheels/events/onerror/cfmlerror.cfm
index d6509569ab..961588a704 100644
--- a/vendor/wheels/events/onerror/cfmlerror.cfm
+++ b/vendor/wheels/events/onerror/cfmlerror.cfm
@@ -65,9 +65,19 @@
+
+
+
+
+
+
URL
-
https://#EncodeForHTML(cgi.server_name)##Replace(cgi.script_name, "/#application.wheels.rewriteFile#", "")##EncodeForHTML(request.cgi.path_info)##EncodeForHTML(cgi.path_info)#?#EncodeForHTML(cgi.query_string)#
+
https://#EncodeForHTML(cgi.server_name)##EncodeForHTML(local.errorUrlBase)##EncodeForHTML(request.cgi.path_info)##EncodeForHTML(cgi.path_info)#?#EncodeForHTML(cgi.query_string)#
diff --git a/vendor/wheels/events/onrequestend/debug.cfm b/vendor/wheels/events/onrequestend/debug.cfm
index ad7b2c944f..a72774cb84 100644
--- a/vendor/wheels/events/onrequestend/debug.cfm
+++ b/vendor/wheels/events/onrequestend/debug.cfm
@@ -7,33 +7,21 @@ OR (StructKeyExists(local.reqHeaders, "X-Fetch") AND local.reqHeaders["X-Fetch"]
OR (StructKeyExists(url, "format") AND ListFindNoCase("json,xml,csv,pdf", url.format))>
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
-
+
-
+
diff --git a/vendor/wheels/tests/specs/global/BuildDebugReloadUrlSpec.cfc b/vendor/wheels/tests/specs/global/BuildDebugReloadUrlSpec.cfc
new file mode 100644
index 0000000000..9f2a60d3e7
--- /dev/null
+++ b/vendor/wheels/tests/specs/global/BuildDebugReloadUrlSpec.cfc
@@ -0,0 +1,179 @@
+component extends="wheels.WheelsTest" {
+
+ function run() {
+
+ g = application.wo
+
+ describe("Tests that $buildDebugReloadUrl", () => {
+
+ // ---------------------------------------------------------------
+ // Root installs (webPath = "/") — these expectations are pinned
+ // byte-for-byte to the output of the previous inline composition in
+ // vendor/wheels/events/onrequestend/debug.cfm (raw cgi.script_name
+ // + path_info + query string, then the rewriteFile strip and the
+ // reload-param scrub). They must never change.
+ // ---------------------------------------------------------------
+
+ it("builds the reload URL for a root install with URL rewriting on", () => {
+ rv = g.$buildDebugReloadUrl(
+ scriptName = "/index.cfm",
+ pathInfo = "/posts",
+ queryString = "",
+ webPath = "/",
+ rewriteFile = "index.cfm"
+ )
+
+ expect(rv).toBe("/posts?reload=")
+ })
+
+ it("preserves the query string on a root install", () => {
+ rv = g.$buildDebugReloadUrl(
+ scriptName = "/index.cfm",
+ pathInfo = "/posts",
+ queryString = "page=2&sort=title",
+ webPath = "/",
+ rewriteFile = "index.cfm"
+ )
+
+ expect(rv).toBe("/posts?page=2&sort=title&reload=")
+ })
+
+ it("scrubs a leading ?reload= parameter from the query string", () => {
+ rv = g.$buildDebugReloadUrl(
+ scriptName = "/index.cfm",
+ pathInfo = "/posts",
+ queryString = "reload=true",
+ webPath = "/",
+ rewriteFile = "index.cfm"
+ )
+
+ expect(rv).toBe("/posts?reload=")
+ })
+
+ it("scrubs an &reload= parameter while keeping the rest of the query string", () => {
+ rv = g.$buildDebugReloadUrl(
+ scriptName = "/index.cfm",
+ pathInfo = "/posts",
+ queryString = "page=2&reload=development",
+ webPath = "/",
+ rewriteFile = "index.cfm"
+ )
+
+ expect(rv).toBe("/posts?page=2&reload=")
+ })
+
+ it("keeps rewriting-off root installs byte-identical to the previous inline composition", () => {
+ // With URL rewriting off, path_info equals script_name (Lucee) so
+ // nothing is appended; the rewriteFile strip leaves the bare query.
+ rv = g.$buildDebugReloadUrl(
+ scriptName = "/index.cfm",
+ pathInfo = "/index.cfm",
+ queryString = "controller=posts&action=index",
+ webPath = "/",
+ rewriteFile = "index.cfm"
+ )
+
+ expect(rv).toBe("?controller=posts&action=index&reload=")
+ })
+
+ it("handles an empty path_info (Adobe engines with rewriting off)", () => {
+ rv = g.$buildDebugReloadUrl(
+ scriptName = "/index.cfm",
+ pathInfo = "",
+ queryString = "controller=posts&action=index",
+ webPath = "/",
+ rewriteFile = "index.cfm"
+ )
+
+ expect(rv).toBe("?controller=posts&action=index&reload=")
+ })
+
+ // ---------------------------------------------------------------
+ // Subfolder (subpath) installs — issue #3344. The base must come
+ // from webPath, not raw cgi.script_name, so the emitted link never
+ // contains the on-disk /public/ segment or the front controller.
+ // ---------------------------------------------------------------
+
+ it("honors webPath on a subfolder install (no /public/, no index.cfm)", () => {
+ rv = g.$buildDebugReloadUrl(
+ scriptName = "/wheelsproject1/public/index.cfm",
+ pathInfo = "/posts",
+ queryString = "",
+ webPath = "/wheelsproject1/",
+ rewriteFile = "index.cfm"
+ )
+
+ expect(rv).toBe("/wheelsproject1/posts?reload=")
+ expect(rv).notToInclude("/public/")
+ expect(rv).notToInclude("index.cfm")
+ })
+
+ it("honors a nested subpath webPath", () => {
+ rv = g.$buildDebugReloadUrl(
+ scriptName = "/team/site/public/index.cfm",
+ pathInfo = "/posts/1",
+ queryString = "page=2",
+ webPath = "/team/site/",
+ rewriteFile = "index.cfm"
+ )
+
+ expect(rv).toBe("/team/site/posts/1?page=2&reload=")
+ })
+
+ it("scrubs reload params on a subfolder install", () => {
+ rv = g.$buildDebugReloadUrl(
+ scriptName = "/wheelsproject1/public/index.cfm",
+ pathInfo = "/posts",
+ queryString = "reload=true",
+ webPath = "/wheelsproject1/",
+ rewriteFile = "index.cfm"
+ )
+
+ expect(rv).toBe("/wheelsproject1/posts?reload=")
+ })
+
+ // ---------------------------------------------------------------
+ // Defensive fallbacks (early boot / error paths).
+ // ---------------------------------------------------------------
+
+ it("falls back to the raw script name when webPath is empty", () => {
+ rv = g.$buildDebugReloadUrl(
+ scriptName = "/wheelsproject1/public/index.cfm",
+ pathInfo = "/posts",
+ queryString = "",
+ webPath = "",
+ rewriteFile = "index.cfm"
+ )
+
+ // Exactly what the previous inline composition produced.
+ expect(rv).toBe("/wheelsproject1/public/posts?reload=")
+ })
+
+ it("skips the rewriteFile strip when rewriteFile is empty", () => {
+ rv = g.$buildDebugReloadUrl(
+ scriptName = "/index.cfm",
+ pathInfo = "/posts",
+ queryString = "",
+ webPath = "/",
+ rewriteFile = ""
+ )
+
+ expect(rv).toBe("/index.cfm/posts?reload=")
+ })
+
+ it("defaults webPath and rewriteFile from application scope when omitted", () => {
+ // The running test app is a root install: webPath "/" and
+ // rewriteFile "index.cfm", so this must match the explicit
+ // root-install case above.
+ rv = g.$buildDebugReloadUrl(
+ scriptName = "/index.cfm",
+ pathInfo = "/posts",
+ queryString = ""
+ )
+
+ expect(rv).toBe("/posts?reload=")
+ })
+
+ })
+ }
+}