Skip to content
Snippets Groups Projects
Commit 6467eaf6 authored by ale's avatar ale
Browse files

Refactor error handling to use a common base type with error codes

Also make ValidationError just a simple error type with an associated
field name, which is a lot easier to reason about.
parent 6273a767
No related branches found
No related tags found
1 merge request!142Refactor validation code for readability
Pipeline #84320 failed
package accountserver package accountserver
import ( import (
"encoding/json"
"errors" "errors"
"fmt"
) )
var ( var (
...@@ -20,142 +18,56 @@ var ( ...@@ -20,142 +18,56 @@ var (
// original error message, we wrap the original error with our custom // original error message, we wrap the original error with our custom
// types. // types.
// AuthError is an authentication error. // The API error type is a wrapper with a specific class (code).
type AuthError struct { type apiError struct {
code int
wrap error wrap error
} }
func (e *AuthError) Error() string { return e.wrap.Error() } func (e *apiError) Error() string { return e.wrap.Error() }
func (e *AuthError) Unwrap() error { return e.wrap } func (e *apiError) Unwrap() error { return e.wrap }
func (e *AuthError) Is(target error) bool { func newAPIError(code int, err error) *apiError {
_, ok := target.(*AuthError) return &apiError{code: code, wrap: err}
return ok
} }
func isAPIErrorWithCode(err error, code int) bool {
var ae *apiError
return errors.As(err, &ae) && ae.code == code
}
const (
codeAuthErr = iota
codeRequestErr
codeBackendErr
)
func newAuthError(err error) error { func newAuthError(err error) error {
return &AuthError{wrap: err} return newAPIError(codeAuthErr, err)
} }
// IsAuthError returns true if err is an authentication / // IsAuthError returns true if err is an authentication /
// authorization error. // authorization error.
func IsAuthError(err error) bool { func IsAuthError(err error) bool {
return errors.Is(err, new(AuthError)) return isAPIErrorWithCode(err, codeAuthErr)
}
// RequestError indicates an issue with validating the request.
type RequestError struct {
wrap error
}
func (e *RequestError) Error() string { return e.wrap.Error() }
func (e *RequestError) Unwrap() error { return e.wrap }
func (e *RequestError) Is(target error) bool {
_, ok := target.(*RequestError)
return ok
} }
func newRequestError(err error) error { func newRequestError(err error) error {
return &RequestError{wrap: err} return newAPIError(codeRequestErr, err)
} }
// IsRequestError returns true if err is a request error (bad // IsRequestError returns true if err is a request error (bad
// request). // request).
func IsRequestError(err error) bool { func IsRequestError(err error) bool {
return errors.Is(err, new(RequestError)) return isAPIErrorWithCode(err, codeRequestErr)
}
type BackendError struct {
wrap error
}
func (e *BackendError) Error() string { return e.wrap.Error() }
func (e *BackendError) Unwrap() error { return e.wrap }
func (e *BackendError) Is(target error) bool {
_, ok := target.(*BackendError)
return ok
} }
func newBackendError(err error) error { func newBackendError(err error) error {
return &BackendError{wrap: err} return newAPIError(codeBackendErr, err)
} }
// IsBackendError returns true if err is a backend error. // IsBackendError returns true if err is a backend error.
func IsBackendError(err error) bool { func IsBackendError(err error) bool {
return errors.Is(err, new(BackendError)) return isAPIErrorWithCode(err, codeBackendErr)
}
// ValidationError holds field-specific information that can be
// serialized as JSON if desired.
type ValidationError struct {
field string
err error
}
// IsValidationError returns true if err is a validation error.
func IsValidationError(err error) bool {
return errors.Is(err, new(ValidationError))
}
// Initialize or extend a validationError.
func newValidationError(field string, err error) *ValidationError {
if field == "" {
field = "_form"
}
return &ValidationError{
field: field,
err: err,
}
}
func newValidationErrorStr(field, s string) *ValidationError {
return newValidationError(field, errors.New(s))
}
func (v *ValidationError) Error() string {
return fmt.Sprintf("\"%s\": %s", v.field, v.err.Error())
}
func (v *ValidationError) Unwrap() error { return v.err }
func (v *ValidationError) Is(target error) bool {
_, ok := target.(*ValidationError)
return ok
}
type wrappedError interface {
Unwrap() error
}
type wrappedErrorMulti interface {
Unwrap() []error
}
func unwrapErrToMap(err error, out map[string]string) {
if t, ok := err.(*ValidationError); ok {
out[t.field] = t.err.Error()
} else if t, ok := err.(wrappedErrorMulti); ok {
for _, innerErr := range t.Unwrap() {
unwrapErrToMap(innerErr, out)
}
} else if t, ok := err.(wrappedError); ok {
unwrapErrToMap(t.Unwrap(), out)
} else {
out["error"] = err.Error()
}
}
// ErrorToJSON returns a nice JSON object representation of an error
// by unwrapping it and checking for ValidationErrors that might be
// field-specific.
func ErrorToJSON(err error) []byte {
values := make(map[string]string)
unwrapErrToMap(err, values)
data, _ := json.Marshal(values)
return data
} }
...@@ -5,7 +5,7 @@ import ( ...@@ -5,7 +5,7 @@ import (
"testing" "testing"
) )
func TestErrors(t *testing.T) { func TestErrors_Nesting(t *testing.T) {
verr := newValidationErrorStr("field", "bad") verr := newValidationErrorStr("field", "bad")
rerr := newRequestError(verr) rerr := newRequestError(verr)
......
...@@ -244,19 +244,19 @@ func (s *forwardServer) ServeHTTP(w http.ResponseWriter, req *http.Request) { ...@@ -244,19 +244,19 @@ func (s *forwardServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
s.proxy.ServeHTTP(w, req) s.proxy.ServeHTTP(w, req)
} }
func errToStatus(err error) (int, bool) { func errToStatus(err error) (status int, structured bool) {
structured = as.IsValidationError(err)
switch { switch {
case err == as.ErrUserNotFound, err == as.ErrResourceNotFound: case err == as.ErrUserNotFound, err == as.ErrResourceNotFound:
return http.StatusNotFound, false status = http.StatusNotFound
case as.IsAuthError(err): case as.IsAuthError(err):
return http.StatusForbidden, false status = http.StatusForbidden
case as.IsRequestError(err): case as.IsRequestError(err):
return http.StatusBadRequest, false status = http.StatusBadRequest
case as.IsValidationError(err):
return http.StatusBadRequest, true
default: default:
return http.StatusInternalServerError, false status = http.StatusInternalServerError
} }
return
} }
// Some requests contain private information that should not be // Some requests contain private information that should not be
......
package accountserver package accountserver
import (
"encoding/json"
"errors"
"fmt"
)
// ValidationError represents a field-specific error.
type ValidationError struct {
field string
err error
}
func (v *ValidationError) Error() string {
return fmt.Sprintf("\"%s\": %s", v.field, v.err.Error())
}
func (v *ValidationError) Unwrap() error { return v.err }
func (v *ValidationError) Is(target error) bool {
_, ok := target.(*ValidationError)
return ok
}
func newValidationError(field string, err error) error {
if field == "" {
field = "_form"
}
return &ValidationError{
field: field,
err: err,
}
}
func newValidationErrorStr(field, s string) error {
return newValidationError(field, errors.New(s))
}
// IsValidationError returns true if err is a validation error.
func IsValidationError(err error) bool {
return errors.Is(err, new(ValidationError))
}
type wrappedError interface {
Unwrap() error
}
type wrappedErrorMulti interface {
Unwrap() []error
}
func unwrapErrToMap(err error, out map[string]string) {
if t, ok := err.(*ValidationError); ok {
out[t.field] = t.err.Error()
} else if t, ok := err.(wrappedErrorMulti); ok {
for _, innerErr := range t.Unwrap() {
unwrapErrToMap(innerErr, out)
}
} else if t, ok := err.(wrappedError); ok {
unwrapErrToMap(t.Unwrap(), out)
} else {
out["error"] = err.Error()
}
}
// ErrorToJSON returns a nice JSON object representation of an error
// by unwrapping it and checking for ValidationErrors that might be
// field-specific.
func ErrorToJSON(err error) []byte {
values := make(map[string]string)
unwrapErrToMap(err, values)
data, _ := json.Marshal(values)
return data
}
// Some validation errors are non-critical, i.e. they can be safely // Some validation errors are non-critical, i.e. they can be safely
// overridden if the request is made by an administrator. // overridden if the request is made by an administrator.
type nonCriticalValidationError struct { type nonCriticalValidationError struct {
...@@ -44,6 +118,7 @@ func unwrapCriticalErrs(err error) bool { ...@@ -44,6 +118,7 @@ func unwrapCriticalErrs(err error) bool {
return true return true
} }
// Returns true if none of the wrapped errors are non-critical.
func isCriticalErr(rctx *RequestContext, err error) bool { func isCriticalErr(rctx *RequestContext, err error) bool {
return !(rctx.Auth.IsAdmin && !unwrapCriticalErrs(err)) return !(rctx.Auth.IsAdmin && !unwrapCriticalErrs(err))
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment