Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
caddyhttp: allow dotted dynamic placeholders to fall through
  • Loading branch information
puneetdixit200 committed May 22, 2026
commit e3a8f3ecaa80448a92e53addb33675c87581eb92
11 changes: 9 additions & 2 deletions modules/caddyhttp/replacer.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
if req != nil {
// query string parameters
if strings.HasPrefix(key, reqURIQueryReplPrefix) {
vals := req.URL.Query()[key[len(reqURIQueryReplPrefix):]]
param := key[len(reqURIQueryReplPrefix):]
vals, ok := req.URL.Query()[param]
if !ok && strings.Contains(param, ".") {
return nil, false
}
// always return true, since the query param might
// be present only in some requests
return strings.Join(vals, ","), true
Expand All @@ -72,7 +76,10 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
// request header fields
if strings.HasPrefix(key, reqHeaderReplPrefix) {
field := key[len(reqHeaderReplPrefix):]
vals := req.Header[textproto.CanonicalMIMEHeaderKey(field)]
vals, ok := req.Header[textproto.CanonicalMIMEHeaderKey(field)]
if !ok && strings.Contains(field, ".") {
return nil, false
}
// always return true, since the header field might
// be present only in some requests
return strings.Join(vals, ","), true
Expand Down
57 changes: 57 additions & 0 deletions modules/caddyhttp/replacer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,60 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
}
}
}

func TestHTTPVarReplacementDynamicFallback(t *testing.T) {
req, _ := http.NewRequest(http.MethodGet, "/foo?a=1&with.dot=ok", nil)
repl := caddy.NewReplacer()
addHTTPVarsToReplacer(repl, req, nil)
repl.Map(func(key string) (any, bool) {
switch key {
case "http.request.header.x-custom-header.upper":
return "HEADER_FALLBACK", true
case "http.request.uri.query.missing.upper":
return "QUERY_FALLBACK", true
}
return nil, false
})

for _, tc := range []struct {
name string
get string
expect string
}{
{
name: "missing header without suffix stays known empty",
get: "http.request.header.x-custom-header",
expect: "",
},
{
name: "missing query without suffix stays known empty",
get: "http.request.uri.query.missing",
expect: "",
},
{
name: "missing dotted header falls through",
get: "http.request.header.x-custom-header.upper",
expect: "HEADER_FALLBACK",
},
{
name: "missing dotted query falls through",
get: "http.request.uri.query.missing.upper",
expect: "QUERY_FALLBACK",
},
{
name: "present dotted query stays known",
get: "http.request.uri.query.with.dot",
expect: "ok",
},
} {
t.Run(tc.name, func(t *testing.T) {
actual, got := repl.GetString(tc.get)
if !got {
t.Fatalf("expected to recognize placeholder %s", tc.get)
}
if actual != tc.expect {
t.Fatalf("expected %s to be %q but got %q", tc.get, tc.expect, actual)
}
})
}
}