From 135c2c42c06729b4e443aa4d57bef46acf5dbe79 Mon Sep 17 00:00:00 2001 From: Armin Becher Date: Thu, 3 Mar 2022 09:37:57 +0100 Subject: [PATCH 1/8] Read response when client closes connection #1232 --- client.go | 14 ++++++------- client_test.go | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index b36ca40824..9c4f290ffe 100644 --- a/client.go +++ b/client.go @@ -13,6 +13,7 @@ import ( "strings" "sync" "sync/atomic" + "syscall" "time" ) @@ -1437,12 +1438,11 @@ func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error) if err == nil { err = bw.Flush() } - if err != nil { - c.releaseWriter(bw) + c.releaseWriter(bw) + if err != nil && !errors.Is(err, syscall.ECONNRESET) { c.closeConn(cc) return true, err } - c.releaseWriter(bw) if c.ReadTimeout > 0 { // Set Deadline every time, since golang has fixed the performance issue @@ -1462,14 +1462,14 @@ func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error) } br := c.acquireReader(conn) - if err = resp.ReadLimitBody(br, c.MaxResponseBodySize); err != nil { - c.releaseReader(br) + err = resp.ReadLimitBody(br, c.MaxResponseBodySize) + c.releaseReader(br) + if err != nil && !errors.Is(err, syscall.ECONNRESET) { c.closeConn(cc) // Don't retry in case of ErrBodyTooLarge since we will just get the same again. retry := err != ErrBodyTooLarge return retry, err } - c.releaseReader(br) if resetConnection || req.ConnectionClose() || resp.ConnectionClose() { c.closeConn(cc) @@ -1477,7 +1477,7 @@ func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error) c.releaseConn(cc) } - return false, err + return false, nil } var ( diff --git a/client_test.go b/client_test.go index 78eda2e58e..aa771c8c5d 100644 --- a/client_test.go +++ b/client_test.go @@ -6,7 +6,9 @@ import ( "crypto/tls" "fmt" "io" + "io/ioutil" "net" + "net/http" "net/url" "os" "regexp" @@ -2840,3 +2842,56 @@ func TestHttpsRequestWithoutParsedURL(t *testing.T) { t.Fatalf("https requests with IsTLS client must succeed") } } + +// See issue #1232 +func TestRstConnResponseWhileSending(t *testing.T) { + const expectedStatus = http.StatusTeapot + const payload = "payload" + + srv, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err.Error()) + } + + go func() { + conn, err := srv.Accept() + if err != nil { + t.Errorf(err.Error()) + } + + // Read at least one byte of the header + // Otherwise we would have an unsolicited response + ioutil.ReadAll(io.LimitReader(conn, 1)) + + // Respond + conn.Write([]byte("HTTP/1.1 418 Teapot\r\n\r\n")) + + // Forcefully close connection + err = conn.(*net.TCPConn).SetLinger(0) + if err != nil { + t.Errorf(err.Error()) + } + conn.Close() + }() + + svrUrl := "http://" + srv.Addr().String() + + client := HostClient{Addr: srv.Addr().String()} + + req := AcquireRequest() + defer ReleaseRequest(req) + resp := AcquireResponse() + defer ReleaseResponse(resp) + + req.Header.SetMethod("POST") + req.SetBodyStream(strings.NewReader(payload), len(payload)) + req.SetRequestURI(svrUrl) + + err = client.Do(req, resp) + if err != nil { + t.Fatal(err.Error()) + } + if expectedStatus != resp.StatusCode() { + t.Fatalf("Expected %d status code, but got %d", expectedStatus, resp.StatusCode()) + } +} From de6709497df5f49b880ec774c6999e9c86b192ad Mon Sep 17 00:00:00 2001 From: Armin Becher Date: Thu, 3 Mar 2022 15:19:47 +0100 Subject: [PATCH 2/8] Fix edge case were client responds with invalid header --- client.go | 2 +- client_test.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++ http.go | 7 +++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/client.go b/client.go index 9c4f290ffe..fa7b3559ad 100644 --- a/client.go +++ b/client.go @@ -1464,7 +1464,7 @@ func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error) br := c.acquireReader(conn) err = resp.ReadLimitBody(br, c.MaxResponseBodySize) c.releaseReader(br) - if err != nil && !errors.Is(err, syscall.ECONNRESET) { + if err != nil { c.closeConn(cc) // Don't retry in case of ErrBodyTooLarge since we will just get the same again. retry := err != ErrBodyTooLarge diff --git a/client_test.go b/client_test.go index aa771c8c5d..37070c67bd 100644 --- a/client_test.go +++ b/client_test.go @@ -4,6 +4,7 @@ import ( "bufio" "bytes" "crypto/tls" + "errors" "fmt" "io" "io/ioutil" @@ -16,6 +17,7 @@ import ( "strings" "sync" "sync/atomic" + "syscall" "testing" "time" @@ -2895,3 +2897,54 @@ func TestRstConnResponseWhileSending(t *testing.T) { t.Fatalf("Expected %d status code, but got %d", expectedStatus, resp.StatusCode()) } } + +// See issue #1232 +func TestRstConnClosedWithoutResponse(t *testing.T) { + const expectedStatus = http.StatusTeapot + const payload = "payload" + + srv, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err.Error()) + } + + go func() { + conn, err := srv.Accept() + if err != nil { + t.Errorf(err.Error()) + } + + // Read at least one byte of the header + // Otherwise we would have an unsolicited response + ioutil.ReadAll(io.LimitReader(conn, 1)) + + // Respond with incomplete header + conn.Write([]byte("Http")) + + // Forcefully close connection + err = conn.(*net.TCPConn).SetLinger(0) + if err != nil { + t.Errorf(err.Error()) + } + conn.Close() + }() + + svrUrl := "http://" + srv.Addr().String() + + client := HostClient{Addr: srv.Addr().String()} + + req := AcquireRequest() + defer ReleaseRequest(req) + resp := AcquireResponse() + defer ReleaseResponse(resp) + + req.Header.SetMethod("POST") + req.SetBodyStream(strings.NewReader(payload), len(payload)) + req.SetRequestURI(svrUrl) + + err = client.Do(req, resp) + + if !errors.Is(err, syscall.ECONNRESET) { + t.Fatalf("Expected connection reset error") + } +} diff --git a/http.go b/http.go index 47431cdcd8..22e59e5331 100644 --- a/http.go +++ b/http.go @@ -12,6 +12,7 @@ import ( "net" "os" "sync" + "syscall" "time" "github.com/valyala/bytebufferpool" @@ -1290,6 +1291,9 @@ func (resp *Response) ReadLimitBody(r *bufio.Reader, maxBodySize int) error { if !resp.mustSkipBody() { err = resp.ReadBody(r, maxBodySize) + if errors.Is(err, syscall.ECONNRESET) { + return nil + } if err != nil { return err } @@ -1297,6 +1301,9 @@ func (resp *Response) ReadLimitBody(r *bufio.Reader, maxBodySize int) error { if resp.Header.ContentLength() == -1 { err = resp.Header.ReadTrailer(r) + if errors.Is(err, syscall.ECONNRESET) { + return nil + } if err != nil && err != io.EOF { return err } From 59d2fccaa23cbc975db7a90a78c2a968ac4bae81 Mon Sep 17 00:00:00 2001 From: Armin Becher Date: Fri, 4 Mar 2022 08:33:56 +0100 Subject: [PATCH 3/8] Follow linter suggestions for tests --- client_test.go | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/client_test.go b/client_test.go index 37070c67bd..62c5546dce 100644 --- a/client_test.go +++ b/client_test.go @@ -2863,10 +2863,16 @@ func TestRstConnResponseWhileSending(t *testing.T) { // Read at least one byte of the header // Otherwise we would have an unsolicited response - ioutil.ReadAll(io.LimitReader(conn, 1)) + _, err = ioutil.ReadAll(io.LimitReader(conn, 1)) + if err != nil { + t.Errorf(err.Error()) + } // Respond - conn.Write([]byte("HTTP/1.1 418 Teapot\r\n\r\n")) + _, err = conn.Write([]byte("HTTP/1.1 418 Teapot\r\n\r\n")) + if err != nil { + t.Errorf(err.Error()) + } // Forcefully close connection err = conn.(*net.TCPConn).SetLinger(0) @@ -2900,7 +2906,6 @@ func TestRstConnResponseWhileSending(t *testing.T) { // See issue #1232 func TestRstConnClosedWithoutResponse(t *testing.T) { - const expectedStatus = http.StatusTeapot const payload = "payload" srv, err := net.Listen("tcp", "127.0.0.1:0") @@ -2916,10 +2921,16 @@ func TestRstConnClosedWithoutResponse(t *testing.T) { // Read at least one byte of the header // Otherwise we would have an unsolicited response - ioutil.ReadAll(io.LimitReader(conn, 1)) + _, err = ioutil.ReadAll(io.LimitReader(conn, 1)) + if err != nil { + t.Errorf(err.Error()) + } // Respond with incomplete header - conn.Write([]byte("Http")) + _, err = conn.Write([]byte("Http")) + if err != nil { + t.Errorf(err.Error()) + } // Forcefully close connection err = conn.(*net.TCPConn).SetLinger(0) From 5c93662226a7fb608fdfd7764601c6055fa892df Mon Sep 17 00:00:00 2001 From: Armin Becher Date: Mon, 7 Mar 2022 08:41:29 +0100 Subject: [PATCH 4/8] Changes after review --- client.go | 5 +++-- http.go | 10 ++-------- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/client.go b/client.go index fa7b3559ad..9068be1793 100644 --- a/client.go +++ b/client.go @@ -1439,7 +1439,8 @@ func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error) err = bw.Flush() } c.releaseWriter(bw) - if err != nil && !errors.Is(err, syscall.ECONNRESET) { + isECONNRESET := errors.Is(err, syscall.ECONNRESET) + if err != nil && !isECONNRESET { c.closeConn(cc) return true, err } @@ -1471,7 +1472,7 @@ func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error) return retry, err } - if resetConnection || req.ConnectionClose() || resp.ConnectionClose() { + if resetConnection || req.ConnectionClose() || resp.ConnectionClose() || isECONNRESET { c.closeConn(cc) } else { c.releaseConn(cc) diff --git a/http.go b/http.go index 22e59e5331..d2a07402ac 100644 --- a/http.go +++ b/http.go @@ -1291,20 +1291,14 @@ func (resp *Response) ReadLimitBody(r *bufio.Reader, maxBodySize int) error { if !resp.mustSkipBody() { err = resp.ReadBody(r, maxBodySize) - if errors.Is(err, syscall.ECONNRESET) { - return nil - } - if err != nil { + if err != nil && !errors.Is(err, syscall.ECONNRESET) { return err } } if resp.Header.ContentLength() == -1 { err = resp.Header.ReadTrailer(r) - if errors.Is(err, syscall.ECONNRESET) { - return nil - } - if err != nil && err != io.EOF { + if err != nil && err != io.EOF && !errors.Is(err, syscall.ECONNRESET) { return err } } From 3b1bcc719150e661dfdb34815dc018c01269858b Mon Sep 17 00:00:00 2001 From: Armin Date: Mon, 7 Mar 2022 09:49:59 +0100 Subject: [PATCH 5/8] Reafactor error check after review --- http.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/http.go b/http.go index d2a07402ac..aede0c6aec 100644 --- a/http.go +++ b/http.go @@ -1291,14 +1291,20 @@ func (resp *Response) ReadLimitBody(r *bufio.Reader, maxBodySize int) error { if !resp.mustSkipBody() { err = resp.ReadBody(r, maxBodySize) - if err != nil && !errors.Is(err, syscall.ECONNRESET) { + if err != nil { + if errors.Is(err, syscall.ECONNRESET) { + return nil + } return err } } if resp.Header.ContentLength() == -1 { err = resp.Header.ReadTrailer(r) - if err != nil && err != io.EOF && !errors.Is(err, syscall.ECONNRESET) { + if err != nil && err != io.EOF { + if errors.Is(err, syscall.ECONNRESET) { + return nil + } return err } } From 4e9614f3b99f311df3e3a73ffb09e15bb508fcee Mon Sep 17 00:00:00 2001 From: Armin Date: Mon, 7 Mar 2022 10:37:27 +0100 Subject: [PATCH 6/8] Handle connection reset on windows --- client.go | 7 +++---- client_test.go | 4 +--- http.go | 5 ++--- tcp.go | 13 +++++++++++++ tcp_windows.go | 13 +++++++++++++ 5 files changed, 32 insertions(+), 10 deletions(-) create mode 100644 tcp.go create mode 100644 tcp_windows.go diff --git a/client.go b/client.go index 9068be1793..2eaab39d4e 100644 --- a/client.go +++ b/client.go @@ -13,7 +13,6 @@ import ( "strings" "sync" "sync/atomic" - "syscall" "time" ) @@ -1439,8 +1438,8 @@ func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error) err = bw.Flush() } c.releaseWriter(bw) - isECONNRESET := errors.Is(err, syscall.ECONNRESET) - if err != nil && !isECONNRESET { + isConnRST := isConnectionReset(err) + if err != nil && !isConnRST { c.closeConn(cc) return true, err } @@ -1472,7 +1471,7 @@ func (c *HostClient) doNonNilReqResp(req *Request, resp *Response) (bool, error) return retry, err } - if resetConnection || req.ConnectionClose() || resp.ConnectionClose() || isECONNRESET { + if resetConnection || req.ConnectionClose() || resp.ConnectionClose() || isConnRST { c.closeConn(cc) } else { c.releaseConn(cc) diff --git a/client_test.go b/client_test.go index 62c5546dce..b5725ad667 100644 --- a/client_test.go +++ b/client_test.go @@ -4,7 +4,6 @@ import ( "bufio" "bytes" "crypto/tls" - "errors" "fmt" "io" "io/ioutil" @@ -17,7 +16,6 @@ import ( "strings" "sync" "sync/atomic" - "syscall" "testing" "time" @@ -2955,7 +2953,7 @@ func TestRstConnClosedWithoutResponse(t *testing.T) { err = client.Do(req, resp) - if !errors.Is(err, syscall.ECONNRESET) { + if !isConnectionReset(err) { t.Fatalf("Expected connection reset error") } } diff --git a/http.go b/http.go index aede0c6aec..75b40c9b74 100644 --- a/http.go +++ b/http.go @@ -12,7 +12,6 @@ import ( "net" "os" "sync" - "syscall" "time" "github.com/valyala/bytebufferpool" @@ -1292,7 +1291,7 @@ func (resp *Response) ReadLimitBody(r *bufio.Reader, maxBodySize int) error { if !resp.mustSkipBody() { err = resp.ReadBody(r, maxBodySize) if err != nil { - if errors.Is(err, syscall.ECONNRESET) { + if isConnectionReset(err) { return nil } return err @@ -1302,7 +1301,7 @@ func (resp *Response) ReadLimitBody(r *bufio.Reader, maxBodySize int) error { if resp.Header.ContentLength() == -1 { err = resp.Header.ReadTrailer(r) if err != nil && err != io.EOF { - if errors.Is(err, syscall.ECONNRESET) { + if isConnectionReset(err) { return nil } return err diff --git a/tcp.go b/tcp.go new file mode 100644 index 0000000000..54d30334ea --- /dev/null +++ b/tcp.go @@ -0,0 +1,13 @@ +//go:build !windows +// +build !windows + +package fasthttp + +import ( + "errors" + "syscall" +) + +func isConnectionReset(err error) bool { + return errors.Is(err, syscall.ECONNRESET) +} diff --git a/tcp_windows.go b/tcp_windows.go new file mode 100644 index 0000000000..5c33025f40 --- /dev/null +++ b/tcp_windows.go @@ -0,0 +1,13 @@ +//go:build windows +// +build windows + +package fasthttp + +import ( + "errors" + "syscall" +) + +func isConnectionReset(err error) bool { + return errors.Is(err, syscall.WSAECONNRESET) +} From e4d02bdb9dbb2181430f6d65344e44d8d2a03182 Mon Sep 17 00:00:00 2001 From: Armin Becher Date: Tue, 8 Mar 2022 09:41:27 +0100 Subject: [PATCH 7/8] Remove format string from test where not needed --- client_test.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/client_test.go b/client_test.go index b5725ad667..f7557dea88 100644 --- a/client_test.go +++ b/client_test.go @@ -2839,7 +2839,7 @@ func TestHttpsRequestWithoutParsedURL(t *testing.T) { _, err := client.doNonNilReqResp(req, &Response{}) if err != nil { - t.Fatalf("https requests with IsTLS client must succeed") + t.Fatal("https requests with IsTLS client must succeed") } } @@ -2850,32 +2850,32 @@ func TestRstConnResponseWhileSending(t *testing.T) { srv, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { - t.Fatal(err.Error()) + t.Fatal(err) } go func() { conn, err := srv.Accept() if err != nil { - t.Errorf(err.Error()) + t.Error(err) } // Read at least one byte of the header // Otherwise we would have an unsolicited response _, err = ioutil.ReadAll(io.LimitReader(conn, 1)) if err != nil { - t.Errorf(err.Error()) + t.Error(err) } // Respond _, err = conn.Write([]byte("HTTP/1.1 418 Teapot\r\n\r\n")) if err != nil { - t.Errorf(err.Error()) + t.Error(err) } // Forcefully close connection err = conn.(*net.TCPConn).SetLinger(0) if err != nil { - t.Errorf(err.Error()) + t.Error(err) } conn.Close() }() @@ -2895,7 +2895,7 @@ func TestRstConnResponseWhileSending(t *testing.T) { err = client.Do(req, resp) if err != nil { - t.Fatal(err.Error()) + t.Fatal(err) } if expectedStatus != resp.StatusCode() { t.Fatalf("Expected %d status code, but got %d", expectedStatus, resp.StatusCode()) @@ -2908,32 +2908,32 @@ func TestRstConnClosedWithoutResponse(t *testing.T) { srv, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { - t.Fatal(err.Error()) + t.Fatal(err) } go func() { conn, err := srv.Accept() if err != nil { - t.Errorf(err.Error()) + t.Error(err) } // Read at least one byte of the header // Otherwise we would have an unsolicited response _, err = ioutil.ReadAll(io.LimitReader(conn, 1)) if err != nil { - t.Errorf(err.Error()) + t.Error(err) } // Respond with incomplete header _, err = conn.Write([]byte("Http")) if err != nil { - t.Errorf(err.Error()) + t.Error(err) } // Forcefully close connection err = conn.(*net.TCPConn).SetLinger(0) if err != nil { - t.Errorf(err.Error()) + t.Error(err) } conn.Close() }() @@ -2954,6 +2954,6 @@ func TestRstConnClosedWithoutResponse(t *testing.T) { err = client.Do(req, resp) if !isConnectionReset(err) { - t.Fatalf("Expected connection reset error") + t.Fatal("Expected connection reset error") } } From 8d7b61156f14658b88482b95b69308fab786067e Mon Sep 17 00:00:00 2001 From: Armin Becher Date: Mon, 14 Mar 2022 10:26:39 +0100 Subject: [PATCH 8/8] Run connection reset tests not on Windows --- client_test.go | 117 ------------------------------------- client_unix_test.go | 136 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+), 117 deletions(-) create mode 100644 client_unix_test.go diff --git a/client_test.go b/client_test.go index f7557dea88..30c8be7be2 100644 --- a/client_test.go +++ b/client_test.go @@ -6,9 +6,7 @@ import ( "crypto/tls" "fmt" "io" - "io/ioutil" "net" - "net/http" "net/url" "os" "regexp" @@ -2842,118 +2840,3 @@ func TestHttpsRequestWithoutParsedURL(t *testing.T) { t.Fatal("https requests with IsTLS client must succeed") } } - -// See issue #1232 -func TestRstConnResponseWhileSending(t *testing.T) { - const expectedStatus = http.StatusTeapot - const payload = "payload" - - srv, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - - go func() { - conn, err := srv.Accept() - if err != nil { - t.Error(err) - } - - // Read at least one byte of the header - // Otherwise we would have an unsolicited response - _, err = ioutil.ReadAll(io.LimitReader(conn, 1)) - if err != nil { - t.Error(err) - } - - // Respond - _, err = conn.Write([]byte("HTTP/1.1 418 Teapot\r\n\r\n")) - if err != nil { - t.Error(err) - } - - // Forcefully close connection - err = conn.(*net.TCPConn).SetLinger(0) - if err != nil { - t.Error(err) - } - conn.Close() - }() - - svrUrl := "http://" + srv.Addr().String() - - client := HostClient{Addr: srv.Addr().String()} - - req := AcquireRequest() - defer ReleaseRequest(req) - resp := AcquireResponse() - defer ReleaseResponse(resp) - - req.Header.SetMethod("POST") - req.SetBodyStream(strings.NewReader(payload), len(payload)) - req.SetRequestURI(svrUrl) - - err = client.Do(req, resp) - if err != nil { - t.Fatal(err) - } - if expectedStatus != resp.StatusCode() { - t.Fatalf("Expected %d status code, but got %d", expectedStatus, resp.StatusCode()) - } -} - -// See issue #1232 -func TestRstConnClosedWithoutResponse(t *testing.T) { - const payload = "payload" - - srv, err := net.Listen("tcp", "127.0.0.1:0") - if err != nil { - t.Fatal(err) - } - - go func() { - conn, err := srv.Accept() - if err != nil { - t.Error(err) - } - - // Read at least one byte of the header - // Otherwise we would have an unsolicited response - _, err = ioutil.ReadAll(io.LimitReader(conn, 1)) - if err != nil { - t.Error(err) - } - - // Respond with incomplete header - _, err = conn.Write([]byte("Http")) - if err != nil { - t.Error(err) - } - - // Forcefully close connection - err = conn.(*net.TCPConn).SetLinger(0) - if err != nil { - t.Error(err) - } - conn.Close() - }() - - svrUrl := "http://" + srv.Addr().String() - - client := HostClient{Addr: srv.Addr().String()} - - req := AcquireRequest() - defer ReleaseRequest(req) - resp := AcquireResponse() - defer ReleaseResponse(resp) - - req.Header.SetMethod("POST") - req.SetBodyStream(strings.NewReader(payload), len(payload)) - req.SetRequestURI(svrUrl) - - err = client.Do(req, resp) - - if !isConnectionReset(err) { - t.Fatal("Expected connection reset error") - } -} diff --git a/client_unix_test.go b/client_unix_test.go new file mode 100644 index 0000000000..f369111d5c --- /dev/null +++ b/client_unix_test.go @@ -0,0 +1,136 @@ +//go:build !windows +// +build !windows + +package fasthttp + +import ( + "io" + "io/ioutil" + "net" + "net/http" + "strings" + "testing" +) + +// See issue #1232 +func TestRstConnResponseWhileSending(t *testing.T) { + const expectedStatus = http.StatusTeapot + const payload = "payload" + + srv, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + go func() { + for { + conn, err := srv.Accept() + if err != nil { + return + } + + // Read at least one byte of the header + // Otherwise we would have an unsolicited response + _, err = ioutil.ReadAll(io.LimitReader(conn, 1)) + if err != nil { + t.Error(err) + } + + // Respond + _, err = conn.Write([]byte("HTTP/1.1 418 Teapot\r\n\r\n")) + if err != nil { + t.Error(err) + } + + // Forcefully close connection + err = conn.(*net.TCPConn).SetLinger(0) + if err != nil { + t.Error(err) + } + conn.Close() + } + }() + + svrUrl := "http://" + srv.Addr().String() + client := HostClient{Addr: srv.Addr().String()} + + for i := 0; i < 100; i++ { + req := AcquireRequest() + defer ReleaseRequest(req) + resp := AcquireResponse() + defer ReleaseResponse(resp) + + req.Header.SetMethod("POST") + req.SetBodyStream(strings.NewReader(payload), len(payload)) + req.SetRequestURI(svrUrl) + + err = client.Do(req, resp) + if err != nil { + t.Fatal(err) + } + if expectedStatus != resp.StatusCode() { + t.Fatalf("Expected %d status code, but got %d", expectedStatus, resp.StatusCode()) + } + } +} + +// See issue #1232 +func TestRstConnClosedWithoutResponse(t *testing.T) { + const payload = "payload" + + srv, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatal(err) + } + defer srv.Close() + + go func() { + for { + conn, err := srv.Accept() + if err != nil { + return + } + + // Read at least one byte of the header + // Otherwise we would have an unsolicited response + _, err = ioutil.ReadAll(io.LimitReader(conn, 1)) + if err != nil { + t.Error(err) + } + + // Respond with incomplete header + _, err = conn.Write([]byte("Http")) + if err != nil { + t.Error(err) + } + + // Forcefully close connection + err = conn.(*net.TCPConn).SetLinger(0) + if err != nil { + t.Error(err) + } + conn.Close() + } + }() + + svrUrl := "http://" + srv.Addr().String() + client := HostClient{Addr: srv.Addr().String()} + + for i := 0; i < 100; i++ { + req := AcquireRequest() + defer ReleaseRequest(req) + resp := AcquireResponse() + defer ReleaseResponse(resp) + + req.Header.SetMethod("POST") + req.SetBodyStream(strings.NewReader(payload), len(payload)) + req.SetRequestURI(svrUrl) + + err = client.Do(req, resp) + + if !isConnectionReset(err) { + t.Fatal("Expected connection reset error") + } + } +}