-
Notifications
You must be signed in to change notification settings - Fork 0
/
response.go
274 lines (242 loc) · 7.31 KB
/
response.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
package utils
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"sort"
log "github.com/sirupsen/logrus"
"github.com/golang/gddo/httputil/header"
"github.com/pkg/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
)
// Error Representation of errors in the API. These are divided into a small
// number of categories, essentially distinguished by whose fault the
// error is; i.e., is this error:
// - a transient problem with the service, so worth trying again?
// - not going to work until the user takes some other action, e.g., updating config?
type Error struct {
Type Type
// a message that can be printed out for the user
Message string `json:"message"`
// the underlying error that can be e.g., logged for developers to look at
Err error
}
func (e *Error) Error() string {
if e.Err != nil {
return e.Err.Error()
}
return e.Message
}
// Type Type of error
type Type string
const (
// Server The operation looked fine on paper, but something went wrong
Server Type = "server"
// Missing The thing you mentioned, whatever it is, just doesn't exist
Missing = "missing"
// User The operation was well-formed, but you asked for something that
// can't happen at present (e.g., because you've not supplied some
// config yet)
User = "user"
)
// MarshalJSON Writes error as json
func (e *Error) MarshalJSON() ([]byte, error) {
var errMsg string
if e.Err != nil {
errMsg = e.Err.Error()
}
jsonable := &struct {
Type string `json:"type"`
Message string `json:"message"`
Err string `json:"error,omitempty"`
}{
Type: string(e.Type),
Message: e.Message,
Err: errMsg,
}
return json.Marshal(jsonable)
}
// UnmarshalJSON Parses json
func (e *Error) UnmarshalJSON(data []byte) error {
jsonable := &struct {
Type string `json:"type"`
Message string `json:"message"`
Err string `json:"error,omitempty"`
}{}
if err := json.Unmarshal(data, &jsonable); err != nil {
return err
}
e.Type = Type(jsonable.Type)
e.Message = jsonable.Message
if jsonable.Err != "" {
e.Err = errors.New(jsonable.Err)
}
return nil
}
// UnexpectedError any unexpected error
func UnexpectedError(message string, underlyingError error) error {
return &Error{
Type: Server,
Err: underlyingError,
Message: message,
}
}
// TypeMissingError indication of underlying type missing
func TypeMissingError(message string, underlyingError error) error {
return &Error{
Type: Missing,
Err: underlyingError,
Message: message,
}
}
// ValidationError Used for indication of validation errors
func ValidationError(kind, message string) error {
return &Error{
Type: User,
Err: fmt.Errorf("%s failed validation", kind),
Message: message,
}
}
// CoverAllError Cover all other errors
func CoverAllError(err error) *Error {
return &Error{
Type: User,
Err: err,
Message: `Error: ` + err.Error(),
}
}
func writeErrorWithCode(w http.ResponseWriter, r *http.Request, code int, err *Error) {
// An Accept header with "application/json" is sent by clients
// understanding how to decode JSON errors. Older clients don't
// send an Accept header, so we just give them the error text.
if len(r.Header.Get("Accept")) > 0 {
switch negotiateContentType(r, []string{"application/json", "text/plain"}) {
case "application/json":
body, encodeErr := json.Marshal(err)
if encodeErr != nil {
w.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, "Error encoding error response: %s\n\nOriginal error: %s", encodeErr.Error(), err.Error())
return
}
w.Header().Set(http.CanonicalHeaderKey("Content-Type"), "application/json; charset=utf-8")
w.WriteHeader(code)
w.Write(body)
return
case "text/plain":
w.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain; charset=utf-8")
w.WriteHeader(code)
fmt.Fprint(w, err.Message)
return
}
}
w.Header().Set(http.CanonicalHeaderKey("Content-Type"), "text/plain; charset=utf-8")
w.WriteHeader(code)
fmt.Fprint(w, err.Error())
}
// StringResponse Used for textual response data. I.e. log data
func StringResponse(w http.ResponseWriter, r *http.Request, result string) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(result))
}
// JSONResponse Marshals response with header
func JSONResponse(w http.ResponseWriter, r *http.Request, result interface{}) {
body, err := json.Marshal(result)
if err != nil {
ErrorResponse(w, r, err)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(body)
}
// ErrorResponse Marshals error
func ErrorResponse(w http.ResponseWriter, r *http.Request, apiError error) {
var outErr *Error
var code int
var ok bool
err := errors.Cause(apiError)
if outErr, ok = err.(*Error); !ok {
outErr = CoverAllError(apiError)
}
log.Error(outErr.Message)
switch apiError.(type) {
case *url.Error:
// Reflect any underlying network error
writeErrorWithCode(w, r, http.StatusInternalServerError, outErr)
case *apierrors.StatusError:
// Reflect any underlying error from Kubernetes API
se := apiError.(*apierrors.StatusError)
writeErrorWithCode(w, r, int(se.ErrStatus.Code), outErr)
default:
switch outErr.Type {
case Missing:
code = http.StatusNotFound
case User:
code = http.StatusBadRequest
case Server:
code = http.StatusInternalServerError
default:
code = http.StatusInternalServerError
}
writeErrorWithCode(w, r, code, outErr)
}
}
// negotiateContentType picks a content type based on the Accept
// header from a request, and a supplied list of available content
// types in order of preference. If the Accept header mentions more
// than one available content type, the one with the highest quality
// (`q`) parameter is chosen; if there are a number of those, the one
// that appears first in the available types is chosen.
func negotiateContentType(r *http.Request, orderedPref []string) string {
specs := header.ParseAccept(r.Header, "Accept")
if len(specs) == 0 {
return orderedPref[0]
}
preferred := []header.AcceptSpec{}
for _, spec := range specs {
if indexOf(orderedPref, spec.Value) < len(orderedPref) {
preferred = append(preferred, spec)
}
}
if len(preferred) > 0 {
sort.Sort(sortAccept{preferred, orderedPref})
return preferred[0].Value
}
return ""
}
// sortAccept Holds accepted response types
type sortAccept struct {
specs []header.AcceptSpec
prefs []string
}
func (s sortAccept) Len() int {
return len(s.specs)
}
// We want to sort by descending order of suitability: higher quality
// to lower quality, and preferred to less preferred.
func (s sortAccept) Less(i, j int) bool {
switch {
case s.specs[i].Q == s.specs[j].Q:
return indexOf(s.prefs, s.specs[i].Value) < indexOf(s.prefs, s.specs[j].Value)
default:
return s.specs[i].Q > s.specs[j].Q
}
}
func (s sortAccept) Swap(i, j int) {
s.specs[i], s.specs[j] = s.specs[j], s.specs[i]
}
// This exists so we can search short slices of strings without
// requiring them to be sorted. Returning the len value if not found
// is so that it can be used directly in a comparison when sorting (a
// `-1` would mean "not found" was sorted before found entries).
func indexOf(ss []string, search string) int {
for i, s := range ss {
if s == search {
return i
}
}
return len(ss)
}