Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS
Mirrored from https://github.com/Slazee/caddy.git
Synced Nov 11, 08:30 UTC
rewrite: scope keyed query replace to its named key (#7818)
* rewrite: scope keyed query replace to its named key * rewrite: cover keyed search_regexp query replace in test * rewrite: provision query replace test via Provision path
alhuda committed
Jun 15, 2026 at 20:31 UTC
ae9bc028e68cf4c49aa42c6549a64293029aff02
2 files changed
+69
-7
modules/caddyhttp/rewrite/rewrite.go
+9
-7
@@ -606,13 +606,15 @@ func (q *queryOps) do(r *http.Request, repl *caddy.Replacer) {
606
continue
607
}
608
609
- for fieldName, vals := range query {
610
- for i := range vals {
611
- if replaceParam.re != nil {
612
- query[fieldName][i] = replaceParam.re.ReplaceAllString(query[fieldName][i], replace)
613
- } else {
614
- query[fieldName][i] = strings.ReplaceAll(query[fieldName][i], search, replace)
615
- }
609
+ vals, ok := query[key]
610
+ if !ok {
611
+ continue
612
+ }
613
+ for i := range vals {
614
+ if replaceParam.re != nil {
615
+ query[key][i] = replaceParam.re.ReplaceAllString(query[key][i], replace)
616
+ } else {
617
+ query[key][i] = strings.ReplaceAll(query[key][i], search, replace)
618
}
619
}
620
}
modules/caddyhttp/rewrite/rewrite_test.go
+60
@@ -483,6 +483,66 @@ func TestQueryOpsRenameNoOpCases(t *testing.T) {
483
}
484
}
485
486
+func TestQueryOpsReplaceScopedToKey(t *testing.T) {
487
+ repl := caddy.NewReplacer()
488
+
489
+ for i, tc := range []struct {
490
+ input *http.Request
491
+ expect map[string][]string
492
+ ops *queryOps
493
+ }{
494
+ {
495
+ // a keyed replace must only touch the named key, even when
496
+ // other keys hold the same value
497
+ ops: &queryOps{
498
+ Replace: []*queryOpsReplacement{{Key: "a", Search: "foo", Replace: "bar"}},
499
+ },
500
+ input: newRequest(t, "GET", "/?a=foo&b=foo"),
501
+ expect: map[string][]string{"a": {"bar"}, "b": {"foo"}},
502
+ },
503
+ {
504
+ // "*" still replaces across every key
505
+ ops: &queryOps{
506
+ Replace: []*queryOpsReplacement{{Key: "*", Search: "foo", Replace: "bar"}},
507
+ },
508
+ input: newRequest(t, "GET", "/?a=foo&b=foo"),
509
+ expect: map[string][]string{"a": {"bar"}, "b": {"bar"}},
510
+ },
511
+ {
512
+ // a keyed replace against a missing key is a no-op
513
+ ops: &queryOps{
514
+ Replace: []*queryOpsReplacement{{Key: "missing", Search: "foo", Replace: "bar"}},
515
+ },
516
+ input: newRequest(t, "GET", "/?a=foo&b=foo"),
517
+ expect: map[string][]string{"a": {"foo"}, "b": {"foo"}},
518
+ },
519
+ {
520
+ // the regexp branch must also stay scoped to the named key
521
+ ops: &queryOps{
522
+ Replace: []*queryOpsReplacement{{Key: "a", SearchRegexp: "f.o", Replace: "bar"}},
523
+ },
524
+ input: newRequest(t, "GET", "/?a=foo&b=foo"),
525
+ expect: map[string][]string{"a": {"bar"}, "b": {"foo"}},
526
+ },
527
+ } {
528
+ repl.Set("http.request.uri", tc.input.RequestURI)
529
+ repl.Set("http.request.uri.path", tc.input.URL.Path)
530
+ repl.Set("http.request.uri.query", tc.input.URL.RawQuery)
531
+
532
+ for _, rep := range tc.ops.Replace {
533
+ if err := rep.Provision(caddy.Context{}); err != nil {
534
+ t.Fatal(err)
535
+ }
536
+ }
537
+
538
+ tc.ops.do(tc.input, repl)
539
+
540
+ if actual := tc.input.URL.Query(); !reflect.DeepEqual(tc.expect, map[string][]string(actual)) {
541
+ t.Errorf("Test %d: Expected query=%v but got %v", i, tc.expect, actual)
542
+ }
543
+ }
544
+}
545
+
546
func newRequest(t *testing.T, method, uri string) *http.Request {
547
req, err := http.NewRequest(method, uri, nil)
548
if err != nil {