core/internal/port/handler_http_api_error.go

213 lines
4.6 KiB
Go

package port
import (
"errors"
"net/http"
"gitea.dwysokinski.me/twhelp/core/internal/domain"
"gitea.dwysokinski.me/twhelp/core/internal/port/internal/apimodel"
"github.com/ettle/strcase"
"github.com/oapi-codegen/runtime"
)
type apiError struct {
status int
code apimodel.ErrorCode
path []string
params map[string]any
message string
}
func (e apiError) Error() string {
return e.message
}
func (e apiError) toResponse() apimodel.ErrorResponse {
return apimodel.ErrorResponse{
Errors: []apimodel.Error{
{
Message: e.message,
Params: e.params,
Path: e.path,
Code: e.code,
},
},
}
}
type apiErrors []apiError
func (es apiErrors) status() int {
if len(es) == 0 {
return http.StatusOK
}
status := es[0].status
for _, e := range es {
if e.status > status {
status = e.status
}
}
return status
}
func (es apiErrors) toResponse() apimodel.ErrorResponse {
resp := apimodel.ErrorResponse{
Errors: make([]apimodel.Error, 0, len(es)),
}
for _, e := range es {
resp.Errors = append(resp.Errors, e.toResponse().Errors...)
}
return resp
}
type errorPathFormatter func(segments []domain.ErrorPathSegment) []string
type apiErrorRenderer struct {
// errorPathFormatter allows to override the default path formatter
// for domain.ValidationError and domain.SliceElementValidationError.
// If errorPathFormatter returns an empty slice, an internal server error is rendered.
errorPathFormatter errorPathFormatter
}
func (re apiErrorRenderer) withErrorPathFormatter(formatter errorPathFormatter) apiErrorRenderer {
// this assignment is done on purpose
//nolint:revive
re.errorPathFormatter = formatter
return re
}
var errAPIInternalServerError = apiError{
status: http.StatusInternalServerError,
code: apimodel.ErrorCodeInternalServerError,
message: "internal server error",
}
func (re apiErrorRenderer) render(w http.ResponseWriter, r *http.Request, errs ...error) {
apiErrs := make(apiErrors, 0, len(errs))
for _, err := range errs {
if err == nil {
continue
}
var apiErr apiError
var domainErr domain.Error
var paramFormatErr *apimodel.InvalidParamFormatError
switch {
case errors.As(err, &apiErr):
case errors.As(err, &paramFormatErr):
apiErr = re.invalidParamFormatErrorToAPIError(paramFormatErr)
case errors.As(err, &domainErr):
apiErr = re.domainErrorToAPIError(domainErr)
default:
apiErr = errAPIInternalServerError
}
apiErrs = append(apiErrs, apiErr)
}
renderJSON(w, r, apiErrs.status(), apiErrs.toResponse())
}
func (re apiErrorRenderer) invalidParamFormatErrorToAPIError(
paramFormatErr *apimodel.InvalidParamFormatError,
) apiError {
location := "$unknown"
switch paramFormatErr.Location {
case runtime.ParamLocationPath:
location = "$path"
case runtime.ParamLocationHeader:
location = "$header"
case runtime.ParamLocationQuery:
location = "$query"
case runtime.ParamLocationCookie:
location = "$cookie"
case runtime.ParamLocationUndefined:
// do nothing
}
return apiError{
status: http.StatusBadRequest,
code: apimodel.ErrorCodeInvalidParamFormat,
path: []string{location, paramFormatErr.ParamName},
message: paramFormatErr.Err.Error(),
}
}
func (re apiErrorRenderer) domainErrorToAPIError(domainErr domain.Error) apiError {
message := domainErr.Error()
var pathSegments []domain.ErrorPathSegment
var err error = domainErr
for {
var withPath domain.ErrorWithPath
if !errors.As(err, &withPath) {
break
}
pathSegments = append(pathSegments, withPath.Path())
u, ok := withPath.(interface {
Unwrap() error
})
if ok {
err = u.Unwrap()
message = err.Error()
} else {
err = nil
}
}
var path []string
if len(pathSegments) > 0 {
if re.errorPathFormatter == nil {
return errAPIInternalServerError
}
path = re.errorPathFormatter(pathSegments)
if len(path) == 0 {
return errAPIInternalServerError
}
}
var params map[string]any
if withParams, ok := domainErr.(domain.ErrorWithParams); ok {
params = withParams.Params()
}
cloned := make(map[string]any, len(params))
for k, v := range params {
cloned[strcase.ToCamel(k)] = v
}
return apiError{
status: re.domainErrorTypeToStatusCode(domainErr.Type()),
code: apimodel.ErrorCode(domainErr.Code()),
path: path,
params: cloned,
message: message,
}
}
func (re apiErrorRenderer) domainErrorTypeToStatusCode(code domain.ErrorType) int {
switch code {
case domain.ErrorTypeIncorrectInput:
return http.StatusBadRequest
case domain.ErrorTypeNotFound:
return http.StatusNotFound
case domain.ErrorTypeUnknown:
fallthrough
default:
return http.StatusInternalServerError
}
}