go net/http: 从被劫持的bufio.Reader中读取数据不应该导致r.Context被取消,

xdnvmnnf  于 2个月前  发布在  Go
关注(0)|答案(7)|浏览(35)

当我在处理https://nhooyr.io/websocket时,我注意到当从Hijack返回的连接关闭时,r.Context()会被取消。这种行为很奇怪,因为在Hijack之后,net/http不应该取消请求上下文。毕竟,它怎么能知道连接已经关闭了呢?

我追踪到了问题的根源,那就是Hijack返回的bufio.Reader。当读取出错时,会导致r.Context被取消。

在我的项目中,我使用了从Hijack返回的所有bufio.Reader进行读取。当一个goroutine关闭连接时,另一个goroutine会从连接中读取数据,因此由于这行代码,上下文会被取消。

我认为最好的做法是不让bufio.Reader上的读取操作导致r.Context被取消,因为在Hijack之后,net/http应该完全不参与进来。

ie3xauqp

ie3xauqp1#

https://golang.org/cl/179458提到了这个问题:net/http: never ever cancel r.Context() after Hijack

ghhaqwfi

ghhaqwfi2#

感谢你报告这个问题@nhooyr!
你是否可以尝试解决一个可复现的问题,以便我们将其转换为测试用例,就像@bradfitz在你的CL中要求的那样?
我已经启动了一个种子测试用例https://play.golang.org/p/gidrGk8M5Dq,但只是为了长时间的事件而关闭了我的电脑。请帮助我继续进行测试,这样可以使你的复现(或在下面内联)

package main

import (
	"context"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/http/httptest"
	"strings"
	"time"
)

func main() {
	cst := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		hj, ok := w.(http.Hijacker)
		if !ok {
			w.WriteHeader(http.StatusNotImplemented)
			return
		}
		conn, bufw, err := hj.Hijack()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		defer conn.Close()

		reqBlob, _ := ioutil.ReadAll(conn)
		fmt.Printf("request blob: %s\n", reqBlob)
		bufw.Write([]byte("HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Encoding: chunked\r\nContent-Length: 2\r\n\r\nok"))
		bufw.Flush()
	}))
	defer cst.Close()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	req, _ := http.NewRequest("POST", cst.URL, strings.NewReader("aaaaaaa*****aaaaaaaaaaaa"))
	req = req.WithContext(ctx)

	resultCh := make(chan []byte)
	go func() {
		defer close(resultCh)
		res, err := cst.Client().Do(req)
		log.Print("Made request!")
		if err != nil {
			log.Fatalf("failed http request: %v", err)
		}
		blob, _ := ioutil.ReadAll(res.Body)
		_ = res.Body.Close()
		resultCh <- blob
	}()

	select {
	case <-ctx.Done():
		log.Fatalf("Surprisingly context was ended with error: %v", ctx.Err())
	case <-time.After(5 * time.Second):
		log.Fatal("Waited for too long!")
	case blob := <-resultCh:
		// Great case!
		// fmt.Printf("%s\n", blob)
		if len(blob) == 0 {
			log.Fatal("Failed to get back response data")
		}
	}
}
jbose2ul

jbose2ul3#

问题的本质在于劫持连接,关闭它,然后从由Hijack返回的bufio Reader中读取并观察上下文。上下文被取消了。

zpqajqem

zpqajqem4#

好的,谢谢你@nhooyr!所以也许https://play.golang.org/p/k5S0LVVXFhb对于你的测试来说已经足够了

package main

import (
	"io/ioutil"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"
)

func TestConnCloseNoCancellation(t *testing.T) {
	cst := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		hj, ok := w.(http.Hijacker)
		defer func() {
			ctx := r.Context()
			select {
			case <-ctx.Done():
				t.Fatalf("client.Context Done with error: %v", ctx.Err())
			default:
			}
		}()
		if !ok {
			w.WriteHeader(http.StatusNotImplemented)
			return
		}
		conn, bufrw, err := hj.Hijack()
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		conn.Close()

		_, _ = ioutil.ReadAll(bufrw)
		bufrw.Write([]byte("HTTP/1.1 200 OK\r\nConnection: keep-alive\r\nContent-Encoding: chunked\r\nContent-Length: 2\r\n\r\nok"))
		bufrw.Flush()
	}))
	defer cst.Close()

	req, _ := http.NewRequest("POST", cst.URL, strings.NewReader("aaaaaaa*****aaaaaaaaaaaa"))
	res, _ := cst.Client().Do(req)
	if res != nil {
		_, _ = ioutil.ReadAll(res.Body)
		_ = res.Body.Close()
	}
}

它产生了

=== RUN   TestConnCloseNoCancellation
--- FAIL: TestConnCloseNoCancellation (0.00s)
    prog.go:18: client.Context Done with error: context canceled
FAIL
pw9qyyiw

pw9qyyiw5#

嘿,@nhooyr,我记得在一个问题中你评论说我会继续做这件事。@clairerhoda想要接手这件事,所以她会继续进行并提交给Go1.14。

cgvd09ve

cgvd09ve6#

https://golang.org/cl/200437提到了这个问题:net/http: don't cancel hijacked connection's request context

相关问题