github.com/grafana/pyroscope@v1.18.0/pkg/util/http/error.go (about) 1 package http 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "net/http" 8 9 "connectrpc.com/connect" 10 "github.com/grafana/dskit/httpgrpc" 11 "google.golang.org/grpc/codes" 12 "google.golang.org/grpc/status" 13 14 "github.com/grafana/pyroscope/pkg/tenant" 15 "github.com/grafana/pyroscope/pkg/util/connectgrpc" 16 ) 17 18 var errorWriter = connect.NewErrorWriter() 19 20 // StatusClientClosedRequest is the status code for when a client request cancellation of an http request 21 const StatusClientClosedRequest = 499 22 23 const ( 24 ErrClientCanceled = "the request was cancelled by the client" 25 ErrDeadlineExceeded = "request timed out, decrease the duration of the request or add more label matchers (prefer exact match over regex match) to reduce the amount of data processed" 26 ) 27 28 // Error write a go error with the correct status code. 29 func Error(w http.ResponseWriter, err error) { 30 var connectErr *connect.Error 31 if ok := errors.As(err, &connectErr); ok { 32 writeErr := errorWriter.Write(w, &http.Request{ 33 Header: http.Header{"Content-Type": []string{"application/json"}}, 34 }, err) 35 if writeErr != nil { 36 http.Error(w, writeErr.Error(), http.StatusInternalServerError) 37 } 38 return 39 } 40 status, cerr := ClientHTTPStatusAndError(err) 41 ErrorWithStatus(w, cerr, status) 42 } 43 44 func ErrorWithStatus(w http.ResponseWriter, err error, status int) { 45 w.Header().Set("X-Content-Type-Options", "nosniff") 46 w.Header().Set("Content-Type", "application/json") 47 48 w.WriteHeader(status) 49 if err := json.NewEncoder(w).Encode(struct { 50 Code connect.Code `json:"code"` 51 Message string `json:"message"` 52 }{ 53 Code: connectgrpc.HTTPToCode(int32(status)), 54 Message: err.Error(), 55 }); err != nil { 56 http.Error(w, err.Error(), status) 57 } 58 } 59 60 // ClientHTTPStatusAndError returns error and http status that is "safe" to return to client without 61 // exposing any implementation details. 62 func ClientHTTPStatusAndError(err error) (int, error) { 63 if err == nil { 64 return http.StatusOK, nil 65 } 66 // todo handle multi errors 67 // me, ok := err.(multierror.MultiError) 68 // if ok && me.Is(context.Canceled) { 69 // return StatusClientClosedRequest, errors.New(ErrClientCanceled) 70 // } 71 // if ok && me.IsDeadlineExceeded() { 72 // return http.StatusGatewayTimeout, errors.New(ErrDeadlineExceeded) 73 // } 74 75 s, isRPC := status.FromError(err) 76 switch { 77 case errors.Is(err, context.Canceled) || 78 (isRPC && s.Code() == codes.Canceled): 79 return StatusClientClosedRequest, errors.New(ErrClientCanceled) 80 case errors.Is(err, context.DeadlineExceeded) || 81 (isRPC && s.Code() == codes.DeadlineExceeded): 82 return http.StatusGatewayTimeout, errors.New(ErrDeadlineExceeded) 83 case errors.Is(err, tenant.ErrNoTenantID): 84 return http.StatusBadRequest, err 85 case isRPC && s.Code() == codes.InvalidArgument: 86 return http.StatusBadRequest, err 87 default: 88 if grpcErr, ok := httpgrpc.HTTPResponseFromError(err); ok { 89 return int(grpcErr.Code), errors.New(string(grpcErr.Body)) 90 } 91 return http.StatusInternalServerError, err 92 } 93 }