-
Notifications
You must be signed in to change notification settings - Fork 0
/
response.go
281 lines (236 loc) · 10.3 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
275
276
277
278
279
280
281
package goravel
import (
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"path/filepath"
"strconv"
"strings"
"github.com/go-chi/chi/v5"
)
// Define an envelope type.
type Response map[string]interface{}
// readIDParam reads interpolated "id" from request URL and returns it and nil. If there is an error
// it returns and 0 and an error.
func (g *Goravel) ReadIDParam(r *http.Request) (int64, error) {
idFromParam := chi.URLParam(r, "id")
id, err := strconv.ParseInt(idFromParam, 10, 64)
if err != nil || id < 1 {
return 0, errors.New("invalid id parameter")
}
return id, nil
}
// writeJSON marshals data structure to encoded JSON response. It returns an error if there are
// any issues, else error is nil.
func (g *Goravel) WriteJSON(w http.ResponseWriter, status int, data Response,
headers ...http.Header) error {
// Use the json.MarshalIndent() function so that whitespace is added to the encoded JSON. Use
// no line prefix and tab indents for each element.
js, err := json.MarshalIndent(data, "", "\t")
if err != nil {
return err
}
// Append a newline to make it easier to view in terminal applications.
js = append(js, '\n')
// At this point, we know that we won't encounter any more errors before writing the response,
// so it's safe to add any headers that we want to include. We loop through the header map
// and add each header to the http.ResponseWriter header map. Note that it's OK if the
// provided header map is nil. Go doesn't through an error if you try to range over (
// or generally, read from) a nil map
if len(headers) > 0 {
for key, value := range headers[0] {
w.Header()[key] = value
}
}
// Add the "Content-Type: application/json" header, then write the status code and JSON response.
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if _, err := w.Write(js); err != nil {
g.ErrorLog.Println(err)
return err
}
return nil
}
// readJSON decodes request Body into corresponding Go type. It triages for any potential errors
// and returns corresponding appropriate errors.
func (g *Goravel) ReadJSON(w http.ResponseWriter, r *http.Request, dst interface{}) error {
// Use http.MaxBytesReader() to limit the size of the request body to 1MB to prevent
// any potential nefarious DoS attacks.
maxBytes := 1_048_576
r.Body = http.MaxBytesReader(w, r.Body, int64(maxBytes))
// Initialize the json.Decoder, and call the DisallowUnknownFields() method on it
// before decoding. So, if the JSON from the client includes any field which
// cannot be mapped to the target destination, the decoder will return an error
// instead of just ignoring the field.
dec := json.NewDecoder(r.Body)
dec.DisallowUnknownFields()
// Decode the request body to the destination.
err := dec.Decode(dst)
if err != nil {
// If there is an error during decoding, start the error triage...
var syntaxError *json.SyntaxError
var unmarshalTypeError *json.UnmarshalTypeError
var invalidUnmarshalError *json.InvalidUnmarshalError
switch {
// Use the error.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 fmt.Errorf("body contains badly-formed JSON at (charcter %d)", syntaxError.Offset)
// In some circumstances Decode() may also return an io.ErrUnexpectedEOF error
// for syntax error 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 fmt.Errorf("body contains incorrect JSON type for field %q",
unmarshalTypeError.Field)
}
return fmt.Errorf("body contains incorrect JSON type (at character %d)",
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 fmt.Errorf("body contains unknown key %s", fieldName)
// If the request body exceeds 1MB in size then 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 fmt.Errorf("body must not be larger than %d bytes", 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, and discuss why it's an appropriate thing to do in this specific
// situation.
case errors.As(err, &invalidUnmarshalError):
panic(err)
// For anything else, return the error message as-is.
default:
return 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 then 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
}
// url.Values:
// type Values map[string][]string
// readString is a helper method on application type that returns a string value from the URL query
// string, or the provided default value if no matching key is found.
func (g *Goravel) ReadStrings(qs url.Values, key string, defaultValue string) string {
// Extract the value for a given key from the URL query string.
// If no key exists this will return an empty string "".
s := qs.Get(key)
// If no key exists (or the value is empty) then return the default value
if s == "" {
return defaultValue
}
// Otherwise, return the string
return s
}
// readCSV is a helper method on application type that reads a string value from the URL query
// string and then splits it into a slice on the comma character. If no matching key is found
// then it returns the provided default value.
func (g *Goravel) ReadCSV(qs url.Values, key string, defaultValue []string) []string {
// Extract the value from the URL query string
csv := qs.Get(key)
// if no key exists (or the value is empty) then return the default value
if csv == "" {
return defaultValue
}
// Otherwise, parse the value into a []string slice and return it.
return strings.Split(csv, ",")
}
// readInt is a helper method on application type that reads a string value from the URL query
// string and converts it to an integer before returning. If no matching key is found then 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, and return the default value.
func (g *Goravel) ReadInt(qs url.Values, key string, defaultValue int) (int, error) {
// Extract the value from the URL query string.
s := qs.Get(key)
// If no key exists (or the value is empty) then return the default value.
if s == "" {
return defaultValue, nil
}
// Try to convert the string value to an int. If this fails, add an error message to the
// validator instance and return the default value.
i, err := strconv.Atoi(s)
if err != nil {
return defaultValue, errors.New("invalid query parameter")
}
// Otherwise, return the converted integer value.
return i, nil
}
// WriteXML sends an XML response
func (g *Goravel) WriteXML(w http.ResponseWriter, status int, data interface{}, headers ...http.Header) error {
out, err := xml.MarshalIndent(data, "", " ")
if err != nil {
return err
}
if len(headers) > 0 {
for key, value := range headers[0] {
w.Header()[key] = value
}
}
w.Header().Set("Content-Type", "application/xml")
w.WriteHeader(status)
_, err = w.Write(out)
if err != nil {
return err
}
return nil
}
// DownloadFile helps users download files from the server
func (g *Goravel) DownloadFile(w http.ResponseWriter, r *http.Request, pathToFile, fileName string) error {
fp := path.Join(pathToFile, fileName)
fileToServe := filepath.Clean(fp)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; file=\"%s\"", fileName))
http.ServeFile(w, r, fileToServe)
return nil
}
// Error404 returns page not found response
func (g *Goravel) Error404(w http.ResponseWriter, r *http.Request) {
g.ErrorStatus(w, http.StatusNotFound)
}
// Error500 returns internal server error response
func (g *Goravel) Error500(w http.ResponseWriter, r *http.Request) {
g.ErrorStatus(w, http.StatusInternalServerError)
}
// ErrorUnauthorized sends an unauthorized status (client is not known)
func (g *Goravel) ErrorUnauthorized(w http.ResponseWriter, r *http.Request) {
g.ErrorStatus(w, http.StatusUnauthorized)
}
// ErrorForbidden returns a forbidden status message (client is known)
func (g *Goravel) ErrorForbidden(w http.ResponseWriter, r *http.Request) {
g.ErrorStatus(w, http.StatusForbidden)
}
// ErrorStatus returns a response with the supplied http status
func (g *Goravel) ErrorStatus(w http.ResponseWriter, status int) {
http.Error(w, http.StatusText(status), status)
}