feat(forwarder): delete JSON body fields via json-: rewrite match

Add a json-: match prefix to http.rewriteRequestBody/rewriteResponseBody
that deletes the matched JSON field (sjson.DeleteBytes) instead of
replacing it, keeping the existing value-regex conditional semantics.
This commit is contained in:
ginuerzh
2026-08-25 21:54:27 +08:00
parent a38a701dc4
commit faeede7881
3 changed files with 116 additions and 9 deletions
+15 -3
View File
@@ -48,8 +48,16 @@ func parseBodyRewrites(vs []config.HTTPBodyRewriteConfig, log logger.Logger) []c
var pattern *regexp.Regexp
var rewriteType string
if js, ok := strings.CutPrefix(v.Match, "json:"); ok {
// json:<path> or json:<path>=<value-regex>
js, ok := strings.CutPrefix(v.Match, "json:")
del := false
if !ok {
// json-:<path> deletes the matched JSON field instead of replacing it.
if js, ok = strings.CutPrefix(v.Match, "json-:"); ok {
del = true
}
}
if ok {
// json:<path>[=<value-regex>] or json-:<path>[=<value-regex>]
path, valRegex, _ := strings.Cut(js, "=")
if valRegex == "" {
valRegex = ".*"
@@ -60,7 +68,11 @@ func parseBodyRewrites(vs []config.HTTPBodyRewriteConfig, log logger.Logger) []c
log.Warnf("invalid JSON value regex %q for path %q: %v", valRegex, path, err)
continue
}
rewriteType = "json:" + path
if del {
rewriteType = "json-:" + path
} else {
rewriteType = "json:" + path
}
} else {
if v.Match != "" {
pattern, _ = regexp.Compile(v.Match)
+18 -6
View File
@@ -497,10 +497,22 @@ func (b *rewriteBody) apply(body []byte, opts ...rewriter.RewriteOption) ([]byte
body = rewritten
}
} else if rw.Pattern != nil {
if strings.HasPrefix(rw.Type, "json:") {
if strings.HasPrefix(rw.Type, "json:") || strings.HasPrefix(rw.Type, "json-:") {
del := strings.HasPrefix(rw.Type, "json-:")
path := rw.Type[5:]
if del {
path = rw.Type[6:]
}
if rw.Pattern.MatchString(gjson.GetBytes(body, path).String()) {
replaced, err := sjson.SetBytes(body, path, string(rw.Replacement))
var (
replaced []byte
err error
)
if del {
replaced, err = sjson.DeleteBytes(body, path)
} else {
replaced, err = sjson.SetBytes(body, path, string(rw.Replacement))
}
if err == nil {
body = replaced
}
@@ -514,9 +526,9 @@ func (b *rewriteBody) apply(body []byte, opts ...rewriter.RewriteOption) ([]byte
}
// shouldApply reports whether a rewrite rule's configured type matches the
// actual request Content-Type. A type prefix "json:" matches only
// "application/json" content. An empty type defaults to "text/html", or "*"
// when a Rewriter is set.
// actual request Content-Type. JSON rules ("json:" or "json-:" Type prefixes)
// match only "application/json" content. An empty type defaults to
// "text/html", or "*" when a Rewriter is set.
func shouldApply(configuredType, actualContentType string, hasRewriter bool) bool {
t := configuredType
if t == "" {
@@ -526,7 +538,7 @@ func shouldApply(configuredType, actualContentType string, hasRewriter bool) boo
t = "text/html"
}
}
if strings.HasPrefix(t, "json:") {
if strings.HasPrefix(t, "json:") || strings.HasPrefix(t, "json-:") {
return actualContentType == "application/json"
}
return t == "*" || strings.Contains(t, actualContentType)
+83
View File
@@ -1960,6 +1960,89 @@ func TestNewRewriteBody(t *testing.T) {
})
}
func TestRewriteJSONDelete(t *testing.T) {
makeRewrite := func(rewriteType, match, replace string) chain.HTTPBodyRewriteSettings {
return chain.HTTPBodyRewriteSettings{
Pattern: regexp.MustCompile(match),
Type: rewriteType,
Replacement: []byte(replace),
}
}
ctx := context.Background()
t.Run("delete matched field", func(t *testing.T) {
src := io.NopCloser(strings.NewReader(`{"user":{"id":123},"keep":1}`))
body, err := newRewriteBody(ctx, src, []chain.HTTPBodyRewriteSettings{
makeRewrite("json-:user.id", "123", ""),
}, "application/json", "", -1, "", "")
if err != nil {
t.Fatal(err)
}
if body == nil {
t.Fatal("expected non-nil body")
}
got, _ := io.ReadAll(body)
body.Close()
if string(got) != `{"user":{},"keep":1}` {
t.Errorf("body = %q, want %q", string(got), `{"user":{},"keep":1}`)
}
})
t.Run("no match leaves body unchanged", func(t *testing.T) {
src := io.NopCloser(strings.NewReader(`{"user":{"id":123},"keep":1}`))
body, err := newRewriteBody(ctx, src, []chain.HTTPBodyRewriteSettings{
makeRewrite("json-:user.id", "^9$", ""),
}, "application/json", "", -1, "", "")
if err != nil {
t.Fatal(err)
}
if body == nil {
t.Fatal("expected non-nil body")
}
got, _ := io.ReadAll(body)
body.Close()
if string(got) != `{"user":{"id":123},"keep":1}` {
t.Errorf("body = %q, want %q (unchanged)", string(got), `{"user":{"id":123},"keep":1}`)
}
})
t.Run("non-json content type filtered out", func(t *testing.T) {
src := io.NopCloser(strings.NewReader(`{"user":{"id":123},"keep":1}`))
body, err := newRewriteBody(ctx, src, []chain.HTTPBodyRewriteSettings{
makeRewrite("json-:user.id", "123", ""),
}, "text/plain", "", -1, "", "")
if err != nil {
t.Fatal(err)
}
if body == nil {
t.Fatal("expected non-nil body")
}
got, _ := io.ReadAll(body)
body.Close()
if string(got) != `{"user":{"id":123},"keep":1}` {
t.Errorf("body = %q, want %q (unchanged)", string(got), `{"user":{"id":123},"keep":1}`)
}
})
t.Run("json: prefix still replaces", func(t *testing.T) {
src := io.NopCloser(strings.NewReader(`{"user":{"id":123},"keep":1}`))
body, err := newRewriteBody(ctx, src, []chain.HTTPBodyRewriteSettings{
makeRewrite("json:user.id", "123", "999"),
}, "application/json", "", -1, "", "")
if err != nil {
t.Fatal(err)
}
if body == nil {
t.Fatal("expected non-nil body")
}
got, _ := io.ReadAll(body)
body.Close()
if string(got) != `{"user":{"id":"999"},"keep":1}` {
t.Errorf("body = %q, want %q", string(got), `{"user":{"id":"999"},"keep":1}`)
}
})
}
func TestRewriteRespBody_CompressedEncodings(t *testing.T) {
tests := []struct {
name string