github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/x/net/http/errors.go (about) 1 // Copyright (c) 2018 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package xhttp 22 23 import ( 24 "context" 25 "encoding/json" 26 "errors" 27 "net/http" 28 "sync" 29 30 "github.com/m3db/m3/src/dbnode/client" 31 xerrors "github.com/m3db/m3/src/x/errors" 32 33 "github.com/prometheus/prometheus/promql" 34 ) 35 36 // ErrorRewriteFn is a function for rewriting response error. 37 type ErrorRewriteFn func(error) error 38 39 var ( 40 errorRewriteFn ErrorRewriteFn = func(err error) error { return err } 41 errorRewriteFnLock sync.RWMutex 42 ) 43 44 // Error is an HTTP JSON error that also sets a return status code. 45 type Error interface { 46 // Fulfill error interface. 47 error 48 49 // Embedding ContainedError allows for the inner error 50 // to be retrieved with all existing error helpers. 51 xerrors.ContainedError 52 53 // Code returns the status code to return to end users. 54 Code() int 55 } 56 57 // NewError creates a new error with an explicit status code 58 // which will override any wrapped error to return specifically 59 // the exact error code desired. 60 func NewError(err error, status int) Error { 61 return errorWithCode{err: err, status: status} 62 } 63 64 type errorWithCode struct { 65 err error 66 status int 67 } 68 69 func (e errorWithCode) Error() string { 70 return e.err.Error() 71 } 72 73 func (e errorWithCode) InnerError() error { 74 return e.err 75 } 76 77 func (e errorWithCode) Code() int { 78 return e.status 79 } 80 81 // ErrorResponse is a generic response for an HTTP error. 82 type ErrorResponse struct { 83 Status string `json:"status"` 84 Error string `json:"error"` 85 } 86 87 type options struct { 88 response []byte 89 } 90 91 // WriteErrorOption is an option to pass to WriteError. 92 type WriteErrorOption func(*options) 93 94 // WithErrorResponse specifies a response to add the WriteError method. 95 func WithErrorResponse(b []byte) WriteErrorOption { 96 return func(o *options) { 97 o.response = b 98 } 99 } 100 101 // WriteError will serve an HTTP error. 102 func WriteError(w http.ResponseWriter, err error, opts ...WriteErrorOption) { 103 var o options 104 for _, fn := range opts { 105 fn(&o) 106 } 107 108 errorRewriteFnLock.RLock() 109 err = errorRewriteFn(err) 110 errorRewriteFnLock.RUnlock() 111 112 statusCode := getStatusCode(err) 113 if o.response == nil { 114 w.Header().Set(HeaderContentType, ContentTypeJSON) 115 w.WriteHeader(statusCode) 116 json.NewEncoder(w).Encode(ErrorResponse{Status: "error", Error: err.Error()}) //nolint:errcheck 117 } else { 118 w.WriteHeader(statusCode) 119 w.Write(o.response) 120 } 121 } 122 123 // SetErrorRewriteFn sets error rewrite function. 124 func SetErrorRewriteFn(f ErrorRewriteFn) ErrorRewriteFn { 125 errorRewriteFnLock.Lock() 126 defer errorRewriteFnLock.Unlock() 127 128 res := errorRewriteFn 129 errorRewriteFn = f 130 return res 131 } 132 133 func getStatusCode(err error) int { 134 switch v := err.(type) { 135 case Error: 136 return v.Code() 137 case error: 138 if xerrors.IsInvalidParams(v) { 139 return http.StatusBadRequest 140 } else if errors.Is(err, context.Canceled) { 141 // This status code was coined by Nginx for exactly the same use case. 142 // https://httpstatuses.com/499 143 return 499 144 } else if errors.Is(err, context.DeadlineExceeded) || client.IsTimeoutError(err) { 145 return http.StatusGatewayTimeout 146 // Also check for prom errors, which can be either a cancellation or a timeout. 147 } else if _, ok := err.(promql.ErrQueryCanceled); ok { // nolint:errorlint 148 return 499 149 } 150 } 151 return http.StatusInternalServerError 152 } 153 154 // IsClientError returns true if this error would result in 4xx status code. 155 func IsClientError(err error) bool { 156 code := getStatusCode(err) 157 return code >= 400 && code < 500 158 }