267 lines
6.3 KiB
Go
267 lines
6.3 KiB
Go
package http
|
|
|
|
import (
|
|
"compress/gzip"
|
|
"context"
|
|
"encoding/gob"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
|
|
"github.com/influxdata/influxdb/v2/kit/platform/errors"
|
|
"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(ctx context.Context, 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(ctx context.Context, 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 unmarshalling
|
|
// 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 &errors.Error{
|
|
Code: errors.EInvalid,
|
|
Msg: fmt.Sprintf("failed to unmarshal %s: %s", encoding, err),
|
|
}
|
|
},
|
|
errFn: func(ctx context.Context, err error) (interface{}, int, error) {
|
|
msg := err.Error()
|
|
if msg == "" {
|
|
msg = "an internal error has occurred"
|
|
}
|
|
code := errors.ErrorCode(err)
|
|
return ErrBody{
|
|
Code: code,
|
|
Msg: msg,
|
|
}, ErrorCodeToStatusCode(ctx, code), 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, r *http.Request, 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")
|
|
|
|
// this marshal block is to catch failures before they hit the http writer.
|
|
// default behavior for http.ResponseWriter is when body is written and no
|
|
// status is set, it writes a 200. Or if a status is set before encoding
|
|
// and an error occurs, there is no means to write a proper status code
|
|
// (i.e. 500) when that is to occur. This brings that step out before
|
|
// and then writes the data and sets the status code after marshaling
|
|
// succeeds.
|
|
var (
|
|
b []byte
|
|
err error
|
|
)
|
|
if a == nil || a.prettyJSON {
|
|
b, err = json.MarshalIndent(v, "", "\t")
|
|
} else {
|
|
b, err = json.Marshal(v)
|
|
}
|
|
if err != nil {
|
|
a.Err(w, r, err)
|
|
return
|
|
}
|
|
|
|
a.write(w, writer, status, b)
|
|
}
|
|
|
|
// Write allows the user to write raw bytes to the response writer. This
|
|
// operation does not have a fail case, all failures here will be logged.
|
|
func (a *API) Write(w http.ResponseWriter, status int, b []byte) {
|
|
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)
|
|
}
|
|
|
|
a.write(w, writer, status, b)
|
|
}
|
|
|
|
func (a *API) write(w http.ResponseWriter, wc io.WriteCloser, status int, b []byte) {
|
|
w.WriteHeader(status)
|
|
if _, err := wc.Write(b); err != nil {
|
|
a.logErr("failed to write to response writer", zap.Error(err))
|
|
}
|
|
|
|
if err := wc.Close(); err != nil {
|
|
a.logErr("failed to close response writer", zap.Error(err))
|
|
}
|
|
}
|
|
|
|
// Err is used for writing an error to the response.
|
|
func (a *API) Err(w http.ResponseWriter, r *http.Request, err error) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
a.logErr("api error encountered", zap.Error(err))
|
|
|
|
v, status, err := a.errFn(r.Context(), err)
|
|
if err != nil {
|
|
a.logErr("failed to write err to response writer", zap.Error(err))
|
|
a.Respond(w, r, http.StatusInternalServerError, ErrBody{
|
|
Code: "internal error",
|
|
Msg: "an unexpected error occurred",
|
|
})
|
|
return
|
|
}
|
|
|
|
if eb, ok := v.(ErrBody); ok {
|
|
w.Header().Set(PlatformErrorCodeHeader, eb.Code)
|
|
}
|
|
|
|
a.Respond(w, r, 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"`
|
|
}
|