250 lines
6.5 KiB
Go
250 lines
6.5 KiB
Go
package http2
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
xhttp2 "golang.org/x/net/http2"
|
|
"golang.org/x/net/http2/hpack"
|
|
)
|
|
|
|
// A raw peer makes GOAWAY and RST_STREAM observable without client retries.
|
|
type framePeer struct {
|
|
net.Conn
|
|
framer *xhttp2.Framer
|
|
}
|
|
|
|
func newFramePeer(t *testing.T, address string, settings ...xhttp2.Setting) *framePeer {
|
|
t.Helper()
|
|
c, err := net.DialTimeout("tcp", address, time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { c.Close() })
|
|
c.SetDeadline(time.Now().Add(5 * time.Second))
|
|
p := &framePeer{c, xhttp2.NewFramer(c, c)}
|
|
if _, err = io.WriteString(c, xhttp2.ClientPreface); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err = p.framer.WriteSettings(settings...); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
func (p *framePeer) get(t *testing.T, id uint32, path string) {
|
|
t.Helper()
|
|
var b bytes.Buffer
|
|
e := hpack.NewEncoder(&b)
|
|
for _, field := range []hpack.HeaderField{{Name: ":method", Value: "GET"}, {Name: ":scheme", Value: "http"}, {Name: ":authority", Value: "localhost"}, {Name: ":path", Value: path}} {
|
|
if err := e.WriteField(field); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := p.framer.WriteHeaders(xhttp2.HeadersFrameParam{StreamID: id, EndStream: true, EndHeaders: true, BlockFragment: b.Bytes()}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
func (p *framePeer) read(t *testing.T) xhttp2.Frame {
|
|
t.Helper()
|
|
f, err := p.framer.ReadFrame()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s, ok := f.(*xhttp2.SettingsFrame); ok && !s.IsAck() {
|
|
if err := p.framer.WriteSettingsAck(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return f
|
|
}
|
|
func (p *framePeer) body(t *testing.T, id uint32) string {
|
|
t.Helper()
|
|
var b bytes.Buffer
|
|
for {
|
|
switch f := p.read(t).(type) {
|
|
case *xhttp2.DataFrame:
|
|
if f.StreamID != id {
|
|
t.Fatalf("unexpected stream %d", f.StreamID)
|
|
}
|
|
b.Write(f.Data())
|
|
if f.StreamEnded() {
|
|
return b.String()
|
|
}
|
|
case *xhttp2.HeadersFrame:
|
|
if f.StreamID == id && f.StreamEnded() {
|
|
return b.String()
|
|
}
|
|
case *xhttp2.RSTStreamFrame:
|
|
t.Fatalf("unexpected RST_STREAM: %v", f)
|
|
}
|
|
}
|
|
}
|
|
func waitSignal(t *testing.T, signal <-chan struct{}) {
|
|
t.Helper()
|
|
select {
|
|
case <-signal:
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("timed out waiting for event")
|
|
}
|
|
}
|
|
func launchServer(t *testing.T, s *Server, ctx context.Context) (string, <-chan error) {
|
|
t.Helper()
|
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
done := make(chan error, 1)
|
|
go func() { done <- s.Serve(ctx, l) }()
|
|
t.Cleanup(func() {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
if err := s.Shutdown(ctx); err != nil {
|
|
t.Errorf("cleanup shutdown: %v", err)
|
|
}
|
|
l.Close()
|
|
})
|
|
return l.Addr().String(), done
|
|
}
|
|
func serveResult(t *testing.T, done <-chan error) error {
|
|
t.Helper()
|
|
select {
|
|
case err := <-done:
|
|
return err
|
|
case <-time.After(3 * time.Second):
|
|
t.Fatal("Serve did not finish")
|
|
return nil
|
|
}
|
|
}
|
|
func TestShutdownSendsGoAwayAndDrains(t *testing.T) {
|
|
started := make(chan struct{})
|
|
release := make(chan struct{})
|
|
defer close(release)
|
|
requestCtx := make(chan context.Context, 1)
|
|
s := New(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/hold" {
|
|
t.Error("new stream reached handler during drain")
|
|
return
|
|
}
|
|
requestCtx <- r.Context()
|
|
close(started)
|
|
select {
|
|
case <-release:
|
|
case <-r.Context().Done():
|
|
return
|
|
}
|
|
io.WriteString(w, "completed")
|
|
}))
|
|
address, done := launchServer(t, s, context.Background())
|
|
p := newFramePeer(t, address)
|
|
p.get(t, 1, "/hold")
|
|
waitSignal(t, started)
|
|
rctx := <-requestCtx
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
stopped := make(chan error, 1)
|
|
go func() { stopped <- s.Shutdown(ctx) }()
|
|
for {
|
|
if f, ok := p.read(t).(*xhttp2.GoAwayFrame); ok {
|
|
if f.ErrCode != xhttp2.ErrCodeNo || f.LastStreamID != 1 {
|
|
t.Fatalf("GOAWAY=%+v", f)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if err := rctx.Err(); err != nil {
|
|
t.Fatalf("graceful shutdown canceled active request: %v", err)
|
|
}
|
|
if stats := s.Stats(); stats.GoAwayFrames != 1 {
|
|
t.Fatalf("expected one observed GOAWAY frame, got %+v", stats)
|
|
}
|
|
select {
|
|
case err := <-done:
|
|
t.Fatalf("Serve returned before drain: %v", err)
|
|
default:
|
|
}
|
|
p.get(t, 3, "/late")
|
|
release <- struct{}{}
|
|
if got := p.body(t, 1); got != "completed" {
|
|
t.Fatalf("body=%q", got)
|
|
}
|
|
p.Close()
|
|
if err := serveResult(t, stopped); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := serveResult(t, done); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stats := s.Stats(); stats.ActiveConnections != 0 || stats.ActiveRequests != 0 || stats.GoAwaySent != 1 {
|
|
t.Fatalf("not drained: %+v", stats)
|
|
}
|
|
}
|
|
func TestRSTStreamCancelsOnlyOneRequest(t *testing.T) {
|
|
started := make(chan struct{}, 2)
|
|
canceled := make(chan string, 2)
|
|
release := make(chan struct{})
|
|
defer close(release)
|
|
s := New(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
started <- struct{}{}
|
|
select {
|
|
case <-r.Context().Done():
|
|
canceled <- r.URL.Path
|
|
return
|
|
case <-release:
|
|
io.WriteString(w, r.URL.Path)
|
|
}
|
|
}))
|
|
s.MaxConcurrentRequests = 2
|
|
address, _ := launchServer(t, s, context.Background())
|
|
p := newFramePeer(t, address)
|
|
p.get(t, 1, "/cancel")
|
|
p.get(t, 3, "/survive")
|
|
waitSignal(t, started)
|
|
waitSignal(t, started)
|
|
if err := p.framer.WriteRSTStream(1, xhttp2.ErrCodeCancel); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case path := <-canceled:
|
|
if path != "/cancel" {
|
|
t.Fatalf("canceled %s", path)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("RST did not cancel request")
|
|
}
|
|
if stats := s.Stats(); stats.RSTStreams != 1 {
|
|
t.Fatalf("expected one observed RST_STREAM, got %+v", stats)
|
|
}
|
|
release <- struct{}{}
|
|
if got := p.body(t, 3); got != "/survive" {
|
|
t.Fatalf("sibling body=%q", got)
|
|
}
|
|
p.get(t, 5, "/reused")
|
|
waitSignal(t, started)
|
|
release <- struct{}{}
|
|
if got := p.body(t, 5); got != "/reused" {
|
|
t.Fatalf("connection reuse body=%q", got)
|
|
}
|
|
}
|
|
func TestCanceledServeContextWaitsForShutdownDeadline(t *testing.T) {
|
|
started, canceled := make(chan struct{}), make(chan struct{})
|
|
s := New(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { close(started); <-r.Context().Done(); close(canceled) }))
|
|
s.ShutdownTimeout = 100 * time.Millisecond
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
address, done := launchServer(t, s, ctx)
|
|
p := newFramePeer(t, address)
|
|
p.get(t, 1, "/hold")
|
|
waitSignal(t, started)
|
|
cancel()
|
|
if err := serveResult(t, done); !errors.Is(err, context.DeadlineExceeded) {
|
|
t.Fatalf("Serve should report drain timeout, got %v", err)
|
|
}
|
|
waitSignal(t, canceled)
|
|
}
|