github.com/dannin/go@v0.0.0-20161031215817-d35dfd405eaa/src/net/http/requestwrite_test.go (about)

     1  // Copyright 2010 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  package http
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"net/url"
    14  	"strings"
    15  	"testing"
    16  )
    17  
    18  type reqWriteTest struct {
    19  	Req  Request
    20  	Body interface{} // optional []byte or func() io.ReadCloser to populate Req.Body
    21  
    22  	// Any of these three may be empty to skip that test.
    23  	WantWrite string // Request.Write
    24  	WantProxy string // Request.WriteProxy
    25  
    26  	WantError error // wanted error from Request.Write
    27  }
    28  
    29  var reqWriteTests = []reqWriteTest{
    30  	// HTTP/1.1 => chunked coding; no body; no trailer
    31  	0: {
    32  		Req: Request{
    33  			Method: "GET",
    34  			URL: &url.URL{
    35  				Scheme: "http",
    36  				Host:   "www.techcrunch.com",
    37  				Path:   "/",
    38  			},
    39  			Proto:      "HTTP/1.1",
    40  			ProtoMajor: 1,
    41  			ProtoMinor: 1,
    42  			Header: Header{
    43  				"Accept":           {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
    44  				"Accept-Charset":   {"ISO-8859-1,utf-8;q=0.7,*;q=0.7"},
    45  				"Accept-Encoding":  {"gzip,deflate"},
    46  				"Accept-Language":  {"en-us,en;q=0.5"},
    47  				"Keep-Alive":       {"300"},
    48  				"Proxy-Connection": {"keep-alive"},
    49  				"User-Agent":       {"Fake"},
    50  			},
    51  			Body:  nil,
    52  			Close: false,
    53  			Host:  "www.techcrunch.com",
    54  			Form:  map[string][]string{},
    55  		},
    56  
    57  		WantWrite: "GET / HTTP/1.1\r\n" +
    58  			"Host: www.techcrunch.com\r\n" +
    59  			"User-Agent: Fake\r\n" +
    60  			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
    61  			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
    62  			"Accept-Encoding: gzip,deflate\r\n" +
    63  			"Accept-Language: en-us,en;q=0.5\r\n" +
    64  			"Keep-Alive: 300\r\n" +
    65  			"Proxy-Connection: keep-alive\r\n\r\n",
    66  
    67  		WantProxy: "GET http://www.techcrunch.com/ HTTP/1.1\r\n" +
    68  			"Host: www.techcrunch.com\r\n" +
    69  			"User-Agent: Fake\r\n" +
    70  			"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" +
    71  			"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\r\n" +
    72  			"Accept-Encoding: gzip,deflate\r\n" +
    73  			"Accept-Language: en-us,en;q=0.5\r\n" +
    74  			"Keep-Alive: 300\r\n" +
    75  			"Proxy-Connection: keep-alive\r\n\r\n",
    76  	},
    77  	// HTTP/1.1 => chunked coding; body; empty trailer
    78  	1: {
    79  		Req: Request{
    80  			Method: "GET",
    81  			URL: &url.URL{
    82  				Scheme: "http",
    83  				Host:   "www.google.com",
    84  				Path:   "/search",
    85  			},
    86  			ProtoMajor:       1,
    87  			ProtoMinor:       1,
    88  			Header:           Header{},
    89  			TransferEncoding: []string{"chunked"},
    90  		},
    91  
    92  		Body: []byte("abcdef"),
    93  
    94  		WantWrite: "GET /search HTTP/1.1\r\n" +
    95  			"Host: www.google.com\r\n" +
    96  			"User-Agent: Go-http-client/1.1\r\n" +
    97  			"Transfer-Encoding: chunked\r\n\r\n" +
    98  			chunk("abcdef") + chunk(""),
    99  
   100  		WantProxy: "GET http://www.google.com/search HTTP/1.1\r\n" +
   101  			"Host: www.google.com\r\n" +
   102  			"User-Agent: Go-http-client/1.1\r\n" +
   103  			"Transfer-Encoding: chunked\r\n\r\n" +
   104  			chunk("abcdef") + chunk(""),
   105  	},
   106  	// HTTP/1.1 POST => chunked coding; body; empty trailer
   107  	2: {
   108  		Req: Request{
   109  			Method: "POST",
   110  			URL: &url.URL{
   111  				Scheme: "http",
   112  				Host:   "www.google.com",
   113  				Path:   "/search",
   114  			},
   115  			ProtoMajor:       1,
   116  			ProtoMinor:       1,
   117  			Header:           Header{},
   118  			Close:            true,
   119  			TransferEncoding: []string{"chunked"},
   120  		},
   121  
   122  		Body: []byte("abcdef"),
   123  
   124  		WantWrite: "POST /search HTTP/1.1\r\n" +
   125  			"Host: www.google.com\r\n" +
   126  			"User-Agent: Go-http-client/1.1\r\n" +
   127  			"Connection: close\r\n" +
   128  			"Transfer-Encoding: chunked\r\n\r\n" +
   129  			chunk("abcdef") + chunk(""),
   130  
   131  		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
   132  			"Host: www.google.com\r\n" +
   133  			"User-Agent: Go-http-client/1.1\r\n" +
   134  			"Connection: close\r\n" +
   135  			"Transfer-Encoding: chunked\r\n\r\n" +
   136  			chunk("abcdef") + chunk(""),
   137  	},
   138  
   139  	// HTTP/1.1 POST with Content-Length, no chunking
   140  	3: {
   141  		Req: Request{
   142  			Method: "POST",
   143  			URL: &url.URL{
   144  				Scheme: "http",
   145  				Host:   "www.google.com",
   146  				Path:   "/search",
   147  			},
   148  			ProtoMajor:    1,
   149  			ProtoMinor:    1,
   150  			Header:        Header{},
   151  			Close:         true,
   152  			ContentLength: 6,
   153  		},
   154  
   155  		Body: []byte("abcdef"),
   156  
   157  		WantWrite: "POST /search HTTP/1.1\r\n" +
   158  			"Host: www.google.com\r\n" +
   159  			"User-Agent: Go-http-client/1.1\r\n" +
   160  			"Connection: close\r\n" +
   161  			"Content-Length: 6\r\n" +
   162  			"\r\n" +
   163  			"abcdef",
   164  
   165  		WantProxy: "POST http://www.google.com/search HTTP/1.1\r\n" +
   166  			"Host: www.google.com\r\n" +
   167  			"User-Agent: Go-http-client/1.1\r\n" +
   168  			"Connection: close\r\n" +
   169  			"Content-Length: 6\r\n" +
   170  			"\r\n" +
   171  			"abcdef",
   172  	},
   173  
   174  	// HTTP/1.1 POST with Content-Length in headers
   175  	4: {
   176  		Req: Request{
   177  			Method: "POST",
   178  			URL:    mustParseURL("http://example.com/"),
   179  			Host:   "example.com",
   180  			Header: Header{
   181  				"Content-Length": []string{"10"}, // ignored
   182  			},
   183  			ContentLength: 6,
   184  		},
   185  
   186  		Body: []byte("abcdef"),
   187  
   188  		WantWrite: "POST / HTTP/1.1\r\n" +
   189  			"Host: example.com\r\n" +
   190  			"User-Agent: Go-http-client/1.1\r\n" +
   191  			"Content-Length: 6\r\n" +
   192  			"\r\n" +
   193  			"abcdef",
   194  
   195  		WantProxy: "POST http://example.com/ HTTP/1.1\r\n" +
   196  			"Host: example.com\r\n" +
   197  			"User-Agent: Go-http-client/1.1\r\n" +
   198  			"Content-Length: 6\r\n" +
   199  			"\r\n" +
   200  			"abcdef",
   201  	},
   202  
   203  	// default to HTTP/1.1
   204  	5: {
   205  		Req: Request{
   206  			Method: "GET",
   207  			URL:    mustParseURL("/search"),
   208  			Host:   "www.google.com",
   209  		},
   210  
   211  		WantWrite: "GET /search HTTP/1.1\r\n" +
   212  			"Host: www.google.com\r\n" +
   213  			"User-Agent: Go-http-client/1.1\r\n" +
   214  			"\r\n",
   215  	},
   216  
   217  	// Request with a 0 ContentLength and a 0 byte body.
   218  	6: {
   219  		Req: Request{
   220  			Method:        "POST",
   221  			URL:           mustParseURL("/"),
   222  			Host:          "example.com",
   223  			ProtoMajor:    1,
   224  			ProtoMinor:    1,
   225  			ContentLength: 0, // as if unset by user
   226  		},
   227  
   228  		Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 0)) },
   229  
   230  		WantWrite: "POST / HTTP/1.1\r\n" +
   231  			"Host: example.com\r\n" +
   232  			"User-Agent: Go-http-client/1.1\r\n" +
   233  			"Transfer-Encoding: chunked\r\n" +
   234  			"\r\n0\r\n\r\n",
   235  
   236  		WantProxy: "POST / HTTP/1.1\r\n" +
   237  			"Host: example.com\r\n" +
   238  			"User-Agent: Go-http-client/1.1\r\n" +
   239  			"Transfer-Encoding: chunked\r\n" +
   240  			"\r\n0\r\n\r\n",
   241  	},
   242  
   243  	// Request with a 0 ContentLength and a nil body.
   244  	7: {
   245  		Req: Request{
   246  			Method:        "POST",
   247  			URL:           mustParseURL("/"),
   248  			Host:          "example.com",
   249  			ProtoMajor:    1,
   250  			ProtoMinor:    1,
   251  			ContentLength: 0, // as if unset by user
   252  		},
   253  
   254  		Body: func() io.ReadCloser { return nil },
   255  
   256  		WantWrite: "POST / HTTP/1.1\r\n" +
   257  			"Host: example.com\r\n" +
   258  			"User-Agent: Go-http-client/1.1\r\n" +
   259  			"Content-Length: 0\r\n" +
   260  			"\r\n",
   261  
   262  		WantProxy: "POST / HTTP/1.1\r\n" +
   263  			"Host: example.com\r\n" +
   264  			"User-Agent: Go-http-client/1.1\r\n" +
   265  			"Content-Length: 0\r\n" +
   266  			"\r\n",
   267  	},
   268  
   269  	// Request with a 0 ContentLength and a 1 byte body.
   270  	8: {
   271  		Req: Request{
   272  			Method:        "POST",
   273  			URL:           mustParseURL("/"),
   274  			Host:          "example.com",
   275  			ProtoMajor:    1,
   276  			ProtoMinor:    1,
   277  			ContentLength: 0, // as if unset by user
   278  		},
   279  
   280  		Body: func() io.ReadCloser { return ioutil.NopCloser(io.LimitReader(strings.NewReader("xx"), 1)) },
   281  
   282  		WantWrite: "POST / HTTP/1.1\r\n" +
   283  			"Host: example.com\r\n" +
   284  			"User-Agent: Go-http-client/1.1\r\n" +
   285  			"Transfer-Encoding: chunked\r\n\r\n" +
   286  			chunk("x") + chunk(""),
   287  
   288  		WantProxy: "POST / HTTP/1.1\r\n" +
   289  			"Host: example.com\r\n" +
   290  			"User-Agent: Go-http-client/1.1\r\n" +
   291  			"Transfer-Encoding: chunked\r\n\r\n" +
   292  			chunk("x") + chunk(""),
   293  	},
   294  
   295  	// Request with a ContentLength of 10 but a 5 byte body.
   296  	9: {
   297  		Req: Request{
   298  			Method:        "POST",
   299  			URL:           mustParseURL("/"),
   300  			Host:          "example.com",
   301  			ProtoMajor:    1,
   302  			ProtoMinor:    1,
   303  			ContentLength: 10, // but we're going to send only 5 bytes
   304  		},
   305  		Body:      []byte("12345"),
   306  		WantError: errors.New("http: ContentLength=10 with Body length 5"),
   307  	},
   308  
   309  	// Request with a ContentLength of 4 but an 8 byte body.
   310  	10: {
   311  		Req: Request{
   312  			Method:        "POST",
   313  			URL:           mustParseURL("/"),
   314  			Host:          "example.com",
   315  			ProtoMajor:    1,
   316  			ProtoMinor:    1,
   317  			ContentLength: 4, // but we're going to try to send 8 bytes
   318  		},
   319  		Body:      []byte("12345678"),
   320  		WantError: errors.New("http: ContentLength=4 with Body length 8"),
   321  	},
   322  
   323  	// Request with a 5 ContentLength and nil body.
   324  	11: {
   325  		Req: Request{
   326  			Method:        "POST",
   327  			URL:           mustParseURL("/"),
   328  			Host:          "example.com",
   329  			ProtoMajor:    1,
   330  			ProtoMinor:    1,
   331  			ContentLength: 5, // but we'll omit the body
   332  		},
   333  		WantError: errors.New("http: Request.ContentLength=5 with nil Body"),
   334  	},
   335  
   336  	// Request with a 0 ContentLength and a body with 1 byte content and an error.
   337  	12: {
   338  		Req: Request{
   339  			Method:        "POST",
   340  			URL:           mustParseURL("/"),
   341  			Host:          "example.com",
   342  			ProtoMajor:    1,
   343  			ProtoMinor:    1,
   344  			ContentLength: 0, // as if unset by user
   345  		},
   346  
   347  		Body: func() io.ReadCloser {
   348  			err := errors.New("Custom reader error")
   349  			errReader := &errorReader{err}
   350  			return ioutil.NopCloser(io.MultiReader(strings.NewReader("x"), errReader))
   351  		},
   352  
   353  		WantError: errors.New("Custom reader error"),
   354  	},
   355  
   356  	// Request with a 0 ContentLength and a body without content and an error.
   357  	13: {
   358  		Req: Request{
   359  			Method:        "POST",
   360  			URL:           mustParseURL("/"),
   361  			Host:          "example.com",
   362  			ProtoMajor:    1,
   363  			ProtoMinor:    1,
   364  			ContentLength: 0, // as if unset by user
   365  		},
   366  
   367  		Body: func() io.ReadCloser {
   368  			err := errors.New("Custom reader error")
   369  			errReader := &errorReader{err}
   370  			return ioutil.NopCloser(errReader)
   371  		},
   372  
   373  		WantError: errors.New("Custom reader error"),
   374  	},
   375  
   376  	// Verify that DumpRequest preserves the HTTP version number, doesn't add a Host,
   377  	// and doesn't add a User-Agent.
   378  	14: {
   379  		Req: Request{
   380  			Method:     "GET",
   381  			URL:        mustParseURL("/foo"),
   382  			ProtoMajor: 1,
   383  			ProtoMinor: 0,
   384  			Header: Header{
   385  				"X-Foo": []string{"X-Bar"},
   386  			},
   387  		},
   388  
   389  		WantWrite: "GET /foo HTTP/1.1\r\n" +
   390  			"Host: \r\n" +
   391  			"User-Agent: Go-http-client/1.1\r\n" +
   392  			"X-Foo: X-Bar\r\n\r\n",
   393  	},
   394  
   395  	// If no Request.Host and no Request.URL.Host, we send
   396  	// an empty Host header, and don't use
   397  	// Request.Header["Host"]. This is just testing that
   398  	// we don't change Go 1.0 behavior.
   399  	15: {
   400  		Req: Request{
   401  			Method: "GET",
   402  			Host:   "",
   403  			URL: &url.URL{
   404  				Scheme: "http",
   405  				Host:   "",
   406  				Path:   "/search",
   407  			},
   408  			ProtoMajor: 1,
   409  			ProtoMinor: 1,
   410  			Header: Header{
   411  				"Host": []string{"bad.example.com"},
   412  			},
   413  		},
   414  
   415  		WantWrite: "GET /search HTTP/1.1\r\n" +
   416  			"Host: \r\n" +
   417  			"User-Agent: Go-http-client/1.1\r\n\r\n",
   418  	},
   419  
   420  	// Opaque test #1 from golang.org/issue/4860
   421  	16: {
   422  		Req: Request{
   423  			Method: "GET",
   424  			URL: &url.URL{
   425  				Scheme: "http",
   426  				Host:   "www.google.com",
   427  				Opaque: "/%2F/%2F/",
   428  			},
   429  			ProtoMajor: 1,
   430  			ProtoMinor: 1,
   431  			Header:     Header{},
   432  		},
   433  
   434  		WantWrite: "GET /%2F/%2F/ HTTP/1.1\r\n" +
   435  			"Host: www.google.com\r\n" +
   436  			"User-Agent: Go-http-client/1.1\r\n\r\n",
   437  	},
   438  
   439  	// Opaque test #2 from golang.org/issue/4860
   440  	17: {
   441  		Req: Request{
   442  			Method: "GET",
   443  			URL: &url.URL{
   444  				Scheme: "http",
   445  				Host:   "x.google.com",
   446  				Opaque: "//y.google.com/%2F/%2F/",
   447  			},
   448  			ProtoMajor: 1,
   449  			ProtoMinor: 1,
   450  			Header:     Header{},
   451  		},
   452  
   453  		WantWrite: "GET http://y.google.com/%2F/%2F/ HTTP/1.1\r\n" +
   454  			"Host: x.google.com\r\n" +
   455  			"User-Agent: Go-http-client/1.1\r\n\r\n",
   456  	},
   457  
   458  	// Testing custom case in header keys. Issue 5022.
   459  	18: {
   460  		Req: Request{
   461  			Method: "GET",
   462  			URL: &url.URL{
   463  				Scheme: "http",
   464  				Host:   "www.google.com",
   465  				Path:   "/",
   466  			},
   467  			Proto:      "HTTP/1.1",
   468  			ProtoMajor: 1,
   469  			ProtoMinor: 1,
   470  			Header: Header{
   471  				"ALL-CAPS": {"x"},
   472  			},
   473  		},
   474  
   475  		WantWrite: "GET / HTTP/1.1\r\n" +
   476  			"Host: www.google.com\r\n" +
   477  			"User-Agent: Go-http-client/1.1\r\n" +
   478  			"ALL-CAPS: x\r\n" +
   479  			"\r\n",
   480  	},
   481  
   482  	// Request with host header field; IPv6 address with zone identifier
   483  	19: {
   484  		Req: Request{
   485  			Method: "GET",
   486  			URL: &url.URL{
   487  				Host: "[fe80::1%en0]",
   488  			},
   489  		},
   490  
   491  		WantWrite: "GET / HTTP/1.1\r\n" +
   492  			"Host: [fe80::1]\r\n" +
   493  			"User-Agent: Go-http-client/1.1\r\n" +
   494  			"\r\n",
   495  	},
   496  
   497  	// Request with optional host header field; IPv6 address with zone identifier
   498  	20: {
   499  		Req: Request{
   500  			Method: "GET",
   501  			URL: &url.URL{
   502  				Host: "www.example.com",
   503  			},
   504  			Host: "[fe80::1%en0]:8080",
   505  		},
   506  
   507  		WantWrite: "GET / HTTP/1.1\r\n" +
   508  			"Host: [fe80::1]:8080\r\n" +
   509  			"User-Agent: Go-http-client/1.1\r\n" +
   510  			"\r\n",
   511  	},
   512  }
   513  
   514  func TestRequestWrite(t *testing.T) {
   515  	for i := range reqWriteTests {
   516  		tt := &reqWriteTests[i]
   517  
   518  		setBody := func() {
   519  			if tt.Body == nil {
   520  				return
   521  			}
   522  			switch b := tt.Body.(type) {
   523  			case []byte:
   524  				tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b))
   525  			case func() io.ReadCloser:
   526  				tt.Req.Body = b()
   527  			}
   528  		}
   529  		setBody()
   530  		if tt.Req.Header == nil {
   531  			tt.Req.Header = make(Header)
   532  		}
   533  
   534  		var braw bytes.Buffer
   535  		err := tt.Req.Write(&braw)
   536  		if g, e := fmt.Sprintf("%v", err), fmt.Sprintf("%v", tt.WantError); g != e {
   537  			t.Errorf("writing #%d, err = %q, want %q", i, g, e)
   538  			continue
   539  		}
   540  		if err != nil {
   541  			continue
   542  		}
   543  
   544  		if tt.WantWrite != "" {
   545  			sraw := braw.String()
   546  			if sraw != tt.WantWrite {
   547  				t.Errorf("Test %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantWrite, sraw)
   548  				continue
   549  			}
   550  		}
   551  
   552  		if tt.WantProxy != "" {
   553  			setBody()
   554  			var praw bytes.Buffer
   555  			err = tt.Req.WriteProxy(&praw)
   556  			if err != nil {
   557  				t.Errorf("WriteProxy #%d: %s", i, err)
   558  				continue
   559  			}
   560  			sraw := praw.String()
   561  			if sraw != tt.WantProxy {
   562  				t.Errorf("Test Proxy %d, expecting:\n%s\nGot:\n%s\n", i, tt.WantProxy, sraw)
   563  				continue
   564  			}
   565  		}
   566  	}
   567  }
   568  
   569  type closeChecker struct {
   570  	io.Reader
   571  	closed bool
   572  }
   573  
   574  func (rc *closeChecker) Close() error {
   575  	rc.closed = true
   576  	return nil
   577  }
   578  
   579  // TestRequestWriteClosesBody tests that Request.Write closes its request.Body.
   580  // It also indirectly tests NewRequest and that it doesn't wrap an existing Closer
   581  // inside a NopCloser, and that it serializes it correctly.
   582  func TestRequestWriteClosesBody(t *testing.T) {
   583  	rc := &closeChecker{Reader: strings.NewReader("my body")}
   584  	req, _ := NewRequest("POST", "http://foo.com/", rc)
   585  	if req.ContentLength != -1 {
   586  		t.Errorf("got req.ContentLength %d, want -1", req.ContentLength)
   587  	}
   588  	buf := new(bytes.Buffer)
   589  	req.Write(buf)
   590  	if !rc.closed {
   591  		t.Error("body not closed after write")
   592  	}
   593  	expected := "POST / HTTP/1.1\r\n" +
   594  		"Host: foo.com\r\n" +
   595  		"User-Agent: Go-http-client/1.1\r\n" +
   596  		"Transfer-Encoding: chunked\r\n\r\n" +
   597  		chunk("my body") +
   598  		chunk("")
   599  	if buf.String() != expected {
   600  		t.Errorf("write:\n got: %s\nwant: %s", buf.String(), expected)
   601  	}
   602  }
   603  
   604  func chunk(s string) string {
   605  	return fmt.Sprintf("%x\r\n%s\r\n", len(s), s)
   606  }
   607  
   608  func mustParseURL(s string) *url.URL {
   609  	u, err := url.Parse(s)
   610  	if err != nil {
   611  		panic(fmt.Sprintf("Error parsing URL %q: %v", s, err))
   612  	}
   613  	return u
   614  }
   615  
   616  type writerFunc func([]byte) (int, error)
   617  
   618  func (f writerFunc) Write(p []byte) (int, error) { return f(p) }
   619  
   620  // TestRequestWriteError tests the Write err != nil checks in (*Request).write.
   621  func TestRequestWriteError(t *testing.T) {
   622  	failAfter, writeCount := 0, 0
   623  	errFail := errors.New("fake write failure")
   624  
   625  	// w is the buffered io.Writer to write the request to. It
   626  	// fails exactly once on its Nth Write call, as controlled by
   627  	// failAfter. It also tracks the number of calls in
   628  	// writeCount.
   629  	w := struct {
   630  		io.ByteWriter // to avoid being wrapped by a bufio.Writer
   631  		io.Writer
   632  	}{
   633  		nil,
   634  		writerFunc(func(p []byte) (n int, err error) {
   635  			writeCount++
   636  			if failAfter == 0 {
   637  				err = errFail
   638  			}
   639  			failAfter--
   640  			return len(p), err
   641  		}),
   642  	}
   643  
   644  	req, _ := NewRequest("GET", "http://example.com/", nil)
   645  	const writeCalls = 4 // number of Write calls in current implementation
   646  	sawGood := false
   647  	for n := 0; n <= writeCalls+2; n++ {
   648  		failAfter = n
   649  		writeCount = 0
   650  		err := req.Write(w)
   651  		var wantErr error
   652  		if n < writeCalls {
   653  			wantErr = errFail
   654  		}
   655  		if err != wantErr {
   656  			t.Errorf("for fail-after %d Writes, err = %v; want %v", n, err, wantErr)
   657  			continue
   658  		}
   659  		if err == nil {
   660  			sawGood = true
   661  			if writeCount != writeCalls {
   662  				t.Fatalf("writeCalls constant is outdated in test")
   663  			}
   664  		}
   665  		if writeCount > writeCalls || writeCount > n+1 {
   666  			t.Errorf("for fail-after %d, saw unexpectedly high (%d) write calls", n, writeCount)
   667  		}
   668  	}
   669  	if !sawGood {
   670  		t.Fatalf("writeCalls constant is outdated in test")
   671  	}
   672  }