...

Source file src/oss.terrastruct.com/util-go/xhttp/err.go

Documentation: oss.terrastruct.com/util-go/xhttp

     1  package xhttp
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"log"
     8  	"net/http"
     9  
    10  	"oss.terrastruct.com/util-go/cmdlog"
    11  )
    12  
    13  // Error represents an HTTP error.
    14  // It's exported only for comparison in tests.
    15  type Error struct {
    16  	Code int
    17  	Resp interface{}
    18  	Err  error
    19  }
    20  
    21  var _ interface {
    22  	Is(error) bool
    23  	Unwrap() error
    24  } = Error{}
    25  
    26  // Errorf creates a new error with code, resp, msg and v.
    27  //
    28  // When returned from an xhttp.HandlerFunc, it will be correctly logged
    29  // and written to the connection. See xhttp.WrapHandlerFunc
    30  func Errorf(code int, resp interface{}, msg string, v ...interface{}) error {
    31  	return errorWrap(code, resp, fmt.Errorf(msg, v...))
    32  }
    33  
    34  // ErrorWrap wraps err with the code and resp for xhttp.HandlerFunc.
    35  //
    36  // When returned from an xhttp.HandlerFunc, it will be correctly logged
    37  // and written to the connection. See xhttp.WrapHandlerFunc
    38  func ErrorWrap(code int, resp interface{}, err error) error {
    39  	return errorWrap(code, resp, err)
    40  }
    41  
    42  func errorWrap(code int, resp interface{}, err error) error {
    43  	if resp == nil {
    44  		resp = http.StatusText(code)
    45  	}
    46  	return Error{code, resp, err}
    47  }
    48  
    49  func (e Error) Unwrap() error {
    50  	return e.Err
    51  }
    52  
    53  func (e Error) Is(err error) bool {
    54  	e2, ok := err.(Error)
    55  	if !ok {
    56  		return false
    57  	}
    58  	return e.Code == e2.Code && e.Resp == e2.Resp && errors.Is(e.Err, e2.Err)
    59  }
    60  
    61  func (e Error) Error() string {
    62  	return fmt.Sprintf("http error with code %v and resp %#v: %v", e.Code, e.Resp, e.Err)
    63  }
    64  
    65  // HandlerFunc is like http.HandlerFunc but returns an error.
    66  // See Errorf and ErrorWrap.
    67  type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
    68  
    69  type HandlerFuncAdapter struct {
    70  	Log  *cmdlog.Logger
    71  	Func HandlerFunc
    72  }
    73  
    74  // ServeHTTP adapts xhttp.HandlerFunc into http.Handler for usage with standard
    75  // HTTP routers like chi.
    76  //
    77  // It logs and writes any error from xhttp.HandlerFunc to the connection.
    78  //
    79  // If err was created with xhttp.Errorf or wrapped with xhttp.WrapError, then the error
    80  // will be logged at the correct level for the status code and xhttp.JSON will be called
    81  // with the code and resp.
    82  //
    83  // 400s are logged as warns and 500s as errors.
    84  //
    85  // If the error was not created with the xhttp helpers then a 500 will be written.
    86  //
    87  // If resp is nil, then resp is set to http.StatusText(code)
    88  //
    89  // If the code is not a 400 or a 500, then an error about about the unexpected error code
    90  // will be logged and a 500 will be written. The original error will also be logged.
    91  func (a HandlerFuncAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    92  	var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    93  		err := a.Func(w, r)
    94  		if err != nil {
    95  			handleError(a.Log, w, err)
    96  		}
    97  	})
    98  
    99  	h.ServeHTTP(w, r)
   100  }
   101  
   102  func handleError(clog *cmdlog.Logger, w http.ResponseWriter, err error) {
   103  	var herr Error
   104  	ok := errors.As(err, &herr)
   105  	if !ok {
   106  		herr = ErrorWrap(http.StatusInternalServerError, nil, err).(Error)
   107  	}
   108  
   109  	var logger *log.Logger
   110  	switch {
   111  	case 400 <= herr.Code && herr.Code < 500:
   112  		logger = clog.Warn
   113  	case 500 <= herr.Code && herr.Code < 600:
   114  		logger = clog.Error
   115  	default:
   116  		logger = clog.Error
   117  
   118  		clog.Error.Printf("unexpected non error http status code %d with resp: %#v", herr.Code, herr.Resp)
   119  
   120  		herr.Code = http.StatusInternalServerError
   121  		herr.Resp = nil
   122  	}
   123  
   124  	if herr.Resp == nil {
   125  		herr.Resp = http.StatusText(herr.Code)
   126  	}
   127  
   128  	logger.Printf("error handling http request: %v", err)
   129  
   130  	ww, ok := w.(writtenResponseWriter)
   131  	if !ok {
   132  		clog.Warn.Printf("response writer does not implement Written, double write logs possible: %#v", w)
   133  	} else if ww.Written() {
   134  		// Avoid double writes if an error occurred while the response was
   135  		// being written.
   136  		return
   137  	}
   138  
   139  	JSON(clog, w, herr.Code, map[string]interface{}{
   140  		"error": herr.Resp,
   141  	})
   142  }
   143  
   144  type writtenResponseWriter interface {
   145  	Written() bool
   146  }
   147  
   148  func JSON(clog *cmdlog.Logger, w http.ResponseWriter, code int, v interface{}) {
   149  	if v == nil {
   150  		v = map[string]interface{}{
   151  			"status": http.StatusText(code),
   152  		}
   153  	}
   154  
   155  	b, err := json.Marshal(v)
   156  	if err != nil {
   157  		clog.Error.Printf("json marshal error: %v", err)
   158  		http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
   159  		return
   160  	}
   161  
   162  	w.Header().Set("Content-Type", "application/json; charset=utf-8")
   163  	w.WriteHeader(code)
   164  	_, _ = w.Write(b)
   165  }
   166  

View as plain text