Skip to content

Commit

Permalink
Add error codes (#1137)
Browse files Browse the repository at this point in the history
* Add error codes so that clients can reliably understand errors. One error was added that allows a client to understand that a store is already tombstoned when remove is requested.

For extensibility error codes are text (reserving numbers is a pain).
The error code will show up in both a header and in the JSON body which can contain additional structured data about the error. This is in addition to sending back a detailed error message as is done now. The main goal was just to send back the error code in the header. Our ideas about the JSON body might change.
  • Loading branch information
gregwebs authored and nolouch committed Jul 18, 2018
1 parent 9eed312 commit 2d3813b
Show file tree
Hide file tree
Showing 12 changed files with 552 additions and 75 deletions.
32 changes: 25 additions & 7 deletions pkg/apiutil/apiutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ package apiutil

import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"strconv"

"github.com/juju/errors"
)
Expand All @@ -31,16 +33,11 @@ func DeferClose(c io.Closer, err *error) {

// JSONError lets callers check for just one error type
type JSONError struct {
err error
Err error
}

func (e JSONError) Error() string {
return e.err.Error()
}

// Cause for compatibility with the errors package
func (e JSONError) Cause() error {
return e.err
return e.Err.Error()
}

func tagJSONError(err error) error {
Expand Down Expand Up @@ -68,3 +65,24 @@ func ReadJSON(r io.ReadCloser, data interface{}) error {

return err
}

// FieldError connects an error to a particular field
type FieldError struct {
error
field string
}

// ParseUint64VarsField connects strconv.ParseUint with request variables
// It hardcodes the base to 10 and bitsize to 64
// Any error returned will connect the requested field to the error via FieldError
func ParseUint64VarsField(vars map[string]string, varName string) (uint64, *FieldError) {
str, ok := vars[varName]
if !ok {
return 0, &FieldError{field: varName, error: fmt.Errorf("field %s not present", varName)}
}
parsed, err := strconv.ParseUint(str, 10, 64)
if err == nil {
return parsed, nil
}
return parsed, &FieldError{field: varName, error: err}
}
206 changes: 206 additions & 0 deletions pkg/error_code/error_code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Copyright 2018 PingCAP, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.

// Package errcode facilitates standardized API error codes.
// The goal is that clients can reliably understand errors by checking against immutable error codes
// A RegisteredCode should never be modified once committed (and released for use by clients).
// Instead a new RegisteredCode should be created.
//
// Note that the error codes are strings.
// Integer error codes have extensibility issues (branches using the same numbers, etc).
//
// This package is designed to have few opinions and be a starting point for how you want to do errors.
// The only requirement is to satisfy the ErrorCode interface (see documentation of ErrCode).
//
// A few generic error codes are provided here.
// You are encouraged to create your own application customized error codes rather than just using generic errors.
//
// See JSONFormat for an opinion on how to send back error information to a client.
// Note that this includes a body of response data (the "data field") with more detailed and structured information.
// This package provides no help on defining conventions, versioning, etc for that data.
package errcode

import (
"net/http"
)

// RegisteredCode helps document that we are using a registered error code that must never change
type RegisteredCode string

const (
// InternalCode means the operation placed the system is in an inconsistent or unrecoverable state.
// Essentially a handled panic.
// This is exactly the same as a HTTP 500, so it is not necessary to send this code over HTTP.
InternalCode RegisteredCode = "internal"
// InvalidInputCode is a validation failure of an input.
// The response will indicate the exact input issue.
InvalidInputCode RegisteredCode = "input"
// NotFoundCode indicates a resource is missing.
// The response can indicate what resource is missing.
// However, when a single resource is requested, that may be implicit.
NotFoundCode RegisteredCode = "missing"
)

/*
ErrorCode is the interface that ties an error and RegisteredCode together.
Note that there are two additional interfaces (HasHTTPCode and HasClientData) that can be defined by an ErrorCode
To further customize behavior
For example, internalError implements HasHTTPCode to change it to a 500.
ErrorCode allows error codes to be defined
without being forced to use a particular struct such as CodedError.
CodedError is convenient for generic errors that wrap many different code errors.
Please see the docs for CodedError.
For an application specific error with a 1:1 mapping between a go error structure and a RegisteredCode,
You probably want to use this interface directly. Example:
const PathBlockedCode RegisteredCode = "path.state.blocked"
type PathBlocked struct {
start uint64 `json:"start"`
end uint64 `json:"end"`
obstacle uint64 `json:"end"`
}
func (e PathBlocked) Error() string {
return fmt.Sprintf("The path %d -> %d has obstacle %d", e.start, e.end, e.obstacle)
}
func (e PathBlocked) Code() RegisteredCode {
return PathBlockedCode
}
*/
type ErrorCode interface {
Error() string // The Error interface
Code() RegisteredCode
}

// HasHTTPCode is defined to override the default of 400 Bad Request
type HasHTTPCode interface {
GetHTTPCode() int
}

// HTTPCode defaults to 400 BadRequest
// This is overidden by defining the HasHTTPCode interface with a GetHTTPCode function
func HTTPCode(errCode ErrorCode) int {
httpCode := http.StatusBadRequest
if hasData, ok := errCode.(HasHTTPCode); ok {
httpCode = hasData.GetHTTPCode()
}
return httpCode
}

// HasClientData is used to defined how to retrieve the data portion of an ErrorCode to be returned to the client.
// Otherwise the struct itself will be assumed to be all the data.
// This is provided for exensibility, but may be unecessary for you.
// Normally data is retrieved with the ClientData function.
type HasClientData interface {
GetClientData() interface{}
}

// ClientData retrieves data from a structure that implements HasClientData
// If HasClientData is not defined it will use the given ErrorCode object.
// Normally this function is used rather than GetClientData.
func ClientData(errCode ErrorCode) interface{} {
var data interface{} = errCode
if hasData, ok := errCode.(HasClientData); ok {
data = hasData.GetClientData()
}
return data
}

// JSONFormat is a standard way to serilalize an ErrorCode to JSON.
// Msg is the string from Error().
// The Data field is filled in by GetClientData
type JSONFormat struct {
Data interface{} `json:"data"`
Msg string `json:"msg"`
Code RegisteredCode `json:"code"`
}

// NewJSONFormat turns an ErrCode into a JSONFormat
func NewJSONFormat(errCode ErrorCode) JSONFormat {
data := ClientData(errCode)
return JSONFormat{Code: errCode.Code(), Data: data, Msg: errCode.Error()}
}

// CodedError is a convenience to attach a code to an error and already satisfy the ErrorCode interface.
// If the error is a struct, that struct will get preseneted as data to the client.
//
// To override the http code or the data representation or just for clearer documentation,
// you are encouraged to wrap CodeError with your own struct that inherits it.
// Look at the implementation of invalidInput, internalError, and notFound.
type CodedError struct {
RegisteredCode RegisteredCode
Err error
}

var _ ErrorCode = (*CodedError)(nil) // assert implements interface
var _ HasClientData = (*CodedError)(nil) // assert implements interface

func (e CodedError) Error() string {
return e.Err.Error()
}

// Code returns the RegisteredCode field
func (e CodedError) Code() RegisteredCode {
return e.RegisteredCode
}

// GetClientData returns the underlying Err field.
func (e CodedError) GetClientData() interface{} {
return e.Err
}

// invalidInput gives the code InvalidInputCode
type invalidInputErr struct{ CodedError }

// NewInvalidInputErr creates an invalidInput with error code of InvalidInputCode
func NewInvalidInputErr(err error) ErrorCode {
return invalidInputErr{CodedError{RegisteredCode: InvalidInputCode, Err: err}}
}

var _ ErrorCode = (*invalidInputErr)(nil) // assert implements interface

// internalError gives the code InvalidInputCode
type internalErr struct{ CodedError }

// GetHTTPCode returns 500
func (e internalErr) GetHTTPCode() int {
return http.StatusInternalServerError
}

// NewInternalErr creates an internalError with error code InternalCode
func NewInternalErr(err error) ErrorCode {
return internalErr{CodedError{RegisteredCode: InternalCode, Err: err}}
}

var _ ErrorCode = (*internalErr)(nil) // assert implements interface
var _ HasHTTPCode = (*internalErr)(nil) // assert implements interface

// notFound gives the code NotFoundCode
type notFoundErr struct{ CodedError }

// HTTPCode returns 404
func (e notFoundErr) GetHTTPCode() int {
return http.StatusNotFound
}

// NewNotFoundErr creates a notFound with error code of InternalCode
func NewNotFoundErr(err error) ErrorCode {
return notFoundErr{CodedError{RegisteredCode: NotFoundCode, Err: err}}
}

var _ ErrorCode = (*notFoundErr)(nil) // assert implements interface
var _ HasHTTPCode = (*notFoundErr)(nil) // assert implements interface
Loading

0 comments on commit 2d3813b

Please sign in to comment.