130 lines
3.9 KiB
Go
130 lines
3.9 KiB
Go
package http2
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
xhttp2 "golang.org/x/net/http2"
|
|
"golang.org/x/net/http2/hpack"
|
|
)
|
|
|
|
// A zero receive window simulates a client that has stopped consuming DATA.
|
|
// Headers need no flow-control credit, so a 204 on another stream should still
|
|
// complete while the large response waits for a WINDOW_UPDATE or cancellation.
|
|
func TestSlowResponseStreamIsolationAndRecovery(t *testing.T) {
|
|
payload := strings.Repeat("x", 32<<10)
|
|
started := make(chan struct{}, 2)
|
|
written := make(chan error, 2)
|
|
server := New(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
if request.URL.Path == "/health" {
|
|
writer.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
if request.URL.Path == "/ready" {
|
|
_, _ = io.WriteString(writer, "ready")
|
|
return
|
|
}
|
|
writer.Header().Set("Content-Length", fmt.Sprint(len(payload)))
|
|
writer.(http.Flusher).Flush()
|
|
started <- struct{}{}
|
|
_, err := io.WriteString(writer, payload)
|
|
written <- err
|
|
}))
|
|
server.MaxConnections = 2
|
|
server.MaxConcurrentRequests = 2
|
|
address, stop := startServer(t, server)
|
|
defer stop()
|
|
|
|
slow := newFramePeer(t, address, xhttp2.Setting{ID: xhttp2.SettingInitialWindowSize, Val: 0})
|
|
defer slow.Close()
|
|
slow.framer.ReadMetaHeaders = hpack.NewDecoder(4096, nil)
|
|
slow.get(t, 1, "/large")
|
|
waitSignal(t, started)
|
|
|
|
// The same TCP connection can still carry response headers for other streams.
|
|
slow.get(t, 3, "/health")
|
|
readNoContent(t, slow, 3)
|
|
select {
|
|
case err := <-written:
|
|
t.Fatalf("large response escaped the zero stream window: %v", err)
|
|
default:
|
|
}
|
|
|
|
// A second connection also remains usable while the first handler is blocked.
|
|
fast := newFramePeer(t, address)
|
|
defer fast.Close()
|
|
fast.framer.ReadMetaHeaders = hpack.NewDecoder(4096, nil)
|
|
fast.get(t, 1, "/ready")
|
|
if body := fast.body(t, 1); body != "ready" {
|
|
t.Fatalf("other connection response: %q", body)
|
|
}
|
|
if stats := server.Stats(); stats.ActiveConnections != 2 || stats.RejectedRequests != 0 {
|
|
t.Fatalf("slow response affected other traffic: %+v", stats)
|
|
}
|
|
|
|
// Restoring stream credit must resume the existing response without reconnecting.
|
|
if err := slow.framer.WriteWindowUpdate(1, uint32(len(payload))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if body := slow.body(t, 1); body != payload {
|
|
t.Fatalf("resumed response mismatch: got %d bytes, want %d", len(body), len(payload))
|
|
}
|
|
select {
|
|
case err := <-written:
|
|
if err != nil {
|
|
t.Fatalf("resumed write: %v", err)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("resumed write did not return")
|
|
}
|
|
|
|
// A new zero-window stream must release its handler when the client cancels.
|
|
slow.get(t, 5, "/large")
|
|
waitSignal(t, started)
|
|
if err := slow.framer.WriteRSTStream(5, xhttp2.ErrCodeCancel); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case err := <-written:
|
|
if err == nil {
|
|
t.Fatal("canceled zero-window write unexpectedly succeeded")
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("RST_STREAM did not unblock the writer")
|
|
}
|
|
|
|
slow.get(t, 7, "/health")
|
|
readNoContent(t, slow, 7)
|
|
deadline := time.Now().Add(time.Second)
|
|
for server.Stats().ActiveRequests != 0 && time.Now().Before(deadline) {
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
if stats := server.Stats(); stats.ActiveRequests != 0 || stats.RSTStreams != 1 || stats.ProtocolErrors != 0 || stats.RejectedRequests != 0 {
|
|
t.Fatalf("flow control did not recover: %+v", stats)
|
|
}
|
|
}
|
|
|
|
func readNoContent(t *testing.T, peer *framePeer, streamID uint32) {
|
|
t.Helper()
|
|
for {
|
|
switch frame := peer.read(t).(type) {
|
|
case *xhttp2.MetaHeadersFrame:
|
|
if frame.StreamID != streamID {
|
|
continue
|
|
}
|
|
if frame.PseudoValue("status") != "204" || !frame.StreamEnded() {
|
|
t.Fatalf("stream %d: expected complete 204, got %+v", streamID, frame)
|
|
}
|
|
return
|
|
case *xhttp2.DataFrame:
|
|
t.Fatalf("unexpected DATA on zero-window stream %d", frame.StreamID)
|
|
case *xhttp2.RSTStreamFrame:
|
|
t.Fatalf("unexpected server reset: %+v", frame)
|
|
}
|
|
}
|
|
}
|