Repository: caddyserver/caddy (language: go)

Issue:
JSON type headers are filtered out when "import" directive is used
## Version
`v2.5.1 h1:bAWwslD1jNeCzDa+jDCNwb8M3UJ2tPa8UZFFzPVmGKs=`

## Caddyfile
There are more settings that are irrelevant and have been stripped out of the config.
```
...

# Snippet "headers"
(headers) {
    header {
        Report-To `{"endpoints":[{"url":"https://inetol.report-uri.com/a/d/g"}],"group":"core","include_subdomains":true,"max_age":31536000}`
        NEL `{"include_subdomains":true,"max_age":31536000,"report_to":"core"}`

        # Test header one
        X-Test-ONE `{"this":true,"that":0}`
    }
}

...

example.com {
    # Import "headers" snippet
    import headers

    # Add additional test header two
    header X-Test-TWO "\{\"this\":false,\"that\":1\}"

    respond / "Hi"
    root * /dev/null
    file_server
}
```

## Issue
When requesting this webpage, all headers should be displayed with the values assigned in the Caddyfile. However, all headers that are imported from a snippet/file and their values contain JSON will appear empty or malformed.

### Expected
```
HTTP/2 200 OK
nel: {"include_subdomains":true,"max_age":31536000,"report_to":"core"}
report-to: {"endpoints":[{"url":"https://inetol.report-uri.com/a/d/g"}],"group":"core","include_subdomains":true,"max_age":31536000}
x-test-one: {"this":true,"that":0}
x-test-two: {"this":false,"that":1}
content-length: 2
date: Tue, 05 Jul 2022 11:12:50 GMT
```

### Got
```
HTTP/2 200 OK
nel: 
report-to: ],"group":"core","include_subdomains":true,"max_age":31536000}
x-test-one: 
x-test-two: {"this":false,"that":1}
content-length: 2
date: Tue, 05 Jul 2022 11:12:50 GMT
```

## Annex
Related to this issue: #2063 


Discussion / hints:
Thanks; this might be a pretty easy fix, just need to find time to look into it. Anyone's welcome to take a stab at it in the meantime!
I think we just need to adjust `import` to use `ReplaceKnown` (or whatever it's called, not checking docs right now) instead of `ReplaceAll` so that it only replaces `{args.*}` placeholders and leaves other things intact.
Okay I looked into it, it's not a bug with `import`, but rather a bug with the `header` directive's placeholder replacement.

Fix patch:
diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go
index f67df92820f..d5237236988 100644
--- a/modules/caddyhttp/headers/headers.go
+++ b/modules/caddyhttp/headers/headers.go
@@ -194,27 +194,27 @@ type RespHeaderOps struct {
 func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
 	// add
 	for fieldName, vals := range ops.Add {
-		fieldName = repl.ReplaceAll(fieldName, "")
+		fieldName = repl.ReplaceKnown(fieldName, "")
 		for _, v := range vals {
-			hdr.Add(fieldName, repl.ReplaceAll(v, ""))
+			hdr.Add(fieldName, repl.ReplaceKnown(v, ""))
 		}
 	}
 
 	// set
 	for fieldName, vals := range ops.Set {
-		fieldName = repl.ReplaceAll(fieldName, "")
+		fieldName = repl.ReplaceKnown(fieldName, "")
 		var newVals []string
 		for i := range vals {
 			// append to new slice so we don't overwrite
 			// the original values in ops.Set
-			newVals = append(newVals, repl.ReplaceAll(vals[i], ""))
+			newVals = append(newVals, repl.ReplaceKnown(vals[i], ""))
 		}
 		hdr.Set(fieldName, strings.Join(newVals, ","))
 	}
 
 	// delete
 	for _, fieldName := range ops.Delete {
-		fieldName = strings.ToLower(repl.ReplaceAll(fieldName, ""))
+		fieldName = strings.ToLower(repl.ReplaceKnown(fieldName, ""))
 		switch {
 		case strings.HasPrefix(fieldName, "*") && strings.HasSuffix(fieldName, "*"):
 			for existingField := range hdr {
@@ -241,13 +241,13 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
 
 	// replace
 	for fieldName, replacements := range ops.Replace {
-		fieldName = http.CanonicalHeaderKey(repl.ReplaceAll(fieldName, ""))
+		fieldName = http.CanonicalHeaderKey(repl.ReplaceKnown(fieldName, ""))
 
 		// all fields...
 		if fieldName == "*" {
 			for _, r := range replacements {
-				search := repl.ReplaceAll(r.Search, "")
-				replace := repl.ReplaceAll(r.Replace, "")
+				search := repl.ReplaceKnown(r.Search, "")
+				replace := repl.ReplaceKnown(r.Replace, "")
 				for fieldName, vals := range hdr {
 					for i := range vals {
 						if r.re != nil {
@@ -263,8 +263,8 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
 
 		// ...or only with the named field
 		for _, r := range replacements {
-			search := repl.ReplaceAll(r.Search, "")
-			replace := repl.ReplaceAll(r.Replace, "")
+			search := repl.ReplaceKnown(r.Search, "")
+			replace := repl.ReplaceKnown(r.Replace, "")
 			for hdrFieldName, vals := range hdr {
 				// see issue #4330 for why we don't simply use hdr[fieldName]
 				if http.CanonicalHeaderKey(hdrFieldName) != fieldName {


Test patch: