echo icon indicating copy to clipboard operation
echo copied to clipboard

Binding errors no longer return a structured error response

Open selda-tutti opened this issue 8 months ago • 1 comments

Issue Description

In earlier versions of echo, binding errors were serialized by DefaultHTTPErrorHandler into JSON. This has apparently changed with the commit fbfe216 fix(DefaultHTTPErrorHandler): return error message when message is an error because the error gets converted into a plain string message and included the "message" field here: https://github.com/labstack/echo/blob/de44c53a5b16f7dca451f337f7221a1448c92007/echo.go#L452

The following example shows that a binding error no longer returns a structured error response.

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"

	"github.com/labstack/echo/v4"
)

func GET(handler echo.HandlerFunc, target string) *httptest.ResponseRecorder {
	e := echo.New()
	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodGet, target, nil)
	c := e.NewContext(req, rec)

	err := handler(c)
	if err != nil {
		e.DefaultHTTPErrorHandler(err, c)
	}
	return rec
}

func main() {
	handler := func(c echo.Context) error {
		var docNum int
		err := echo.PathParamsBinder(c).
			MustInt("docNum", &docNum).
			BindError()
		if err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, err)
		}
		return c.JSON(http.StatusOK, map[string]interface{}{"received": docNum})
	}
	resp := GET(handler, "/doc/1234")
	fmt.Println(resp.Body.String())
}

With v4.10.2:

➜  go run main.go
{"field":"docNum","message":"required field value is empty"}

With v4.13.3:

➜  go run main.go                              
{"message":"code=400, message=required field value is empty, field=docNum"}

If this change was on purpose and there is no aim to serialize binding errors, in this case the json tag in BindingError seems to be superfluous. https://github.com/labstack/echo/blob/de44c53a5b16f7dca451f337f7221a1448c92007/binder.go#L71

selda-tutti avatar May 06 '25 08:05 selda-tutti

note here. This was not intentional and you can always create own error handler function.

e := echo.New()
e.HTTPErrorHandler = func(err error, c echo.Context) {
	//
}

or if you are going to wrap binding error with echo.NewHTTPError

		if err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, err)
		}

you could wrap then into something that implements json.Marshaler interface.

type jsonError struct {
	err error
}

func (e jsonError) MarshalJSON() ([]byte, error) {
	return json.Marshal(e.err)
}

and return it from handle as

		err := echo.PathParamsBinder(c).
			MustInt("docNum", &docNum).
			BindError()
		if err != nil {
			return echo.NewHTTPError(http.StatusBadRequest, &jsonError{err: err})
		}

these are things that can be done without waiting...

we could make echo.BindingError to implement json.Marshaler interface

func (be *BindingError) MarshalJSON() ([]byte, error) {
	type tmp struct {
		BindingError
	}
	return json.Marshal(tmp{BindingError{be.Field, be.HTTPError, be.Values}})
}

aldas avatar Oct 04 '25 17:10 aldas