diff --git a/pkg/agent/protocol/http/proxy.go b/pkg/agent/protocol/http/proxy.go index 29274c58..ccea74e1 100644 --- a/pkg/agent/protocol/http/proxy.go +++ b/pkg/agent/protocol/http/proxy.go @@ -74,16 +74,6 @@ func NewProxy(c ProxyConfig, d Disruption) (protocol.Proxy, error) { }, nil } -// contains verifies if a list of strings contains the given string -func contains(list []string, target string) bool { - for _, element := range list { - if element == target { - return true - } - } - return false -} - // httpClient defines the method for executing HTTP requests. It is used to allow mocking // the client in tests type httpClient interface { @@ -97,56 +87,85 @@ type httpHandler struct { client httpClient } -func (h *httpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { - var statusCode int - headers := http.Header{} - body := io.NopCloser(strings.NewReader(h.disruption.ErrorBody)) - - excluded := contains(h.disruption.Excluded, req.URL.Path) - - if !excluded && h.disruption.ErrorRate > 0 && rand.Float32() <= h.disruption.ErrorRate { - // force error code - statusCode = int(h.disruption.ErrorCode) - } else { - req.Host = h.upstreamURL.Host - req.URL.Host = h.upstreamURL.Host - req.URL.Scheme = h.upstreamURL.Scheme - req.RequestURI = "" - originServerResponse, srvErr := h.client.Do(req) - if srvErr != nil { - rw.WriteHeader(http.StatusInternalServerError) - _, _ = fmt.Fprint(rw, srvErr) - return +// isExcluded checks whether a request should be proxied through without any kind of modification whatsoever. +func (h *httpHandler) isExcluded(r *http.Request) bool { + for _, excluded := range h.disruption.Excluded { + if strings.EqualFold(r.URL.Path, excluded) { + return true } + } - headers = originServerResponse.Header - statusCode = originServerResponse.StatusCode - body = originServerResponse.Body + return false +} - defer func() { - _ = originServerResponse.Body.Close() - }() - } +// proxy proxies a request to the upstream URL. +// Request is performed immediately, but response won't be copied to the client until wait has been consumed. +func (h *httpHandler) proxy(rw http.ResponseWriter, req *http.Request, wait <-chan time.Time) { + upstreamReq := req.Clone(context.Background()) + upstreamReq.Host = h.upstreamURL.Host + upstreamReq.URL.Host = h.upstreamURL.Host + upstreamReq.URL.Scheme = h.upstreamURL.Scheme + upstreamReq.RequestURI = "" // It is an error to set this field in an HTTP client request. - if !excluded && h.disruption.AverageDelay > 0 { - delay := int64(h.disruption.AverageDelay) - if h.disruption.DelayVariation > 0 { - variation := int64(h.disruption.DelayVariation) - delay = delay + variation - 2*rand.Int63n(variation) - } - time.Sleep(time.Duration(delay)) + response, err := h.client.Do(req) + <-wait + + if err != nil { + rw.WriteHeader(http.StatusBadGateway) + _, _ = fmt.Fprint(rw, err) + return } - // return response to the client - for key, values := range headers { + defer func() { + // Fully consume and then close upstream response body. + _, _ = io.Copy(io.Discard, response.Body) + _ = response.Body.Close() + }() + + // Mirror headers. + for key, values := range response.Header { for _, value := range values { rw.Header().Add(key, value) } } - rw.WriteHeader(statusCode) + + // Mirror status code. + rw.WriteHeader(response.StatusCode) // ignore errors writing body, nothing to do. - _, _ = io.Copy(rw, body) + _, _ = io.Copy(rw, response.Body) +} + +// fault consumes wait and then writes to rw the configured error code and body. +func (h *httpHandler) fault(rw http.ResponseWriter, wait <-chan time.Time) { + <-wait + + rw.WriteHeader(int(h.disruption.ErrorCode)) + _, _ = rw.Write([]byte(h.disruption.ErrorBody)) +} + +func (h *httpHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + if h.isExcluded(req) { + //nolint:contextcheck // Unclear which context the linter requires us to propagate here. + h.proxy(rw, req, time.After(0)) + return + } + + delay := h.disruption.AverageDelay + if h.disruption.DelayVariation > 0 { + variation := int64(h.disruption.DelayVariation) + delay += time.Duration(variation - 2*rand.Int63n(variation)) + } + + responseTimer := time.After(delay) + + if h.disruption.ErrorRate > 0 && rand.Float32() <= h.disruption.ErrorRate { + h.fault(rw, responseTimer) + return + } + + //nolint:contextcheck // Unclear which context the linter requires us to propagate here. + h.proxy(rw, req, responseTimer) } // Start starts the execution of the proxy