Description
Issue
GO1.7's functionality for HTTP 1.1's keep-alive, net/http.Server.ReadTimeout, and net/http.CloseNotifier is not consistent with the documented and/or expected behavior, and cannot be used together properly. The timer created by setting the server's readTimeout value is reset only at the end of processing each request and is not paused/reset when we have finished reading in each request, causing CloseNotify() to signal without cause.
What did I do
This test creates a simple http server and then sends 2 requests, each of which takes 40ms of processing time with a 70ms gap between them, responding 200OK if the 40ms of work completes, and 503 if it receives a closeNotify signal before finishing that work.
Expected Result
We should receive 2 200 OK messages and not receive a signal from CloseNotify().
We have a readTimeout of 100ms, and a 70ms delay between receiving a response for the first request and sending the second request, so both requests should be read within the timeout and be able to complete their work.
Actual Result
We actually see that during the second request, we receive a closeNotify() signal approximately 30ms after the client has sent the second request. At this time we have clearly read the second request and started processing it with a still alive TCP connection, but still receive a closeNotify() signal, which should not be happening. There is also an attached wireshark log showing this.
package main
import (
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"sync"
"testing"
"time"
)
func TestServerKeepAliveReadTimeoutCloseNotify(t *testing.T) {
var startMu sync.Mutex
var start time.Time
testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cn := w.(http.CloseNotifier)
select {
case <-time.After(40 * time.Millisecond):
w.WriteHeader(http.StatusOK)
case <-cn.CloseNotify():
startMu.Lock()
delta := time.Since(start)
startMu.Unlock()
t.Errorf("Unexpected CloseNotify received after %s", delta)
w.WriteHeader(http.StatusServiceUnavailable)
}
}))
testServer.Config.ReadTimeout = 100 * time.Millisecond
testServer.Start()
defer testServer.Close()
for i := 0; i < 2; i++ {
startMu.Lock()
start = time.Now()
startMu.Unlock()
resp, err := http.Get(testServer.URL)
if err != nil {
t.Fatal(err)
}
_, err = io.Copy(ioutil.Discard, resp.Body)
if err != nil {
t.Fatal(err)
}
err = resp.Body.Close()
if err != nil {
t.Fatal(err)
}
time.Sleep(70 * time.Millisecond)
}
}
Test output
$ go test notifierIssue_test.go
--- FAIL: TestServerKeepAliveReadTimeoutCloseNotify (0.22s)
notifierIssue_test.go:26: Unexpected CloseNotify received after 30.752781ms
FAIL
FAIL command-line-arguments 0.225s
Potential Fix
Currently the read timeout is only reset here: https://github.com/golang/go/blob/go1.7/src/net/http/server.go#L749 after a request has finished being processed and we start waiting on the next one. However, the timeout is never paused/reset for the period between the end of reading the request and the end of sending the response, as it should be.
What version of Go are you using (go version
)?
go version go1.7 darwin/amd64
What operating system and processor architecture are you using (go env
)?
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GORACE=""
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/darwin_amd64"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/4r/1g2c4css78b1yn2q32dmqqm0l05w9h/T/go-build102138663=/tmp/go-build -gno-record-gcc-switches -fno-common"
CXX="clang++"
CGO_ENABLED="1"