From ffcc60709ac48040b4cde93e6797864dcf9f13c3 Mon Sep 17 00:00:00 2001 From: Jose Diaz-Gonzalez Date: Thu, 5 Nov 2026 01:36:10 -0500 Subject: [PATCH] fix: correct traefik route label escaping Path-based route rules for traefik kept a stray backslash before each backtick, so traefik rejected the router and requests fell through to the web process. The nginx template tests now also supply the route variables and cover route rendering. --- plugins/nginx-vhosts/template_test.go | 46 +++++++++++++++++++++++ plugins/traefik-vhosts/internal-functions | 12 ++---- tests/unit/proxy-route-traefik.bats | 6 +++ 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/plugins/nginx-vhosts/template_test.go b/plugins/nginx-vhosts/template_test.go index fa5beacc9..4248e5532 100644 --- a/plugins/nginx-vhosts/template_test.go +++ b/plugins/nginx-vhosts/template_test.go @@ -60,6 +60,8 @@ func defaultVars() map[string]interface{} { "PROXY_X_FORWARDED_PROTO": "$scheme", "PROXY_X_FORWARDED_SSL": "", "HTTP2_DIRECTIVE_SUPPORTED": "true", + "PROXY_ROUTES": "", + "PROXY_ROUTE_LISTENERS": "", } } @@ -265,6 +267,50 @@ func TestTemplate_XForwardedSSL(t *testing.T) { mustNotContain(t, out2, "X-Forwarded-Ssl") } +func TestTemplate_RouteLocationAndUpstream(t *testing.T) { + v := defaultVars() + v["PROXY_ROUTES"] = "api|5001|/api/v0|0" + v["PROXY_ROUTE_LISTENERS"] = "api|5001|10.0.0.3:5001,10.0.0.4:5001" + out := renderTemplate(t, v) + mustContain(t, out, "location /api/v0 {") + mustContain(t, out, "proxy_pass http://app-api-5001;") + mustNotContain(t, out, "proxy_pass http://app-api-5001/;") + mustContain(t, out, "upstream app-api-5001 {") + mustContain(t, out, "server 10.0.0.3:5001;") + mustContain(t, out, "server 10.0.0.4:5001;") +} + +func TestTemplate_RouteStripPrefix(t *testing.T) { + v := defaultVars() + v["PROXY_ROUTES"] = "api|5001|/api/v0|1" + v["PROXY_ROUTE_LISTENERS"] = "api|5001|10.0.0.3:5001" + out := renderTemplate(t, v) + mustContain(t, out, "proxy_pass http://app-api-5001/;") +} + +func TestTemplate_RouteOmittedOnRedirect(t *testing.T) { + v := defaultVars() + v["PROXY_PORT_MAP"] = "http:80:5000 https:443:5000" + v["SSL_INUSE"] = "true" + v["SSL_SERVER_NAME"] = "app.example.com" + v["PROXY_ROUTES"] = "api|5001|/api/v0|0" + v["PROXY_ROUTE_LISTENERS"] = "api|5001|10.0.0.3:5001" + out := renderTemplate(t, v) + + port80, port443, ok := strings.Cut(out, "listen [::]:443") + if !ok { + t.Fatalf("expected an https server block in output:\n%s", out) + } + mustNotContain(t, port80, "location /api/v0") + mustContain(t, port443, "location /api/v0 {") +} + +func TestTemplate_NoRoutesRendersNoRouteBlocks(t *testing.T) { + out := renderTemplate(t, defaultVars()) + mustNotContain(t, out, "upstream app-api-") + mustNotContain(t, out, "location /api/v0") +} + func TestTemplate_NginxConfDIncludeAlways(t *testing.T) { cases := []struct { name string diff --git a/plugins/traefik-vhosts/internal-functions b/plugins/traefik-vhosts/internal-functions index 10dbd601e..29df8bd6f 100755 --- a/plugins/traefik-vhosts/internal-functions +++ b/plugins/traefik-vhosts/internal-functions @@ -23,8 +23,7 @@ fn-traefik-route-labels() { service_name="${APP}-${route_proc}-${route_slug}" path_len="${#route_path}" - # Triple-backslash for the same eval-survival reason as fn-traefik-host-rule. - local rule="PathPrefix(\\\`${route_path}\\\`)" + local rule="PathPrefix(\`${route_path}\`)" if [[ -n "$host_rule" ]]; then rule="(${host_rule}) && ${rule}" fi @@ -78,17 +77,12 @@ fn-traefik-host-rule() { return fi - # Backticks must be double-escaped so they survive the docker-args eval - # path. A single backslash leaves a bare backtick in the final string, - # which then runs as a command substitution at eval time and breaks the - # docker run invocation. Triple-backslash matches the existing emission - # convention used by docker-args-process-deploy. for domain in $(echo "$app_domains" | xargs); do if [[ -z "$result" ]]; then - result="Host(\\\`${domain}\\\`)" + result="Host(\`${domain}\`)" continue fi - result="${result} || Host(\\\`${domain}\\\`)" + result="${result} || Host(\`${domain}\`)" done echo "$result" } diff --git a/tests/unit/proxy-route-traefik.bats b/tests/unit/proxy-route-traefik.bats index 19609105e..7af93a89c 100644 --- a/tests/unit/proxy-route-traefik.bats +++ b/tests/unit/proxy-route-traefik.bats @@ -39,6 +39,12 @@ teardown() { echo "status: $status" assert_success + run /bin/bash -c "docker inspect $TEST_APP.api.1 --format '{{ index .Config.Labels \"traefik.http.routers.$TEST_APP-api-api-v0.rule\" }}'" + echo "output: $output" + echo "status: $status" + assert_success + assert_output "(Host(\`$TEST_APP.$DOKKU_DOMAIN\`)) && PathPrefix(\`/api/v0\`)" + # Give the daemon a moment to read the new labels. sleep 5