Skip to content
Merged
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
21 changes: 21 additions & 0 deletions binder.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ import (
*/

// BindingError represents an error that occurred while binding request data.
//
// Note: JSON serialization is handled by the MarshalJSON method below, not by the
// struct tags (which are kept for documentation). MarshalJSON emits {"field","message"}.
type BindingError struct {
// Field is the field name where value binding failed
Field string `json:"field"`
Expand All @@ -88,6 +91,24 @@ func (be *BindingError) Error() string {
return fmt.Sprintf("%s, field=%s", be.HTTPError.Error(), be.Field)
}

// MarshalJSON implements json.Marshaler so that binding errors are serialized into
// a structured response (e.g. {"field":"id","message":"..."}) rather than being
// flattened to a generic message. DefaultHTTPErrorHandler routes errors that
// implement json.Marshaler through their own encoding.
func (be *BindingError) MarshalJSON() ([]byte, error) {
message := be.Message
if message == "" {
message = http.StatusText(be.Code)
}
return json.Marshal(struct {
Field string `json:"field"`
Message string `json:"message"`
}{
Field: be.Field,
Message: message,
})
}

// ValueBinder provides utility methods for binding query or path parameter to various Go built-in types
type ValueBinder struct {
// ValueFunc is used to get single parameter (first) value from request
Expand Down
46 changes: 46 additions & 0 deletions binder_error_response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: © 2015 LabStack LLC and Echo contributors

package echo

import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

// Regression test for #2771: a BindingError returned from a handler must be
// serialized by DefaultHTTPErrorHandler into a structured response that retains
// the field name (and the binder message), not flattened to {"message":"Bad Request"}.
func TestBindingError_serializesToStructuredJSON(t *testing.T) {
e := New()
e.GET("/doc", func(c *Context) error {
var docNum int
return QueryParamsBinder(c).MustInt("docNum", &docNum).BindError()
})

req := httptest.NewRequest(http.MethodGet, "/doc?docNum=abc", nil)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)

assert.Equal(t, http.StatusBadRequest, rec.Code)

var body map[string]any
assert.NoError(t, json.Unmarshal(rec.Body.Bytes(), &body))
assert.Equal(t, "docNum", body["field"], "binding error response must retain the field name")
assert.Equal(t, "failed to bind field value to int", body["message"], "binding error response must retain the binder message")
}

// When the binding error carries no message, MarshalJSON falls back to the
// status text (mirroring DefaultHTTPErrorHandler's *HTTPError branch).
func TestBindingError_marshalJSON_emptyMessageFallsBackToStatusText(t *testing.T) {
be := &BindingError{Field: "name", HTTPError: &HTTPError{Code: http.StatusBadRequest}}

b, err := be.MarshalJSON()

assert.NoError(t, err)
assert.JSONEq(t, `{"field":"name","message":"Bad Request"}`, string(b))
}
Loading