From f63f175a77fe6ceef7e10e87dc0d218b97a4d3c9 Mon Sep 17 00:00:00 2001 From: Vibhu Pandey Date: Fri, 5 Sep 2025 21:17:19 +0530 Subject: [PATCH] fix(binding): better error messages (#9010) --- pkg/http/binding/binding.go | 3 +- pkg/http/binding/json.go | 31 ++++++++++++++-- pkg/http/binding/json_test.go | 68 +++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 pkg/http/binding/json_test.go diff --git a/pkg/http/binding/binding.go b/pkg/http/binding/binding.go index bd5eb1851943..feda8ea2cdb4 100644 --- a/pkg/http/binding/binding.go +++ b/pkg/http/binding/binding.go @@ -7,7 +7,8 @@ import ( ) var ( - ErrCodeInvalidRequestBody = errors.MustNewCode("invalid_request_body") + ErrCodeInvalidRequestBody = errors.MustNewCode("invalid_request_body") + ErrCodeInvalidRequestField = errors.MustNewCode("invalid_request_field") ) var ( diff --git a/pkg/http/binding/json.go b/pkg/http/binding/json.go index 8299456998fe..e0503d57bae5 100644 --- a/pkg/http/binding/json.go +++ b/pkg/http/binding/json.go @@ -7,6 +7,11 @@ import ( "github.com/SigNoz/signoz/pkg/errors" ) +const ( + ErrMessageInvalidJSON string = "request body contains invalid JSON, please verify the format and try again." + ErrMessageInvalidField string = "request body contains invalid field value" +) + var _ Binding = (*jsonBinding)(nil) type jsonBinding struct{} @@ -32,9 +37,29 @@ func (b *jsonBinding) BindBody(body io.Reader, obj any, opts ...BindBodyOption) } if err := decoder.Decode(obj); err != nil { - return errors. - New(errors.TypeInvalidInput, ErrCodeInvalidRequestBody, "request body is invalid"). - WithAdditional(err.Error()) + var syntaxError *json.SyntaxError + var unmarshalError *json.InvalidUnmarshalError + var unmarshalTypeError *json.UnmarshalTypeError + + if errors.As(err, &syntaxError) || errors.As(err, &unmarshalError) || errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) { + return errors. + New(errors.TypeInvalidInput, ErrCodeInvalidRequestBody, ErrMessageInvalidJSON). + WithAdditional(err.Error()) + } + + if errors.As(err, &unmarshalTypeError) { + if unmarshalTypeError.Field != "" { + return errors. + New(errors.TypeInvalidInput, ErrCodeInvalidRequestField, ErrMessageInvalidField). + WithAdditional("value of type '" + unmarshalTypeError.Value + "' was received for field '" + unmarshalTypeError.Field + "', try sending '" + unmarshalTypeError.Type.String() + "' instead?") + } + + return errors. + New(errors.TypeInvalidInput, ErrCodeInvalidRequestField, ErrMessageInvalidField). + WithAdditional("value of type '" + unmarshalTypeError.Value + "' was received, try sending '" + unmarshalTypeError.Type.String() + "' instead?") + } + + return err } return nil diff --git a/pkg/http/binding/json_test.go b/pkg/http/binding/json_test.go new file mode 100644 index 000000000000..4e29832fe56f --- /dev/null +++ b/pkg/http/binding/json_test.go @@ -0,0 +1,68 @@ +package binding + +import ( + "encoding/json" + "io" + "strings" + "testing" + + "github.com/SigNoz/signoz/pkg/errors" + "github.com/stretchr/testify/assert" +) + +type s struct { + A int `json:"a"` +} + +func (req *s) UnmarshalJSON(b []byte) error { + type Alias s + + var temp Alias + if err := json.Unmarshal(b, &temp); err != nil { + return err + } + + if temp.A <= 10 { + return errors.New(errors.TypeInvalidInput, errors.CodeInvalidInput, "a must be greater than 10") + } + + *req = s(temp) + return nil +} + +type n struct { + S s `json:"s"` +} + +func TestJSONBinding_BindBodyErrors(t *testing.T) { + testCases := []struct { + name string + body string + obj any + opts []BindBodyOption + code errors.Code + message string + a []string + }{ + {name: "Empty", body: "", opts: nil, obj: &struct{}{}, code: ErrCodeInvalidRequestBody, message: ErrMessageInvalidJSON, a: []string{io.EOF.Error()}}, + {name: "String", body: "invalid json", opts: nil, obj: &struct{}{}, code: ErrCodeInvalidRequestBody, message: ErrMessageInvalidJSON, a: []string{"invalid character 'i' looking for beginning of value"}}, + {name: "Invalid", body: `{"a":"b}`, opts: nil, obj: &struct{}{}, code: ErrCodeInvalidRequestBody, message: ErrMessageInvalidJSON, a: []string{io.ErrUnexpectedEOF.Error()}}, + {name: "CustomValid", body: `{"a":9}`, opts: nil, obj: new(s), code: errors.CodeInvalidInput, message: "a must be greater than 10", a: []string{}}, + {name: "CustomInvalidJSON", body: `{"a:9}`, opts: nil, obj: new(s), code: ErrCodeInvalidRequestBody, message: ErrMessageInvalidJSON, a: []string{io.ErrUnexpectedEOF.Error()}}, + {name: "CustomMismatchedType", body: `{"a":"b"}`, opts: nil, obj: new(s), code: ErrCodeInvalidRequestField, message: ErrMessageInvalidField, a: []string{`value of type 'string' was received for field 'a', try sending 'int' instead?`}}, + {name: "CustomNestedMismatchedType", body: `{"s":{"a":"b"}}`, opts: nil, obj: new(n), code: ErrCodeInvalidRequestField, message: ErrMessageInvalidField, a: []string{`value of type 'string' was received for field 's.a', try sending 'int' instead?`}}, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + err := JSON.BindBody(strings.NewReader(testCase.body), testCase.obj, testCase.opts...) + assert.Error(t, err) + + typ, c, m, _, _, a := errors.Unwrapb(err) + assert.Equal(t, errors.TypeInvalidInput, typ) + assert.Equal(t, testCase.code, c) + assert.Equal(t, testCase.message, m) + assert.ElementsMatch(t, testCase.a, a) + }) + } +}