197 lines
5.6 KiB
Go
197 lines
5.6 KiB
Go
package http
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"strings"
|
|
|
|
errors2 "github.com/influxdata/influxdb/v2/kit/platform/errors"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// ErrorHandler is the error handler in http package.
|
|
type ErrorHandler struct {
|
|
logger *zap.Logger
|
|
}
|
|
|
|
func NewErrorHandler(logger *zap.Logger) ErrorHandler {
|
|
return ErrorHandler{logger: logger}
|
|
}
|
|
|
|
// HandleHTTPError encodes err with the appropriate status code and format,
|
|
// sets the X-Platform-Error-Code headers on the response.
|
|
// We're no longer using X-Influx-Error and X-Influx-Reference.
|
|
// and sets the response status to the corresponding status code.
|
|
func (h ErrorHandler) HandleHTTPError(ctx context.Context, err error, w http.ResponseWriter) {
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
code := errors2.ErrorCode(err)
|
|
var msg string
|
|
if _, ok := err.(*errors2.Error); ok {
|
|
msg = err.Error()
|
|
} else {
|
|
msg = "An internal error has occurred - check server logs"
|
|
h.logger.Warn("internal error not returned to client", zap.Error(err))
|
|
}
|
|
|
|
WriteErrorResponse(ctx, w, code, msg)
|
|
}
|
|
|
|
func WriteErrorResponse(ctx context.Context, w http.ResponseWriter, code string, msg string) {
|
|
w.Header().Set(PlatformErrorCodeHeader, code)
|
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
|
w.WriteHeader(ErrorCodeToStatusCode(ctx, code))
|
|
e := struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
}{
|
|
Code: code,
|
|
Message: msg,
|
|
}
|
|
b, _ := json.Marshal(e)
|
|
_, _ = w.Write(b)
|
|
}
|
|
|
|
// StatusCodeToErrorCode maps a http status code integer to an
|
|
// influxdb error code string.
|
|
func StatusCodeToErrorCode(statusCode int) string {
|
|
errorCode, ok := httpStatusCodeToInfluxDBError[statusCode]
|
|
if ok {
|
|
return errorCode
|
|
}
|
|
|
|
return errors2.EInternal
|
|
}
|
|
|
|
// ErrorCodeToStatusCode maps an influxdb error code string to a
|
|
// http status code integer.
|
|
func ErrorCodeToStatusCode(ctx context.Context, code string) int {
|
|
// If the client disconnects early or times out then return a different
|
|
// error than the passed in error code. Client timeouts return a 408
|
|
// while disconnections return a non-standard Nginx HTTP 499 code.
|
|
if err := ctx.Err(); err == context.DeadlineExceeded {
|
|
return http.StatusRequestTimeout
|
|
} else if err == context.Canceled {
|
|
return 499 // https://httpstatuses.com/499
|
|
}
|
|
|
|
// Otherwise map internal error codes to HTTP status codes.
|
|
statusCode, ok := influxDBErrorToStatusCode[code]
|
|
if ok {
|
|
return statusCode
|
|
}
|
|
return http.StatusInternalServerError
|
|
}
|
|
|
|
// influxDBErrorToStatusCode is a mapping of ErrorCode to http status code.
|
|
var influxDBErrorToStatusCode = map[string]int{
|
|
errors2.EInternal: http.StatusInternalServerError,
|
|
errors2.ENotImplemented: http.StatusNotImplemented,
|
|
errors2.EInvalid: http.StatusBadRequest,
|
|
errors2.EUnprocessableEntity: http.StatusUnprocessableEntity,
|
|
errors2.EEmptyValue: http.StatusBadRequest,
|
|
errors2.EConflict: http.StatusUnprocessableEntity,
|
|
errors2.ENotFound: http.StatusNotFound,
|
|
errors2.EUnavailable: http.StatusServiceUnavailable,
|
|
errors2.EForbidden: http.StatusForbidden,
|
|
errors2.ETooManyRequests: http.StatusTooManyRequests,
|
|
errors2.EUnauthorized: http.StatusUnauthorized,
|
|
errors2.EMethodNotAllowed: http.StatusMethodNotAllowed,
|
|
errors2.ETooLarge: http.StatusRequestEntityTooLarge,
|
|
}
|
|
|
|
var httpStatusCodeToInfluxDBError = map[int]string{}
|
|
|
|
func init() {
|
|
for k, v := range influxDBErrorToStatusCode {
|
|
httpStatusCodeToInfluxDBError[v] = k
|
|
}
|
|
}
|
|
|
|
// CheckErrorStatus for status and any error in the response.
|
|
func CheckErrorStatus(code int, res *http.Response) error {
|
|
err := CheckError(res)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if res.StatusCode != code {
|
|
return fmt.Errorf("unexpected status code: %s", res.Status)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// CheckError reads the http.Response and returns an error if one exists.
|
|
// It will automatically recognize the errors returned by Influx services
|
|
// and decode the error into an internal error type. If the error cannot
|
|
// be determined in that way, it will create a generic error message.
|
|
//
|
|
// If there is no error, then this returns nil.
|
|
func CheckError(resp *http.Response) (err error) {
|
|
switch resp.StatusCode / 100 {
|
|
case 4, 5:
|
|
// We will attempt to parse this error outside of this block.
|
|
case 2:
|
|
return nil
|
|
default:
|
|
// TODO(jsternberg): Figure out what to do here?
|
|
return &errors2.Error{
|
|
Code: errors2.EInternal,
|
|
Msg: fmt.Sprintf("unexpected status code: %d %s", resp.StatusCode, resp.Status),
|
|
}
|
|
}
|
|
|
|
perr := &errors2.Error{
|
|
Code: StatusCodeToErrorCode(resp.StatusCode),
|
|
}
|
|
|
|
if resp.StatusCode == http.StatusUnsupportedMediaType {
|
|
perr.Msg = fmt.Sprintf("invalid media type: %q", resp.Header.Get("Content-Type"))
|
|
return perr
|
|
}
|
|
|
|
contentType := resp.Header.Get("Content-Type")
|
|
if contentType == "" {
|
|
// Assume JSON if there is no content-type.
|
|
contentType = "application/json"
|
|
}
|
|
mediatype, _, _ := mime.ParseMediaType(contentType)
|
|
|
|
var buf bytes.Buffer
|
|
if _, err := io.Copy(&buf, resp.Body); err != nil {
|
|
perr.Msg = "failed to read error response"
|
|
perr.Err = err
|
|
return perr
|
|
}
|
|
|
|
switch mediatype {
|
|
case "application/json":
|
|
if err := json.Unmarshal(buf.Bytes(), perr); err != nil {
|
|
perr.Msg = fmt.Sprintf("attempted to unmarshal error as JSON but failed: %q", err)
|
|
perr.Err = firstLineAsError(buf)
|
|
}
|
|
default:
|
|
perr.Err = firstLineAsError(buf)
|
|
}
|
|
|
|
if perr.Code == "" {
|
|
// given it was unset during attempt to unmarshal as JSON
|
|
perr.Code = StatusCodeToErrorCode(resp.StatusCode)
|
|
}
|
|
|
|
return perr
|
|
}
|
|
func firstLineAsError(buf bytes.Buffer) error {
|
|
line, _ := buf.ReadString('\n')
|
|
return errors.New(strings.TrimSuffix(line, "\n"))
|
|
}
|