github.com/brycereitano/goa@v0.0.0-20170315073847-8ffa6c85e265/error.go (about)

     1  /*
     2  Package goa standardizes on structured error responses: a request that fails because of an
     3  invalid input or an unexpected condition produces a response that contains a structured error.
     4  
     5  The error data structures returned to clients contains five fields: an ID, a code, a status, a
     6  detail and metadata. The ID is unique for the occurrence of the error, it helps correlate the
     7  content of the response with the content of the service logs. The code defines the class of error
     8  (e.g.  "invalid_parameter_type") and the status the corresponding HTTP status (e.g. 400). The detail
     9  contains a message specific to the error occurrence. The metadata contains key/value pairs that
    10  provide contextual information (name of parameters, value of invalid parameter etc.).
    11  
    12  Instances of Error can be created via Error Class functions.
    13  See http://goa.design/implement/error_handling.html
    14  All instance of errors created via a error class implement the ServiceError interface. This
    15  interface is leveraged by the error handler middleware to produce the error responses.
    16  
    17  The code generated by goagen calls the helper functions exposed in this file when it encounters
    18  invalid data (wrong type, validation errors etc.) such as InvalidParamTypeError,
    19  InvalidAttributeTypeError etc. These methods return errors that get merged with any previously
    20  encountered error via the Error Merge method. The helper functions are error classes stored in
    21  global variable. This means your code can override their values to produce arbitrary error
    22  responses.
    23  
    24  goa includes an error handler middleware that takes care of mapping back any error returned by
    25  previously called middleware or action handler into HTTP responses. If the error was created via an
    26  error class then the corresponding content including the HTTP status is used otherwise an internal
    27  error is returned. Errors that bubble up all the way to the top (i.e. not handled by the error
    28  middleware) also generate an internal error response.
    29  */
    30  package goa
    31  
    32  import (
    33  	"crypto/rand"
    34  	"encoding/base64"
    35  	"fmt"
    36  	"io"
    37  	"strings"
    38  )
    39  
    40  var (
    41  	// ErrorMediaIdentifier is the media type identifier used for error responses.
    42  	ErrorMediaIdentifier = "application/vnd.goa.error"
    43  
    44  	// ErrBadRequest is a generic bad request error.
    45  	ErrBadRequest = NewErrorClass("bad_request", 400)
    46  
    47  	// ErrUnauthorized is a generic unauthorized error.
    48  	ErrUnauthorized = NewErrorClass("unauthorized", 401)
    49  
    50  	// ErrInvalidRequest is the class of errors produced by the generated code when a request
    51  	// parameter or payload fails to validate.
    52  	ErrInvalidRequest = NewErrorClass("invalid_request", 400)
    53  
    54  	// ErrInvalidEncoding is the error produced when a request body fails to be decoded.
    55  	ErrInvalidEncoding = NewErrorClass("invalid_encoding", 400)
    56  
    57  	// ErrRequestBodyTooLarge is the error produced when the size of a request body exceeds
    58  	// MaxRequestBodyLength bytes.
    59  	ErrRequestBodyTooLarge = NewErrorClass("request_too_large", 413)
    60  
    61  	// ErrNoAuthMiddleware is the error produced when no auth middleware is mounted for a
    62  	// security scheme defined in the design.
    63  	ErrNoAuthMiddleware = NewErrorClass("no_auth_middleware", 500)
    64  
    65  	// ErrInvalidFile is the error produced by ServeFiles when requested to serve non-existant
    66  	// or non-readable files.
    67  	ErrInvalidFile = NewErrorClass("invalid_file", 404)
    68  
    69  	// ErrNotFound is the error returned to requests that don't match a registered handler.
    70  	ErrNotFound = NewErrorClass("not_found", 404)
    71  
    72  	// ErrInternal is the class of error used for uncaught errors.
    73  	ErrInternal = NewErrorClass("internal", 500)
    74  )
    75  
    76  type (
    77  	// ErrorClass is an error generating function.
    78  	// It accepts a message and optional key value pairs and produces errors that implement
    79  	// ServiceError.
    80  	// If the message is a string or a fmt.Stringer then the string value is used.
    81  	// If the message is an error then the string returned by Error() is used.
    82  	// Otherwise the string produced using fmt.Sprintf("%v") is used.
    83  	// The optional key value pairs are intended to provide additional contextual information
    84  	// and are returned to the client.
    85  	ErrorClass func(message interface{}, keyvals ...interface{}) error
    86  
    87  	// ServiceError is the interface implemented by all errors created using a ErrorClass
    88  	// function.
    89  	ServiceError interface {
    90  		// ServiceError extends the error interface
    91  		error
    92  		// ResponseStatus dictates the status used to build the response sent to the client.
    93  		ResponseStatus() int
    94  		// Token is a unique value associated with the occurrence of the error.
    95  		Token() string
    96  	}
    97  
    98  	// ErrorResponse contains the details of a error response. It implements ServiceError.
    99  	// This struct is mainly intended for clients to decode error responses.
   100  	ErrorResponse struct {
   101  		// ID is the unique error instance identifier.
   102  		ID string `json:"id" xml:"id" form:"id"`
   103  		// Code identifies the class of errors.
   104  		Code string `json:"code" xml:"code" form:"code"`
   105  		// Status is the HTTP status code used by responses that cary the error.
   106  		Status int `json:"status" xml:"status" form:"status"`
   107  		// Detail describes the specific error occurrence.
   108  		Detail string `json:"detail" xml:"detail" form:"detail"`
   109  		// Meta contains additional key/value pairs useful to clients.
   110  		Meta map[string]interface{} `json:"meta,omitempty" xml:"meta,omitempty" form:"meta,omitempty"`
   111  	}
   112  )
   113  
   114  // NewErrorClass creates a new error class.
   115  // It is the responsibility of the client to guarantee uniqueness of code.
   116  func NewErrorClass(code string, status int) ErrorClass {
   117  	return func(message interface{}, keyvals ...interface{}) error {
   118  		var msg string
   119  		switch actual := message.(type) {
   120  		case string:
   121  			msg = actual
   122  		case error:
   123  			msg = actual.Error()
   124  		case fmt.Stringer:
   125  			msg = actual.String()
   126  		default:
   127  			msg = fmt.Sprintf("%v", actual)
   128  		}
   129  		var meta map[string]interface{}
   130  		l := len(keyvals)
   131  		if l > 0 {
   132  			meta = make(map[string]interface{})
   133  		}
   134  		for i := 0; i < l; i += 2 {
   135  			k := keyvals[i]
   136  			var v interface{} = "MISSING"
   137  			if i+1 < l {
   138  				v = keyvals[i+1]
   139  			}
   140  			meta[fmt.Sprintf("%v", k)] = v
   141  		}
   142  		return &ErrorResponse{ID: newErrorID(), Code: code, Status: status, Detail: msg, Meta: meta}
   143  	}
   144  }
   145  
   146  // MissingPayloadError is the error produced when a request is missing a required payload.
   147  func MissingPayloadError() error {
   148  	return ErrInvalidRequest("missing required payload")
   149  }
   150  
   151  // InvalidParamTypeError is the error produced when the type of a parameter does not match the type
   152  // defined in the design.
   153  func InvalidParamTypeError(name string, val interface{}, expected string) error {
   154  	msg := fmt.Sprintf("invalid value %#v for parameter %#v, must be a %s", val, name, expected)
   155  	return ErrInvalidRequest(msg, "param", name, "value", val, "expected", expected)
   156  }
   157  
   158  // MissingParamError is the error produced for requests that are missing path or querystring
   159  // parameters.
   160  func MissingParamError(name string) error {
   161  	msg := fmt.Sprintf("missing required parameter %#v", name)
   162  	return ErrInvalidRequest(msg, "name", name)
   163  }
   164  
   165  // InvalidAttributeTypeError is the error produced when the type of payload field does not match
   166  // the type defined in the design.
   167  func InvalidAttributeTypeError(ctx string, val interface{}, expected string) error {
   168  	msg := fmt.Sprintf("type of %s must be %s but got value %#v", ctx, expected, val)
   169  	return ErrInvalidRequest(msg, "attribute", ctx, "value", val, "expected", expected)
   170  }
   171  
   172  // MissingAttributeError is the error produced when a request payload is missing a required field.
   173  func MissingAttributeError(ctx, name string) error {
   174  	msg := fmt.Sprintf("attribute %#v of %s is missing and required", name, ctx)
   175  	return ErrInvalidRequest(msg, "attribute", name, "parent", ctx)
   176  }
   177  
   178  // MissingHeaderError is the error produced when a request is missing a required header.
   179  func MissingHeaderError(name string) error {
   180  	msg := fmt.Sprintf("missing required HTTP header %#v", name)
   181  	return ErrInvalidRequest(msg, "name", name)
   182  }
   183  
   184  // InvalidEnumValueError is the error produced when the value of a parameter or payload field does
   185  // not match one the values defined in the design Enum validation.
   186  func InvalidEnumValueError(ctx string, val interface{}, allowed []interface{}) error {
   187  	elems := make([]string, len(allowed))
   188  	for i, a := range allowed {
   189  		elems[i] = fmt.Sprintf("%#v", a)
   190  	}
   191  	msg := fmt.Sprintf("value of %s must be one of %s but got value %#v", ctx, strings.Join(elems, ", "), val)
   192  	return ErrInvalidRequest(msg, "attribute", ctx, "value", val, "expected", strings.Join(elems, ", "))
   193  }
   194  
   195  // InvalidFormatError is the error produced when the value of a parameter or payload field does not
   196  // match the format validation defined in the design.
   197  func InvalidFormatError(ctx, target string, format Format, formatError error) error {
   198  	msg := fmt.Sprintf("%s must be formatted as a %s but got value %#v, %s", ctx, format, target, formatError.Error())
   199  	return ErrInvalidRequest(msg, "attribute", ctx, "value", target, "expected", format, "error", formatError.Error())
   200  }
   201  
   202  // InvalidPatternError is the error produced when the value of a parameter or payload field does
   203  // not match the pattern validation defined in the design.
   204  func InvalidPatternError(ctx, target string, pattern string) error {
   205  	msg := fmt.Sprintf("%s must match the regexp %#v but got value %#v", ctx, pattern, target)
   206  	return ErrInvalidRequest(msg, "attribute", ctx, "value", target, "regexp", pattern)
   207  }
   208  
   209  // InvalidRangeError is the error produced when the value of a parameter or payload field does
   210  // not match the range validation defined in the design. value may be a int or a float64.
   211  func InvalidRangeError(ctx string, target interface{}, value interface{}, min bool) error {
   212  	comp := "greater than or equal to"
   213  	if !min {
   214  		comp = "less than or equal to"
   215  	}
   216  	msg := fmt.Sprintf("%s must be %s %d but got value %#v", ctx, comp, value, target)
   217  	return ErrInvalidRequest(msg, "attribute", ctx, "value", target, "comp", comp, "expected", value)
   218  }
   219  
   220  // InvalidLengthError is the error produced when the value of a parameter or payload field does
   221  // not match the length validation defined in the design.
   222  func InvalidLengthError(ctx string, target interface{}, ln, value int, min bool) error {
   223  	comp := "greater than or equal to"
   224  	if !min {
   225  		comp = "less than or equal to"
   226  	}
   227  	msg := fmt.Sprintf("length of %s must be %s %d but got value %#v (len=%d)", ctx, comp, value, target, ln)
   228  	return ErrInvalidRequest(msg, "attribute", ctx, "value", target, "len", ln, "comp", comp, "expected", value)
   229  }
   230  
   231  // NoAuthMiddleware is the error produced when goa is unable to lookup a auth middleware for a
   232  // security scheme defined in the design.
   233  func NoAuthMiddleware(schemeName string) error {
   234  	msg := fmt.Sprintf("Auth middleware for security scheme %s is not mounted", schemeName)
   235  	return ErrNoAuthMiddleware(msg, "scheme", schemeName)
   236  }
   237  
   238  // Error returns the error occurrence details.
   239  func (e *ErrorResponse) Error() string {
   240  	msg := fmt.Sprintf("[%s] %d %s: %s", e.ID, e.Status, e.Code, e.Detail)
   241  	for k, v := range e.Meta {
   242  		msg += ", " + fmt.Sprintf("%s: %v", k, v)
   243  	}
   244  	return msg
   245  }
   246  
   247  // ResponseStatus is the status used to build responses.
   248  func (e *ErrorResponse) ResponseStatus() int { return e.Status }
   249  
   250  // Token is the unique error occurrence identifier.
   251  func (e *ErrorResponse) Token() string { return e.ID }
   252  
   253  // MergeErrors updates an error by merging another into it. It first converts other into a
   254  // ServiceError if not already one - producing an internal error in that case. The merge algorithm
   255  // is:
   256  //
   257  // * If any of e or other is an internal error then the result is an internal error
   258  //
   259  // * If the status or code of e and other don't match then the result is a 400 "bad_request"
   260  //
   261  // The Detail field is updated by concatenating the Detail fields of e and other separated
   262  // by a semi-colon. The MetaValues field of is updated by merging the map of other MetaValues
   263  // into e's where values in e with identical keys to values in other get overwritten.
   264  //
   265  // Merge returns the updated error. This is useful in case the error was initially nil in
   266  // which case other is returned.
   267  func MergeErrors(err, other error) error {
   268  	if err == nil {
   269  		if other == nil {
   270  			return nil
   271  		}
   272  		return asErrorResponse(other)
   273  	}
   274  	if other == nil {
   275  		return asErrorResponse(err)
   276  	}
   277  	e := asErrorResponse(err)
   278  	o := asErrorResponse(other)
   279  	switch {
   280  	case e.Status == 500 || o.Status == 500:
   281  		if e.Status != 500 {
   282  			e.Status = 500
   283  			e.Code = "internal_error"
   284  		}
   285  	case e.Status != o.Status || e.Code != o.Code:
   286  		e.Status = 400
   287  		e.Code = "bad_request"
   288  	}
   289  	e.Detail = e.Detail + "; " + o.Detail
   290  
   291  	if e.Meta == nil && len(o.Meta) > 0 {
   292  		e.Meta = make(map[string]interface{})
   293  	}
   294  	for k, v := range o.Meta {
   295  		e.Meta[k] = v
   296  	}
   297  	return e
   298  }
   299  
   300  func asErrorResponse(err error) *ErrorResponse {
   301  	e, ok := err.(*ErrorResponse)
   302  	if !ok {
   303  		return &ErrorResponse{Status: 500, Code: "internal_error", Detail: err.Error()}
   304  	}
   305  	return e
   306  }
   307  
   308  // If you're curious - simplifying a bit - the probability of 2 values being equal for n 6-bytes
   309  // values is n^2 / 2^49. For n = 1 million this gives around 1 chance in 500. 6 bytes seems to be a
   310  // good trade-off between probability of clashes and length of ID (6 * 4/3 = 8 chars) since clashes
   311  // are not catastrophic.
   312  func newErrorID() string {
   313  	b := make([]byte, 6)
   314  	io.ReadFull(rand.Reader, b)
   315  	return base64.StdEncoding.EncodeToString(b)
   316  }