func (l Logger) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { for _, rule := range l.Rules { if httpserver.Path(r.URL.Path).Matches(rule.PathScope) { // Record the response responseRecorder := httpserver.NewResponseRecorder(w) // Attach the Replacer we'll use so that other middlewares can // set their own placeholders if they want to. rep := httpserver.NewReplacer(r, responseRecorder, CommonLogEmptyValue) responseRecorder.Replacer = rep // Bon voyage, request! status, err := l.Next.ServeHTTP(responseRecorder, r) if status >= 400 { // There was an error up the chain, but no response has been written yet. // The error must be handled here so the log entry will record the response size. if l.ErrorFunc != nil { l.ErrorFunc(responseRecorder, r, status) } else { // Default failover error handler responseRecorder.WriteHeader(status) fmt.Fprintf(responseRecorder, "%d %s", status, http.StatusText(status)) } status = 0 } // Write log entry rule.Log.Println(rep.Replace(rule.Format)) return status, err } } return l.Next.ServeHTTP(w, r) }
// ServeHTTP implements the httpserver.Handler interface and serves requests, // setting headers on the response according to the configured rules. func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { replacer := httpserver.NewReplacer(r, nil, "") rww := &responseWriterWrapper{w: w} for _, rule := range h.Rules { if httpserver.Path(r.URL.Path).Matches(rule.Path) { for name := range rule.Headers { // One can either delete a header, add multiple values to a header, or simply // set a header. if strings.HasPrefix(name, "-") { rww.delHeader(strings.TrimLeft(name, "-")) } else if strings.HasPrefix(name, "+") { for _, value := range rule.Headers[name] { rww.Header().Add(strings.TrimLeft(name, "+"), replacer.Replace(value)) } } else { for _, value := range rule.Headers[name] { rww.Header().Set(name, replacer.Replace(value)) } } } } } return h.Next.ServeHTTP(rww, r) }
// ServeHTTP implements the httpserver.Handler interface. func (rd Redirect) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { for _, rule := range rd.Rules { if (rule.FromPath == "/" || r.URL.Path == rule.FromPath) && schemeMatches(rule, r) && rule.Match(r) { to := httpserver.NewReplacer(r, nil, "").Replace(rule.To) if rule.Meta { safeTo := html.EscapeString(to) fmt.Fprintf(w, metaRedir, safeTo, safeTo) } else { http.Redirect(w, r, to, rule.Code) } return 0, nil } } return rd.Next.ServeHTTP(w, r) }
// ServeHTTP implements the httpserver.Handler interface and serves requests, // setting headers on the response according to the configured rules. func (h Headers) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { replacer := httpserver.NewReplacer(r, nil, "") for _, rule := range h.Rules { if httpserver.Path(r.URL.Path).Matches(rule.Path) { for _, header := range rule.Headers { if strings.HasPrefix(header.Name, "-") { w.Header().Del(strings.TrimLeft(header.Name, "-")) } else { w.Header().Set(header.Name, replacer.Replace(header.Value)) } } } } return h.Next.ServeHTTP(w, r) }
func TestReverseProxy(t *testing.T) { log.SetOutput(ioutil.Discard) defer log.SetOutput(os.Stderr) var requestReceived bool backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestReceived = true w.Write([]byte("Hello, client")) })) defer backend.Close() // set up proxy p := &Proxy{ Next: httpserver.EmptyNext, // prevents panic in some cases when test fails Upstreams: []Upstream{newFakeUpstream(backend.URL, false)}, } // create request and response recorder r, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatalf("Failed to create request: %v", err) } w := httptest.NewRecorder() p.ServeHTTP(w, r) if !requestReceived { t.Error("Expected backend to receive request, but it didn't") } // Make sure {upstream} placeholder is set rr := httpserver.NewResponseRecorder(httptest.NewRecorder()) rr.Replacer = httpserver.NewReplacer(r, rr, "-") p.ServeHTTP(rr, r) if got, want := rr.Replacer.Replace("{upstream}"), backend.URL; got != want { t.Errorf("Expected custom placeholder {upstream} to be set (%s), but it wasn't; got: %s", want, got) } }
func newReplacer(r *http.Request) httpserver.Replacer { return httpserver.NewReplacer(r, nil, "") }
// ServeHTTP satisfies the httpserver.Handler interface. func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { // start by selecting most specific matching upstream config upstream := p.match(r) if upstream == nil { return p.Next.ServeHTTP(w, r) } // this replacer is used to fill in header field values replacer := httpserver.NewReplacer(r, nil, "") // outreq is the request that makes a roundtrip to the backend outreq := createUpstreamRequest(r) // since Select() should give us "up" hosts, keep retrying // hosts until timeout (or until we get a nil host). start := time.Now() for time.Now().Sub(start) < tryDuration { host := upstream.Select(r) if host == nil { return http.StatusBadGateway, errUnreachable } if rr, ok := w.(*httpserver.ResponseRecorder); ok && rr.Replacer != nil { rr.Replacer.Set("upstream", host.Name) } proxy := host.ReverseProxy // a backend's name may contain more than just the host, // so we parse it as a URL to try to isolate the host. if nameURL, err := url.Parse(host.Name); err == nil { outreq.Host = nameURL.Host if proxy == nil { proxy = NewSingleHostReverseProxy(nameURL, host.WithoutPathPrefix, http.DefaultMaxIdleConnsPerHost) } // use upstream credentials by default if outreq.Header.Get("Authorization") == "" && nameURL.User != nil { pwd, _ := nameURL.User.Password() outreq.SetBasicAuth(nameURL.User.Username(), pwd) } } else { outreq.Host = host.Name } if proxy == nil { return http.StatusInternalServerError, errors.New("proxy for host '" + host.Name + "' is nil") } // set headers for request going upstream if host.UpstreamHeaders != nil { // modify headers for request that will be sent to the upstream host mutateHeadersByRules(outreq.Header, host.UpstreamHeaders, replacer) if hostHeaders, ok := outreq.Header["Host"]; ok && len(hostHeaders) > 0 { outreq.Host = hostHeaders[len(hostHeaders)-1] } } // prepare a function that will update response // headers coming back downstream var downHeaderUpdateFn respUpdateFn if host.DownstreamHeaders != nil { downHeaderUpdateFn = createRespHeaderUpdateFn(host.DownstreamHeaders, replacer) } // tell the proxy to serve the request atomic.AddInt64(&host.Conns, 1) backendErr := proxy.ServeHTTP(w, outreq, downHeaderUpdateFn) atomic.AddInt64(&host.Conns, -1) // if no errors, we're done here; otherwise failover if backendErr == nil { return 0, nil } timeout := host.FailTimeout if timeout == 0 { timeout = 10 * time.Second } atomic.AddInt32(&host.Fails, 1) go func(host *UpstreamHost, timeout time.Duration) { time.Sleep(timeout) atomic.AddInt32(&host.Fails, -1) }(host, timeout) } return http.StatusBadGateway, errUnreachable }
// ServeHTTP satisfies the httpserver.Handler interface. func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) { // start by selecting most specific matching upstream config upstream := p.match(r) if upstream == nil { return p.Next.ServeHTTP(w, r) } // this replacer is used to fill in header field values replacer := httpserver.NewReplacer(r, nil, "") // outreq is the request that makes a roundtrip to the backend outreq := createUpstreamRequest(r) // record and replace outreq body body, err := newBufferedBody(outreq.Body) if err != nil { return http.StatusBadRequest, errors.New("failed to read downstream request body") } if body != nil { outreq.Body = body } // The keepRetrying function will return true if we should // loop and try to select another host, or false if we // should break and stop retrying. start := time.Now() keepRetrying := func() bool { // if we've tried long enough, break if time.Since(start) >= upstream.GetTryDuration() { return false } // otherwise, wait and try the next available host time.Sleep(upstream.GetTryInterval()) return true } var backendErr error for { // since Select() should give us "up" hosts, keep retrying // hosts until timeout (or until we get a nil host). host := upstream.Select(r) if host == nil { if backendErr == nil { backendErr = errors.New("no hosts available upstream") } if !keepRetrying() { break } continue } if rr, ok := w.(*httpserver.ResponseRecorder); ok && rr.Replacer != nil { rr.Replacer.Set("upstream", host.Name) } proxy := host.ReverseProxy // a backend's name may contain more than just the host, // so we parse it as a URL to try to isolate the host. if nameURL, err := url.Parse(host.Name); err == nil { outreq.Host = nameURL.Host if proxy == nil { proxy = NewSingleHostReverseProxy(nameURL, host.WithoutPathPrefix, http.DefaultMaxIdleConnsPerHost) } // use upstream credentials by default if outreq.Header.Get("Authorization") == "" && nameURL.User != nil { pwd, _ := nameURL.User.Password() outreq.SetBasicAuth(nameURL.User.Username(), pwd) } } else { outreq.Host = host.Name } if proxy == nil { return http.StatusInternalServerError, errors.New("proxy for host '" + host.Name + "' is nil") } // set headers for request going upstream if host.UpstreamHeaders != nil { // modify headers for request that will be sent to the upstream host mutateHeadersByRules(outreq.Header, host.UpstreamHeaders, replacer) if hostHeaders, ok := outreq.Header["Host"]; ok && len(hostHeaders) > 0 { outreq.Host = hostHeaders[len(hostHeaders)-1] } } // prepare a function that will update response // headers coming back downstream var downHeaderUpdateFn respUpdateFn if host.DownstreamHeaders != nil { downHeaderUpdateFn = createRespHeaderUpdateFn(host.DownstreamHeaders, replacer) } // rewind request body to its beginning if err := body.rewind(); err != nil { return http.StatusInternalServerError, errors.New("unable to rewind downstream request body") } // tell the proxy to serve the request atomic.AddInt64(&host.Conns, 1) backendErr = proxy.ServeHTTP(w, outreq, downHeaderUpdateFn) atomic.AddInt64(&host.Conns, -1) // if no errors, we're done here if backendErr == nil { return 0, nil } if _, ok := backendErr.(httpserver.MaxBytesExceeded); ok { return http.StatusRequestEntityTooLarge, backendErr } // failover; remember this failure for some time if // request failure counting is enabled timeout := host.FailTimeout if timeout > 0 { atomic.AddInt32(&host.Fails, 1) go func(host *UpstreamHost, timeout time.Duration) { time.Sleep(timeout) atomic.AddInt32(&host.Fails, -1) }(host, timeout) } // if we've tried long enough, break if !keepRetrying() { break } } return http.StatusBadGateway, backendErr }
// buildEnv returns a set of CGI environment variables for the request. func (h Handler) buildEnv(r *http.Request, rule Rule, fpath string) (map[string]string, error) { var env map[string]string // Get absolute path of requested resource absPath := filepath.Join(h.AbsRoot, fpath) // Separate remote IP and port; more lenient than net.SplitHostPort var ip, port string if idx := strings.LastIndex(r.RemoteAddr, ":"); idx > -1 { ip = r.RemoteAddr[:idx] port = r.RemoteAddr[idx+1:] } else { ip = r.RemoteAddr } // Remove [] from IPv6 addresses ip = strings.Replace(ip, "[", "", 1) ip = strings.Replace(ip, "]", "", 1) // Split path in preparation for env variables. // Previous rule.canSplit checks ensure this can never be -1. splitPos := rule.splitPos(fpath) // Request has the extension; path was split successfully docURI := fpath[:splitPos+len(rule.SplitPath)] pathInfo := fpath[splitPos+len(rule.SplitPath):] scriptName := fpath scriptFilename := absPath // Strip PATH_INFO from SCRIPT_NAME scriptName = strings.TrimSuffix(scriptName, pathInfo) // Get the request URI. The request URI might be as it came in over the wire, // or it might have been rewritten internally by the rewrite middleware (see issue #256). // If it was rewritten, there will be a header indicating the original URL, // which is needed to get the correct RequestURI value for PHP apps. reqURI := r.URL.RequestURI() if origURI := r.Header.Get(internalRewriteFieldName); origURI != "" { reqURI = origURI } // Some variables are unused but cleared explicitly to prevent // the parent environment from interfering. env = map[string]string{ // Variables defined in CGI 1.1 spec "AUTH_TYPE": "", // Not used "CONTENT_LENGTH": r.Header.Get("Content-Length"), "CONTENT_TYPE": r.Header.Get("Content-Type"), "GATEWAY_INTERFACE": "CGI/1.1", "PATH_INFO": pathInfo, "QUERY_STRING": r.URL.RawQuery, "REMOTE_ADDR": ip, "REMOTE_HOST": ip, // For speed, remote host lookups disabled "REMOTE_PORT": port, "REMOTE_IDENT": "", // Not used "REMOTE_USER": "", // Not used "REQUEST_METHOD": r.Method, "SERVER_NAME": h.ServerName, "SERVER_PORT": h.ServerPort, "SERVER_PROTOCOL": r.Proto, "SERVER_SOFTWARE": h.SoftwareName + "/" + h.SoftwareVersion, // Other variables "DOCUMENT_ROOT": h.AbsRoot, "DOCUMENT_URI": docURI, "HTTP_HOST": r.Host, // added here, since not always part of headers "REQUEST_URI": reqURI, "SCRIPT_FILENAME": scriptFilename, "SCRIPT_NAME": scriptName, } // compliance with the CGI specification that PATH_TRANSLATED // should only exist if PATH_INFO is defined. // Info: https://www.ietf.org/rfc/rfc3875 Page 14 if env["PATH_INFO"] != "" { env["PATH_TRANSLATED"] = filepath.Join(h.AbsRoot, pathInfo) // Info: http://www.oreilly.com/openbook/cgi/ch02_04.html } // Some web apps rely on knowing HTTPS or not if r.TLS != nil { env["HTTPS"] = "on" } replacer := httpserver.NewReplacer(r, nil, "") // Add env variables from config for _, envVar := range rule.EnvVars { // replace request placeholders in environment variables env[envVar[0]] = replacer.Replace(envVar[1]) } // Add all HTTP headers (except Caddy-Rewrite-Original-URI ) to env variables for field, val := range r.Header { if strings.ToLower(field) == strings.ToLower(internalRewriteFieldName) { continue } header := strings.ToUpper(field) header = headerNameReplacer.Replace(header) env["HTTP_"+header] = strings.Join(val, ", ") } return env, nil }
func TestDownstreamHeadersUpdate(t *testing.T) { log.SetOutput(ioutil.Discard) defer log.SetOutput(os.Stderr) backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Merge-Me", "Initial") w.Header().Add("Remove-Me", "Remove-Value") w.Header().Add("Replace-Me", "Replace-Value") w.Write([]byte("Hello, client")) })) defer backend.Close() upstream := newFakeUpstream(backend.URL, false) upstream.host.DownstreamHeaders = http.Header{ "+Merge-Me": {"Merge-Value"}, "+Add-Me": {"Add-Value"}, "-Remove-Me": {""}, "Replace-Me": {"{hostname}"}, } // set up proxy p := &Proxy{ Next: httpserver.EmptyNext, // prevents panic in some cases when test fails Upstreams: []Upstream{upstream}, } // create request and response recorder r, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatalf("Failed to create request: %v", err) } w := httptest.NewRecorder() p.ServeHTTP(w, r) replacer := httpserver.NewReplacer(r, nil, "") actualHeaders := w.Header() headerKey := "Merge-Me" values, ok := actualHeaders[headerKey] if !ok { t.Errorf("Downstream response does not contain expected %v header. Expected header should be added", headerKey) } else if len(values) < 2 && (values[0] != "Initial" || values[1] != replacer.Replace("{hostname}")) { t.Errorf("Values for header `+Merge-Me` should be merged. Got %v", values) } headerKey = "Add-Me" if _, ok := actualHeaders[headerKey]; !ok { t.Errorf("Downstream response does not contain expected %v header", headerKey) } headerKey = "Remove-Me" if _, ok := actualHeaders[headerKey]; ok { t.Errorf("Downstream response should not contain %v header received from upstream", headerKey) } headerKey = "Replace-Me" headerValue := replacer.Replace("{hostname}") value, ok := actualHeaders[headerKey] if !ok { t.Errorf("Downstream response should contain %v header and not remove it", headerKey) } else if len(value) > 0 && headerValue != value[0] { t.Errorf("Downstream response should have header %v with value %v. Instead value was %v", headerKey, headerValue, value) } }
func TestUpstreamHeadersUpdate(t *testing.T) { log.SetOutput(ioutil.Discard) defer log.SetOutput(os.Stderr) var actualHeaders http.Header var actualHost string backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, client")) actualHeaders = r.Header actualHost = r.Host })) defer backend.Close() upstream := newFakeUpstream(backend.URL, false) upstream.host.UpstreamHeaders = http.Header{ "Connection": {"{>Connection}"}, "Upgrade": {"{>Upgrade}"}, "+Merge-Me": {"Merge-Value"}, "+Add-Me": {"Add-Value"}, "-Remove-Me": {""}, "Replace-Me": {"{hostname}"}, "Host": {"{>Host}"}, } // set up proxy p := &Proxy{ Next: httpserver.EmptyNext, // prevents panic in some cases when test fails Upstreams: []Upstream{upstream}, } // create request and response recorder r, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatalf("Failed to create request: %v", err) } w := httptest.NewRecorder() const expectHost = "example.com" //add initial headers r.Header.Add("Merge-Me", "Initial") r.Header.Add("Remove-Me", "Remove-Value") r.Header.Add("Replace-Me", "Replace-Value") r.Header.Add("Host", expectHost) p.ServeHTTP(w, r) replacer := httpserver.NewReplacer(r, nil, "") headerKey := "Merge-Me" values, ok := actualHeaders[headerKey] if !ok { t.Errorf("Request sent to upstream backend does not contain expected %v header. Expected header to be added", headerKey) } else if len(values) < 2 && (values[0] != "Initial" || values[1] != replacer.Replace("{hostname}")) { t.Errorf("Values for proxy header `+Merge-Me` should be merged. Got %v", values) } headerKey = "Add-Me" if _, ok := actualHeaders[headerKey]; !ok { t.Errorf("Request sent to upstream backend does not contain expected %v header", headerKey) } headerKey = "Remove-Me" if _, ok := actualHeaders[headerKey]; ok { t.Errorf("Request sent to upstream backend should not contain %v header", headerKey) } headerKey = "Replace-Me" headerValue := replacer.Replace("{hostname}") value, ok := actualHeaders[headerKey] if !ok { t.Errorf("Request sent to upstream backend should not remove %v header", headerKey) } else if len(value) > 0 && headerValue != value[0] { t.Errorf("Request sent to upstream backend should replace value of %v header with %v. Instead value was %v", headerKey, headerValue, value) } if actualHost != expectHost { t.Errorf("Request sent to upstream backend should have value of Host with %s, but got %s", expectHost, actualHost) } }
func TestDownstreamHeadersUpdate(t *testing.T) { log.SetOutput(ioutil.Discard) defer log.SetOutput(os.Stderr) backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Merge-Me", "Initial") w.Header().Add("Remove-Me", "Remove-Value") w.Header().Add("Replace-Me", "Replace-Value") w.Header().Add("Content-Type", "text/html") w.Header().Add("Overwrite-Me", "Overwrite-Value") w.Write([]byte("Hello, client")) })) defer backend.Close() upstream := newFakeUpstream(backend.URL, false) upstream.host.DownstreamHeaders = http.Header{ "+Merge-Me": {"Merge-Value"}, "+Add-Me": {"Add-Value"}, "-Remove-Me": {""}, "Replace-Me": {"{hostname}"}, } // set up proxy p := &Proxy{ Next: httpserver.EmptyNext, // prevents panic in some cases when test fails Upstreams: []Upstream{upstream}, } // create request and response recorder r, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatalf("Failed to create request: %v", err) } w := httptest.NewRecorder() // set a predefined skip header w.Header().Set("Content-Type", "text/css") // set a predefined overwritten header w.Header().Set("Overwrite-Me", "Initial") p.ServeHTTP(w, r) replacer := httpserver.NewReplacer(r, nil, "") actualHeaders := w.Header() headerKey := "Merge-Me" got := actualHeaders[headerKey] expect := []string{"Initial", "Merge-Value"} if !reflect.DeepEqual(got, expect) { t.Errorf("Downstream response does not contain expected %s header: expect %v, but got %v", headerKey, expect, got) } headerKey = "Add-Me" got = actualHeaders[headerKey] expect = []string{"Add-Value"} if !reflect.DeepEqual(got, expect) { t.Errorf("Downstream response does not contain expected %s header: expect %v, but got %v", headerKey, expect, got) } headerKey = "Remove-Me" if _, ok := actualHeaders[headerKey]; ok { t.Errorf("Downstream response should not contain %v header received from upstream", headerKey) } headerKey = "Replace-Me" got = actualHeaders[headerKey] expect = []string{replacer.Replace("{hostname}")} if !reflect.DeepEqual(got, expect) { t.Errorf("Downstream response does not contain expected %s header: expect %v, but got %v", headerKey, expect, got) } headerKey = "Content-Type" got = actualHeaders[headerKey] expect = []string{"text/css"} if !reflect.DeepEqual(got, expect) { t.Errorf("Downstream response does not contain expected %s header: expect %v, but got %v", headerKey, expect, got) } headerKey = "Overwrite-Me" got = actualHeaders[headerKey] expect = []string{"Overwrite-Value"} if !reflect.DeepEqual(got, expect) { t.Errorf("Downstream response does not contain expected %s header: expect %v, but got %v", headerKey, expect, got) } }
func TestUpstreamHeadersUpdate(t *testing.T) { log.SetOutput(ioutil.Discard) defer log.SetOutput(os.Stderr) var actualHeaders http.Header var actualHost string backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Hello, client")) actualHeaders = r.Header actualHost = r.Host })) defer backend.Close() upstream := newFakeUpstream(backend.URL, false) upstream.host.UpstreamHeaders = http.Header{ "Connection": {"{>Connection}"}, "Upgrade": {"{>Upgrade}"}, "+Merge-Me": {"Merge-Value"}, "+Add-Me": {"Add-Value"}, "-Remove-Me": {""}, "Replace-Me": {"{hostname}"}, "Host": {"{>Host}"}, } // set up proxy p := &Proxy{ Next: httpserver.EmptyNext, // prevents panic in some cases when test fails Upstreams: []Upstream{upstream}, } // create request and response recorder r, err := http.NewRequest("GET", "/", nil) if err != nil { t.Fatalf("Failed to create request: %v", err) } w := httptest.NewRecorder() const expectHost = "example.com" //add initial headers r.Header.Add("Merge-Me", "Initial") r.Header.Add("Remove-Me", "Remove-Value") r.Header.Add("Replace-Me", "Replace-Value") r.Header.Add("Host", expectHost) p.ServeHTTP(w, r) replacer := httpserver.NewReplacer(r, nil, "") headerKey := "Merge-Me" got := actualHeaders[headerKey] expect := []string{"Initial", "Merge-Value"} if !reflect.DeepEqual(got, expect) { t.Errorf("Request sent to upstream backend does not contain expected %v header: expect %v, but got %v", headerKey, expect, got) } headerKey = "Add-Me" got = actualHeaders[headerKey] expect = []string{"Add-Value"} if !reflect.DeepEqual(got, expect) { t.Errorf("Request sent to upstream backend does not contain expected %v header: expect %v, but got %v", headerKey, expect, got) } headerKey = "Remove-Me" if _, ok := actualHeaders[headerKey]; ok { t.Errorf("Request sent to upstream backend should not contain %v header", headerKey) } headerKey = "Replace-Me" got = actualHeaders[headerKey] expect = []string{replacer.Replace("{hostname}")} if !reflect.DeepEqual(got, expect) { t.Errorf("Request sent to upstream backend does not contain expected %v header: expect %v, but got %v", headerKey, expect, got) } if actualHost != expectHost { t.Errorf("Request sent to upstream backend should have value of Host with %s, but got %s", expectHost, actualHost) } }