Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 2 additions & 46 deletions internal/bodymutator/body_mutator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ package bodymutator

import (
"fmt"
"strings"

"github.com/tidwall/gjson"
"github.com/tidwall/sjson"

"github.com/envoyproxy/ai-gateway/internal/filterapi"
Expand Down Expand Up @@ -37,50 +37,6 @@ func (b *BodyMutator) HasMutations() bool {
return b != nil && b.bodyMutations != nil
}

// isJSONValue checks if a string represents a JSON value (not a plain string)
func isJSONValue(value string) bool {
value = strings.TrimSpace(value)

// Check for quoted strings (JSON strings)
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
return true
}

// Check for numbers (integers or floats)
if value == "0" || value == "true" || value == "false" || value == "null" {
return true
}

// Check for positive/negative numbers
if len(value) > 0 {
first := value[0]
if (first >= '0' && first <= '9') || first == '-' || first == '+' {
// Simple number check - contains only digits, dots, +, -, e, E
isNumber := true
for _, r := range value {
if (r < '0' || r > '9') && r != '.' && r != '-' && r != '+' && r != 'e' && r != 'E' {
isNumber = false
break
}
}
if isNumber {
return true
}
}
}

// Check for objects or arrays
if strings.HasPrefix(value, "{") && strings.HasSuffix(value, "}") {
return true
}
if strings.HasPrefix(value, "[") && strings.HasSuffix(value, "]") {
return true
}

// Default to plain string
return false
}

// Mutate mutates the request body based on the body mutations.
func (b *BodyMutator) Mutate(requestBody []byte) ([]byte, error) {
if b.bodyMutations == nil {
Expand Down Expand Up @@ -109,7 +65,7 @@ func (b *BodyMutator) Mutate(requestBody []byte) ([]byte, error) {
if field.Path != "" {
// Check value type to determine appropriate sjson method
// TODO handle JSON value check in configuration load time too.
if isJSONValue(field.Value) {
if gjson.Valid(field.Value) {
// Use SetRawBytes for JSON values (quoted strings, numbers, booleans, objects, arrays)
mutatedBody, err = sjson.SetRawBytesOptions(mutatedBody, field.Path, []byte(field.Value), &sjson.Options{ReplaceInPlace: true})
} else {
Expand Down
56 changes: 56 additions & 0 deletions internal/bodymutator/body_mutator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,59 @@ func TestBodyMutator_Mutate_InvalidJSONValue(t *testing.T) {
require.Equal(t, "not valid json but will be treated as string", result["service_tier"])
require.Equal(t, "valid", result["valid_field"])
}

func TestBodyMutator_Mutate_JSONLookingStrings(t *testing.T) {
for _, value := range []string{
"1.0.0",
"+1",
"01",
"1e",
"-",
`[abc]`,
`{"key":}`,
`"invalid\q"`,
"",
" ",
} {
t.Run(value, func(t *testing.T) {
mutator := NewBodyMutator(&filterapi.HTTPBodyMutation{
Set: []filterapi.HTTPBodyField{{Path: "metadata", Value: value}},
}, nil)
mutated, err := mutator.Mutate([]byte(`{"model":"test-model","messages":[]}`))
require.NoError(t, err)
var result map[string]interface{}
require.NoError(t, json.Unmarshal(mutated, &result), "mutation produced %s", mutated)
require.Equal(t, value, result["metadata"])
require.Equal(t, "test-model", result["model"])
})
}
}

func TestBodyMutator_Mutate_JSONLiterals(t *testing.T) {
for _, value := range []string{
`"quoted"`,
`42`,
`-0.7`,
`1e2`,
`true`,
`false`,
`null`,
`{"nested": [1, true, null]}`,
`["item", 2]`,
` {"key": "value"} `,
} {
t.Run(value, func(t *testing.T) {
mutator := NewBodyMutator(&filterapi.HTTPBodyMutation{
Set: []filterapi.HTTPBodyField{{Path: "metadata", Value: value}},
}, nil)
mutated, err := mutator.Mutate([]byte(`{"model":"test-model"}`))
require.NoError(t, err)
var result map[string]interface{}
require.NoError(t, json.Unmarshal(mutated, &result))
var expected interface{}
require.NoError(t, json.Unmarshal([]byte(value), &expected))
require.Equal(t, expected, result["metadata"])
require.Equal(t, "test-model", result["model"])
})
}
}
Loading