headers: Support placeholders in replacement search patterns (#7117)

* fix: resolve http.request placeholders in header directive find operation

- Skip regex compilation during provision when placeholders are detected
- Compile regex at runtime after placeholder replacement
- Preserves performance for static regexes while enabling dynamic placeholders
- Fixes #7109

* test: add tests for placeholder detection in header replacements

- Test containsPlaceholders function edge cases
- Test provision skips compilation for dynamic regexes
- Test end-to-end placeholder replacement functionality
This commit is contained in:
Zongze Wu
2025-07-14 13:55:00 -07:00
committed by GitHub
parent aff88d4b26
commit bbf1dfcea2
3 changed files with 203 additions and 0 deletions
+104
View File
@@ -272,3 +272,107 @@ type nextHandler func(http.ResponseWriter, *http.Request) error
func (f nextHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) error {
return f(w, r)
}
func TestContainsPlaceholders(t *testing.T) {
for i, tc := range []struct {
input string
expected bool
}{
{"static", false},
{"{placeholder}", true},
{"prefix-{placeholder}-suffix", true},
{"{}", false},
{"no-braces", false},
{"{unclosed", false},
{"unopened}", false},
} {
actual := containsPlaceholders(tc.input)
if actual != tc.expected {
t.Errorf("Test %d: containsPlaceholders(%q) = %v, expected %v", i, tc.input, actual, tc.expected)
}
}
}
func TestHeaderProvisionSkipsPlaceholders(t *testing.T) {
ops := &HeaderOps{
Replace: map[string][]Replacement{
"Static": {
Replacement{SearchRegexp: ":443", Replace: "STATIC"},
},
"Dynamic": {
Replacement{SearchRegexp: ":{http.request.local.port}", Replace: "DYNAMIC"},
},
},
}
err := ops.Provision(caddy.Context{})
if err != nil {
t.Fatalf("Provision failed: %v", err)
}
// Static regex should be precompiled
if ops.Replace["Static"][0].re == nil {
t.Error("Expected static regex to be precompiled")
}
// Dynamic regex with placeholder should not be precompiled
if ops.Replace["Dynamic"][0].re != nil {
t.Error("Expected dynamic regex with placeholder to not be precompiled")
}
}
func TestPlaceholderInSearchRegexp(t *testing.T) {
handler := Handler{
Response: &RespHeaderOps{
HeaderOps: &HeaderOps{
Replace: map[string][]Replacement{
"Test-Header": {
Replacement{
SearchRegexp: ":{http.request.local.port}",
Replace: "PLACEHOLDER-WORKS",
},
},
},
},
},
}
// Provision the handler
err := handler.Provision(caddy.Context{})
if err != nil {
t.Fatalf("Provision failed: %v", err)
}
replacement := handler.Response.HeaderOps.Replace["Test-Header"][0]
t.Logf("After provision - SearchRegexp: %q, re: %v", replacement.SearchRegexp, replacement.re)
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "http://localhost:443/", nil)
repl := caddy.NewReplacer()
repl.Set("http.request.local.port", "443")
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
rr.Header().Set("Test-Header", "prefix:443suffix")
t.Logf("Initial header: %v", rr.Header())
next := nextHandler(func(w http.ResponseWriter, r *http.Request) error {
w.WriteHeader(200)
return nil
})
err = handler.ServeHTTP(rr, req, next)
if err != nil {
t.Fatalf("ServeHTTP failed: %v", err)
}
t.Logf("Final header: %v", rr.Header())
result := rr.Header().Get("Test-Header")
expected := "prefixPLACEHOLDER-WORKSsuffix"
if result != expected {
t.Errorf("Expected header value %q, got %q", expected, result)
}
}