Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(http): improve error handling and response to api consumer for org service #16716

Merged
merged 2 commits into from
Feb 4, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat(kit): add http api decoder/responder
  • Loading branch information
jsteenb2 committed Feb 4, 2020
commit 5f34f6108bb30c1d56e443160e85754d6fdc0ba0
245 changes: 245 additions & 0 deletions kit/transport/http/api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
package http

import (
"compress/gzip"
"encoding/gob"
"encoding/json"
"fmt"
"io"
"net/http"

"github.com/influxdata/influxdb"
"go.uber.org/zap"
)

// PlatformErrorCodeHeader shows the error code of platform error.
const PlatformErrorCodeHeader = "X-Platform-Error-Code"

// API provides a consolidated means for handling API interface concerns.
// Concerns such as decoding/encoding request and response bodies as well
// as adding headers for content type and content encoding.
type API struct {
logger *zap.Logger

prettyJSON bool
encodeGZIP bool

unmarshalErrFn func(encoding string, err error) error
okErrFn func(err error) error
errFn func(err error) (interface{}, int, error)
}

// APIOptFn is a functional option for setting fields on the API type.
type APIOptFn func(*API)

// WithLog sets the logger.
func WithLog(logger *zap.Logger) APIOptFn {
return func(api *API) {
api.logger = logger
}
}

// WithErrFn sets the err handling func for issues when writing to the response body.
func WithErrFn(fn func(err error) (interface{}, int, error)) APIOptFn {
return func(api *API) {
api.errFn = fn
}
}

// WithOKErrFn is an error handler for failing validation for request bodies.
func WithOKErrFn(fn func(err error) error) APIOptFn {
return func(api *API) {
api.okErrFn = fn
}
}

// WithPrettyJSON sets the json encoder to marshal indent or not.
func WithPrettyJSON(b bool) APIOptFn {
return func(api *API) {
api.prettyJSON = b
}
}

// WithEncodeGZIP sets the encoder to gzip contents.
func WithEncodeGZIP() APIOptFn {
return func(api *API) {
api.encodeGZIP = true
}
}

// WithUnmarshalErrFn sets the error handler for errors that occur when unmarshaling
// the request body.
func WithUnmarshalErrFn(fn func(encoding string, err error) error) APIOptFn {
return func(api *API) {
api.unmarshalErrFn = fn
}
}

// NewAPI creates a new API type.
func NewAPI(opts ...APIOptFn) *API {
api := API{
logger: zap.NewNop(),
prettyJSON: true,
unmarshalErrFn: func(encoding string, err error) error {
return &influxdb.Error{
Code: influxdb.EInvalid,
Msg: fmt.Sprintf("failed to unmarshal %s: %s", encoding, err),
}
},
errFn: func(err error) (interface{}, int, error) {
code := influxdb.ErrorCode(err)
httpStatusCode, ok := statusCodePlatformError[code]
if !ok {
httpStatusCode = http.StatusBadRequest
}
msg := err.Error()
if msg == "" {
msg = "an internal error has occurred"
}
return ErrBody{
Code: code,
Msg: msg,
}, httpStatusCode, nil
},
}
for _, o := range opts {
o(&api)
}
return &api
}

// DecodeJSON decodes reader with json.
func (a *API) DecodeJSON(r io.Reader, v interface{}) error {
return a.decode("json", json.NewDecoder(r), v)
}

// DecodeGob decodes reader with gob.
func (a *API) DecodeGob(r io.Reader, v interface{}) error {
return a.decode("gob", gob.NewDecoder(r), v)
}

type (
decoder interface {
Decode(interface{}) error
}

oker interface {
OK() error
}
)

func (a *API) decode(encoding string, dec decoder, v interface{}) error {
if err := dec.Decode(v); err != nil {
if a != nil && a.unmarshalErrFn != nil {
return a.unmarshalErrFn(encoding, err)
}
return err
}

if vv, ok := v.(oker); ok {
err := vv.OK()
if a != nil && a.okErrFn != nil {
return a.okErrFn(err)
}
return err
}

return nil
}

// Respond writes to the response writer, handling all errors in writing.
func (a *API) Respond(w http.ResponseWriter, status int, v interface{}) {
if status == http.StatusNoContent {
w.WriteHeader(status)
return
}

var writer io.WriteCloser = noopCloser{Writer: w}
// we'll double close to make sure its always closed even
//on issues before the write
defer writer.Close()

if a != nil && a.encodeGZIP {
w.Header().Set("Content-Encoding", "gzip")
writer = gzip.NewWriter(w)
}

w.Header().Set("Content-Type", "application/json; charset=utf-8")
enc := json.NewEncoder(writer)
if a == nil || a.prettyJSON {
enc.SetIndent("", "\t")
}

w.WriteHeader(status)
if err := enc.Encode(v); err != nil {
a.Err(w, err)
return
}

if err := writer.Close(); err != nil {
a.Err(w, err)
return
}
}

// Err is used for writing an error to the response.
func (a *API) Err(w http.ResponseWriter, err error) {
if err == nil {
return
}

a.logErr("api error encountered", zap.Error(err))

v, status, err := a.errFn(err)
if err != nil {
a.logErr("failed to write err to response writer", zap.Error(err))
a.Respond(w, http.StatusInternalServerError, ErrBody{
Code: "internal error",
Msg: "an unexpected error occured",
})
return
}

if eb, ok := v.(ErrBody); ok {
w.Header().Set(PlatformErrorCodeHeader, eb.Code)
}

a.Respond(w, status, v)
}

func (a *API) logErr(msg string, fields ...zap.Field) {
if a == nil || a.logger == nil {
return
}
a.logger.Error(msg, fields...)
}

type noopCloser struct {
io.Writer
}

func (n noopCloser) Close() error {
return nil
}

// ErrBody is an err response body.
type ErrBody struct {
Code string `json:"code"`
Msg string `json:"message"`
}

// statusCodePlatformError is the map convert platform.Error to error
var statusCodePlatformError = map[string]int{
influxdb.EInternal: http.StatusInternalServerError,
influxdb.EInvalid: http.StatusBadRequest,
influxdb.EUnprocessableEntity: http.StatusUnprocessableEntity,
influxdb.EEmptyValue: http.StatusBadRequest,
influxdb.EConflict: http.StatusUnprocessableEntity,
influxdb.ENotFound: http.StatusNotFound,
influxdb.EUnavailable: http.StatusServiceUnavailable,
influxdb.EForbidden: http.StatusForbidden,
influxdb.ETooManyRequests: http.StatusTooManyRequests,
influxdb.EUnauthorized: http.StatusUnauthorized,
influxdb.EMethodNotAllowed: http.StatusMethodNotAllowed,
influxdb.ETooLarge: http.StatusRequestEntityTooLarge,
}
Loading