diff --git a/errors.go b/errors.go index 8fe343a..bec5151 100644 --- a/errors.go +++ b/errors.go @@ -14,6 +14,11 @@ type ErrorWithStatus interface { StatusCode() int } +type ErrorWithDetail interface { + error + DetailMsg() string +} + // HTTPError is the error response used by the serialization part of the framework. type HTTPError struct { Err error `json:"-" xml:"-"` // Developer readable error message. Not shown to the user to avoid security leaks. @@ -40,7 +45,7 @@ func (e HTTPError) Error() string { title = "HTTP Error" } } - return fmt.Sprintf("%d %s: %s", code, title, e.Detail) + return fmt.Sprintf("%d %s: %s", code, title, e.DetailMsg()) } func (e HTTPError) StatusCode() int { @@ -50,6 +55,10 @@ func (e HTTPError) StatusCode() int { return e.Status } +func (e HTTPError) DetailMsg() string { + return e.Detail +} + func (e HTTPError) Unwrap() error { return e.Err } // BadRequestError is an error used to return a 400 status code. @@ -123,7 +132,7 @@ func (e NotAcceptableError) Unwrap() error { return HTTPError(e) } // ErrorHandler is the default error handler used by the framework. // It transforms any error into the unified error type [HTTPError], -// Using the [ErrorWithStatus] interface. +// Using the [ErrorWithStatus] and [ErrorWithDetail] interface. func ErrorHandler(err error) error { errResponse := HTTPError{ Err: err, @@ -141,11 +150,17 @@ func ErrorHandler(err error) error { errResponse.Status = errorStatus.StatusCode() } + // Check for detail + var errorDetail ErrorWithDetail + if errors.As(err, &errorDetail) { + errResponse.Detail = errorDetail.DetailMsg() + } + if errResponse.Title == "" { errResponse.Title = http.StatusText(errResponse.Status) } - slog.Error("Error "+errResponse.Title, "status", errResponse.StatusCode(), "detail", errResponse.Detail, "error", errResponse.Err) + slog.Error("Error "+errResponse.Title, "status", errResponse.StatusCode(), "detail", errResponse.DetailMsg(), "error", errResponse.Err) return errResponse } diff --git a/errors_test.go b/errors_test.go index a0ee09a..e0e3184 100644 --- a/errors_test.go +++ b/errors_test.go @@ -10,12 +10,17 @@ import ( type myError struct { status int + err HTTPError + detail string } var _ ErrorWithStatus = myError{} +var _ ErrorWithDetail = myError{} -func (e myError) Error() string { return "test error" } -func (e myError) StatusCode() int { return e.status } +func (e myError) Error() string { return "test error" } +func (e myError) StatusCode() int { return e.status } +func (e myError) DetailMsg() string { return e.detail } +func (e myError) Unwrap() error { return HTTPError(e.err) } func TestErrorHandler(t *testing.T) { t.Run("basic error", func(t *testing.T) { @@ -39,7 +44,7 @@ func TestErrorHandler(t *testing.T) { require.Equal(t, http.StatusNotFound, errResponse.(HTTPError).StatusCode()) }) - t.Run("error with status ", func(t *testing.T) { + t.Run("error with status", func(t *testing.T) { err := myError{ status: http.StatusNotFound, } @@ -50,6 +55,18 @@ func TestErrorHandler(t *testing.T) { require.Equal(t, http.StatusNotFound, errResponse.(HTTPError).StatusCode()) }) + t.Run("error with detail", func(t *testing.T) { + err := myError{ + detail: "my detail", + } + errResponse := ErrorHandler(err) + require.ErrorAs(t, errResponse, &HTTPError{}) + require.Contains(t, errResponse.Error(), "Internal Server Error") + require.Contains(t, errResponse.Error(), "500") + require.Contains(t, errResponse.Error(), "my detail") + require.Equal(t, http.StatusInternalServerError, errResponse.(HTTPError).StatusCode()) + }) + t.Run("conflict error", func(t *testing.T) { err := ConflictError{ Err: errors.New("Conflict"), diff --git a/examples/basic/main.go b/examples/basic/main.go index 84833dc..5ae6a49 100644 --- a/examples/basic/main.go +++ b/examples/basic/main.go @@ -22,6 +22,22 @@ type MyResponse struct { BestFramework string `json:"best"` } +type MyError struct { + Err error `json:"error"` + Message string `json:"message"` +} + +var _ fuego.ErrorWithStatus = MyError{} +var _ fuego.ErrorWithDetail = MyError{} + +func (e MyError) Error() string { return e.Err.Error() } + +func (e MyError) StatusCode() int { return http.StatusTeapot } + +func (e MyError) DetailMsg() string { + return strings.Split(e.Error(), " ")[1] +} + func main() { s := fuego.NewServer( fuego.WithAddr("localhost:8088"), @@ -59,6 +75,10 @@ func main() { w.Write([]byte("Hello, World!")) }) + fuego.Get(s, "/custom-err", func(c *fuego.ContextNoBody) (string, error) { + return "hello", MyError{Err: errors.New("my error")} + }) + s.Run() }