package native import ( "bufio" "context" "errors" "io" "net" "net/http" "strings" "testing" "time" ) type observedReadConn struct { net.Conn reading chan struct{} } func (c *observedReadConn) Read(data []byte) (int, error) { select { case c.reading <- struct{}{}: default: } return c.Conn.Read(data) } func TestDisconnectWatchStopsBeforeReaderReuse(t *testing.T) { server, client := net.Pipe() defer server.Close() defer client.Close() conn := &observedReadConn{Conn: server, reading: make(chan struct{}, 1)} reader := bufio.NewReader(conn) ctx, cancel := context.WithCancel(context.Background()) defer cancel() watch := newClientDisconnectWatch(reader, conn, cancel) select { case <-conn.reading: case <-time.After(time.Second): t.Fatal("disconnect watcher did not start reading") } watch.stop() if ctx.Err() != nil { t.Fatalf("interrupting the watcher canceled a healthy request: %v", ctx.Err()) } if err := server.SetReadDeadline(time.Now().Add(time.Second)); err != nil { t.Fatal(err) } writeDone := make(chan error, 1) go func() { _, err := io.WriteString(client, "GET /next HTTP/1.1\r\n") writeDone <- err }() line, err := reader.ReadString('\n') if err != nil || line != "GET /next HTTP/1.1\r\n" { t.Fatalf("reader retained watcher data or deadline: line=%q err=%v", line, err) } if err := <-writeDone; err != nil { t.Fatal(err) } } func TestDisconnectWatchPreservesPipelineAndWriteFailureCancels(t *testing.T) { server, client := net.Pipe() defer server.Close() defer client.Close() pipeline := "GET /next HTTP/1.1\r\nHost: test\r\n\r\n" reader := bufio.NewReader(strings.NewReader(pipeline)) ctx, cancel := context.WithCancel(context.Background()) defer cancel() watch := newClientDisconnectWatch(reader, server, cancel) select { case <-watch.done: case <-time.After(time.Second): t.Fatal("watcher should stop when pipelined data is available") } watch.stop() if ctx.Err() != nil { t.Fatal("pipeline data canceled the current request") } remaining, err := io.ReadAll(reader) if err != nil || string(remaining) != pipeline { t.Fatalf("pipeline bytes changed: data=%q err=%v", remaining, err) } // With pending pipeline data, EOF detection is deferred. A failed stream // write must still cancel downstream work even when the handler ignores it. client.Close() writer := newResponseWriter(http.MethodGet) writer.configure(&http.Request{ProtoMajor: 1, ProtoMinor: 1}, server, false, time.Second, cancel) writer.Flush() if !errors.Is(ctx.Err(), context.Canceled) { t.Fatal("failed Flush did not cancel request context") } if _, err := writer.Write([]byte("more")); err == nil { t.Fatal("failed Flush was not returned by subsequent Write") } if err := writer.writeTo(server, false, time.Second); err == nil { t.Fatal("failed Flush was not returned on response completion") } writer.reset("") if writer.cancel != nil || writer.writeErr != nil || writer.conn != nil { t.Fatal("pooled response retained the previous request") } }