From a7c2412c3b86df83f6cb47432d4850c4ecef3f6a Mon Sep 17 00:00:00 2001 From: Tino Date: Sun, 3 Dec 2017 18:36:16 +0100 Subject: [PATCH 1/8] Implement custom noroute html response PR for #56 --- config/config.go | 1 + config/default.go | 23 ++++++++++---------- config/load.go | 1 + fabio.properties | 8 +++++++ main.go | 17 +++++++++++++++ proxy/http_proxy.go | 5 +++++ registry/backend.go | 4 ++++ registry/consul/backend.go | 8 +++++++ registry/static/backend.go | 19 +++++++++++++++- route/no_route.go | 44 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 route/no_route.go diff --git a/config/config.go b/config/config.go index 135a80b8a..ad99bc884 100644 --- a/config/config.go +++ b/config/config.go @@ -120,6 +120,7 @@ type Consul struct { Scheme string Token string KVPath string + NoRouteHTMLPath string TagPrefix string Register bool ServiceAddr string diff --git a/config/default.go b/config/default.go index 044c470e3..c01b22914 100644 --- a/config/default.go +++ b/config/default.go @@ -47,17 +47,18 @@ var defaultConfig = &Config{ Registry: Registry{ Backend: "consul", Consul: Consul{ - Addr: "localhost:8500", - Scheme: "http", - KVPath: "/fabio/config", - TagPrefix: "urlprefix-", - Register: true, - ServiceAddr: ":9998", - ServiceName: "fabio", - ServiceStatus: []string{"passing"}, - CheckInterval: time.Second, - CheckTimeout: 3 * time.Second, - CheckScheme: "http", + Addr: "localhost:8500", + Scheme: "http", + KVPath: "/fabio/config", + NoRouteHTMLPath: "/fabio/noroute.html", + TagPrefix: "urlprefix-", + Register: true, + ServiceAddr: ":9998", + ServiceName: "fabio", + ServiceStatus: []string{"passing"}, + CheckInterval: time.Second, + CheckTimeout: 3 * time.Second, + CheckScheme: "http", }, Timeout: 10 * time.Second, Retry: 500 * time.Millisecond, diff --git a/config/load.go b/config/load.go index 54fde1a3d..692379fd7 100644 --- a/config/load.go +++ b/config/load.go @@ -160,6 +160,7 @@ func load(cmdline, environ, envprefix []string, props *properties.Properties) (c f.StringVar(&cfg.Registry.Consul.Addr, "registry.consul.addr", defaultConfig.Registry.Consul.Addr, "address of the consul agent") f.StringVar(&cfg.Registry.Consul.Token, "registry.consul.token", defaultConfig.Registry.Consul.Token, "token for consul agent") f.StringVar(&cfg.Registry.Consul.KVPath, "registry.consul.kvpath", defaultConfig.Registry.Consul.KVPath, "consul KV path for manual overrides") + f.StringVar(&cfg.Registry.Consul.NoRouteHTMLPath, "registry.consul.noroutehtmlpath", defaultConfig.Registry.Consul.NoRouteHTMLPath, "consul KV path for HTML returned when no route is found") f.StringVar(&cfg.Registry.Consul.TagPrefix, "registry.consul.tagprefix", defaultConfig.Registry.Consul.TagPrefix, "prefix for consul tags") f.BoolVar(&cfg.Registry.Consul.Register, "registry.consul.register.enabled", defaultConfig.Registry.Consul.Register, "register fabio in consul") f.StringVar(&cfg.Registry.Consul.ServiceAddr, "registry.consul.register.addr", defaultConfig.Registry.Consul.ServiceAddr, "service registration address") diff --git a/fabio.properties b/fabio.properties index 092a19639..809295115 100644 --- a/fabio.properties +++ b/fabio.properties @@ -567,6 +567,14 @@ # # registry.consul.kvpath = /fabio/config +# registry.consul.noroutehtmlpath configures the KV path for the HTML of the +# noroutes page. +# +# The consul KV path is watched for changes. +# +# The default is +# +# registry.consul.kvpath = /fabio/noroutes.html # registry.consul.service.status configures the valid service status # values for services included in the routing table. diff --git a/main.go b/main.go index dd47dc7ea..f1ccda211 100644 --- a/main.go +++ b/main.go @@ -115,6 +115,8 @@ func main() { initBackend(cfg) startAdmin(cfg) + go watchNoRouteHTML(cfg) + first := make(chan bool) go watchBackend(cfg, first) log.Print("[INFO] Waiting for first routing table") @@ -406,6 +408,21 @@ func watchBackend(cfg *config.Config, first chan bool) { } } +func watchNoRouteHTML(cfg *config.Config) { + var last string + html := registry.Default.WatchNoRouteHTML() + + for { + next := <-html + + if next == last { + continue + } + route.SetHTML(next) + last = next + } +} + func logRoutes(t route.Table, last, next, format string) { fmtDiff := func(diffs []dmp.Diff) string { var b bytes.Buffer diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index 42bc744ba..d0300188f 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -2,6 +2,7 @@ package proxy import ( "crypto/tls" + "io" "net" "net/http" "net/http/httputil" @@ -63,6 +64,10 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { t := p.Lookup(r) if t == nil { w.WriteHeader(p.Config.NoRouteStatus) + html := route.GetHTML() + if html != "" { + io.WriteString(w, html) + } return } diff --git a/registry/backend.go b/registry/backend.go index 4c734994d..bec176754 100644 --- a/registry/backend.go +++ b/registry/backend.go @@ -22,6 +22,10 @@ type Backend interface { // WatchManual watches the registry for changes in the manual // overrides and pushes them if there is a difference. WatchManual() chan string + + // WatchNoRouteHTML watches the registry for changes in the html returned + // when a requested route is not found + WatchNoRouteHTML() chan string } var Default Backend diff --git a/registry/consul/backend.go b/registry/consul/backend.go index cfa9d9e6e..921e2c222 100644 --- a/registry/consul/backend.go +++ b/registry/consul/backend.go @@ -92,6 +92,14 @@ func (b *be) WatchManual() chan string { return kv } +func (b *be) WatchNoRouteHTML() chan string { + log.Printf("[INFO] consul: Watching KV path %q", b.cfg.NoRouteHTMLPath) + + html := make(chan string) + go watchKV(b.c, b.cfg.NoRouteHTMLPath, html) + return html +} + // datacenter returns the datacenter of the local agent func datacenter(c *api.Client) (string, error) { self, err := c.Agent().Self() diff --git a/registry/static/backend.go b/registry/static/backend.go index 8c42b8819..a2fc86080 100644 --- a/registry/static/backend.go +++ b/registry/static/backend.go @@ -2,7 +2,13 @@ // backend which uses statically configured routes. package static -import "github.com/fabiolb/fabio/registry" +import ( + "io/ioutil" + "log" + + "github.com/fabiolb/fabio/registry" + "github.com/fabiolb/fabio/route" +) type be struct{} @@ -38,3 +44,14 @@ func (b *be) WatchServices() chan string { func (b *be) WatchManual() chan string { return make(chan string) } + +// WatchNoRouteHTML implementation that reads the noroute html from a +// noroute.html file if it exists +func (b *be) WatchNoRouteHTML() chan string { + data, err := ioutil.ReadFile("noroute.html") + if err != nil { + log.Println("[WARN] No noroute.html to read noroute html from") + } + route.SetHTML(string(data)) + return make(chan string) +} diff --git a/route/no_route.go b/route/no_route.go new file mode 100644 index 000000000..98566ed28 --- /dev/null +++ b/route/no_route.go @@ -0,0 +1,44 @@ +package route + +import ( + "log" + "sync" + "sync/atomic" +) + +// HTML Wrapper struct so we can store the html string in an atomic.Value +type HTML struct { + value string +} + +// html stores the no route html string +var store atomic.Value + +func init() { + store.Store(HTML{""}) +} + +// GetHTML returns the HTML for not found routes. The function is safe to be +// called from multiple goroutines. +func GetHTML() string { + return store.Load().(HTML).value +} + +// hmu guards the atomic writes in SetHTML. +var hmu sync.Mutex + +// SetHTML sets the current noroute html. The function is safe to be called from +// multiple goroutines. +func SetHTML(h string) { + hmu.Lock() + defer hmu.Unlock() + + html := HTML{h} + store.Store(html) + + if h == "" { + log.Print("[INFO] Unset noroute HTML") + } else { + log.Printf("[INFO] Set noroute HTML (%d bytes)", len(h)) + } +} From 23546e10da52c2fe216a658a27cde3f98825091f Mon Sep 17 00:00:00 2001 From: Tino Date: Wed, 6 Dec 2017 21:12:31 +0100 Subject: [PATCH 2/8] Add integration test --- proxy/http_integration_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/proxy/http_integration_test.go b/proxy/http_integration_test.go index 485ee7d9f..b5f55ac5a 100644 --- a/proxy/http_integration_test.go +++ b/proxy/http_integration_test.go @@ -75,7 +75,22 @@ func TestProxyRequestIDHeader(t *testing.T) { } } -func TestProxyNoRouteStaus(t *testing.T) { +func TestProxyNoRouteHTML(t *testing.T) { + want := "503" + route.SetHTML(want) + proxy := httptest.NewServer(&HTTPProxy{ + Transport: http.DefaultTransport, + Lookup: func(*http.Request) *route.Target { return nil }, + }) + defer proxy.Close() + + _, got := mustGet(proxy.URL) + if !bytes.Equal(got, []byte(want)) { + t.Fatalf("got %d want %d", got, want) + } +} + +func TestProxyNoRouteStatus(t *testing.T) { proxy := httptest.NewServer(&HTTPProxy{ Config: config.Proxy{NoRouteStatus: 999}, Transport: http.DefaultTransport, From 1fae4c481721103d1da5afcfded9c3220a657ea3 Mon Sep 17 00:00:00 2001 From: Tino Date: Wed, 6 Dec 2017 21:12:43 +0100 Subject: [PATCH 3/8] Remove mutex --- route/no_route.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/route/no_route.go b/route/no_route.go index 98566ed28..d16f80873 100644 --- a/route/no_route.go +++ b/route/no_route.go @@ -2,7 +2,6 @@ package route import ( "log" - "sync" "sync/atomic" ) @@ -24,15 +23,9 @@ func GetHTML() string { return store.Load().(HTML).value } -// hmu guards the atomic writes in SetHTML. -var hmu sync.Mutex - // SetHTML sets the current noroute html. The function is safe to be called from // multiple goroutines. func SetHTML(h string) { - hmu.Lock() - defer hmu.Unlock() - html := HTML{h} store.Store(html) From 3e08824703812ee6ef1aa0c1c82e068ee68e2fbe Mon Sep 17 00:00:00 2001 From: Tino Date: Wed, 6 Dec 2017 21:15:29 +0100 Subject: [PATCH 4/8] Remove superfluous constructs of store --- route/no_route.go | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/route/no_route.go b/route/no_route.go index d16f80873..5273eb241 100644 --- a/route/no_route.go +++ b/route/no_route.go @@ -5,29 +5,21 @@ import ( "sync/atomic" ) -// HTML Wrapper struct so we can store the html string in an atomic.Value -type HTML struct { - value string -} - -// html stores the no route html string -var store atomic.Value +var store atomic.Value // string func init() { - store.Store(HTML{""}) + store.Store("") } -// GetHTML returns the HTML for not found routes. The function is safe to be -// called from multiple goroutines. +// GetHTML returns the HTML for not found routes. func GetHTML() string { - return store.Load().(HTML).value + return store.Load().(string) } -// SetHTML sets the current noroute html. The function is safe to be called from -// multiple goroutines. +// SetHTML sets the current noroute html. func SetHTML(h string) { - html := HTML{h} - store.Store(html) + // html := HTML{h} + store.Store(h) if h == "" { log.Print("[INFO] Unset noroute HTML") From 883be3510dfc1265e48becf56a86ea6b0f2d061a Mon Sep 17 00:00:00 2001 From: Tino Date: Wed, 6 Dec 2017 21:37:52 +0100 Subject: [PATCH 5/8] Make file and static backend work with config too --- config/config.go | 6 ++++-- config/load.go | 2 ++ fabio.properties | 18 +++++++++++++++++- main.go | 4 ++-- registry/file/backend.go | 10 ++++++---- registry/static/backend.go | 18 +++++++++--------- 6 files changed, 40 insertions(+), 18 deletions(-) diff --git a/config/config.go b/config/config.go index ad99bc884..2bee33796 100644 --- a/config/config.go +++ b/config/config.go @@ -108,11 +108,13 @@ type Registry struct { } type Static struct { - Routes string + NoRouteHTMLPath string + Routes string } type File struct { - Path string + NoRouteHTMLPath string + Path string } type Consul struct { diff --git a/config/load.go b/config/load.go index 692379fd7..282f4fd7c 100644 --- a/config/load.go +++ b/config/load.go @@ -156,7 +156,9 @@ func load(cmdline, environ, envprefix []string, props *properties.Properties) (c f.DurationVar(&cfg.Registry.Timeout, "registry.timeout", defaultConfig.Registry.Timeout, "timeout for registry to become available") f.DurationVar(&cfg.Registry.Retry, "registry.retry", defaultConfig.Registry.Retry, "retry interval during startup") f.StringVar(&cfg.Registry.File.Path, "registry.file.path", defaultConfig.Registry.File.Path, "path to file based routing table") + f.StringVar(&cfg.Registry.File.NoRouteHTMLPath, "registry.file.noroutehtmlpath", defaultConfig.Registry.File.NoRouteHTMLPath, "path to file for HTML returned when no route is found") f.StringVar(&cfg.Registry.Static.Routes, "registry.static.routes", defaultConfig.Registry.Static.Routes, "static routes") + f.StringVar(&cfg.Registry.Static.NoRouteHTMLPath, "registry.static.noroutehtmlpath", defaultConfig.Registry.Static.NoRouteHTMLPath, "path to file for HTML returned when no route is found") f.StringVar(&cfg.Registry.Consul.Addr, "registry.consul.addr", defaultConfig.Registry.Consul.Addr, "address of the consul agent") f.StringVar(&cfg.Registry.Consul.Token, "registry.consul.token", defaultConfig.Registry.Consul.Token, "token for consul agent") f.StringVar(&cfg.Registry.Consul.KVPath, "registry.consul.kvpath", defaultConfig.Registry.Consul.KVPath, "consul KV path for manual overrides") diff --git a/fabio.properties b/fabio.properties index 809295115..578d764ad 100644 --- a/fabio.properties +++ b/fabio.properties @@ -535,6 +535,14 @@ # registry.static.routes = +# registry.static.noroutehtmlpath configures the KV path for the HTML of the +# noroutes page. +# +# The default is +# +# registry.static.noroutehtmlpath = + + # registry.file.path configures a file based routing table. # The value configures the path to the file with the routing table. # @@ -543,6 +551,14 @@ # registry.file.path = +# registry.file.noroutehtmlpath configures the KV path for the HTML of the +# noroutes page. +# +# The default is +# +# registry.file.noroutehtmlpath = + + # registry.consul.addr configures the address of the consul agent to connect to. # # The default is @@ -574,7 +590,7 @@ # # The default is # -# registry.consul.kvpath = /fabio/noroutes.html +# registry.consul.noroutehtmlpath = /fabio/noroutes.html # registry.consul.service.status configures the valid service status # values for services included in the routing table. diff --git a/main.go b/main.go index f1ccda211..53f60a75d 100644 --- a/main.go +++ b/main.go @@ -343,9 +343,9 @@ func initBackend(cfg *config.Config) { for { switch cfg.Registry.Backend { case "file": - registry.Default, err = file.NewBackend(cfg.Registry.File.Path) + registry.Default, err = file.NewBackend(&cfg.Registry.File) case "static": - registry.Default, err = static.NewBackend(cfg.Registry.Static.Routes) + registry.Default, err = static.NewBackend(&cfg.Registry.Static) case "consul": registry.Default, err = consul.NewBackend(&cfg.Registry.Consul) default: diff --git a/registry/file/backend.go b/registry/file/backend.go index ff1109d5d..d9bdc1fd2 100644 --- a/registry/file/backend.go +++ b/registry/file/backend.go @@ -6,15 +6,17 @@ import ( "io/ioutil" "log" + "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/registry" "github.com/fabiolb/fabio/registry/static" ) -func NewBackend(filename string) (registry.Backend, error) { - data, err := ioutil.ReadFile(filename) +func NewBackend(cfg *config.File) (registry.Backend, error) { + data, err := ioutil.ReadFile(cfg.Path) if err != nil { - log.Println("[ERROR] Cannot read routes from ", filename) + log.Println("[ERROR] Cannot read routes from ", cfg.Path) return nil, err } - return static.NewBackend(string(data)) + staticCfg := config.Static{cfg.NoRouteHTMLPath, string(data)} + return static.NewBackend(&staticCfg) } diff --git a/registry/static/backend.go b/registry/static/backend.go index a2fc86080..064d06569 100644 --- a/registry/static/backend.go +++ b/registry/static/backend.go @@ -6,17 +6,17 @@ import ( "io/ioutil" "log" + "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/registry" "github.com/fabiolb/fabio/route" ) -type be struct{} - -var staticRoutes string +type be struct { + cfg *config.Static +} -func NewBackend(routes string) (registry.Backend, error) { - staticRoutes = routes - return &be{}, nil +func NewBackend(cfg *config.Static) (registry.Backend, error) { + return &be{cfg}, nil } func (b *be) Register() error { @@ -37,7 +37,7 @@ func (b *be) WriteManual(value string, version uint64) (ok bool, err error) { func (b *be) WatchServices() chan string { ch := make(chan string, 1) - ch <- staticRoutes + ch <- b.cfg.Routes return ch } @@ -48,9 +48,9 @@ func (b *be) WatchManual() chan string { // WatchNoRouteHTML implementation that reads the noroute html from a // noroute.html file if it exists func (b *be) WatchNoRouteHTML() chan string { - data, err := ioutil.ReadFile("noroute.html") + data, err := ioutil.ReadFile(b.cfg.NoRouteHTMLPath) if err != nil { - log.Println("[WARN] No noroute.html to read noroute html from") + log.Printf("[WARN] Could not read NoRouteHTMLPath (%s)", b.cfg.NoRouteHTMLPath) } route.SetHTML(string(data)) return make(chan string) From 81ea194203669fe7bf43128905bfe5abd6e4162c Mon Sep 17 00:00:00 2001 From: Tino Date: Wed, 6 Dec 2017 21:46:22 +0100 Subject: [PATCH 6/8] Move noroute store to it's own package --- main.go | 3 ++- {route => noroute}/no_route.go | 2 +- proxy/http_integration_test.go | 3 ++- proxy/http_proxy.go | 3 ++- registry/static/backend.go | 4 ++-- 5 files changed, 9 insertions(+), 6 deletions(-) rename {route => noroute}/no_route.go (96%) diff --git a/main.go b/main.go index 53f60a75d..4de4b6830 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ import ( "github.com/fabiolb/fabio/exit" "github.com/fabiolb/fabio/logger" "github.com/fabiolb/fabio/metrics" + "github.com/fabiolb/fabio/noroute" "github.com/fabiolb/fabio/proxy" "github.com/fabiolb/fabio/proxy/tcp" "github.com/fabiolb/fabio/registry" @@ -418,7 +419,7 @@ func watchNoRouteHTML(cfg *config.Config) { if next == last { continue } - route.SetHTML(next) + noroute.SetHTML(next) last = next } } diff --git a/route/no_route.go b/noroute/no_route.go similarity index 96% rename from route/no_route.go rename to noroute/no_route.go index 5273eb241..613c66e64 100644 --- a/route/no_route.go +++ b/noroute/no_route.go @@ -1,4 +1,4 @@ -package route +package noroute import ( "log" diff --git a/proxy/http_integration_test.go b/proxy/http_integration_test.go index b5f55ac5a..efa80ed3f 100644 --- a/proxy/http_integration_test.go +++ b/proxy/http_integration_test.go @@ -20,6 +20,7 @@ import ( "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/logger" + "github.com/fabiolb/fabio/noroute" "github.com/fabiolb/fabio/proxy/internal" "github.com/fabiolb/fabio/route" "github.com/pascaldekloe/goe/verify" @@ -77,7 +78,7 @@ func TestProxyRequestIDHeader(t *testing.T) { func TestProxyNoRouteHTML(t *testing.T) { want := "503" - route.SetHTML(want) + noroute.SetHTML(want) proxy := httptest.NewServer(&HTTPProxy{ Transport: http.DefaultTransport, Lookup: func(*http.Request) *route.Target { return nil }, diff --git a/proxy/http_proxy.go b/proxy/http_proxy.go index d0300188f..89314af71 100644 --- a/proxy/http_proxy.go +++ b/proxy/http_proxy.go @@ -14,6 +14,7 @@ import ( "github.com/fabiolb/fabio/config" "github.com/fabiolb/fabio/logger" "github.com/fabiolb/fabio/metrics" + "github.com/fabiolb/fabio/noroute" "github.com/fabiolb/fabio/proxy/gzip" "github.com/fabiolb/fabio/route" "github.com/fabiolb/fabio/uuid" @@ -64,7 +65,7 @@ func (p *HTTPProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { t := p.Lookup(r) if t == nil { w.WriteHeader(p.Config.NoRouteStatus) - html := route.GetHTML() + html := noroute.GetHTML() if html != "" { io.WriteString(w, html) } diff --git a/registry/static/backend.go b/registry/static/backend.go index 064d06569..98bf7091c 100644 --- a/registry/static/backend.go +++ b/registry/static/backend.go @@ -7,8 +7,8 @@ import ( "log" "github.com/fabiolb/fabio/config" + "github.com/fabiolb/fabio/noroute" "github.com/fabiolb/fabio/registry" - "github.com/fabiolb/fabio/route" ) type be struct { @@ -52,6 +52,6 @@ func (b *be) WatchNoRouteHTML() chan string { if err != nil { log.Printf("[WARN] Could not read NoRouteHTMLPath (%s)", b.cfg.NoRouteHTMLPath) } - route.SetHTML(string(data)) + noroute.SetHTML(string(data)) return make(chan string) } From 2a09ecb9eecb10899114cf8282a7ae05134ce603 Mon Sep 17 00:00:00 2001 From: Tino Date: Wed, 6 Dec 2017 21:50:33 +0100 Subject: [PATCH 7/8] Rename and add test --- noroute/{no_route.go => store.go} | 0 noroute/store_test.go | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+) rename noroute/{no_route.go => store.go} (100%) create mode 100644 noroute/store_test.go diff --git a/noroute/no_route.go b/noroute/store.go similarity index 100% rename from noroute/no_route.go rename to noroute/store.go diff --git a/noroute/store_test.go b/noroute/store_test.go new file mode 100644 index 000000000..c65acd464 --- /dev/null +++ b/noroute/store_test.go @@ -0,0 +1,19 @@ +package noroute + +import ( + "testing" +) + +func TestStoreSetGet(t *testing.T) { + got := GetHTML() + if got != "" { + t.Fatalf("Expected unset noroute html to be an empty string, got %s", got) + } + + want := "Fancy!" + SetHTML(want) + got = GetHTML() + if got != want { + t.Fatalf("got %s, want %s", got, want) + } +} From 24728a1d4ae670d0e6d5ceb98a620faddc30d192 Mon Sep 17 00:00:00 2001 From: Tino Date: Wed, 6 Dec 2017 21:58:05 +0100 Subject: [PATCH 8/8] Fix error message fmt in test --- proxy/http_integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/proxy/http_integration_test.go b/proxy/http_integration_test.go index efa80ed3f..be9122ed2 100644 --- a/proxy/http_integration_test.go +++ b/proxy/http_integration_test.go @@ -87,7 +87,7 @@ func TestProxyNoRouteHTML(t *testing.T) { _, got := mustGet(proxy.URL) if !bytes.Equal(got, []byte(want)) { - t.Fatalf("got %d want %d", got, want) + t.Fatalf("got %s want %s", got, want) } }