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  }