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
+35
View File
@@ -141,6 +141,14 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error {
if r.SearchRegexp == "" {
continue
}
// Check if it contains placeholders
if containsPlaceholders(r.SearchRegexp) {
// Contains placeholders, skips precompilation, and recompiles at runtime
continue
}
// Does not contain placeholders, safe to precompile
re, err := regexp.Compile(r.SearchRegexp)
if err != nil {
return fmt.Errorf("replacement %d for header field '%s': %v", i, fieldName, err)
@@ -151,6 +159,20 @@ func (ops *HeaderOps) Provision(_ caddy.Context) error {
return nil
}
// containsCaddyPlaceholders checks if the string contains Caddy placeholder syntax {key}
func containsPlaceholders(s string) bool {
openIdx := strings.Index(s, "{")
if openIdx == -1 {
return false
}
closeIdx := strings.Index(s[openIdx+1:], "}")
if closeIdx == -1 {
return false
}
// Make sure there is content between the brackets
return closeIdx > 0
}
func (ops HeaderOps) validate() error {
for fieldName, replacements := range ops.Replace {
for _, r := range replacements {
@@ -269,7 +291,15 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
for fieldName, vals := range hdr {
for i := range vals {
if r.re != nil {
// Use precompiled regular expressions
hdr[fieldName][i] = r.re.ReplaceAllString(hdr[fieldName][i], replace)
} else if r.SearchRegexp != "" {
// Runtime compilation of regular expressions
searchRegexp := repl.ReplaceKnown(r.SearchRegexp, "")
if re, err := regexp.Compile(searchRegexp); err == nil {
hdr[fieldName][i] = re.ReplaceAllString(hdr[fieldName][i], replace)
}
// If compilation fails, skip this replacement
} else {
hdr[fieldName][i] = strings.ReplaceAll(hdr[fieldName][i], search, replace)
}
@@ -291,6 +321,11 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
for i := range vals {
if r.re != nil {
hdr[hdrFieldName][i] = r.re.ReplaceAllString(hdr[hdrFieldName][i], replace)
} else if r.SearchRegexp != "" {
searchRegexp := repl.ReplaceKnown(r.SearchRegexp, "")
if re, err := regexp.Compile(searchRegexp); err == nil {
hdr[hdrFieldName][i] = re.ReplaceAllString(hdr[hdrFieldName][i], replace)
}
} else {
hdr[hdrFieldName][i] = strings.ReplaceAll(hdr[hdrFieldName][i], search, replace)
}