mirror of
https://github.com/zap-proto/http.git
synced 2026-10-04 03:07:19 +00:00
Do took no context, so the only bound on an exchange was the transport-wide read timeout. A caller whose own deadline was 30s could wait fifteen minutes, and a cancelled caller could not stop a read already on the wire at all. DoContext sets the conn deadline from ctx and closes the conn when ctx is cancelled — the one thing that unblocks a read in flight. A conn closed that way is never pooled, and the error is ctx's cause. A streamed body keeps ctx's hold until the stream ends. The dial honours ctx too. Do is DoContext with a background context, so the Client shape that fasthttp.HostClient satisfies is unchanged. Co-authored-by: Hanzo Dev <[email protected]>
115 lines
3.6 KiB
Go
115 lines
3.6 KiB
Go
package http_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/valyala/fasthttp"
|
|
"github.com/zap-proto/http"
|
|
)
|
|
|
|
// slow answers after hold, so a caller can be cancelled while its read is in
|
|
// flight — the one case a read deadline set at the start cannot see coming.
|
|
func slow(hold time.Duration) fasthttp.RequestHandler {
|
|
return func(c *fasthttp.RequestCtx) {
|
|
time.Sleep(hold)
|
|
c.SetBodyString("late")
|
|
}
|
|
}
|
|
|
|
func exchange(ctx context.Context, tr *http.Transport, path string) error {
|
|
req := fasthttp.AcquireRequest()
|
|
defer fasthttp.ReleaseRequest(req)
|
|
resp := fasthttp.AcquireResponse()
|
|
defer fasthttp.ReleaseResponse(resp)
|
|
req.Header.SetMethod(fasthttp.MethodGet)
|
|
req.SetRequestURI(path)
|
|
return tr.DoContext(ctx, req, resp)
|
|
}
|
|
|
|
// Cancelling the caller stops a read already waiting on the wire, and says why.
|
|
func TestDoContextCancelStopsAnInFlightRead(t *testing.T) {
|
|
addr, shutdown := listen(t, slow(5*time.Second))
|
|
defer shutdown()
|
|
tr := http.Dial("tcp", addr)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
time.AfterFunc(50*time.Millisecond, cancel)
|
|
|
|
start := time.Now()
|
|
err := exchange(ctx, tr, "/slow")
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("got %v, want context.Canceled", err)
|
|
}
|
|
if d := time.Since(start); d > time.Second {
|
|
t.Fatalf("cancellation took %v; the read was not interrupted", d)
|
|
}
|
|
}
|
|
|
|
// A deadline shorter than the transport's read timeout wins.
|
|
func TestDoContextDeadlineWins(t *testing.T) {
|
|
addr, shutdown := listen(t, slow(5*time.Second))
|
|
defer shutdown()
|
|
tr := http.Dial("tcp", addr) // default read timeout is 30s
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 80*time.Millisecond)
|
|
defer cancel()
|
|
start := time.Now()
|
|
if err := exchange(ctx, tr, "/slow"); !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("got %v, want context.DeadlineExceeded", err)
|
|
}
|
|
if d := time.Since(start); d > time.Second {
|
|
t.Fatalf("deadline took %v to fire", d)
|
|
}
|
|
}
|
|
|
|
// An already-cancelled context never touches the network.
|
|
func TestDoContextRefusesADeadContext(t *testing.T) {
|
|
tr := http.Dial("tcp", "127.0.0.1:1") // nothing listens; a dial would fail differently
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
if err := exchange(ctx, tr, "/x"); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("got %v, want context.Canceled", err)
|
|
}
|
|
}
|
|
|
|
// A connection cancellation closed must not go back to the pool: the next call
|
|
// on the same transport has to succeed on a fresh one.
|
|
func TestDoContextNeverPoolsACancelledConn(t *testing.T) {
|
|
var slowNext = true
|
|
addr, shutdown := listen(t, func(c *fasthttp.RequestCtx) {
|
|
if string(c.Path()) == "/slow" && slowNext {
|
|
time.Sleep(2 * time.Second)
|
|
}
|
|
c.SetBodyString("ok")
|
|
})
|
|
defer shutdown()
|
|
tr := http.Dial("tcp", addr)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
_ = exchange(ctx, tr, "/slow")
|
|
cancel()
|
|
|
|
if err := exchange(context.Background(), tr, "/fast"); err != nil {
|
|
t.Fatalf("the call after a cancellation failed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Plain Do is DoContext with no deadline: it still completes.
|
|
func TestDoIsUnboundedDoContext(t *testing.T) {
|
|
addr, shutdown := listen(t, func(c *fasthttp.RequestCtx) { c.SetBodyString("ok") })
|
|
defer shutdown()
|
|
tr := http.Dial("tcp", addr)
|
|
req := fasthttp.AcquireRequest()
|
|
defer fasthttp.ReleaseRequest(req)
|
|
resp := fasthttp.AcquireResponse()
|
|
defer fasthttp.ReleaseResponse(resp)
|
|
req.Header.SetMethod(fasthttp.MethodGet)
|
|
req.SetRequestURI("/")
|
|
if err := tr.Do(req, resp); err != nil || string(resp.Body()) != "ok" {
|
|
t.Fatalf("Do: err=%v body=%q", err, resp.Body())
|
|
}
|
|
}
|