/
handler.go
160 lines (128 loc) · 3.91 KB
/
handler.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
/*
Copyright IBM Corp. All Rights Reserved.
SPDX-License-Identifier: Apache-2.0
*/
package web
import (
"bytes"
"encoding/json"
"errors"
"io"
"net/http"
"strings"
"github.com/gorilla/mux"
)
const (
apiVersion = "/v1"
)
type ResponseErr struct {
Reason string
}
type Config struct {
MaxReqSize uint16
}
type HttpHandler struct {
r *mux.Router
Logger logger
}
type logger interface {
Debugf(template string, args ...interface{})
Warnf(template string, args ...interface{})
Infof(template string, args ...interface{})
Errorf(template string, args ...interface{})
}
type ReqContext struct {
ResponseWriter http.ResponseWriter
Req *http.Request
Vars map[string]string
Query interface{}
}
//go:generate counterfeiter -o mocks/request_handler.go -fake-name FakeRequestHandler . RequestHandler
type RequestHandler interface {
// HandleRequest dispatches the request in the backend by parsing the given request context
// and returning a status code and a response back to the client.
HandleRequest(*ReqContext) (response interface{}, statusCode int)
// ParsePayload parses the payload to handler specific form or returns an error
ParsePayload([]byte) (interface{}, error)
}
func NewHttpHandler(l logger) *HttpHandler {
return &HttpHandler{r: mux.NewRouter(), Logger: l}
}
func (h *HttpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
h.r.ServeHTTP(w, req)
}
func (h *HttpHandler) RegisterURI(uri string, method string, rh RequestHandler) {
f := func(backToClient http.ResponseWriter, req *http.Request) {
h.handle(backToClient, req, rh)
}
h.r.HandleFunc(apiVersion+uri, f).Methods(method)
}
func (h *HttpHandler) handle(backToClient http.ResponseWriter, req *http.Request, rh RequestHandler) {
if _, err := negotiateContentType(req); err != nil {
sendErr(backToClient, http.StatusBadRequest, "bad content type", h.Logger, err)
return
}
reqPayload, err := io.ReadAll(req.Body)
if err != nil {
sendErr(backToClient, http.StatusBadRequest, "failed reading request", h.Logger, err)
return
}
o, err := rh.ParsePayload(reqPayload)
if err != nil {
sendErr(backToClient, http.StatusBadRequest, "failed parsing request", h.Logger, err)
return
}
reqCtx := &ReqContext{
Query: o,
ResponseWriter: backToClient,
Req: req,
Vars: mux.Vars(req),
}
resultFromBackend, statusCode := rh.HandleRequest(reqCtx)
response := &bytes.Buffer{}
encoder := json.NewEncoder(response)
err = encoder.Encode(resultFromBackend)
if err != nil {
sendErr(backToClient, http.StatusInternalServerError, "failed encoding response from backend", h.Logger, err)
return
}
if statusCode/100 != 2 {
sendErr(backToClient, statusCode, response.String(), h.Logger, nil)
return
}
if !isWebSocket(req.Header) {
backToClient.Header().Set("Content-Type", "application/json")
backToClient.WriteHeader(http.StatusOK)
backToClient.Write(response.Bytes())
}
}
func isWebSocket(h http.Header) bool {
upgrade, ok := h["Upgrade"]
return ok && upgrade[0] == "websocket"
}
func sendErr(resp http.ResponseWriter, code int, errToClient string, l logger, errLogged error) {
if errLogged != nil {
l.Warnf("Failed processing request: %v", errLogged)
}
encoder := json.NewEncoder(resp)
resp.Header().Set("Content-Type", "application/json")
resp.WriteHeader(code)
if err := encoder.Encode(&ResponseErr{Reason: errToClient}); err != nil {
l.Warnf("Failed encoding response: %v", err)
}
}
func negotiateContentType(req *http.Request) (string, error) {
acceptReq := req.Header.Get("Accept")
if len(acceptReq) == 0 {
return "application/json", nil
}
options := strings.Split(acceptReq, ",")
for _, opt := range options {
if strings.Contains(opt, "application/json") ||
strings.Contains(opt, "application/*") ||
strings.Contains(opt, "*/*") {
return "application/json", nil
}
}
return "", errors.New("response Content-Type is application/json only")
}