diff --git a/binding/json.go b/binding/json.go index f4ae921a..047b9910 100644 --- a/binding/json.go +++ b/binding/json.go @@ -7,6 +7,7 @@ package binding import ( "bytes" "errors" + "fmt" "io" "net/http" @@ -50,7 +51,12 @@ func decodeJSON(r io.Reader, obj any) error { decoder.DisallowUnknownFields() } if err := decoder.Decode(obj); err != nil { + if errors.Is(err, io.EOF) { + return fmt.Errorf("empty request body: %w", err) + } + return err } + return validate(obj) } diff --git a/binding/json_external_test.go b/binding/json_external_test.go new file mode 100644 index 00000000..dee7b0d0 --- /dev/null +++ b/binding/json_external_test.go @@ -0,0 +1,39 @@ +package binding_test + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestJSONBindingEmptyBodyReturnsHelpfulError(t *testing.T) { + type Req struct { + Name string `json:"name" binding:"required"` + } + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + + req, err := http.NewRequest(http.MethodPost, "/", bytes.NewBuffer(nil)) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + + c.Request = req + + var r Req + err = c.ShouldBindJSON(&r) + + require.Error(t, err) + + // Error message should be more descriptive than plain EOF, + // while still preserving io.EOF via wrapping. + assert.NotEqual(t, "EOF", err.Error()) + assert.Contains(t, err.Error(), "empty request body") + assert.ErrorIs(t, err, io.EOF) +}