Source file src/net/http/httputil/reverseproxy_test.go

Documentation: net/http/httputil

     1  // Copyright 2011 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Reverse proxy tests.
     6  
     7  package httputil
     8  
     9  import (
    10  	"bufio"
    11  	"bytes"
    12  	"context"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"io/ioutil"
    17  	"log"
    18  	"net/http"
    19  	"net/http/httptest"
    20  	"net/url"
    21  	"os"
    22  	"reflect"
    23  	"strconv"
    24  	"strings"
    25  	"sync"
    26  	"testing"
    27  	"time"
    28  )
    29  
    30  const fakeHopHeader = "X-Fake-Hop-Header-For-Test"
    31  
    32  func init() {
    33  	inOurTests = true
    34  	hopHeaders = append(hopHeaders, fakeHopHeader)
    35  }
    36  
    37  func TestReverseProxy(t *testing.T) {
    38  	const backendResponse = "I am the backend"
    39  	const backendStatus = 404
    40  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    41  		if r.Method == "GET" && r.FormValue("mode") == "hangup" {
    42  			c, _, _ := w.(http.Hijacker).Hijack()
    43  			c.Close()
    44  			return
    45  		}
    46  		if len(r.TransferEncoding) > 0 {
    47  			t.Errorf("backend got unexpected TransferEncoding: %v", r.TransferEncoding)
    48  		}
    49  		if r.Header.Get("X-Forwarded-For") == "" {
    50  			t.Errorf("didn't get X-Forwarded-For header")
    51  		}
    52  		if c := r.Header.Get("Connection"); c != "" {
    53  			t.Errorf("handler got Connection header value %q", c)
    54  		}
    55  		if c := r.Header.Get("Te"); c != "trailers" {
    56  			t.Errorf("handler got Te header value %q; want 'trailers'", c)
    57  		}
    58  		if c := r.Header.Get("Upgrade"); c != "" {
    59  			t.Errorf("handler got Upgrade header value %q", c)
    60  		}
    61  		if c := r.Header.Get("Proxy-Connection"); c != "" {
    62  			t.Errorf("handler got Proxy-Connection header value %q", c)
    63  		}
    64  		if g, e := r.Host, "some-name"; g != e {
    65  			t.Errorf("backend got Host header %q, want %q", g, e)
    66  		}
    67  		w.Header().Set("Trailers", "not a special header field name")
    68  		w.Header().Set("Trailer", "X-Trailer")
    69  		w.Header().Set("X-Foo", "bar")
    70  		w.Header().Set("Upgrade", "foo")
    71  		w.Header().Set(fakeHopHeader, "foo")
    72  		w.Header().Add("X-Multi-Value", "foo")
    73  		w.Header().Add("X-Multi-Value", "bar")
    74  		http.SetCookie(w, &http.Cookie{Name: "flavor", Value: "chocolateChip"})
    75  		w.WriteHeader(backendStatus)
    76  		w.Write([]byte(backendResponse))
    77  		w.Header().Set("X-Trailer", "trailer_value")
    78  		w.Header().Set(http.TrailerPrefix+"X-Unannounced-Trailer", "unannounced_trailer_value")
    79  	}))
    80  	defer backend.Close()
    81  	backendURL, err := url.Parse(backend.URL)
    82  	if err != nil {
    83  		t.Fatal(err)
    84  	}
    85  	proxyHandler := NewSingleHostReverseProxy(backendURL)
    86  	proxyHandler.ErrorLog = log.New(ioutil.Discard, "", 0) // quiet for tests
    87  	frontend := httptest.NewServer(proxyHandler)
    88  	defer frontend.Close()
    89  	frontendClient := frontend.Client()
    90  
    91  	getReq, _ := http.NewRequest("GET", frontend.URL, nil)
    92  	getReq.Host = "some-name"
    93  	getReq.Header.Set("Connection", "close")
    94  	getReq.Header.Set("Te", "trailers")
    95  	getReq.Header.Set("Proxy-Connection", "should be deleted")
    96  	getReq.Header.Set("Upgrade", "foo")
    97  	getReq.Close = true
    98  	res, err := frontendClient.Do(getReq)
    99  	if err != nil {
   100  		t.Fatalf("Get: %v", err)
   101  	}
   102  	if g, e := res.StatusCode, backendStatus; g != e {
   103  		t.Errorf("got res.StatusCode %d; expected %d", g, e)
   104  	}
   105  	if g, e := res.Header.Get("X-Foo"), "bar"; g != e {
   106  		t.Errorf("got X-Foo %q; expected %q", g, e)
   107  	}
   108  	if c := res.Header.Get(fakeHopHeader); c != "" {
   109  		t.Errorf("got %s header value %q", fakeHopHeader, c)
   110  	}
   111  	if g, e := res.Header.Get("Trailers"), "not a special header field name"; g != e {
   112  		t.Errorf("header Trailers = %q; want %q", g, e)
   113  	}
   114  	if g, e := len(res.Header["X-Multi-Value"]), 2; g != e {
   115  		t.Errorf("got %d X-Multi-Value header values; expected %d", g, e)
   116  	}
   117  	if g, e := len(res.Header["Set-Cookie"]), 1; g != e {
   118  		t.Fatalf("got %d SetCookies, want %d", g, e)
   119  	}
   120  	if g, e := res.Trailer, (http.Header{"X-Trailer": nil}); !reflect.DeepEqual(g, e) {
   121  		t.Errorf("before reading body, Trailer = %#v; want %#v", g, e)
   122  	}
   123  	if cookie := res.Cookies()[0]; cookie.Name != "flavor" {
   124  		t.Errorf("unexpected cookie %q", cookie.Name)
   125  	}
   126  	bodyBytes, _ := ioutil.ReadAll(res.Body)
   127  	if g, e := string(bodyBytes), backendResponse; g != e {
   128  		t.Errorf("got body %q; expected %q", g, e)
   129  	}
   130  	if g, e := res.Trailer.Get("X-Trailer"), "trailer_value"; g != e {
   131  		t.Errorf("Trailer(X-Trailer) = %q ; want %q", g, e)
   132  	}
   133  	if g, e := res.Trailer.Get("X-Unannounced-Trailer"), "unannounced_trailer_value"; g != e {
   134  		t.Errorf("Trailer(X-Unannounced-Trailer) = %q ; want %q", g, e)
   135  	}
   136  
   137  	// Test that a backend failing to be reached or one which doesn't return
   138  	// a response results in a StatusBadGateway.
   139  	getReq, _ = http.NewRequest("GET", frontend.URL+"/?mode=hangup", nil)
   140  	getReq.Close = true
   141  	res, err = frontendClient.Do(getReq)
   142  	if err != nil {
   143  		t.Fatal(err)
   144  	}
   145  	res.Body.Close()
   146  	if res.StatusCode != http.StatusBadGateway {
   147  		t.Errorf("request to bad proxy = %v; want 502 StatusBadGateway", res.Status)
   148  	}
   149  
   150  }
   151  
   152  // Issue 16875: remove any proxied headers mentioned in the "Connection"
   153  // header value.
   154  func TestReverseProxyStripHeadersPresentInConnection(t *testing.T) {
   155  	const fakeConnectionToken = "X-Fake-Connection-Token"
   156  	const backendResponse = "I am the backend"
   157  
   158  	// someConnHeader is some arbitrary header to be declared as a hop-by-hop header
   159  	// in the Request's Connection header.
   160  	const someConnHeader = "X-Some-Conn-Header"
   161  
   162  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   163  		if c := r.Header.Get(fakeConnectionToken); c != "" {
   164  			t.Errorf("handler got header %q = %q; want empty", fakeConnectionToken, c)
   165  		}
   166  		if c := r.Header.Get(someConnHeader); c != "" {
   167  			t.Errorf("handler got header %q = %q; want empty", someConnHeader, c)
   168  		}
   169  		w.Header().Set("Connection", someConnHeader+", "+fakeConnectionToken)
   170  		w.Header().Set(someConnHeader, "should be deleted")
   171  		w.Header().Set(fakeConnectionToken, "should be deleted")
   172  		io.WriteString(w, backendResponse)
   173  	}))
   174  	defer backend.Close()
   175  	backendURL, err := url.Parse(backend.URL)
   176  	if err != nil {
   177  		t.Fatal(err)
   178  	}
   179  	proxyHandler := NewSingleHostReverseProxy(backendURL)
   180  	frontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   181  		proxyHandler.ServeHTTP(w, r)
   182  		if c := r.Header.Get(someConnHeader); c != "original value" {
   183  			t.Errorf("handler modified header %q = %q; want %q", someConnHeader, c, "original value")
   184  		}
   185  	}))
   186  	defer frontend.Close()
   187  
   188  	getReq, _ := http.NewRequest("GET", frontend.URL, nil)
   189  	getReq.Header.Set("Connection", someConnHeader+", "+fakeConnectionToken)
   190  	getReq.Header.Set(someConnHeader, "original value")
   191  	getReq.Header.Set(fakeConnectionToken, "should be deleted")
   192  	res, err := frontend.Client().Do(getReq)
   193  	if err != nil {
   194  		t.Fatalf("Get: %v", err)
   195  	}
   196  	defer res.Body.Close()
   197  	bodyBytes, err := ioutil.ReadAll(res.Body)
   198  	if err != nil {
   199  		t.Fatalf("reading body: %v", err)
   200  	}
   201  	if got, want := string(bodyBytes), backendResponse; got != want {
   202  		t.Errorf("got body %q; want %q", got, want)
   203  	}
   204  	if c := res.Header.Get(someConnHeader); c != "" {
   205  		t.Errorf("handler got header %q = %q; want empty", someConnHeader, c)
   206  	}
   207  	if c := res.Header.Get(fakeConnectionToken); c != "" {
   208  		t.Errorf("handler got header %q = %q; want empty", fakeConnectionToken, c)
   209  	}
   210  }
   211  
   212  func TestXForwardedFor(t *testing.T) {
   213  	const prevForwardedFor = "client ip"
   214  	const backendResponse = "I am the backend"
   215  	const backendStatus = 404
   216  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   217  		if r.Header.Get("X-Forwarded-For") == "" {
   218  			t.Errorf("didn't get X-Forwarded-For header")
   219  		}
   220  		if !strings.Contains(r.Header.Get("X-Forwarded-For"), prevForwardedFor) {
   221  			t.Errorf("X-Forwarded-For didn't contain prior data")
   222  		}
   223  		w.WriteHeader(backendStatus)
   224  		w.Write([]byte(backendResponse))
   225  	}))
   226  	defer backend.Close()
   227  	backendURL, err := url.Parse(backend.URL)
   228  	if err != nil {
   229  		t.Fatal(err)
   230  	}
   231  	proxyHandler := NewSingleHostReverseProxy(backendURL)
   232  	frontend := httptest.NewServer(proxyHandler)
   233  	defer frontend.Close()
   234  
   235  	getReq, _ := http.NewRequest("GET", frontend.URL, nil)
   236  	getReq.Host = "some-name"
   237  	getReq.Header.Set("Connection", "close")
   238  	getReq.Header.Set("X-Forwarded-For", prevForwardedFor)
   239  	getReq.Close = true
   240  	res, err := frontend.Client().Do(getReq)
   241  	if err != nil {
   242  		t.Fatalf("Get: %v", err)
   243  	}
   244  	if g, e := res.StatusCode, backendStatus; g != e {
   245  		t.Errorf("got res.StatusCode %d; expected %d", g, e)
   246  	}
   247  	bodyBytes, _ := ioutil.ReadAll(res.Body)
   248  	if g, e := string(bodyBytes), backendResponse; g != e {
   249  		t.Errorf("got body %q; expected %q", g, e)
   250  	}
   251  }
   252  
   253  var proxyQueryTests = []struct {
   254  	baseSuffix string // suffix to add to backend URL
   255  	reqSuffix  string // suffix to add to frontend's request URL
   256  	want       string // what backend should see for final request URL (without ?)
   257  }{
   258  	{"", "", ""},
   259  	{"?sta=tic", "?us=er", "sta=tic&us=er"},
   260  	{"", "?us=er", "us=er"},
   261  	{"?sta=tic", "", "sta=tic"},
   262  }
   263  
   264  func TestReverseProxyQuery(t *testing.T) {
   265  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   266  		w.Header().Set("X-Got-Query", r.URL.RawQuery)
   267  		w.Write([]byte("hi"))
   268  	}))
   269  	defer backend.Close()
   270  
   271  	for i, tt := range proxyQueryTests {
   272  		backendURL, err := url.Parse(backend.URL + tt.baseSuffix)
   273  		if err != nil {
   274  			t.Fatal(err)
   275  		}
   276  		frontend := httptest.NewServer(NewSingleHostReverseProxy(backendURL))
   277  		req, _ := http.NewRequest("GET", frontend.URL+tt.reqSuffix, nil)
   278  		req.Close = true
   279  		res, err := frontend.Client().Do(req)
   280  		if err != nil {
   281  			t.Fatalf("%d. Get: %v", i, err)
   282  		}
   283  		if g, e := res.Header.Get("X-Got-Query"), tt.want; g != e {
   284  			t.Errorf("%d. got query %q; expected %q", i, g, e)
   285  		}
   286  		res.Body.Close()
   287  		frontend.Close()
   288  	}
   289  }
   290  
   291  func TestReverseProxyFlushInterval(t *testing.T) {
   292  	const expected = "hi"
   293  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   294  		w.Write([]byte(expected))
   295  	}))
   296  	defer backend.Close()
   297  
   298  	backendURL, err := url.Parse(backend.URL)
   299  	if err != nil {
   300  		t.Fatal(err)
   301  	}
   302  
   303  	proxyHandler := NewSingleHostReverseProxy(backendURL)
   304  	proxyHandler.FlushInterval = time.Microsecond
   305  
   306  	frontend := httptest.NewServer(proxyHandler)
   307  	defer frontend.Close()
   308  
   309  	req, _ := http.NewRequest("GET", frontend.URL, nil)
   310  	req.Close = true
   311  	res, err := frontend.Client().Do(req)
   312  	if err != nil {
   313  		t.Fatalf("Get: %v", err)
   314  	}
   315  	defer res.Body.Close()
   316  	if bodyBytes, _ := ioutil.ReadAll(res.Body); string(bodyBytes) != expected {
   317  		t.Errorf("got body %q; expected %q", bodyBytes, expected)
   318  	}
   319  }
   320  
   321  func TestReverseProxyFlushIntervalHeaders(t *testing.T) {
   322  	const expected = "hi"
   323  	stopCh := make(chan struct{})
   324  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   325  		w.Header().Add("MyHeader", expected)
   326  		w.WriteHeader(200)
   327  		w.(http.Flusher).Flush()
   328  		<-stopCh
   329  	}))
   330  	defer backend.Close()
   331  	defer close(stopCh)
   332  
   333  	backendURL, err := url.Parse(backend.URL)
   334  	if err != nil {
   335  		t.Fatal(err)
   336  	}
   337  
   338  	proxyHandler := NewSingleHostReverseProxy(backendURL)
   339  	proxyHandler.FlushInterval = time.Microsecond
   340  
   341  	frontend := httptest.NewServer(proxyHandler)
   342  	defer frontend.Close()
   343  
   344  	req, _ := http.NewRequest("GET", frontend.URL, nil)
   345  	req.Close = true
   346  
   347  	ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
   348  	defer cancel()
   349  	req = req.WithContext(ctx)
   350  
   351  	res, err := frontend.Client().Do(req)
   352  	if err != nil {
   353  		t.Fatalf("Get: %v", err)
   354  	}
   355  	defer res.Body.Close()
   356  
   357  	if res.Header.Get("MyHeader") != expected {
   358  		t.Errorf("got header %q; expected %q", res.Header.Get("MyHeader"), expected)
   359  	}
   360  }
   361  
   362  func TestReverseProxyCancelation(t *testing.T) {
   363  	const backendResponse = "I am the backend"
   364  
   365  	reqInFlight := make(chan struct{})
   366  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   367  		close(reqInFlight) // cause the client to cancel its request
   368  
   369  		select {
   370  		case <-time.After(10 * time.Second):
   371  			// Note: this should only happen in broken implementations, and the
   372  			// closenotify case should be instantaneous.
   373  			t.Error("Handler never saw CloseNotify")
   374  			return
   375  		case <-w.(http.CloseNotifier).CloseNotify():
   376  		}
   377  
   378  		w.WriteHeader(http.StatusOK)
   379  		w.Write([]byte(backendResponse))
   380  	}))
   381  
   382  	defer backend.Close()
   383  
   384  	backend.Config.ErrorLog = log.New(ioutil.Discard, "", 0)
   385  
   386  	backendURL, err := url.Parse(backend.URL)
   387  	if err != nil {
   388  		t.Fatal(err)
   389  	}
   390  
   391  	proxyHandler := NewSingleHostReverseProxy(backendURL)
   392  
   393  	// Discards errors of the form:
   394  	// http: proxy error: read tcp 127.0.0.1:44643: use of closed network connection
   395  	proxyHandler.ErrorLog = log.New(ioutil.Discard, "", 0)
   396  
   397  	frontend := httptest.NewServer(proxyHandler)
   398  	defer frontend.Close()
   399  	frontendClient := frontend.Client()
   400  
   401  	getReq, _ := http.NewRequest("GET", frontend.URL, nil)
   402  	go func() {
   403  		<-reqInFlight
   404  		frontendClient.Transport.(*http.Transport).CancelRequest(getReq)
   405  	}()
   406  	res, err := frontendClient.Do(getReq)
   407  	if res != nil {
   408  		t.Errorf("got response %v; want nil", res.Status)
   409  	}
   410  	if err == nil {
   411  		// This should be an error like:
   412  		// Get http://127.0.0.1:58079: read tcp 127.0.0.1:58079:
   413  		//    use of closed network connection
   414  		t.Error("Server.Client().Do() returned nil error; want non-nil error")
   415  	}
   416  }
   417  
   418  func req(t *testing.T, v string) *http.Request {
   419  	req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(v)))
   420  	if err != nil {
   421  		t.Fatal(err)
   422  	}
   423  	return req
   424  }
   425  
   426  // Issue 12344
   427  func TestNilBody(t *testing.T) {
   428  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   429  		w.Write([]byte("hi"))
   430  	}))
   431  	defer backend.Close()
   432  
   433  	frontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
   434  		backURL, _ := url.Parse(backend.URL)
   435  		rp := NewSingleHostReverseProxy(backURL)
   436  		r := req(t, "GET / HTTP/1.0\r\n\r\n")
   437  		r.Body = nil // this accidentally worked in Go 1.4 and below, so keep it working
   438  		rp.ServeHTTP(w, r)
   439  	}))
   440  	defer frontend.Close()
   441  
   442  	res, err := http.Get(frontend.URL)
   443  	if err != nil {
   444  		t.Fatal(err)
   445  	}
   446  	defer res.Body.Close()
   447  	slurp, err := ioutil.ReadAll(res.Body)
   448  	if err != nil {
   449  		t.Fatal(err)
   450  	}
   451  	if string(slurp) != "hi" {
   452  		t.Errorf("Got %q; want %q", slurp, "hi")
   453  	}
   454  }
   455  
   456  // Issue 15524
   457  func TestUserAgentHeader(t *testing.T) {
   458  	const explicitUA = "explicit UA"
   459  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   460  		if r.URL.Path == "/noua" {
   461  			if c := r.Header.Get("User-Agent"); c != "" {
   462  				t.Errorf("handler got non-empty User-Agent header %q", c)
   463  			}
   464  			return
   465  		}
   466  		if c := r.Header.Get("User-Agent"); c != explicitUA {
   467  			t.Errorf("handler got unexpected User-Agent header %q", c)
   468  		}
   469  	}))
   470  	defer backend.Close()
   471  	backendURL, err := url.Parse(backend.URL)
   472  	if err != nil {
   473  		t.Fatal(err)
   474  	}
   475  	proxyHandler := NewSingleHostReverseProxy(backendURL)
   476  	proxyHandler.ErrorLog = log.New(ioutil.Discard, "", 0) // quiet for tests
   477  	frontend := httptest.NewServer(proxyHandler)
   478  	defer frontend.Close()
   479  	frontendClient := frontend.Client()
   480  
   481  	getReq, _ := http.NewRequest("GET", frontend.URL, nil)
   482  	getReq.Header.Set("User-Agent", explicitUA)
   483  	getReq.Close = true
   484  	res, err := frontendClient.Do(getReq)
   485  	if err != nil {
   486  		t.Fatalf("Get: %v", err)
   487  	}
   488  	res.Body.Close()
   489  
   490  	getReq, _ = http.NewRequest("GET", frontend.URL+"/noua", nil)
   491  	getReq.Header.Set("User-Agent", "")
   492  	getReq.Close = true
   493  	res, err = frontendClient.Do(getReq)
   494  	if err != nil {
   495  		t.Fatalf("Get: %v", err)
   496  	}
   497  	res.Body.Close()
   498  }
   499  
   500  type bufferPool struct {
   501  	get func() []byte
   502  	put func([]byte)
   503  }
   504  
   505  func (bp bufferPool) Get() []byte  { return bp.get() }
   506  func (bp bufferPool) Put(v []byte) { bp.put(v) }
   507  
   508  func TestReverseProxyGetPutBuffer(t *testing.T) {
   509  	const msg = "hi"
   510  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   511  		io.WriteString(w, msg)
   512  	}))
   513  	defer backend.Close()
   514  
   515  	backendURL, err := url.Parse(backend.URL)
   516  	if err != nil {
   517  		t.Fatal(err)
   518  	}
   519  
   520  	var (
   521  		mu  sync.Mutex
   522  		log []string
   523  	)
   524  	addLog := func(event string) {
   525  		mu.Lock()
   526  		defer mu.Unlock()
   527  		log = append(log, event)
   528  	}
   529  	rp := NewSingleHostReverseProxy(backendURL)
   530  	const size = 1234
   531  	rp.BufferPool = bufferPool{
   532  		get: func() []byte {
   533  			addLog("getBuf")
   534  			return make([]byte, size)
   535  		},
   536  		put: func(p []byte) {
   537  			addLog("putBuf-" + strconv.Itoa(len(p)))
   538  		},
   539  	}
   540  	frontend := httptest.NewServer(rp)
   541  	defer frontend.Close()
   542  
   543  	req, _ := http.NewRequest("GET", frontend.URL, nil)
   544  	req.Close = true
   545  	res, err := frontend.Client().Do(req)
   546  	if err != nil {
   547  		t.Fatalf("Get: %v", err)
   548  	}
   549  	slurp, err := ioutil.ReadAll(res.Body)
   550  	res.Body.Close()
   551  	if err != nil {
   552  		t.Fatalf("reading body: %v", err)
   553  	}
   554  	if string(slurp) != msg {
   555  		t.Errorf("msg = %q; want %q", slurp, msg)
   556  	}
   557  	wantLog := []string{"getBuf", "putBuf-" + strconv.Itoa(size)}
   558  	mu.Lock()
   559  	defer mu.Unlock()
   560  	if !reflect.DeepEqual(log, wantLog) {
   561  		t.Errorf("Log events = %q; want %q", log, wantLog)
   562  	}
   563  }
   564  
   565  func TestReverseProxy_Post(t *testing.T) {
   566  	const backendResponse = "I am the backend"
   567  	const backendStatus = 200
   568  	var requestBody = bytes.Repeat([]byte("a"), 1<<20)
   569  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   570  		slurp, err := ioutil.ReadAll(r.Body)
   571  		if err != nil {
   572  			t.Errorf("Backend body read = %v", err)
   573  		}
   574  		if len(slurp) != len(requestBody) {
   575  			t.Errorf("Backend read %d request body bytes; want %d", len(slurp), len(requestBody))
   576  		}
   577  		if !bytes.Equal(slurp, requestBody) {
   578  			t.Error("Backend read wrong request body.") // 1MB; omitting details
   579  		}
   580  		w.Write([]byte(backendResponse))
   581  	}))
   582  	defer backend.Close()
   583  	backendURL, err := url.Parse(backend.URL)
   584  	if err != nil {
   585  		t.Fatal(err)
   586  	}
   587  	proxyHandler := NewSingleHostReverseProxy(backendURL)
   588  	frontend := httptest.NewServer(proxyHandler)
   589  	defer frontend.Close()
   590  
   591  	postReq, _ := http.NewRequest("POST", frontend.URL, bytes.NewReader(requestBody))
   592  	res, err := frontend.Client().Do(postReq)
   593  	if err != nil {
   594  		t.Fatalf("Do: %v", err)
   595  	}
   596  	if g, e := res.StatusCode, backendStatus; g != e {
   597  		t.Errorf("got res.StatusCode %d; expected %d", g, e)
   598  	}
   599  	bodyBytes, _ := ioutil.ReadAll(res.Body)
   600  	if g, e := string(bodyBytes), backendResponse; g != e {
   601  		t.Errorf("got body %q; expected %q", g, e)
   602  	}
   603  }
   604  
   605  type RoundTripperFunc func(*http.Request) (*http.Response, error)
   606  
   607  func (fn RoundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
   608  	return fn(req)
   609  }
   610  
   611  // Issue 16036: send a Request with a nil Body when possible
   612  func TestReverseProxy_NilBody(t *testing.T) {
   613  	backendURL, _ := url.Parse("http://fake.tld/")
   614  	proxyHandler := NewSingleHostReverseProxy(backendURL)
   615  	proxyHandler.ErrorLog = log.New(ioutil.Discard, "", 0) // quiet for tests
   616  	proxyHandler.Transport = RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
   617  		if req.Body != nil {
   618  			t.Error("Body != nil; want a nil Body")
   619  		}
   620  		return nil, errors.New("done testing the interesting part; so force a 502 Gateway error")
   621  	})
   622  	frontend := httptest.NewServer(proxyHandler)
   623  	defer frontend.Close()
   624  
   625  	res, err := frontend.Client().Get(frontend.URL)
   626  	if err != nil {
   627  		t.Fatal(err)
   628  	}
   629  	defer res.Body.Close()
   630  	if res.StatusCode != 502 {
   631  		t.Errorf("status code = %v; want 502 (Gateway Error)", res.Status)
   632  	}
   633  }
   634  
   635  // Issue 14237. Test ModifyResponse and that an error from it
   636  // causes the proxy to return StatusBadGateway, or StatusOK otherwise.
   637  func TestReverseProxyModifyResponse(t *testing.T) {
   638  	backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   639  		w.Header().Add("X-Hit-Mod", fmt.Sprintf("%v", r.URL.Path == "/mod"))
   640  	}))
   641  	defer backendServer.Close()
   642  
   643  	rpURL, _ := url.Parse(backendServer.URL)
   644  	rproxy := NewSingleHostReverseProxy(rpURL)
   645  	rproxy.ErrorLog = log.New(ioutil.Discard, "", 0) // quiet for tests
   646  	rproxy.ModifyResponse = func(resp *http.Response) error {
   647  		if resp.Header.Get("X-Hit-Mod") != "true" {
   648  			return fmt.Errorf("tried to by-pass proxy")
   649  		}
   650  		return nil
   651  	}
   652  
   653  	frontendProxy := httptest.NewServer(rproxy)
   654  	defer frontendProxy.Close()
   655  
   656  	tests := []struct {
   657  		url      string
   658  		wantCode int
   659  	}{
   660  		{frontendProxy.URL + "/mod", http.StatusOK},
   661  		{frontendProxy.URL + "/schedule", http.StatusBadGateway},
   662  	}
   663  
   664  	for i, tt := range tests {
   665  		resp, err := http.Get(tt.url)
   666  		if err != nil {
   667  			t.Fatalf("failed to reach proxy: %v", err)
   668  		}
   669  		if g, e := resp.StatusCode, tt.wantCode; g != e {
   670  			t.Errorf("#%d: got res.StatusCode %d; expected %d", i, g, e)
   671  		}
   672  		resp.Body.Close()
   673  	}
   674  }
   675  
   676  type failingRoundTripper struct{}
   677  
   678  func (failingRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
   679  	return nil, errors.New("some error")
   680  }
   681  
   682  type staticResponseRoundTripper struct{ res *http.Response }
   683  
   684  func (rt staticResponseRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
   685  	return rt.res, nil
   686  }
   687  
   688  func TestReverseProxyErrorHandler(t *testing.T) {
   689  	tests := []struct {
   690  		name           string
   691  		wantCode       int
   692  		errorHandler   func(http.ResponseWriter, *http.Request, error)
   693  		transport      http.RoundTripper // defaults to failingRoundTripper
   694  		modifyResponse func(*http.Response) error
   695  	}{
   696  		{
   697  			name:     "default",
   698  			wantCode: http.StatusBadGateway,
   699  		},
   700  		{
   701  			name:         "errorhandler",
   702  			wantCode:     http.StatusTeapot,
   703  			errorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { rw.WriteHeader(http.StatusTeapot) },
   704  		},
   705  		{
   706  			name: "modifyresponse_noerr",
   707  			transport: staticResponseRoundTripper{
   708  				&http.Response{StatusCode: 345, Body: http.NoBody},
   709  			},
   710  			modifyResponse: func(res *http.Response) error {
   711  				res.StatusCode++
   712  				return nil
   713  			},
   714  			errorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { rw.WriteHeader(http.StatusTeapot) },
   715  			wantCode:     346,
   716  		},
   717  		{
   718  			name: "modifyresponse_err",
   719  			transport: staticResponseRoundTripper{
   720  				&http.Response{StatusCode: 345, Body: http.NoBody},
   721  			},
   722  			modifyResponse: func(res *http.Response) error {
   723  				res.StatusCode++
   724  				return errors.New("some error to trigger errorHandler")
   725  			},
   726  			errorHandler: func(rw http.ResponseWriter, req *http.Request, err error) { rw.WriteHeader(http.StatusTeapot) },
   727  			wantCode:     http.StatusTeapot,
   728  		},
   729  	}
   730  
   731  	for _, tt := range tests {
   732  		t.Run(tt.name, func(t *testing.T) {
   733  			target := &url.URL{
   734  				Scheme: "http",
   735  				Host:   "dummy.tld",
   736  				Path:   "/",
   737  			}
   738  			rproxy := NewSingleHostReverseProxy(target)
   739  			rproxy.Transport = tt.transport
   740  			rproxy.ModifyResponse = tt.modifyResponse
   741  			if rproxy.Transport == nil {
   742  				rproxy.Transport = failingRoundTripper{}
   743  			}
   744  			rproxy.ErrorLog = log.New(ioutil.Discard, "", 0) // quiet for tests
   745  			if tt.errorHandler != nil {
   746  				rproxy.ErrorHandler = tt.errorHandler
   747  			}
   748  			frontendProxy := httptest.NewServer(rproxy)
   749  			defer frontendProxy.Close()
   750  
   751  			resp, err := http.Get(frontendProxy.URL + "/test")
   752  			if err != nil {
   753  				t.Fatalf("failed to reach proxy: %v", err)
   754  			}
   755  			if g, e := resp.StatusCode, tt.wantCode; g != e {
   756  				t.Errorf("got res.StatusCode %d; expected %d", g, e)
   757  			}
   758  			resp.Body.Close()
   759  		})
   760  	}
   761  }
   762  
   763  // Issue 16659: log errors from short read
   764  func TestReverseProxy_CopyBuffer(t *testing.T) {
   765  	backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   766  		out := "this call was relayed by the reverse proxy"
   767  		// Coerce a wrong content length to induce io.UnexpectedEOF
   768  		w.Header().Set("Content-Length", fmt.Sprintf("%d", len(out)*2))
   769  		fmt.Fprintln(w, out)
   770  	}))
   771  	defer backendServer.Close()
   772  
   773  	rpURL, err := url.Parse(backendServer.URL)
   774  	if err != nil {
   775  		t.Fatal(err)
   776  	}
   777  
   778  	var proxyLog bytes.Buffer
   779  	rproxy := NewSingleHostReverseProxy(rpURL)
   780  	rproxy.ErrorLog = log.New(&proxyLog, "", log.Lshortfile)
   781  	donec := make(chan bool, 1)
   782  	frontendProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   783  		defer func() { donec <- true }()
   784  		rproxy.ServeHTTP(w, r)
   785  	}))
   786  	defer frontendProxy.Close()
   787  
   788  	if _, err = frontendProxy.Client().Get(frontendProxy.URL); err == nil {
   789  		t.Fatalf("want non-nil error")
   790  	}
   791  	// The race detector complains about the proxyLog usage in logf in copyBuffer
   792  	// and our usage below with proxyLog.Bytes() so we're explicitly using a
   793  	// channel to ensure that the ReverseProxy's ServeHTTP is done before we
   794  	// continue after Get.
   795  	<-donec
   796  
   797  	expected := []string{
   798  		"EOF",
   799  		"read",
   800  	}
   801  	for _, phrase := range expected {
   802  		if !bytes.Contains(proxyLog.Bytes(), []byte(phrase)) {
   803  			t.Errorf("expected log to contain phrase %q", phrase)
   804  		}
   805  	}
   806  }
   807  
   808  type staticTransport struct {
   809  	res *http.Response
   810  }
   811  
   812  func (t *staticTransport) RoundTrip(r *http.Request) (*http.Response, error) {
   813  	return t.res, nil
   814  }
   815  
   816  func BenchmarkServeHTTP(b *testing.B) {
   817  	res := &http.Response{
   818  		StatusCode: 200,
   819  		Body:       ioutil.NopCloser(strings.NewReader("")),
   820  	}
   821  	proxy := &ReverseProxy{
   822  		Director:  func(*http.Request) {},
   823  		Transport: &staticTransport{res},
   824  	}
   825  
   826  	w := httptest.NewRecorder()
   827  	r := httptest.NewRequest("GET", "/", nil)
   828  
   829  	b.ReportAllocs()
   830  	for i := 0; i < b.N; i++ {
   831  		proxy.ServeHTTP(w, r)
   832  	}
   833  }
   834  
   835  func TestServeHTTPDeepCopy(t *testing.T) {
   836  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   837  		w.Write([]byte("Hello Gopher!"))
   838  	}))
   839  	defer backend.Close()
   840  	backendURL, err := url.Parse(backend.URL)
   841  	if err != nil {
   842  		t.Fatal(err)
   843  	}
   844  
   845  	type result struct {
   846  		before, after string
   847  	}
   848  
   849  	resultChan := make(chan result, 1)
   850  	proxyHandler := NewSingleHostReverseProxy(backendURL)
   851  	frontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   852  		before := r.URL.String()
   853  		proxyHandler.ServeHTTP(w, r)
   854  		after := r.URL.String()
   855  		resultChan <- result{before: before, after: after}
   856  	}))
   857  	defer frontend.Close()
   858  
   859  	want := result{before: "/", after: "/"}
   860  
   861  	res, err := frontend.Client().Get(frontend.URL)
   862  	if err != nil {
   863  		t.Fatalf("Do: %v", err)
   864  	}
   865  	res.Body.Close()
   866  
   867  	got := <-resultChan
   868  	if got != want {
   869  		t.Errorf("got = %+v; want = %+v", got, want)
   870  	}
   871  }
   872  
   873  // Issue 18327: verify we always do a deep copy of the Request.Header map
   874  // before any mutations.
   875  func TestClonesRequestHeaders(t *testing.T) {
   876  	log.SetOutput(ioutil.Discard)
   877  	defer log.SetOutput(os.Stderr)
   878  	req, _ := http.NewRequest("GET", "http://foo.tld/", nil)
   879  	req.RemoteAddr = "1.2.3.4:56789"
   880  	rp := &ReverseProxy{
   881  		Director: func(req *http.Request) {
   882  			req.Header.Set("From-Director", "1")
   883  		},
   884  		Transport: roundTripperFunc(func(req *http.Request) (*http.Response, error) {
   885  			if v := req.Header.Get("From-Director"); v != "1" {
   886  				t.Errorf("From-Directory value = %q; want 1", v)
   887  			}
   888  			return nil, io.EOF
   889  		}),
   890  	}
   891  	rp.ServeHTTP(httptest.NewRecorder(), req)
   892  
   893  	if req.Header.Get("From-Director") == "1" {
   894  		t.Error("Director header mutation modified caller's request")
   895  	}
   896  	if req.Header.Get("X-Forwarded-For") != "" {
   897  		t.Error("X-Forward-For header mutation modified caller's request")
   898  	}
   899  
   900  }
   901  
   902  type roundTripperFunc func(req *http.Request) (*http.Response, error)
   903  
   904  func (fn roundTripperFunc) RoundTrip(req *http.Request) (*http.Response, error) {
   905  	return fn(req)
   906  }
   907  
   908  func TestModifyResponseClosesBody(t *testing.T) {
   909  	req, _ := http.NewRequest("GET", "http://foo.tld/", nil)
   910  	req.RemoteAddr = "1.2.3.4:56789"
   911  	closeCheck := new(checkCloser)
   912  	logBuf := new(bytes.Buffer)
   913  	outErr := errors.New("ModifyResponse error")
   914  	rp := &ReverseProxy{
   915  		Director: func(req *http.Request) {},
   916  		Transport: &staticTransport{&http.Response{
   917  			StatusCode: 200,
   918  			Body:       closeCheck,
   919  		}},
   920  		ErrorLog: log.New(logBuf, "", 0),
   921  		ModifyResponse: func(*http.Response) error {
   922  			return outErr
   923  		},
   924  	}
   925  	rec := httptest.NewRecorder()
   926  	rp.ServeHTTP(rec, req)
   927  	res := rec.Result()
   928  	if g, e := res.StatusCode, http.StatusBadGateway; g != e {
   929  		t.Errorf("got res.StatusCode %d; expected %d", g, e)
   930  	}
   931  	if !closeCheck.closed {
   932  		t.Errorf("body should have been closed")
   933  	}
   934  	if g, e := logBuf.String(), outErr.Error(); !strings.Contains(g, e) {
   935  		t.Errorf("ErrorLog %q does not contain %q", g, e)
   936  	}
   937  }
   938  
   939  type checkCloser struct {
   940  	closed bool
   941  }
   942  
   943  func (cc *checkCloser) Close() error {
   944  	cc.closed = true
   945  	return nil
   946  }
   947  
   948  func (cc *checkCloser) Read(b []byte) (int, error) {
   949  	return len(b), nil
   950  }
   951  
   952  // Issue 23643: panic on body copy error
   953  func TestReverseProxy_PanicBodyError(t *testing.T) {
   954  	log.SetOutput(ioutil.Discard)
   955  	defer log.SetOutput(os.Stderr)
   956  	backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   957  		out := "this call was relayed by the reverse proxy"
   958  		// Coerce a wrong content length to induce io.ErrUnexpectedEOF
   959  		w.Header().Set("Content-Length", fmt.Sprintf("%d", len(out)*2))
   960  		fmt.Fprintln(w, out)
   961  	}))
   962  	defer backendServer.Close()
   963  
   964  	rpURL, err := url.Parse(backendServer.URL)
   965  	if err != nil {
   966  		t.Fatal(err)
   967  	}
   968  
   969  	rproxy := NewSingleHostReverseProxy(rpURL)
   970  
   971  	// Ensure that the handler panics when the body read encounters an
   972  	// io.ErrUnexpectedEOF
   973  	defer func() {
   974  		err := recover()
   975  		if err == nil {
   976  			t.Fatal("handler should have panicked")
   977  		}
   978  		if err != http.ErrAbortHandler {
   979  			t.Fatal("expected ErrAbortHandler, got", err)
   980  		}
   981  	}()
   982  	req, _ := http.NewRequest("GET", "http://foo.tld/", nil)
   983  	rproxy.ServeHTTP(httptest.NewRecorder(), req)
   984  }
   985  
   986  func TestSelectFlushInterval(t *testing.T) {
   987  	tests := []struct {
   988  		name string
   989  		p    *ReverseProxy
   990  		req  *http.Request
   991  		res  *http.Response
   992  		want time.Duration
   993  	}{
   994  		{
   995  			name: "default",
   996  			res:  &http.Response{},
   997  			p:    &ReverseProxy{FlushInterval: 123},
   998  			want: 123,
   999  		},
  1000  		{
  1001  			name: "server-sent events overrides non-zero",
  1002  			res: &http.Response{
  1003  				Header: http.Header{
  1004  					"Content-Type": {"text/event-stream"},
  1005  				},
  1006  			},
  1007  			p:    &ReverseProxy{FlushInterval: 123},
  1008  			want: -1,
  1009  		},
  1010  		{
  1011  			name: "server-sent events overrides zero",
  1012  			res: &http.Response{
  1013  				Header: http.Header{
  1014  					"Content-Type": {"text/event-stream"},
  1015  				},
  1016  			},
  1017  			p:    &ReverseProxy{FlushInterval: 0},
  1018  			want: -1,
  1019  		},
  1020  	}
  1021  	for _, tt := range tests {
  1022  		t.Run(tt.name, func(t *testing.T) {
  1023  			got := tt.p.flushInterval(tt.req, tt.res)
  1024  			if got != tt.want {
  1025  				t.Errorf("flushLatency = %v; want %v", got, tt.want)
  1026  			}
  1027  		})
  1028  	}
  1029  }
  1030  
  1031  func TestReverseProxyWebSocket(t *testing.T) {
  1032  	backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1033  		if upgradeType(r.Header) != "websocket" {
  1034  			t.Error("unexpected backend request")
  1035  			http.Error(w, "unexpected request", 400)
  1036  			return
  1037  		}
  1038  		c, _, err := w.(http.Hijacker).Hijack()
  1039  		if err != nil {
  1040  			t.Error(err)
  1041  			return
  1042  		}
  1043  		defer c.Close()
  1044  		io.WriteString(c, "HTTP/1.1 101 Switching Protocols\r\nConnection: upgrade\r\nUpgrade: WebSocket\r\n\r\n")
  1045  		bs := bufio.NewScanner(c)
  1046  		if !bs.Scan() {
  1047  			t.Errorf("backend failed to read line from client: %v", bs.Err())
  1048  			return
  1049  		}
  1050  		fmt.Fprintf(c, "backend got %q\n", bs.Text())
  1051  	}))
  1052  	defer backendServer.Close()
  1053  
  1054  	backURL, _ := url.Parse(backendServer.URL)
  1055  	rproxy := NewSingleHostReverseProxy(backURL)
  1056  	rproxy.ErrorLog = log.New(ioutil.Discard, "", 0) // quiet for tests
  1057  	rproxy.ModifyResponse = func(res *http.Response) error {
  1058  		res.Header.Add("X-Modified", "true")
  1059  		return nil
  1060  	}
  1061  
  1062  	handler := http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  1063  		rw.Header().Set("X-Header", "X-Value")
  1064  		rproxy.ServeHTTP(rw, req)
  1065  	})
  1066  
  1067  	frontendProxy := httptest.NewServer(handler)
  1068  	defer frontendProxy.Close()
  1069  
  1070  	req, _ := http.NewRequest("GET", frontendProxy.URL, nil)
  1071  	req.Header.Set("Connection", "Upgrade")
  1072  	req.Header.Set("Upgrade", "websocket")
  1073  
  1074  	c := frontendProxy.Client()
  1075  	res, err := c.Do(req)
  1076  	if err != nil {
  1077  		t.Fatal(err)
  1078  	}
  1079  	if res.StatusCode != 101 {
  1080  		t.Fatalf("status = %v; want 101", res.Status)
  1081  	}
  1082  
  1083  	got := res.Header.Get("X-Header")
  1084  	want := "X-Value"
  1085  	if got != want {
  1086  		t.Errorf("Header(XHeader) = %q; want %q", got, want)
  1087  	}
  1088  
  1089  	if upgradeType(res.Header) != "websocket" {
  1090  		t.Fatalf("not websocket upgrade; got %#v", res.Header)
  1091  	}
  1092  	rwc, ok := res.Body.(io.ReadWriteCloser)
  1093  	if !ok {
  1094  		t.Fatalf("response body is of type %T; does not implement ReadWriteCloser", res.Body)
  1095  	}
  1096  	defer rwc.Close()
  1097  
  1098  	if got, want := res.Header.Get("X-Modified"), "true"; got != want {
  1099  		t.Errorf("response X-Modified header = %q; want %q", got, want)
  1100  	}
  1101  
  1102  	io.WriteString(rwc, "Hello\n")
  1103  	bs := bufio.NewScanner(rwc)
  1104  	if !bs.Scan() {
  1105  		t.Fatalf("Scan: %v", bs.Err())
  1106  	}
  1107  	got = bs.Text()
  1108  	want = `backend got "Hello"`
  1109  	if got != want {
  1110  		t.Errorf("got %#q, want %#q", got, want)
  1111  	}
  1112  }
  1113  
  1114  func TestUnannouncedTrailer(t *testing.T) {
  1115  	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  1116  		w.WriteHeader(http.StatusOK)
  1117  		w.(http.Flusher).Flush()
  1118  		w.Header().Set(http.TrailerPrefix+"X-Unannounced-Trailer", "unannounced_trailer_value")
  1119  	}))
  1120  	defer backend.Close()
  1121  	backendURL, err := url.Parse(backend.URL)
  1122  	if err != nil {
  1123  		t.Fatal(err)
  1124  	}
  1125  	proxyHandler := NewSingleHostReverseProxy(backendURL)
  1126  	proxyHandler.ErrorLog = log.New(ioutil.Discard, "", 0) // quiet for tests
  1127  	frontend := httptest.NewServer(proxyHandler)
  1128  	defer frontend.Close()
  1129  	frontendClient := frontend.Client()
  1130  
  1131  	res, err := frontendClient.Get(frontend.URL)
  1132  	if err != nil {
  1133  		t.Fatalf("Get: %v", err)
  1134  	}
  1135  
  1136  	ioutil.ReadAll(res.Body)
  1137  
  1138  	if g, w := res.Trailer.Get("X-Unannounced-Trailer"), "unannounced_trailer_value"; g != w {
  1139  		t.Errorf("Trailer(X-Unannounced-Trailer) = %q; want %q", g, w)
  1140  	}
  1141  
  1142  }
  1143  
  1144  func TestSingleJoinSlash(t *testing.T) {
  1145  	tests := []struct {
  1146  		slasha   string
  1147  		slashb   string
  1148  		expected string
  1149  	}{
  1150  		{"https://www.google.com/", "/favicon.ico", "https://www.google.com/favicon.ico"},
  1151  		{"https://www.google.com", "/favicon.ico", "https://www.google.com/favicon.ico"},
  1152  		{"https://www.google.com", "favicon.ico", "https://www.google.com/favicon.ico"},
  1153  		{"https://www.google.com", "", "https://www.google.com/"},
  1154  		{"", "favicon.ico", "/favicon.ico"},
  1155  	}
  1156  	for _, tt := range tests {
  1157  		if got := singleJoiningSlash(tt.slasha, tt.slashb); got != tt.expected {
  1158  			t.Errorf("singleJoiningSlash(%s,%s) want %s got %s",
  1159  				tt.slasha,
  1160  				tt.slashb,
  1161  				tt.expected,
  1162  				got)
  1163  		}
  1164  	}
  1165  }
  1166  

View as plain text