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.
This commit is contained in:
Jose Diaz-Gonzalez
2026-11-05 01:36:10 -05:00
parent fb7d0f57bf
commit ffcc60709a
3 changed files with 55 additions and 9 deletions

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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