github.com/pyroscope-io/pyroscope@v0.37.3-0.20230725203016-5f6947968bd0/pkg/server/httputils/default.go (about) 1 package httputils 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "strconv" 10 "strings" 11 12 "github.com/gorilla/mux" 13 "github.com/hashicorp/go-multierror" 14 "github.com/pyroscope-io/pyroscope/pkg/model" 15 "github.com/sirupsen/logrus" 16 ) 17 18 type DefaultImpl struct { 19 logger logrus.FieldLogger 20 } 21 22 func NewDefaultHelper(logger logrus.FieldLogger) *DefaultImpl { 23 return &DefaultImpl{ 24 logger: logger, 25 } 26 } 27 28 func (*DefaultImpl) MustJSON(_ *http.Request, w http.ResponseWriter, v interface{}) { 29 resp, err := json.Marshal(v) 30 if err != nil { 31 panic(err) 32 } 33 w.Header().Set("Content-Type", "application/json") 34 _, _ = w.Write(resp) 35 } 36 37 func (*DefaultImpl) mustJSONError(_ *http.Request, w http.ResponseWriter, code int, v interface{}) { 38 resp, err := json.Marshal(v) 39 if err != nil { 40 panic(err) 41 } 42 w.Header().Set("Content-Type", "application/json") 43 w.WriteHeader(code) 44 _, _ = w.Write(resp) 45 } 46 47 // HandleError replies to the request with an appropriate message as 48 // JSON-encoded body and writes a corresponding message to the log 49 // with debug log level. 50 // 51 // Any error of a type not defined in this package or pkg/model, will be 52 // treated as an internal server error causing response code 500. Such 53 // errors are not sent but only logged with error log level. 54 func (d *DefaultImpl) HandleError(r *http.Request, w http.ResponseWriter, err error) { 55 d.ErrorCode(r, w, d.Logger(r), err, -1) 56 } 57 58 // ErrorCode replies to the request with the specified error message 59 // as JSON-encoded body and writes corresponding message to the log. 60 // 61 // If HTTP code is less than or equal zero, it will be deduced based on 62 // the error. If it fails, StatusInternalServerError will be returned 63 // without the response body. The error can be of 'multierror.Error' type. 64 // 65 // The call writes messages with the debug log level except the case 66 // when the code is StatusInternalServerError which is logged as an error. 67 // 68 // It does not end the HTTP request; the caller should ensure no further 69 // writes are done to w. 70 func (d *DefaultImpl) ErrorCode(r *http.Request, w http.ResponseWriter, logger logrus.FieldLogger, err error, code int) { 71 switch { 72 case err == nil: 73 return 74 case code > 0: 75 case model.IsAuthenticationError(err): 76 code = http.StatusUnauthorized 77 err = model.ErrCredentialsInvalid 78 case model.IsAuthorizationError(err): 79 code = http.StatusForbidden 80 case model.IsValidationError(err): 81 code = http.StatusBadRequest 82 case model.IsNotFoundError(err): 83 code = http.StatusNotFound 84 case IsJSONError(err): 85 code = http.StatusBadRequest 86 switch { 87 case errors.Is(err, io.EOF): 88 err = ErrRequestBodyRequired 89 case errors.Is(err, io.ErrUnexpectedEOF): 90 // https://github.com/golang/go/issues/25956 91 err = ErrRequestBodyJSONInvalid 92 } 93 default: 94 // No response code provided and it can't be determined. 95 code = http.StatusInternalServerError 96 } 97 98 var e Errors 99 if m := new(multierror.Error); errors.As(err, &m) { 100 m.ErrorFormat = listFormatFunc 101 for _, x := range m.Errors { 102 e.Errors = append(e.Errors, x.Error()) 103 } 104 } else { 105 e.Errors = []string{err.Error()} 106 } 107 108 if logger != nil { 109 // Internal errors must not be shown to users but 110 // logged with error log level. 111 logger = logger.WithError(err).WithField("code", code) 112 msg := strings.ToLower(http.StatusText(code)) 113 if code == http.StatusInternalServerError { 114 w.WriteHeader(code) 115 logger.Error(msg) 116 return 117 } 118 logger.Debug(msg) 119 } 120 121 d.mustJSONError(r, w, code, e) 122 } 123 124 var ( 125 ErrParamIDRequired = model.ValidationError{Err: errors.New("id parameter is required")} 126 ErrRequestBodyRequired = model.ValidationError{Err: errors.New("request body required")} 127 ErrRequestBodyJSONInvalid = model.ValidationError{Err: errors.New("request body contains malformed JSON")} 128 ) 129 130 type Errors struct { 131 Errors []string `json:"errors"` 132 } 133 134 func listFormatFunc(es []error) string { 135 if len(es) == 1 { 136 return es[0].Error() 137 } 138 points := make([]string, len(es)) 139 for i, err := range es { 140 points[i] = err.Error() 141 } 142 return strings.Join(points, "; ") 143 } 144 145 func (*DefaultImpl) IDFromRequest(r *http.Request) (uint, error) { 146 v, ok := mux.Vars(r)["id"] 147 if !ok { 148 return 0, ErrParamIDRequired 149 } 150 id, err := strconv.ParseUint(v, 10, 0) 151 if err != nil { 152 return 0, model.ValidationError{Err: fmt.Errorf("id parameter is invalid: %w", err)} 153 } 154 return uint(id), nil 155 } 156 157 // Logger creates a new logger scoped to the request 158 // and enriches it with the known fields. 159 func (d *DefaultImpl) Logger(r *http.Request) logrus.FieldLogger { 160 fields := logrus.Fields{ 161 "url": r.URL.String(), 162 "method": r.Method, 163 "remote": r.RemoteAddr, 164 } 165 u, ok := model.UserFromContext(r.Context()) 166 if ok { 167 fields["user"] = u.Name 168 } 169 var k model.APIKey 170 k, ok = model.APIKeyFromContext(r.Context()) 171 if ok { 172 fields["api_key"] = k.Name 173 } 174 return d.logger.WithFields(fields) 175 } 176 177 func (d *DefaultImpl) WriteResponseJSON(r *http.Request, w http.ResponseWriter, res interface{}) { 178 w.Header().Set("Content-Type", "application/json") 179 if err := json.NewEncoder(w).Encode(res); err != nil { 180 d.WriteJSONEncodeError(r, w, err) 181 } 182 } 183 184 func (*DefaultImpl) WriteResponseFile(_ *http.Request, w http.ResponseWriter, filename string, content []byte) { 185 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%v", filename)) 186 w.Header().Set("Content-Type", "application/octet-stream") 187 w.Write(content) 188 w.(http.Flusher).Flush() 189 } 190 191 func (d *DefaultImpl) WriteInvalidMethodError(r *http.Request, w http.ResponseWriter) { 192 d.writeErrorMessage(r, w, http.StatusMethodNotAllowed, "method not allowed") 193 } 194 195 func (d *DefaultImpl) WriteInvalidParameterError(r *http.Request, w http.ResponseWriter, err error) { 196 d.WriteError(r, w, http.StatusBadRequest, err, "invalid parameter") 197 } 198 199 func (d *DefaultImpl) WriteInternalServerError(r *http.Request, w http.ResponseWriter, err error, msg string) { 200 d.WriteError(r, w, http.StatusInternalServerError, err, msg) 201 } 202 203 func (d *DefaultImpl) WriteJSONEncodeError(r *http.Request, w http.ResponseWriter, err error) { 204 d.WriteInternalServerError(r, w, err, "encoding response body") 205 } 206 207 func (d *DefaultImpl) WriteError(r *http.Request, w http.ResponseWriter, code int, err error, msg string) { 208 d.logger.WithError(err).Error(msg) 209 d.writeMessage(r, w, code, "%s: %q", msg, err) 210 } 211 212 func (d *DefaultImpl) writeErrorMessage(r *http.Request, w http.ResponseWriter, code int, msg string) { 213 d.logger.Error(msg) 214 d.writeMessage(r, w, code, msg) 215 } 216 217 func (*DefaultImpl) writeMessage(_ *http.Request, w http.ResponseWriter, code int, format string, args ...interface{}) { 218 w.WriteHeader(code) 219 _, _ = fmt.Fprintf(w, format, args...) 220 _, _ = fmt.Fprintln(w) 221 }