2019-03-14 08:30:04 +00:00
|
|
|
package server
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"context"
|
2024-11-18 08:56:04 +00:00
|
|
|
"crypto/tls"
|
2020-01-06 15:56:05 +00:00
|
|
|
"errors"
|
|
|
|
"io"
|
2019-03-14 08:30:04 +00:00
|
|
|
"net"
|
|
|
|
"net/http"
|
2020-01-06 15:56:05 +00:00
|
|
|
"strings"
|
2019-03-14 08:30:04 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
2020-08-17 16:04:03 +00:00
|
|
|
ptypes "github.com/traefik/paerser/types"
|
2020-09-16 13:46:04 +00:00
|
|
|
"github.com/traefik/traefik/v2/pkg/config/static"
|
2022-03-17 17:02:08 +00:00
|
|
|
tcprouter "github.com/traefik/traefik/v2/pkg/server/router/tcp"
|
2020-09-16 13:46:04 +00:00
|
|
|
"github.com/traefik/traefik/v2/pkg/tcp"
|
2024-11-18 08:56:04 +00:00
|
|
|
"golang.org/x/net/http2"
|
2019-03-14 08:30:04 +00:00
|
|
|
)
|
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
func TestShutdownHijacked(t *testing.T) {
|
2024-09-13 13:54:04 +00:00
|
|
|
router, err := tcprouter.NewRouter()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2022-03-17 17:02:08 +00:00
|
|
|
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
2020-01-06 15:56:05 +00:00
|
|
|
conn, _, err := rw.(http.Hijacker).Hijack()
|
|
|
|
require.NoError(t, err)
|
2019-03-14 08:30:04 +00:00
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
resp := http.Response{StatusCode: http.StatusOK}
|
|
|
|
err = resp.Write(conn)
|
|
|
|
require.NoError(t, err)
|
|
|
|
}))
|
2020-12-29 09:54:03 +00:00
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
testShutdown(t, router)
|
|
|
|
}
|
2019-03-14 08:30:04 +00:00
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
func TestShutdownHTTP(t *testing.T) {
|
2024-09-13 13:54:04 +00:00
|
|
|
router, err := tcprouter.NewRouter()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2022-03-17 17:02:08 +00:00
|
|
|
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
2019-03-14 08:30:04 +00:00
|
|
|
rw.WriteHeader(http.StatusOK)
|
2020-01-06 15:56:05 +00:00
|
|
|
time.Sleep(time.Second)
|
2019-03-14 08:30:04 +00:00
|
|
|
}))
|
2020-12-29 09:54:03 +00:00
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
testShutdown(t, router)
|
|
|
|
}
|
2019-03-14 08:30:04 +00:00
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
func TestShutdownTCP(t *testing.T) {
|
2022-03-17 17:02:08 +00:00
|
|
|
router, err := tcprouter.NewRouter()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
err = router.AddRoute("HostSNI(`*`)", 0, tcp.HandlerFunc(func(conn tcp.WriteCloser) {
|
2022-12-07 09:56:05 +00:00
|
|
|
_, err := http.ReadRequest(bufio.NewReader(conn))
|
|
|
|
if err != nil {
|
|
|
|
return
|
2020-01-06 15:56:05 +00:00
|
|
|
}
|
2022-12-07 09:56:05 +00:00
|
|
|
|
|
|
|
resp := http.Response{StatusCode: http.StatusOK}
|
|
|
|
_ = resp.Write(conn)
|
2020-01-06 15:56:05 +00:00
|
|
|
}))
|
2022-03-17 17:02:08 +00:00
|
|
|
require.NoError(t, err)
|
2020-01-06 15:56:05 +00:00
|
|
|
|
|
|
|
testShutdown(t, router)
|
|
|
|
}
|
|
|
|
|
2022-03-17 17:02:08 +00:00
|
|
|
func testShutdown(t *testing.T, router *tcprouter.Router) {
|
2020-12-29 09:54:03 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
epConfig := &static.EntryPointsTransport{}
|
|
|
|
epConfig.SetDefaults()
|
|
|
|
|
|
|
|
epConfig.LifeCycle.RequestAcceptGraceTimeout = 0
|
2020-08-17 16:04:03 +00:00
|
|
|
epConfig.LifeCycle.GraceTimeOut = ptypes.Duration(5 * time.Second)
|
2024-04-11 13:48:04 +00:00
|
|
|
epConfig.RespondingTimeouts.ReadTimeout = ptypes.Duration(5 * time.Second)
|
|
|
|
epConfig.RespondingTimeouts.WriteTimeout = ptypes.Duration(5 * time.Second)
|
2020-01-06 15:56:05 +00:00
|
|
|
|
|
|
|
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
|
|
|
|
// We explicitly use an IPV4 address because on Alpine, with an IPV6 address
|
|
|
|
// there seems to be shenanigans related to properly cleaning up file descriptors
|
|
|
|
Address: "127.0.0.1:0",
|
|
|
|
Transport: epConfig,
|
|
|
|
ForwardedHeaders: &static.ForwardedHeaders{},
|
2022-04-04 09:46:07 +00:00
|
|
|
HTTP2: &static.HTTP2Config{},
|
2022-02-14 16:18:08 +00:00
|
|
|
}, nil)
|
2019-03-14 08:30:04 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
conn, err := startEntrypoint(entryPoint, router)
|
|
|
|
require.NoError(t, err)
|
2022-12-07 09:56:05 +00:00
|
|
|
t.Cleanup(func() { _ = conn.Close() })
|
2019-03-14 08:30:04 +00:00
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
epAddr := entryPoint.listener.Addr().String()
|
|
|
|
|
|
|
|
request, err := http.NewRequest(http.MethodHead, "http://127.0.0.1:8082", nil)
|
2019-03-14 08:30:04 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2020-03-18 13:50:06 +00:00
|
|
|
time.Sleep(100 * time.Millisecond)
|
2020-01-06 15:56:05 +00:00
|
|
|
|
2022-12-07 09:56:05 +00:00
|
|
|
// We need to do a write on conn before the shutdown to make it "exist".
|
2020-01-06 15:56:05 +00:00
|
|
|
// Because the connection indeed exists as far as TCP is concerned,
|
2021-06-25 19:08:11 +00:00
|
|
|
// but since we only pass it along to the HTTP server after at least one byte is peeked,
|
|
|
|
// the HTTP server (and hence its shutdown) does not know about the connection until that first byte peeked.
|
2019-03-14 08:30:04 +00:00
|
|
|
err = request.Write(conn)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2022-12-07 09:56:05 +00:00
|
|
|
reader := bufio.NewReaderSize(conn, 1)
|
2021-03-08 08:58:04 +00:00
|
|
|
// Wait for first byte in response.
|
|
|
|
_, err = reader.Peek(1)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
go entryPoint.Shutdown(context.Background())
|
|
|
|
|
|
|
|
// Make sure that new connections are not permitted anymore.
|
|
|
|
// Note that this should be true not only after Shutdown has returned,
|
|
|
|
// but technically also as early as the Shutdown has closed the listener,
|
|
|
|
// i.e. during the shutdown and before the gracetime is over.
|
|
|
|
var testOk bool
|
2024-02-19 14:44:03 +00:00
|
|
|
for range 10 {
|
2020-01-06 15:56:05 +00:00
|
|
|
loopConn, err := net.Dial("tcp", epAddr)
|
|
|
|
if err == nil {
|
|
|
|
loopConn.Close()
|
2020-03-18 13:50:06 +00:00
|
|
|
time.Sleep(100 * time.Millisecond)
|
2020-01-06 15:56:05 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
if !strings.HasSuffix(err.Error(), "connection refused") && !strings.HasSuffix(err.Error(), "reset by peer") {
|
|
|
|
t.Fatalf(`unexpected error: got %v, wanted "connection refused" or "reset by peer"`, err)
|
|
|
|
}
|
|
|
|
testOk = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if !testOk {
|
|
|
|
t.Fatal("entry point never closed")
|
|
|
|
}
|
|
|
|
|
|
|
|
// And make sure that the connection we had opened before shutting things down is still operational
|
|
|
|
|
2021-03-08 08:58:04 +00:00
|
|
|
resp, err := http.ReadResponse(reader, request)
|
2019-03-14 08:30:04 +00:00
|
|
|
require.NoError(t, err)
|
2020-01-06 15:56:05 +00:00
|
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
2019-03-14 08:30:04 +00:00
|
|
|
}
|
|
|
|
|
2022-03-17 17:02:08 +00:00
|
|
|
func startEntrypoint(entryPoint *TCPEntryPoint, router *tcprouter.Router) (net.Conn, error) {
|
2020-02-11 00:26:04 +00:00
|
|
|
go entryPoint.Start(context.Background())
|
2020-01-06 15:56:05 +00:00
|
|
|
|
|
|
|
entryPoint.SwitchRouter(router)
|
|
|
|
|
2024-02-19 14:44:03 +00:00
|
|
|
for range 10 {
|
2021-03-08 08:58:04 +00:00
|
|
|
conn, err := net.Dial("tcp", entryPoint.listener.Addr().String())
|
2020-01-06 15:56:05 +00:00
|
|
|
if err != nil {
|
2020-03-18 13:50:06 +00:00
|
|
|
time.Sleep(100 * time.Millisecond)
|
2020-01-06 15:56:05 +00:00
|
|
|
continue
|
|
|
|
}
|
2021-03-08 08:58:04 +00:00
|
|
|
|
|
|
|
return conn, err
|
2020-01-06 15:56:05 +00:00
|
|
|
}
|
2021-03-08 08:58:04 +00:00
|
|
|
|
|
|
|
return nil, errors.New("entry point never started")
|
2020-01-06 15:56:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func TestReadTimeoutWithoutFirstByte(t *testing.T) {
|
|
|
|
epConfig := &static.EntryPointsTransport{}
|
|
|
|
epConfig.SetDefaults()
|
2024-04-11 13:48:04 +00:00
|
|
|
epConfig.RespondingTimeouts.ReadTimeout = ptypes.Duration(2 * time.Second)
|
2020-01-06 15:56:05 +00:00
|
|
|
|
2019-03-14 08:30:04 +00:00
|
|
|
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
|
2020-01-06 15:56:05 +00:00
|
|
|
Address: ":0",
|
|
|
|
Transport: epConfig,
|
2019-03-14 08:30:04 +00:00
|
|
|
ForwardedHeaders: &static.ForwardedHeaders{},
|
2022-04-04 09:46:07 +00:00
|
|
|
HTTP2: &static.HTTP2Config{},
|
2022-02-14 16:18:08 +00:00
|
|
|
}, nil)
|
2019-03-14 08:30:04 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2024-09-13 13:54:04 +00:00
|
|
|
router, err := tcprouter.NewRouter()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2022-03-17 17:02:08 +00:00
|
|
|
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
2020-01-06 15:56:05 +00:00
|
|
|
rw.WriteHeader(http.StatusOK)
|
2019-03-14 08:30:04 +00:00
|
|
|
}))
|
2019-09-26 09:00:06 +00:00
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
conn, err := startEntrypoint(entryPoint, router)
|
2019-03-14 08:30:04 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
errChan := make(chan error)
|
2019-03-14 08:30:04 +00:00
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
go func() {
|
|
|
|
b := make([]byte, 2048)
|
|
|
|
_, err := conn.Read(b)
|
|
|
|
errChan <- err
|
|
|
|
}()
|
2019-03-14 08:30:04 +00:00
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
select {
|
|
|
|
case err := <-errChan:
|
|
|
|
require.Equal(t, io.EOF, err)
|
2020-03-18 13:50:06 +00:00
|
|
|
case <-time.Tick(5 * time.Second):
|
2020-01-06 15:56:05 +00:00
|
|
|
t.Error("Timeout while read")
|
|
|
|
}
|
2019-03-14 08:30:04 +00:00
|
|
|
}
|
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
func TestReadTimeoutWithFirstByte(t *testing.T) {
|
|
|
|
epConfig := &static.EntryPointsTransport{}
|
|
|
|
epConfig.SetDefaults()
|
2024-04-11 13:48:04 +00:00
|
|
|
epConfig.RespondingTimeouts.ReadTimeout = ptypes.Duration(2 * time.Second)
|
2020-01-06 15:56:05 +00:00
|
|
|
|
2019-03-14 08:30:04 +00:00
|
|
|
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
|
2020-01-06 15:56:05 +00:00
|
|
|
Address: ":0",
|
|
|
|
Transport: epConfig,
|
2019-03-14 08:30:04 +00:00
|
|
|
ForwardedHeaders: &static.ForwardedHeaders{},
|
2022-04-04 09:46:07 +00:00
|
|
|
HTTP2: &static.HTTP2Config{},
|
2022-02-14 16:18:08 +00:00
|
|
|
}, nil)
|
2019-03-14 08:30:04 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2024-09-13 13:54:04 +00:00
|
|
|
router, err := tcprouter.NewRouter()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2022-03-17 17:02:08 +00:00
|
|
|
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
2020-01-06 15:56:05 +00:00
|
|
|
rw.WriteHeader(http.StatusOK)
|
2019-03-14 08:30:04 +00:00
|
|
|
}))
|
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
conn, err := startEntrypoint(entryPoint, router)
|
2019-03-14 08:30:04 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
_, err = conn.Write([]byte("GET /some HTTP/1.1\r\n"))
|
2019-03-14 08:30:04 +00:00
|
|
|
require.NoError(t, err)
|
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
errChan := make(chan error)
|
2019-03-14 08:30:04 +00:00
|
|
|
|
2020-01-06 15:56:05 +00:00
|
|
|
go func() {
|
|
|
|
b := make([]byte, 2048)
|
|
|
|
_, err := conn.Read(b)
|
|
|
|
errChan <- err
|
|
|
|
}()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case err := <-errChan:
|
|
|
|
require.Equal(t, io.EOF, err)
|
2020-03-18 13:50:06 +00:00
|
|
|
case <-time.Tick(5 * time.Second):
|
2020-01-06 15:56:05 +00:00
|
|
|
t.Error("Timeout while read")
|
|
|
|
}
|
2019-03-14 08:30:04 +00:00
|
|
|
}
|
2024-01-02 15:40:06 +00:00
|
|
|
|
|
|
|
func TestKeepAliveMaxRequests(t *testing.T) {
|
|
|
|
epConfig := &static.EntryPointsTransport{}
|
|
|
|
epConfig.SetDefaults()
|
|
|
|
epConfig.KeepAliveMaxRequests = 3
|
|
|
|
|
|
|
|
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
|
|
|
|
Address: ":0",
|
|
|
|
Transport: epConfig,
|
|
|
|
ForwardedHeaders: &static.ForwardedHeaders{},
|
|
|
|
HTTP2: &static.HTTP2Config{},
|
|
|
|
}, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2024-09-13 13:54:04 +00:00
|
|
|
router, err := tcprouter.NewRouter()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2024-01-02 15:40:06 +00:00
|
|
|
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
rw.WriteHeader(http.StatusOK)
|
|
|
|
}))
|
|
|
|
|
|
|
|
conn, err := startEntrypoint(entryPoint, router)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
http.DefaultClient.Transport = &http.Transport{
|
|
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
|
|
return conn, nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := http.Get("http://" + entryPoint.listener.Addr().String())
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.False(t, resp.Close)
|
|
|
|
err = resp.Body.Close()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
resp, err = http.Get("http://" + entryPoint.listener.Addr().String())
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.False(t, resp.Close)
|
|
|
|
err = resp.Body.Close()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
resp, err = http.Get("http://" + entryPoint.listener.Addr().String())
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, resp.Close)
|
|
|
|
err = resp.Body.Close()
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestKeepAliveMaxTime(t *testing.T) {
|
|
|
|
epConfig := &static.EntryPointsTransport{}
|
|
|
|
epConfig.SetDefaults()
|
|
|
|
epConfig.KeepAliveMaxTime = ptypes.Duration(time.Millisecond)
|
|
|
|
|
|
|
|
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
|
|
|
|
Address: ":0",
|
|
|
|
Transport: epConfig,
|
|
|
|
ForwardedHeaders: &static.ForwardedHeaders{},
|
|
|
|
HTTP2: &static.HTTP2Config{},
|
|
|
|
}, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2024-09-13 13:54:04 +00:00
|
|
|
router, err := tcprouter.NewRouter()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2024-01-02 15:40:06 +00:00
|
|
|
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
rw.WriteHeader(http.StatusOK)
|
|
|
|
}))
|
|
|
|
|
|
|
|
conn, err := startEntrypoint(entryPoint, router)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
http.DefaultClient.Transport = &http.Transport{
|
|
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
|
|
return conn, nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := http.Get("http://" + entryPoint.listener.Addr().String())
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.False(t, resp.Close)
|
|
|
|
err = resp.Body.Close()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
time.Sleep(time.Millisecond)
|
|
|
|
|
|
|
|
resp, err = http.Get("http://" + entryPoint.listener.Addr().String())
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.True(t, resp.Close)
|
|
|
|
err = resp.Body.Close()
|
|
|
|
require.NoError(t, err)
|
|
|
|
}
|
2024-11-18 08:56:04 +00:00
|
|
|
|
|
|
|
func TestKeepAliveH2c(t *testing.T) {
|
|
|
|
epConfig := &static.EntryPointsTransport{}
|
|
|
|
epConfig.SetDefaults()
|
|
|
|
epConfig.KeepAliveMaxRequests = 1
|
|
|
|
|
|
|
|
entryPoint, err := NewTCPEntryPoint(context.Background(), &static.EntryPoint{
|
|
|
|
Address: ":0",
|
|
|
|
Transport: epConfig,
|
|
|
|
ForwardedHeaders: &static.ForwardedHeaders{},
|
|
|
|
HTTP2: &static.HTTP2Config{},
|
|
|
|
}, nil)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
router, err := tcprouter.NewRouter()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
router.SetHTTPHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
|
|
rw.WriteHeader(http.StatusOK)
|
|
|
|
}))
|
|
|
|
|
|
|
|
conn, err := startEntrypoint(entryPoint, router)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
http2Transport := &http2.Transport{
|
|
|
|
AllowHTTP: true,
|
|
|
|
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
|
|
|
return conn, nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
client := &http.Client{Transport: http2Transport}
|
|
|
|
|
|
|
|
resp, err := client.Get("http://" + entryPoint.listener.Addr().String())
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.False(t, resp.Close)
|
|
|
|
err = resp.Body.Close()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
_, err = client.Get("http://" + entryPoint.listener.Addr().String())
|
|
|
|
require.Error(t, err)
|
|
|
|
// Unlike HTTP/1, where we can directly check `resp.Close`, HTTP/2 uses a different
|
|
|
|
// mechanism: it sends a GOAWAY frame when the connection is closing.
|
|
|
|
// We can only check the error type. The error received should be poll.ErrClosed from
|
|
|
|
// the `internal/poll` package, but we cannot directly reference the error type due to
|
|
|
|
// package restrictions. Since this error message ("use of closed network connection")
|
|
|
|
// is distinct and specific, we rely on its consistency, assuming it is stable and unlikely
|
|
|
|
// to change.
|
|
|
|
require.Contains(t, err.Error(), "use of closed network connection")
|
|
|
|
}
|