Files
threshold/internal/servers/controller/handlers/errors.go
T

216 lines
6.6 KiB
Go
Raw Normal View History

2020-05-26 13:46:21 -07:00
package handlers
import (
"context"
"fmt"
"io"
"net/http"
"sort"
"strings"
2020-05-26 13:46:21 -07:00
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
2020-11-05 16:54:03 -05:00
"github.com/hashicorp/boundary/internal/errors"
2020-08-10 17:15:02 -04:00
pb "github.com/hashicorp/boundary/internal/gen/controller/api"
2020-05-26 13:46:21 -07:00
"github.com/hashicorp/go-hclog"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
genericUniquenessMsg = "Invalid request. Request attempted to make second resource with the same field value that must be unique."
genericNotFoundMsg = "Unable to find requested resource."
)
type apiError struct {
2020-12-07 14:25:16 -07:00
status int32
inner *pb.Error
}
func (e *apiError) Error() string {
2020-12-07 14:25:16 -07:00
res := fmt.Sprintf("Status: %d, Kind: %q, Error: %q", e.status, e.inner.GetKind(), e.inner.GetMessage())
var dets []string
for _, rf := range e.inner.GetDetails().GetRequestFields() {
dets = append(dets, fmt.Sprintf("{name: %q, desc: %q}", rf.GetName(), rf.GetDescription()))
}
if len(dets) > 0 {
det := strings.Join(dets, ", ")
res = fmt.Sprintf("%s, Details: {%s}", res, det)
}
return res
}
func (e *apiError) Is(target error) bool {
var tApiErr *apiError
if !errors.As(target, &tApiErr) {
return false
}
2020-12-07 14:25:16 -07:00
return tApiErr.inner.Kind == e.inner.Kind && tApiErr.status == e.status
}
// ApiErrorWithCode returns an api error with the provided code.
func ApiErrorWithCode(c codes.Code) error {
2020-12-07 14:25:16 -07:00
return &apiError{
status: int32(runtime.HTTPStatusFromCode(c)),
inner: &pb.Error{
Kind: c.String(),
},
}
}
// ApiErrorWithCodeAndMessage returns an api error with the provided code and message.
func ApiErrorWithCodeAndMessage(c codes.Code, msg string, args ...interface{}) error {
2020-12-07 14:25:16 -07:00
return &apiError{
status: int32(runtime.HTTPStatusFromCode(c)),
inner: &pb.Error{
Kind: c.String(),
Message: fmt.Sprintf(msg, args...),
},
}
}
// NotFoundError returns an ApiError indicating a resource couldn't be found.
2020-09-08 23:57:26 -04:00
func NotFoundError() error {
2020-12-07 14:25:16 -07:00
return &apiError{
status: http.StatusNotFound,
inner: &pb.Error{
Kind: codes.NotFound.String(),
Message: "Resource not found.",
},
}
2020-09-08 23:57:26 -04:00
}
// NotFoundErrorf returns an ApiError indicating a resource couldn't be found.
2020-05-26 13:46:21 -07:00
func NotFoundErrorf(msg string, a ...interface{}) error {
2020-12-07 14:25:16 -07:00
return &apiError{
status: http.StatusNotFound,
inner: &pb.Error{
Kind: codes.NotFound.String(),
Message: fmt.Sprintf(msg, a...),
},
}
2020-05-26 13:46:21 -07:00
}
var unauthorizedError = &apiError{
status: http.StatusForbidden,
inner: &pb.Error{
Kind: codes.PermissionDenied.String(),
Message: "Forbidden.",
},
}
2020-07-22 00:14:28 -04:00
func ForbiddenError() error {
return unauthorizedError
}
var unauthenticatedError = &apiError{
status: http.StatusUnauthorized,
inner: &pb.Error{
Kind: codes.Unauthenticated.String(),
Message: "Unauthenticated, or invalid token.",
},
2020-07-22 00:14:28 -04:00
}
2020-09-08 23:57:26 -04:00
func UnauthenticatedError() error {
return unauthenticatedError
2020-09-08 23:57:26 -04:00
}
func InvalidArgumentErrorf(msg string, fields map[string]string) error {
err := ApiErrorWithCodeAndMessage(codes.InvalidArgument, msg)
var apiErr *apiError
if !errors.As(err, &apiErr) {
hclog.L().Error("Unable to build invalid argument api error.", "original error", err)
}
if len(fields) > 0 {
apiErr.inner.Details = &pb.ErrorDetails{}
}
for k, v := range fields {
apiErr.inner.Details.RequestFields = append(apiErr.inner.Details.RequestFields, &pb.FieldError{Name: k, Description: v})
2020-05-26 13:46:21 -07:00
}
sort.Slice(apiErr.inner.GetDetails().GetRequestFields(), func(i, j int) bool {
return apiErr.inner.Details.RequestFields[i].GetName() < apiErr.inner.Details.RequestFields[j].GetName()
})
return apiErr
2020-05-26 13:46:21 -07:00
}
// Converts a known errors into an error that can presented to an end user over the API.
func backendErrorToApiError(inErr error) error {
stErr := status.Convert(inErr)
switch {
case errors.Is(inErr, runtime.ErrNotMatch):
// grpc gateway uses this error when the path was not matched, but the error uses codes.Unimplemented which doesn't match the intention.
// Overwrite the error to match our expected behavior.
2020-12-07 14:25:16 -07:00
return &apiError{
status: http.StatusNotFound,
inner: &pb.Error{
Kind: codes.NotFound.String(),
Message: http.StatusText(http.StatusNotFound),
},
}
case status.Code(inErr) == codes.Unimplemented:
2020-05-26 17:11:29 -07:00
// Instead of returning a 501 we always want to return a 405 when a method isn't implemented.
2020-12-07 14:25:16 -07:00
return &apiError{
status: http.StatusMethodNotAllowed,
inner: &pb.Error{
Kind: codes.Unimplemented.String(),
Message: stErr.Message(),
},
}
2021-03-05 06:34:06 -08:00
case errors.Match(errors.T(errors.RecordNotFound), inErr):
return NotFoundErrorf(genericNotFoundMsg)
2021-03-05 06:34:06 -08:00
case errors.Match(errors.T(errors.InvalidFieldMask), inErr), errors.Match(errors.T(errors.EmptyFieldMask), inErr):
return InvalidArgumentErrorf("Error in provided request", map[string]string{"update_mask": "Invalid update mask provided."})
2021-03-05 06:34:06 -08:00
case errors.IsUniqueError(inErr):
return InvalidArgumentErrorf(genericUniquenessMsg, nil)
2020-05-26 17:11:29 -07:00
}
2021-03-20 06:52:47 -04:00
var statusCode int32 = http.StatusInternalServerError
var domainErr *errors.Err
if errors.As(inErr, &domainErr) && domainErr.Code >= 400 && domainErr.Code <= 599 {
// Domain error codes 400-599 align with http client and server error codes, use the domain error code instead of 500
statusCode = int32(domainErr.Code)
}
2020-12-07 14:25:16 -07:00
// TODO: Don't return potentially sensitive information (like which user id an account
// is already associated with when attempting to re-associate it).
return &apiError{
2021-03-20 06:52:47 -04:00
status: statusCode,
2020-12-07 14:25:16 -07:00
inner: &pb.Error{Kind: codes.Internal.String(), Message: inErr.Error()},
2020-05-26 13:46:21 -07:00
}
}
func ErrorHandler(logger hclog.Logger) runtime.ErrorHandlerFunc {
2020-05-26 13:46:21 -07:00
const errorFallback = `{"error": "failed to marshal error message"}`
return func(ctx context.Context, _ *runtime.ServeMux, mar runtime.Marshaler, w http.ResponseWriter, r *http.Request, inErr error) {
// API specified error, otherwise we need to translate repo/db errors.
var apiErr *apiError
isApiErr := errors.As(inErr, &apiErr)
if !isApiErr {
if err := backendErrorToApiError(inErr); err != nil && !errors.As(err, &apiErr) {
logger.Error("failed to cast error to api error", "error", err)
}
2020-05-26 13:46:21 -07:00
}
2020-12-07 14:25:16 -07:00
if apiErr.status == http.StatusInternalServerError {
logger.Error("internal error returned", "error", inErr)
2020-05-26 13:46:21 -07:00
}
buf, merr := mar.Marshal(apiErr.inner)
2020-05-26 13:46:21 -07:00
if merr != nil {
logger.Error("failed to marshal error response", "response", fmt.Sprintf("%#v", apiErr.inner), "error", merr)
2020-05-26 13:46:21 -07:00
w.WriteHeader(http.StatusInternalServerError)
if _, err := io.WriteString(w, errorFallback); err != nil {
logger.Error("failed to write response", "error", err)
}
return
}
w.Header().Set("Content-Type", mar.ContentType(apiErr.inner))
2020-12-07 14:25:16 -07:00
w.WriteHeader(int(apiErr.status))
2020-05-26 13:46:21 -07:00
if _, err := w.Write(buf); err != nil {
logger.Error("failed to send response chunk", "error", err)
return
}
}
}