/
backend_handler.go
180 lines (151 loc) · 4.9 KB
/
backend_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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package handlers
import (
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"strings"
"syscall"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/alphagov/router/logger"
)
var TLSSkipVerify bool
func NewBackendHandler(
backendID string,
backendURL *url.URL,
connectTimeout, headerTimeout time.Duration,
logger logger.Logger,
) http.Handler {
proxy := httputil.NewSingleHostReverseProxy(backendURL)
proxy.Transport = newBackendTransport(
backendID,
connectTimeout, headerTimeout,
logger,
)
defaultDirector := proxy.Director
proxy.Director = func(req *http.Request) {
defaultDirector(req)
// Set the Host header to match the backend hostname instead of the one from the incoming request.
req.Host = backendURL.Host
// Setting a blank User-Agent causes the http lib not to output one, whereas if there
// is no header, it will output a default one.
// See: https://github.com/golang/go/blob/release-branch.go1.5/src/net/http/request.go#L419
if _, present := req.Header["User-Agent"]; !present {
req.Header.Set("User-Agent", "")
}
populateViaHeader(req.Header, fmt.Sprintf("%d.%d", req.ProtoMajor, req.ProtoMinor))
}
return proxy
}
func populateViaHeader(header http.Header, httpVersion string) {
via := httpVersion + " router"
if prior, ok := header["Via"]; ok {
via = strings.Join(prior, ", ") + ", " + via
}
header.Set("Via", via)
}
type backendTransport struct {
backendID string
wrapped *http.Transport
logger logger.Logger
}
// Construct a backendTransport that wraps an http.Transport and implements http.RoundTripper.
// This allows us to intercept the response from the backend and modify it before it's copied
// back to the client.
func newBackendTransport(
backendID string,
connectTimeout, headerTimeout time.Duration,
logger logger.Logger,
) *backendTransport {
transport := http.Transport{}
transport.DialContext = (&net.Dialer{
Timeout: connectTimeout, // Configured by caller
KeepAlive: 30 * time.Second, // same as DefaultTransport
DualStack: true, // same as DefaultTransport
}).DialContext
// Remember, we have one transport per backend
//
// Using the below settings, and (for example) we have 25 backends
// 25 * 60 = 1500
// we will have a maximum of 1500 open idle connections
//
// The Go http.DefaultTransport sets this to 100,
// we set to 60 because of potential file handle limits
// because we have multiple backends
transport.MaxIdleConns = 60
// This is an arbitrarily selected number that is less than 60
transport.MaxIdleConnsPerHost = 20
// By default, idle connections do not expire,
// unless they are closed by the other end of the connection,
// and sometimes the other end will silently close the connection.
// We should expire idle connections after a while.
//
// We arbitrarily chose 10 minutes
transport.IdleConnTimeout = 10 * time.Minute
// If we do not configure the timeouts, then connections will hang
//
// Configured by the caller
transport.ResponseHeaderTimeout = headerTimeout
//
// Same values as http.DefaultTransport
transport.TLSHandshakeTimeout = 10 * time.Second
transport.ExpectContinueTimeout = 1 * time.Second
if TLSSkipVerify {
// #nosec G402 -- TODO: fix tests to use TLS properly.
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
}
return &backendTransport{backendID, &transport, logger}
}
func closeBody(resp *http.Response) {
if resp != nil && resp.Body != nil {
resp.Body.Close()
}
}
func (bt *backendTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
var responseCode int
var startTime = time.Now()
backendRequestCountMetric.With(prometheus.Labels{
"backend_id": bt.backendID,
"request_method": req.Method,
}).Inc()
defer func() {
durationSeconds := time.Since(startTime).Seconds()
backendResponseDurationSecondsMetric.With(prometheus.Labels{
"backend_id": bt.backendID,
"request_method": req.Method,
"response_code": fmt.Sprintf("%d", responseCode),
}).Observe(durationSeconds)
}()
resp, err = bt.wrapped.RoundTrip(req)
if err != nil {
var nerr net.Error
switch {
case errors.Is(err, syscall.ECONNREFUSED):
responseCode = http.StatusBadGateway
case errors.As(err, &nerr) && nerr.Timeout():
responseCode = http.StatusGatewayTimeout
default:
responseCode = http.StatusInternalServerError
}
closeBody(resp)
logger.NotifySentry(logger.ReportableError{Error: err, Request: req, Response: resp})
bt.logger.LogFromBackendRequest(
map[string]interface{}{"error": err.Error(), "status": responseCode},
req,
)
return newErrorResponse(responseCode), nil
}
responseCode = resp.StatusCode
populateViaHeader(resp.Header, fmt.Sprintf("%d.%d", resp.ProtoMajor, resp.ProtoMinor))
return
}
func newErrorResponse(status int) (resp *http.Response) {
resp = &http.Response{StatusCode: status}
resp.Body = io.NopCloser(strings.NewReader(""))
return
}