@marcel91200 / caddy Mirror · read-only / commits / ae9bc028
Fork 0

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 {