-
Notifications
You must be signed in to change notification settings - Fork 0
/
request.go
173 lines (142 loc) · 6.23 KB
/
request.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
package reactor
import (
"encoding/json"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/unknowntpo/todos/internal/domain/errors"
"github.com/unknowntpo/todos/pkg/validator"
"github.com/julienschmidt/httprouter"
)
func (rc *Reactor) WriteJSON(w http.ResponseWriter, status int, data interface{}) error {
const op errors.Op = "Context.WriteJSON"
js, err := json.MarshalIndent(data, "", "\t")
if err != nil {
return errors.E(op, err)
}
// Append a newline to make it easier to view in terminal applications.
js = append(js, '\n')
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(js)
return nil
}
func (rc *Reactor) ReadJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
const op errors.Op = "Context.ReadJSON"
maxBytes := 1_048_576
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
err := dec.Decode(dst)
if err != nil {
// If there is an error during decoding, start the triage...
var syntaxError *json.SyntaxError
var unmarshalTypeError *json.UnmarshalTypeError
var invalidUnmarshalError *json.InvalidUnmarshalError
switch {
// Use the errors.As() function to check whether the error has the type
// *json.SyntaxError. If it does, then return a plain-english error message
// which includes the location of the problem.
case errors.As(err, &syntaxError):
return errors.E(op, errors.Msg("%s: body contains badly-formed JSON (at character %d)").Format(op, syntaxError.Offset))
// In some circumstances Decode() may also return an io.ErrUnexpectedEOF error
// for syntax errors in the JSON. So we check for this using errors.Is() and
// return a generic error message. There is an open issue regarding this at
// https://github.com/golang/go/issues/25956.
case errors.Is(err, io.ErrUnexpectedEOF):
return errors.New("body contains badly-formed JSON")
// Likewise, catch any *json.UnmarshalTypeError errors. These occur when the
// JSON value is the wrong type for the target destination. If the error relates
// to a specific field, then we include that in our error message to make it
// easier for the client to debug.
case errors.As(err, &unmarshalTypeError):
if unmarshalTypeError.Field != "" {
return errors.E(op, errors.Msg("body contains incorrect JSON type for field %q").Format(unmarshalTypeError.Field))
}
return errors.E(op, errors.Msg("body contains incorrect JSON type (at character %d)").Format(unmarshalTypeError.Offset))
// An io.EOF error will be returned by Decode() if the request body is empty. We
// check for this with errors.Is() and return a plain-english error message
// instead.
case errors.Is(err, io.EOF):
return errors.New("body must not be empty")
// If the JSON contains a field which cannot be mapped to the target destination
// then Decode() will now return an error message in the format "json: unknown
// field "<name>"". We check for this, extract the field name from the error,
// and interpolate it into our custom error message. Note that there's an open
// issue at https://github.com/golang/go/issues/29035 regarding turning this
// into a distinct error type in the future.
case strings.HasPrefix(err.Error(), "json: unknown field "):
fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ")
return errors.E(op, errors.Msg("body contains unknown key %s").Format(fieldName))
// If the request body exceeds 1MB in size the decode will now fail with the
// error "http: request body too large". There is an open issue about turning
// this into a distinct error type at https://github.com/golang/go/issues/30715.
case err.Error() == "http: request body too large":
return errors.E(op, errors.Msg("body must not be larger than %d bytes").Format(maxBytes))
// A json.InvalidUnmarshalError error will be returned if we pass a non-nil
// pointer to Decode(). We catch this and panic, rather than returning an error
// to our handler. At the end of this chapter we'll talk about panicking
// versus returning errors, and discuss why it's an appropriate thing to do in
// this specific situation.
case errors.As(err, &invalidUnmarshalError):
panic(err)
default:
return errors.E(op, err)
}
}
// Call Decode() again, using a pointer to an empty anonymous struct as the
// destination. If the request body only contained a single JSON value this will
// return an io.EOF error. So if we get anything else, we know that there is
// additional data in the request body and we return our own custom error message.
err = dec.Decode(&struct{}{})
if err != io.EOF {
return errors.New("body must only contain a single JSON value")
}
return nil
}
func (rc *Reactor) ReadIDParam(r *http.Request) (int64, error) {
params := httprouter.ParamsFromContext(r.Context())
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil || id < 1 {
return 0, errors.New("invalid id parameter")
}
return id, nil
}
// ReadString returns a string value from the query string, or the provided
// default value if no matching key could be found.
func (rc *Reactor) ReadString(qs url.Values, key string, defaultValue string) string {
s := qs.Get(key)
if s == "" {
return defaultValue
}
return s
}
// The ReadCSV reads a string value from the query string and then splits it
// into a slice on the comma character. If no matching key count be found, it returns
// the provided default value.
func (rc *Reactor) ReadCSV(qs url.Values, key string, defaultValue []string) []string {
csv := qs.Get(key)
if csv == "" {
return defaultValue
}
return strings.Split(csv, ",")
}
// The ReadInt reads a string value from the query string and converts it to an
// integer before returning. If no matching key count be found it returns the provided
// default value. If the value couldn't be converted to an integer, then we record an
// error message in the provided Validator instance.
// TODO: Use interface to accept different validator.
func (rc *Reactor) ReadInt(qs url.Values, key string, defaultValue int, v *validator.Validator) int {
s := qs.Get(key)
if s == "" {
return defaultValue
}
i, err := strconv.Atoi(s)
if err != nil {
v.AddError(key, "must be an integer value")
return defaultValue
}
return i
}