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
14
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
27
28
29
30 func Errorf(code int, resp interface{}, msg string, v ...interface{}) error {
31 return errorWrap(code, resp, fmt.Errorf(msg, v...))
32 }
33
34
35
36
37
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
66
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
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
135
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