-
Notifications
You must be signed in to change notification settings - Fork 14
/
Copy pathresponse.go
149 lines (126 loc) · 5.21 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
/*
Copyright © 2019-2024 Acronis International GmbH.
*/
package restapi
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"github.com/prometheus/client_golang/prometheus"
"github.com/acronis/go-appkit/log"
)
// ContentTypeAppJSON represents MIME media type for JSON.
const ContentTypeAppJSON = "application/json"
// ContentTypeAppSCIMJSON represents MIME media type for SCIM JSON.
const ContentTypeAppSCIMJSON = "application/scim+json"
// Does JSON marshaling with disabled HTML escaping
func jsonMarshal(v interface{}) ([]byte, error) {
var buffer bytes.Buffer
encoder := json.NewEncoder(&buffer)
encoder.SetEscapeHTML(false)
err := encoder.Encode(v)
if err != nil {
return nil, err
}
return buffer.Bytes()[:buffer.Len()-1], nil
}
// RespondJSON sends response with 200 HTTP status code, does JSON marshaling of data and writes result in response's body.
func RespondJSON(rw http.ResponseWriter, respData interface{}, logger log.FieldLogger) {
RespondCodeAndJSON(rw, http.StatusOK, respData, logger)
}
// RespondCodeAndJSON sends a response with the passed status code and sets the "Content-Type"
// to "application/json" if it's not already set. It performs JSON marshaling of the data and
// writes the result to the response's body.
func RespondCodeAndJSON(rw http.ResponseWriter, statusCode int, respData interface{}, logger log.FieldLogger) {
if respData == nil {
rw.WriteHeader(statusCode)
return
}
if rw.Header().Get("Content-Type") == "" {
rw.Header().Set("Content-Type", ContentTypeAppJSON)
}
respJSON, err := jsonMarshal(respData)
if err != nil {
if logger != nil {
logger.Error("error while marshaling json for response body", log.Error(err))
}
rw.WriteHeader(http.StatusInternalServerError)
return
}
rw.WriteHeader(statusCode)
if _, err = rw.Write(respJSON); err != nil {
if logger != nil {
logger.Error("error while writing response body", log.Error(err))
}
}
}
// ErrorResponseData is used for answer on requests with error
type ErrorResponseData struct {
Err *Error `json:"error"`
}
func (e *ErrorResponseData) Error() string {
return fmt.Sprintf("HTTP error occurs: %v", e.Err)
}
var respondError = RespondWrappedError
// DisableWrappingErrorInResponse disables wrapping error ({"error": {"domain": "{domain}", ...} -> {"domain": "{domain}", ...})
// in response body.
func DisableWrappingErrorInResponse() {
respondError = RespondNoWrappedError
}
// RespondError sets HTTP status code in response and writes error in body in JSON format.
// Also, it logs info (code and message) about error.
func RespondError(rw http.ResponseWriter, httpStatusCode int, err *Error, logger log.FieldLogger) {
respondError(rw, httpStatusCode, err, logger)
}
// RespondWrappedError sets HTTP status code in response and writes wrapped error in body in JSON format.
// Also, it logs info (code and message) about error.
func RespondWrappedError(rw http.ResponseWriter, httpStatusCode int, err *Error, logger log.FieldLogger) {
logAndCollectMetricsForErrorIfNeeded(err, logger)
RespondCodeAndJSON(rw, httpStatusCode, ErrorResponseData{err}, logger)
}
// RespondNoWrappedError sets HTTP status code in response and writes non-wrapped error in body in JSON format.
// Also, it logs info (code and message) about error.
func RespondNoWrappedError(rw http.ResponseWriter, httpStatusCode int, err *Error, logger log.FieldLogger) {
logAndCollectMetricsForErrorIfNeeded(err, logger)
RespondCodeAndJSON(rw, httpStatusCode, err, logger)
}
// RespondInternalError sends response with 500 HTTP status code and internal error in body in JSON format.
func RespondInternalError(rw http.ResponseWriter, domain string, logger log.FieldLogger) {
RespondError(rw, http.StatusInternalServerError, NewInternalError(domain), logger)
}
// RespondMalformedRequestError creates Error from passed MalformedRequestError and then call RespondError.
func RespondMalformedRequestError(rw http.ResponseWriter, domain string, reqErr *MalformedRequestError, logger log.FieldLogger) {
err := NewError(domain, httpCode2ErrorCode(reqErr.HTTPStatusCode), reqErr.Message)
RespondError(rw, reqErr.HTTPStatusCode, err, logger)
}
// RespondMalformedRequestOrInternalError calls RespondMalformedRequestError (if passed error is *MalformedRequestError)
// or RespondInternalError (in other cases).
func RespondMalformedRequestOrInternalError(rw http.ResponseWriter, domain string, err error, logger log.FieldLogger) {
var reqErr *MalformedRequestError
if errors.As(err, &reqErr) {
RespondMalformedRequestError(rw, domain, reqErr, logger)
return
}
RespondInternalError(rw, domain, logger)
}
func logAndCollectMetricsForErrorIfNeeded(err *Error, logger log.FieldLogger) {
if logger != nil {
flds := []log.Field{log.String("error_code", err.Code), log.String("error_message", err.Message)}
if len(err.Context) != 0 {
ctxLines := make([]string, 0, len(err.Context))
for k, v := range err.Context {
ctxLines = append(ctxLines, fmt.Sprintf("%s: %v", k, v))
}
flds = append(flds, log.Strings("error_context", ctxLines))
}
logger.Error("error in response", flds...)
}
if metricsResponseErrors != nil {
metricsResponseErrors.With(prometheus.Labels{
metricsLabelResponseErrorDomain: err.Domain,
metricsLabelResponseErrorCode: err.Code,
}).Inc()
}
}