mirror of
https://github.com/pgsty/minio.git
synced 2026-09-15 23:14:04 +03:00
fix(http): honor configured request header deadlines
Signed-off-by: Feng Ruohang <rh@vonng.com> (cherry picked from commit 0d48d32d7e038ae1ea5966f3d7e0cb86780a6311) Signed-off-by: Feng Ruohang <rh@vonng.com>
This commit is contained in:
@@ -34,6 +34,8 @@ type DeadlineConn struct {
|
||||
net.Conn
|
||||
readDeadline time.Duration // sets the read deadline on a connection.
|
||||
readSetAt time.Time
|
||||
readExplicit time.Time // last deadline requested by the caller.
|
||||
readDeadlineStrict bool // idle renewal must not extend readExplicit.
|
||||
writeDeadline time.Duration // sets the write deadline on a connection.
|
||||
writeSetAt time.Time
|
||||
abortReads, abortWrites atomic.Bool // A deadline was set to indicate caller wanted the conn to time out.
|
||||
@@ -59,17 +61,31 @@ func (c *DeadlineConn) setReadDeadline() {
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.abortReads.Load() {
|
||||
if c.abortReads.Load() || c.infReads.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if now.Sub(c.readSetAt) > updateInterval {
|
||||
c.Conn.SetReadDeadline(now.Add(c.readDeadline + updateInterval))
|
||||
deadline := now.Add(c.readDeadline + updateInterval)
|
||||
if c.readDeadlineStrict && !c.readExplicit.IsZero() && c.readExplicit.Before(deadline) {
|
||||
deadline = c.readExplicit
|
||||
}
|
||||
c.Conn.SetReadDeadline(deadline)
|
||||
c.readSetAt = now
|
||||
}
|
||||
}
|
||||
|
||||
// SetReadDeadlineStrict controls whether idle renewal may extend a deadline set
|
||||
// by SetReadDeadline or SetDeadline. The default is false. Explicit zero and
|
||||
// past deadlines retain their disable/cancel semantics in either mode.
|
||||
func (c *DeadlineConn) SetReadDeadlineStrict(strict bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.readDeadlineStrict = strict
|
||||
c.readSetAt = time.Time{}
|
||||
}
|
||||
|
||||
func (c *DeadlineConn) setWriteDeadline() {
|
||||
// Do not set a Write deadline, if upstream wants to cancel all reads.
|
||||
if c.writeDeadline <= 0 || c.abortWrites.Load() || c.infWrites.Load() {
|
||||
@@ -115,6 +131,7 @@ func (c *DeadlineConn) SetDeadline(t time.Time) error {
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.readSetAt = time.Time{}
|
||||
c.readExplicit = t
|
||||
c.writeSetAt = time.Time{}
|
||||
c.abortReads.Store(!t.IsZero() && time.Until(t) < 0)
|
||||
c.abortWrites.Store(!t.IsZero() && time.Until(t) < 0)
|
||||
@@ -132,6 +149,7 @@ func (c *DeadlineConn) SetReadDeadline(t time.Time) error {
|
||||
c.abortReads.Store(!t.IsZero() && time.Until(t) < 0)
|
||||
c.infReads.Store(t.IsZero())
|
||||
c.readSetAt = time.Time{}
|
||||
c.readExplicit = t
|
||||
return c.Conn.SetReadDeadline(t)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package deadlineconn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type deadlineRecorder struct {
|
||||
net.Conn
|
||||
read, write time.Time
|
||||
}
|
||||
|
||||
func (c *deadlineRecorder) SetReadDeadline(t time.Time) error { c.read = t; return nil }
|
||||
func (c *deadlineRecorder) SetWriteDeadline(t time.Time) error { c.write = t; return nil }
|
||||
func (c *deadlineRecorder) SetDeadline(t time.Time) error { c.read = t; c.write = t; return nil }
|
||||
func (c *deadlineRecorder) Read([]byte) (int, error) { return 0, io.EOF }
|
||||
func (c *deadlineRecorder) Write(b []byte) (int, error) { return len(b), nil }
|
||||
|
||||
func TestStrictReadDeadline(t *testing.T) {
|
||||
for _, both := range []bool{false, true} {
|
||||
name := "SetReadDeadline"
|
||||
if both {
|
||||
name = "SetDeadline"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
raw := &deadlineRecorder{}
|
||||
conn := New(raw).WithReadDeadline(time.Second).WithWriteDeadline(time.Second)
|
||||
conn.SetReadDeadlineStrict(true)
|
||||
absolute := time.Now().Add(100 * time.Millisecond)
|
||||
if both {
|
||||
conn.SetDeadline(absolute)
|
||||
} else {
|
||||
conn.SetReadDeadline(absolute)
|
||||
}
|
||||
conn.Read(nil)
|
||||
if !raw.read.Equal(absolute) {
|
||||
t.Fatalf("absolute read deadline extended: %s, want %s", raw.read, absolute)
|
||||
}
|
||||
// Strictness only affects reads. Existing rolling writes remain unchanged.
|
||||
conn.SetWriteDeadline(absolute)
|
||||
conn.Write([]byte("x"))
|
||||
if !raw.write.After(absolute) {
|
||||
t.Fatal("strict read mode changed write semantics")
|
||||
}
|
||||
// Body phase can renew even when the header deadline was shorter than idle.
|
||||
conn.SetReadDeadlineStrict(false)
|
||||
conn.Read(nil)
|
||||
if !raw.read.After(absolute) {
|
||||
t.Fatal("body deadline did not renew")
|
||||
}
|
||||
// Keep-alive installs a new absolute deadline; do not reuse the first header's cap.
|
||||
next := time.Now().Add(200 * time.Millisecond)
|
||||
conn.SetReadDeadlineStrict(true)
|
||||
conn.SetReadDeadline(next)
|
||||
conn.Read(nil)
|
||||
if !raw.read.Equal(next) {
|
||||
t.Fatalf("second deadline = %s, want %s", raw.read, next)
|
||||
}
|
||||
// A longer header deadline must still allow the socket idle bound.
|
||||
later := time.Now().Add(time.Hour)
|
||||
conn.SetReadDeadline(later)
|
||||
conn.Read(nil)
|
||||
if !raw.read.Before(later) || raw.read.Before(time.Now()) {
|
||||
t.Fatal("idle deadline not applied within absolute cap")
|
||||
}
|
||||
// Explicit zero disables both the absolute cap and idle renewal.
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
conn.Read(nil)
|
||||
if !raw.read.IsZero() {
|
||||
t.Fatal("zero deadline re-enabled idle timeout")
|
||||
}
|
||||
past := time.Unix(1, 0)
|
||||
conn.SetReadDeadline(past)
|
||||
if _, err := conn.Read(nil); err == nil {
|
||||
t.Fatal("past deadline did not abort")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultReadDeadlineStillRenews(t *testing.T) {
|
||||
raw := &deadlineRecorder{}
|
||||
conn := New(raw).WithReadDeadline(time.Second)
|
||||
absolute := time.Now().Add(100 * time.Millisecond)
|
||||
conn.SetReadDeadline(absolute)
|
||||
conn.Read(nil)
|
||||
if !raw.read.After(absolute) {
|
||||
t.Fatal("default internode-style read no longer renews")
|
||||
}
|
||||
if Unwrap(conn) != raw {
|
||||
t.Fatal("Unwrap changed the underlying connection")
|
||||
}
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
conn.Read(nil)
|
||||
if !raw.read.IsZero() {
|
||||
t.Fatal("default zero deadline no longer disables timeout")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictExpiredFutureReadDeadline(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
conn := New(server).WithReadDeadline(time.Second)
|
||||
conn.SetReadDeadlineStrict(true)
|
||||
absolute := time.Now().Add(30 * time.Millisecond)
|
||||
conn.SetReadDeadline(absolute)
|
||||
time.Sleep(60 * time.Millisecond)
|
||||
_, err := conn.Read(make([]byte, 1))
|
||||
var ne net.Error
|
||||
if !errors.As(err, &ne) || !ne.Timeout() {
|
||||
t.Fatalf("expired absolute read: %v", err)
|
||||
}
|
||||
if time.Since(absolute) > 300*time.Millisecond {
|
||||
t.Fatal("expired future deadline was extended by idle renewal")
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentStrictReadDeadline(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
conn := New(server).WithReadDeadline(time.Second)
|
||||
conn.SetReadDeadlineStrict(true)
|
||||
var wg sync.WaitGroup
|
||||
wg.Go(func() {
|
||||
for range 100 {
|
||||
conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
conn.SetReadDeadlineStrict(false)
|
||||
conn.SetReadDeadlineStrict(true)
|
||||
}
|
||||
conn.Close()
|
||||
})
|
||||
for {
|
||||
if _, err := conn.Read(make([]byte, 1)); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// Force multiple real update intervals without resetting the explicit cap.
|
||||
func TestStrictReadDeadlineRepeatedRenewal(t *testing.T) {
|
||||
raw := &deadlineRecorder{}
|
||||
conn := New(raw).WithReadDeadline(2 * time.Second)
|
||||
conn.SetReadDeadlineStrict(true)
|
||||
absolute := time.Now().Add(time.Second)
|
||||
conn.SetReadDeadline(absolute)
|
||||
for range 3 {
|
||||
conn.Read(nil)
|
||||
if !raw.read.Equal(absolute) {
|
||||
t.Fatalf("idle renewal changed absolute deadline: %s, want %s", raw.read, absolute)
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//go:build linux
|
||||
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/deadlineconn"
|
||||
)
|
||||
|
||||
// The optional drive dialer must retain its legacy rolling read semantics.
|
||||
func TestInternodeDialReadDeadline(t *testing.T) {
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
accepted := make(chan net.Conn, 1)
|
||||
go func() {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
accepted <- nil
|
||||
return
|
||||
}
|
||||
accepted <- conn
|
||||
}()
|
||||
dial := NewInternodeDialContext(time.Second, TCPOptions{DriveOPTimeout: func() time.Duration { return 200 * time.Millisecond }})
|
||||
conn, err := dial(t.Context(), "tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
peer := <-accepted
|
||||
if peer == nil {
|
||||
t.Fatal("accept failed")
|
||||
}
|
||||
defer peer.Close()
|
||||
if _, ok := conn.(*deadlineconn.DeadlineConn); !ok {
|
||||
t.Fatalf("drive connection type %T", conn)
|
||||
}
|
||||
sent := make(chan error, 1)
|
||||
go func() {
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
if _, err := io.WriteString(peer, "a"); err != nil {
|
||||
sent <- err
|
||||
return
|
||||
}
|
||||
time.Sleep(700 * time.Millisecond)
|
||||
_, err := io.WriteString(peer, "b")
|
||||
sent <- err
|
||||
}()
|
||||
conn.SetReadDeadline(time.Now().Add(50 * time.Millisecond))
|
||||
var b [1]byte
|
||||
if _, err := io.ReadFull(conn, b[:]); err != nil || b[0] != 'a' {
|
||||
t.Fatalf("rolling explicit deadline: %q %v", b, err)
|
||||
}
|
||||
conn.SetReadDeadline(time.Time{})
|
||||
if _, err := io.ReadFull(conn, b[:]); err != nil || b[0] != 'b' {
|
||||
t.Fatalf("disabled read deadline: %q %v", b, err)
|
||||
}
|
||||
if err := <-sent; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn.SetReadDeadline(time.Now().Add(time.Minute))
|
||||
_, err = conn.Read(b[:])
|
||||
var ne net.Error
|
||||
if !errors.As(err, &ne) || !ne.Timeout() {
|
||||
t.Fatalf("drive idle timeout: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -70,7 +70,10 @@ func (listener *httpListener) Accept() (conn net.Conn, err error) {
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
return deadlineconn.New(result.conn).WithReadDeadline(listener.opts.IdleTimeout).WithWriteDeadline(listener.opts.IdleTimeout), result.err
|
||||
conn := deadlineconn.New(result.conn).WithReadDeadline(listener.opts.IdleTimeout).WithWriteDeadline(listener.opts.IdleTimeout)
|
||||
// Server.Init switches to rolling reads only after HTTP/1 headers are read.
|
||||
conn.SetReadDeadlineStrict(true)
|
||||
return conn, nil
|
||||
case <-listener.ctxDoneCh:
|
||||
}
|
||||
return nil, syscall.EINVAL
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
"github.com/minio/minio/internal/deadlineconn"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -123,6 +124,32 @@ func (srv *Server) Init(listenCtx context.Context, listenErrCallback func(listen
|
||||
srv.listener = listener
|
||||
srv.listenerMutex.Unlock()
|
||||
|
||||
connState := srv.ConnState
|
||||
srv.ConnState = func(conn net.Conn, state http.ConnState) {
|
||||
raw := conn
|
||||
if tlsConn, ok := raw.(*tls.Conn); ok {
|
||||
if tlsConn.ConnectionState().NegotiatedProtocol == "h2" {
|
||||
// HTTP/2 owns its stream deadlines; do not change the connection.
|
||||
raw = nil
|
||||
} else {
|
||||
raw = tlsConn.NetConn()
|
||||
}
|
||||
}
|
||||
if dc, ok := raw.(*deadlineconn.DeadlineConn); ok {
|
||||
switch state {
|
||||
case http.StateNew, http.StateIdle:
|
||||
dc.SetReadDeadlineStrict(true)
|
||||
case http.StateActive:
|
||||
// net/http has finished reading the headers, including buffered
|
||||
// requests. Keep ReadTimeout as a rolling idle limit for uploads.
|
||||
dc.SetReadDeadlineStrict(false)
|
||||
}
|
||||
}
|
||||
if connState != nil {
|
||||
connState(conn, state)
|
||||
}
|
||||
}
|
||||
|
||||
var l net.Listener = listener
|
||||
if tlsConfig != nil {
|
||||
l = tls.NewListener(listener, tlsConfig)
|
||||
|
||||
@@ -0,0 +1,635 @@
|
||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
||||
//
|
||||
// This file is part of MinIO Object Storage stack
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
stdhttp "net/http"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/minio/minio/internal/deadlineconn"
|
||||
)
|
||||
|
||||
// Real sockets exercise net/http's TLS, read-ahead and connection-state transitions.
|
||||
func startDeadlineServer(t *testing.T, secure bool, idle, header time.Duration, handler stdhttp.Handler, hook func(net.Conn, stdhttp.ConnState), configure ...func(*Server)) (*Server, string) {
|
||||
t.Helper()
|
||||
srv := NewServer([]string{"127.0.0.1:0"}).UseHandler(handler).
|
||||
UseTCPOptions(TCPOptions{IdleTimeout: idle}).UseIdleTimeout(idle).
|
||||
UseReadTimeout(idle).UseWriteTimeout(idle).UseReadHeaderTimeout(header)
|
||||
srv.ConnState = hook
|
||||
for _, fn := range configure {
|
||||
fn(srv)
|
||||
}
|
||||
if secure {
|
||||
cert, err := getTLSCert()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv.UseTLSConfig(&tls.Config{Certificates: []tls.Certificate{cert}, NextProtos: []string{"http/1.1", "h2"}})
|
||||
}
|
||||
serve, err := srv.Init(context.Background(), func(_ string, err error) { t.Error(err) })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- serve() }()
|
||||
t.Cleanup(func() {
|
||||
srv.Close()
|
||||
select {
|
||||
case err := <-done:
|
||||
if !errors.Is(err, stdhttp.ErrServerClosed) {
|
||||
t.Errorf("Serve: %v", err)
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Error("Serve did not stop")
|
||||
}
|
||||
})
|
||||
return srv, srv.listener.Addr().String()
|
||||
}
|
||||
|
||||
func dialDeadlineServer(t *testing.T, addr string, secure bool) net.Conn {
|
||||
t.Helper()
|
||||
conn, err := net.DialTimeout("tcp", addr, time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { conn.Close() })
|
||||
if err := conn.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if secure {
|
||||
tc := tls.Client(conn, &tls.Config{InsecureSkipVerify: true, NextProtos: []string{"http/1.1"}})
|
||||
if err := tc.Handshake(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return tc
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
func writeDeadlineRequest(t *testing.T, conn net.Conn, data string) {
|
||||
t.Helper()
|
||||
if _, err := io.WriteString(conn, data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func readDeadlineResponse(t *testing.T, r *bufio.Reader) {
|
||||
t.Helper()
|
||||
resp, err := stdhttp.ReadResponse(r, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != stdhttp.StatusNoContent {
|
||||
t.Fatalf("status: %s", resp.Status)
|
||||
}
|
||||
if _, err := io.Copy(io.Discard, resp.Body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func requireDeadlineRejection(t *testing.T, conn net.Conn) {
|
||||
t.Helper()
|
||||
resp, err := stdhttp.ReadResponse(bufio.NewReader(conn), nil)
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
t.Fatalf("request accepted: %s", resp.Status)
|
||||
}
|
||||
var ne net.Error
|
||||
if errors.As(err, &ne) && ne.Timeout() {
|
||||
t.Fatalf("client timed out before server rejected request: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerReadHeaderDeadline(t *testing.T) {
|
||||
for _, secure := range []bool{false, true} {
|
||||
for _, second := range []bool{false, true} {
|
||||
for _, trickle := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("tls=%t/second=%t/trickle=%t", secure, second, trickle), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
var calls atomic.Int32
|
||||
_, addr := startDeadlineServer(t, secure, 2*time.Second, 650*time.Millisecond, stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) { calls.Add(1); w.WriteHeader(204) }), nil)
|
||||
conn := dialDeadlineServer(t, addr, secure)
|
||||
expected := int32(0)
|
||||
if second {
|
||||
writeDeadlineRequest(t, conn, "GET /first HTTP/1.1\r\nHost: localhost\r\n\r\n")
|
||||
readDeadlineResponse(t, bufio.NewReader(conn))
|
||||
expected = 1
|
||||
// Header time starts anew for the second request, after the keep-alive wait.
|
||||
time.Sleep(750 * time.Millisecond)
|
||||
}
|
||||
writeDeadlineRequest(t, conn, "GET /slow HTTP/1.1\r\nHost: localhost\r\nX-Slow: ")
|
||||
if trickle {
|
||||
for range 10 {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if _, err := io.WriteString(conn, "x"); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
time.Sleep(950 * time.Millisecond)
|
||||
}
|
||||
_, _ = io.WriteString(conn, "done\r\n\r\n")
|
||||
requireDeadlineRejection(t, conn)
|
||||
if got := calls.Load(); got != expected {
|
||||
t.Fatalf("handler calls = %d, want %d", got, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerKeepAliveDeadline(t *testing.T) {
|
||||
for _, secure := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("tls=%t", secure), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, addr := startDeadlineServer(t, secure, 800*time.Millisecond, 200*time.Millisecond, stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) { w.WriteHeader(204) }), nil)
|
||||
conn := dialDeadlineServer(t, addr, secure)
|
||||
br := bufio.NewReader(conn)
|
||||
for range 2 {
|
||||
writeDeadlineRequest(t, conn, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
|
||||
readDeadlineResponse(t, br)
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}
|
||||
// An incomplete method must not renew the keep-alive deadline on each byte.
|
||||
writeDeadlineRequest(t, conn, "G")
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
_, _ = io.WriteString(conn, "E")
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
_, _ = io.WriteString(conn, "T")
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
conn.SetReadDeadline(time.Now().Add(150 * time.Millisecond))
|
||||
requireDeadlineRejection(t, conn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runContinuousUpload(t *testing.T, secure, chunked, expect bool, idle, period time.Duration, chunks int) {
|
||||
t.Helper()
|
||||
nread := make(chan int64, 1)
|
||||
_, addr := startDeadlineServer(t, secure, idle, 2*time.Second, stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
n, err := io.Copy(io.Discard, r.Body)
|
||||
if err != nil {
|
||||
t.Errorf("body read: %v", err)
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
nread <- n
|
||||
w.WriteHeader(204)
|
||||
}), nil)
|
||||
conn := dialDeadlineServer(t, addr, secure)
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(chunks)*period + 5*time.Second))
|
||||
headers := "POST / HTTP/1.1\r\nHost: localhost\r\n"
|
||||
if chunked {
|
||||
headers += "Transfer-Encoding: chunked\r\n"
|
||||
} else {
|
||||
headers += fmt.Sprintf("Content-Length: %d\r\n", chunks)
|
||||
}
|
||||
if expect {
|
||||
headers += "Expect: 100-continue\r\n"
|
||||
}
|
||||
writeDeadlineRequest(t, conn, headers+"\r\n")
|
||||
br := bufio.NewReader(conn)
|
||||
if expect {
|
||||
resp, err := stdhttp.ReadResponse(br, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 100 {
|
||||
t.Fatalf("expected 100 Continue, got %s", resp.Status)
|
||||
}
|
||||
}
|
||||
start := time.Now()
|
||||
for range chunks {
|
||||
time.Sleep(period)
|
||||
if chunked {
|
||||
writeDeadlineRequest(t, conn, "1\r\nx\r\n")
|
||||
} else {
|
||||
writeDeadlineRequest(t, conn, "x")
|
||||
}
|
||||
}
|
||||
if chunked {
|
||||
writeDeadlineRequest(t, conn, "0\r\n\r\n")
|
||||
}
|
||||
readDeadlineResponse(t, br)
|
||||
if n := <-nread; n != int64(chunks) {
|
||||
t.Fatalf("read %d bytes, want %d", n, chunks)
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed <= idle {
|
||||
t.Fatalf("upload took %s, must exceed idle %s", elapsed, idle)
|
||||
} else {
|
||||
t.Logf("continuous upload %s > idle %s", elapsed, idle)
|
||||
}
|
||||
// Verify a fresh request after body EOF and read-deadline cancellation.
|
||||
writeDeadlineRequest(t, conn, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
|
||||
readDeadlineResponse(t, br)
|
||||
}
|
||||
|
||||
func TestServerContinuousUpload(t *testing.T) {
|
||||
for _, secure := range []bool{false, true} {
|
||||
for _, chunked := range []bool{false, true} {
|
||||
for _, expect := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("tls=%t/chunked=%t/expect=%t", secure, chunked, expect), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
runContinuousUpload(t, secure, chunked, expect, 300*time.Millisecond, 80*time.Millisecond, 16)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerDefaultIdleLongUpload(t *testing.T) {
|
||||
if os.Getenv("SILO_TEST_LONG_UPLOAD") != "1" {
|
||||
t.Skip("set SILO_TEST_LONG_UPLOAD=1 for >30s default-idle regression")
|
||||
}
|
||||
t.Parallel()
|
||||
for _, secure := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("tls=%t", secure), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
runContinuousUpload(t, secure, false, false, DefaultIdleTimeout, time.Second, 33)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerIdleBodyDeadline(t *testing.T) {
|
||||
for _, secure := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("tls=%t", secure), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
result := make(chan error, 1)
|
||||
_, addr := startDeadlineServer(t, secure, 200*time.Millisecond, time.Second, stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
_, err := io.Copy(io.Discard, r.Body)
|
||||
result <- err
|
||||
w.WriteHeader(400)
|
||||
}), nil)
|
||||
conn := dialDeadlineServer(t, addr, secure)
|
||||
writeDeadlineRequest(t, conn, "PUT / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 2\r\n\r\nx")
|
||||
select {
|
||||
case err := <-result:
|
||||
var ne net.Error
|
||||
if !errors.As(err, &ne) || !ne.Timeout() {
|
||||
t.Fatalf("expected server body timeout, got %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("idle body did not time out")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerBackgroundReadNoDeadline(t *testing.T) {
|
||||
for _, secure := range []bool{false, true} {
|
||||
for _, body := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("tls=%t/body=%t", secure, body), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, addr := startDeadlineServer(t, secure, 150*time.Millisecond, time.Second, stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
if _, err := io.Copy(io.Discard, r.Body); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
t.Errorf("background read canceled handler: %v", r.Context().Err())
|
||||
return
|
||||
case <-time.After(700 * time.Millisecond):
|
||||
}
|
||||
w.WriteHeader(204)
|
||||
}), nil)
|
||||
conn := dialDeadlineServer(t, addr, secure)
|
||||
req := "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n"
|
||||
if body {
|
||||
req = "POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 1\r\n\r\nx"
|
||||
}
|
||||
writeDeadlineRequest(t, conn, req)
|
||||
readDeadlineResponse(t, bufio.NewReader(conn))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerTLSHandshakeReadDeadline(t *testing.T) {
|
||||
_, addr := startDeadlineServer(t, true, 2*time.Second, 200*time.Millisecond, stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) { t.Error("unexpected handler") }), nil)
|
||||
conn := dialDeadlineServer(t, addr, false)
|
||||
conn.SetReadDeadline(time.Now().Add(time.Second))
|
||||
// Partial TLS record header: server must wait for bytes and enforce its own deadline.
|
||||
writeDeadlineRequest(t, conn, "\x16\x03")
|
||||
var b [1]byte
|
||||
_, err := conn.Read(b[:])
|
||||
if err == nil {
|
||||
t.Fatal("expected handshake failure")
|
||||
}
|
||||
var ne net.Error
|
||||
if errors.As(err, &ne) && ne.Timeout() {
|
||||
t.Fatalf("client timeout before server handshake deadline: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerConnStateHook(t *testing.T) {
|
||||
for _, secure := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("tls=%t", secure), func(t *testing.T) {
|
||||
states := make(chan stdhttp.ConnState, 16)
|
||||
_, addr := startDeadlineServer(t, secure, time.Second, 500*time.Millisecond, stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) { w.WriteHeader(204) }), func(_ net.Conn, s stdhttp.ConnState) { states <- s })
|
||||
conn := dialDeadlineServer(t, addr, secure)
|
||||
br := bufio.NewReader(conn)
|
||||
for range 2 {
|
||||
writeDeadlineRequest(t, conn, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
|
||||
readDeadlineResponse(t, br)
|
||||
}
|
||||
for _, want := range []stdhttp.ConnState{stdhttp.StateNew, stdhttp.StateActive, stdhttp.StateIdle, stdhttp.StateActive, stdhttp.StateIdle} {
|
||||
select {
|
||||
case got := <-states:
|
||||
if got != want {
|
||||
t.Fatalf("state %v, want %v", got, want)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("missing state %v", want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerHTTP2Deadlines(t *testing.T) {
|
||||
bodyErr := make(chan error, 1)
|
||||
started := make(chan struct{})
|
||||
var connections atomic.Int32
|
||||
_, addr := startDeadlineServer(t, true, 400*time.Millisecond, 200*time.Millisecond, stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
if r.ProtoMajor != 2 {
|
||||
t.Errorf("expected HTTP/2, got %s", r.Proto)
|
||||
}
|
||||
if r.Method == "PUT" {
|
||||
// Isolate the native read timer: otherwise the equal-duration write
|
||||
// timer can win and close the stream with a different error.
|
||||
if err := stdhttp.NewResponseController(w).SetWriteDeadline(time.Time{}); err != nil {
|
||||
bodyErr <- err
|
||||
close(started)
|
||||
return
|
||||
}
|
||||
close(started)
|
||||
_, err := io.Copy(io.Discard, r.Body)
|
||||
bodyErr <- err
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(204)
|
||||
}), func(_ net.Conn, state stdhttp.ConnState) {
|
||||
if state == stdhttp.StateNew {
|
||||
connections.Add(1)
|
||||
}
|
||||
}, func(srv *Server) { srv.IdleTimeout = 3 * time.Second })
|
||||
// Configure only HTTP/2 so the server's HTTP/1-first ALPN preference
|
||||
// cannot turn this into an HTTP/1 smoke test.
|
||||
protocols := new(stdhttp.Protocols)
|
||||
protocols.SetHTTP2(true)
|
||||
tr := &stdhttp.Transport{Protocols: protocols, TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
||||
t.Cleanup(tr.CloseIdleConnections)
|
||||
client := &stdhttp.Client{Transport: tr, Timeout: 3 * time.Second}
|
||||
for range 2 {
|
||||
resp, err := client.Get("https://" + addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.TLS == nil || resp.TLS.NegotiatedProtocol != "h2" || resp.Proto != "HTTP/2.0" || resp.StatusCode != 204 {
|
||||
t.Fatalf("unexpected %s %s", resp.Proto, resp.Status)
|
||||
}
|
||||
}
|
||||
pr, pw := io.Pipe()
|
||||
defer pr.Close()
|
||||
defer pw.Close()
|
||||
req, err := stdhttp.NewRequest("PUT", "https://"+addr, pr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
resp, err := client.Do(req)
|
||||
if resp != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
done <- err
|
||||
}()
|
||||
select {
|
||||
case <-started:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("HTTP/2 PUT handler did not start")
|
||||
}
|
||||
// A stalled stream must not set a deadline on other multiplexed requests.
|
||||
resp, err := client.Get("https://" + addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 204 {
|
||||
t.Fatalf("healthy HTTP/2 stream: %s", resp.Status)
|
||||
}
|
||||
// Native HTTP/2 still applies its existing per-stream ReadTimeout.
|
||||
select {
|
||||
case err := <-bodyErr:
|
||||
var ne net.Error
|
||||
if !errors.As(err, &ne) || !ne.Timeout() {
|
||||
t.Fatalf("expected native HTTP/2 read timeout, got %v", err)
|
||||
}
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("native HTTP/2 stream timeout was lost")
|
||||
}
|
||||
pw.Close()
|
||||
<-done
|
||||
resp, err = client.Get("https://" + addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if connections.Load() != 1 {
|
||||
t.Fatalf("HTTP/2 stream timeout replaced the connection: %d connections", connections.Load())
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerEarlyBodyClose(t *testing.T) {
|
||||
for _, secure := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("tls=%t", secure), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, addr := startDeadlineServer(t, secure, 200*time.Millisecond, time.Second, stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
if err := r.Body.Close(); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
w.WriteHeader(204)
|
||||
}), nil)
|
||||
conn := dialDeadlineServer(t, addr, secure)
|
||||
br := bufio.NewReader(conn)
|
||||
writeDeadlineRequest(t, conn, "PUT / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 2\r\n\r\nx")
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
writeDeadlineRequest(t, conn, "y")
|
||||
readDeadlineResponse(t, br)
|
||||
writeDeadlineRequest(t, conn, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
|
||||
readDeadlineResponse(t, br)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerPipelinedDeadline(t *testing.T) {
|
||||
for _, secure := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("tls=%t", secure), func(t *testing.T) {
|
||||
_, addr := startDeadlineServer(t, secure, 300*time.Millisecond, 200*time.Millisecond, stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
if _, err := io.Copy(io.Discard, r.Body); err != nil {
|
||||
t.Error(err)
|
||||
w.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(204)
|
||||
}), nil)
|
||||
conn := dialDeadlineServer(t, addr, secure)
|
||||
// Second request headers arrive in the first socket read; body continues
|
||||
// past ReadTimeout to require the buffered request's StateActive hook.
|
||||
writeDeadlineRequest(t, conn, "GET /first HTTP/1.1\r\nHost: localhost\r\n\r\nPUT /second HTTP/1.1\r\nHost: localhost\r\nContent-Length: 8\r\n\r\n")
|
||||
br := bufio.NewReader(conn)
|
||||
readDeadlineResponse(t, br)
|
||||
for range 8 {
|
||||
time.Sleep(80 * time.Millisecond)
|
||||
writeDeadlineRequest(t, conn, "x")
|
||||
}
|
||||
readDeadlineResponse(t, br)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerHijackedDeadline(t *testing.T) {
|
||||
for _, secure := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("tls=%t", secure), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
done := make(chan error, 1)
|
||||
_, addr := startDeadlineServer(t, secure, 150*time.Millisecond, time.Second, stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
conn, rw, err := stdhttp.NewResponseController(w).Hijack()
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
if !secure {
|
||||
if _, ok := deadlineconn.Unwrap(conn).(*net.TCPConn); !ok {
|
||||
done <- errors.New("grid-style Unwrap no longer returns TCPConn")
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err = rw.WriteString("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: test\r\n\r\n")
|
||||
if err == nil {
|
||||
err = rw.Flush()
|
||||
}
|
||||
if err != nil {
|
||||
done <- err
|
||||
return
|
||||
}
|
||||
b, err := rw.ReadByte()
|
||||
if err == nil {
|
||||
_, err = conn.Write([]byte{b})
|
||||
}
|
||||
done <- err
|
||||
}), nil)
|
||||
conn := dialDeadlineServer(t, addr, secure)
|
||||
writeDeadlineRequest(t, conn, "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: Upgrade\r\nUpgrade: test\r\n\r\n")
|
||||
br := bufio.NewReader(conn)
|
||||
resp, err := stdhttp.ReadResponse(br, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != 101 {
|
||||
t.Fatalf("expected upgrade, got %s", resp.Status)
|
||||
}
|
||||
time.Sleep(700 * time.Millisecond)
|
||||
writeDeadlineRequest(t, conn, "x")
|
||||
b, err := br.ReadByte()
|
||||
if err != nil || b != 'x' {
|
||||
t.Fatalf("hijacked echo: %q, %v", b, err)
|
||||
}
|
||||
if err := <-done; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runContinuousDownload(t *testing.T, secure bool, idle, period time.Duration, chunks int) {
|
||||
t.Helper()
|
||||
_, addr := startDeadlineServer(t, secure, idle, 2*time.Second, stdhttp.HandlerFunc(func(w stdhttp.ResponseWriter, r *stdhttp.Request) {
|
||||
w.Header().Set("Content-Length", fmt.Sprint(chunks))
|
||||
for range chunks {
|
||||
time.Sleep(period)
|
||||
if _, err := io.WriteString(w, "x"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if err := stdhttp.NewResponseController(w).Flush(); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}), nil)
|
||||
conn := dialDeadlineServer(t, addr, secure)
|
||||
conn.SetDeadline(time.Now().Add(time.Duration(chunks)*period + 5*time.Second))
|
||||
start := time.Now()
|
||||
writeDeadlineRequest(t, conn, "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n")
|
||||
resp, err := stdhttp.ReadResponse(bufio.NewReader(conn), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
n, err := io.Copy(io.Discard, resp.Body)
|
||||
if err != nil || n != int64(chunks) {
|
||||
t.Fatalf("download: %d bytes, %v", n, err)
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed <= idle {
|
||||
t.Fatalf("download must outlast idle: %s <= %s", elapsed, idle)
|
||||
} else {
|
||||
t.Logf("continuous download %s > idle %s", elapsed, idle)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerContinuousDownload(t *testing.T) {
|
||||
for _, secure := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("tls=%t", secure), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
runContinuousDownload(t, secure, 300*time.Millisecond, 80*time.Millisecond, 16)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerDefaultIdleLongDownload(t *testing.T) {
|
||||
if os.Getenv("SILO_TEST_LONG_UPLOAD") != "1" {
|
||||
t.Skip("set SILO_TEST_LONG_UPLOAD=1 for >30s default-idle transfer regressions")
|
||||
}
|
||||
t.Parallel()
|
||||
for _, secure := range []bool{false, true} {
|
||||
t.Run(fmt.Sprintf("tls=%t", secure), func(t *testing.T) {
|
||||
t.Parallel()
|
||||
runContinuousDownload(t, secure, DefaultIdleTimeout, time.Second, 33)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user