-
Notifications
You must be signed in to change notification settings - Fork 2
/
hooks.go
162 lines (132 loc) · 4.11 KB
/
hooks.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
package kcd
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"github.com/expectedsh/errors"
"github.com/go-chi/chi/middleware"
validation "github.com/go-ozzo/ozzo-validation/v4"
)
// defaultValidateHook is the default validation hook.
// It use 'ozzo-validation' to validate structure.
// A structure must implement 'ValidatableWithContext' or 'Validatable'
func defaultValidateHook(ctx context.Context, input interface{}) error {
var err validation.Errors
switch v := input.(type) {
case validation.ValidatableWithContext:
err = v.ValidateWithContext(ctx).(validation.Errors)
case validation.Validatable:
err = v.Validate().(validation.Errors)
}
if len(err) == 0 {
return nil
}
return errors.
NewWithKind(errors.KindInvalidArgument, "the request has one or multiple invalid fields").
WithMetadata("kcd.fields", err)
}
// defaultRenderHook is the default render hook.
// It marshals the payload to JSON, or returns an empty body if the payload is nil.
func defaultRenderHook(w http.ResponseWriter, _ *http.Request, statusCode int, response interface{}) error {
if response != nil {
marshal, err := json.Marshal(response)
if err != nil {
return outputError{Err: err}
}
w.Header().Set("Content-type", "application/json")
w.WriteHeader(statusCode)
if _, err := w.Write(marshal); err != nil {
return err
}
} else {
w.WriteHeader(statusCode)
}
return nil
}
// errorResponse is the default response that send the default error hook
type errorResponse struct {
ErrorDescription string `json:"error_description"`
Error errors.Kind `json:"error"`
Fields map[string]string `json:"fields,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// defaultErrorHook is the default error hook.
// It check the error and return the corresponding response to the client.
func defaultErrorHook(w http.ResponseWriter, r *http.Request, err error) {
fmt.Println("ERROR: ", err)
response := errorResponse{
ErrorDescription: "internal server error",
Error: errors.KindInternal,
Fields: map[string]string{},
Metadata: map[string]interface{}{},
}
reqID := middleware.GetReqID(r.Context())
if reqID != "" {
response.Metadata["request_id"] = reqID
}
switch e := err.(type) {
case *errors.Error:
w.WriteHeader(e.Kind.ToStatusCode())
response.ErrorDescription = e.Message
response.Error = e.Kind
// todo: don't use string literal for kcd.*
metadata, ok := e.GetMetadata(errorKeyFields)
if ok {
m, okMap := metadata.(validation.Errors)
if okMap {
for k, v := range m {
response.Fields[k] = v.Error()
}
}
}
metadata, ok = e.GetMetadata(errorKeyMetadata)
if ok {
m, okMap := metadata.(map[string]interface{})
if okMap {
for k, v := range m {
response.Metadata[k] = v
}
}
}
case *inputError:
w.WriteHeader(http.StatusBadRequest)
response.Error = errors.KindInvalidArgument
response.ErrorDescription = http.StatusText(http.StatusBadRequest)
switch e.extractor {
case queryTag, pathTag, headerTag:
response.Fields[e.field] = e.message
case jsonTag:
response.ErrorDescription = e.message
}
case *outputError:
w.WriteHeader(http.StatusInternalServerError)
response.Error = errors.KindInternal
response.ErrorDescription = e.Error()
}
// todo: use a log hook to log kcd real (critic) error
marshal, err := json.Marshal(response)
if err != nil {
return
}
w.Header().Set("Content-type", "application/json")
_, _ = w.Write(marshal)
}
// defaultBindHook returns a Bind hook with the default logic, with configurable MaxBodyBytes.
func defaultBindHook(maxBodyBytes int64) BindHook {
return func(w http.ResponseWriter, r *http.Request, in interface{}) error {
r.Body = http.MaxBytesReader(w, r.Body, maxBodyBytes)
if r.ContentLength == 0 {
return nil
}
bytesBody, err := ioutil.ReadAll(r.Body)
if err != nil {
return inputError{extractor: jsonTag, message: "unable to read body"}
}
if err := json.Unmarshal(bytesBody, in); err != nil {
return inputError{extractor: jsonTag, message: "unable to unmarshal request"}
}
return nil
}
}