-
Notifications
You must be signed in to change notification settings - Fork 720
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
12 changed files
with
552 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.