Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added a GOAWAY frame send on client transport shutdown #4248

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions internal/transport/http2_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,13 @@ func (t *http2Client) Close() error {
// should unblock it so that the goroutine eventually exits.
t.kpDormancyCond.Signal()
}
// The HTTP/2 spec mentions that a GOAWAY frame should be sent before a connection close. If this close() function
// ever starts to take in an HTTP/2 error code the peer will be able to get more information about the reason
// behind the connection close.
if err := t.framer.fr.WriteGoAway(math.MaxUint32, http2.ErrCodeNo, []byte{}); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If writing the GOAWAY fails, we still need to close the conn and do the other cleanup below, like adding an error to any streams that are still open.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's possible we want to do some refactoring here. I think I was expecting we'd pass an error to http2Client.Close, and that would dictate the code used here -- OR, pass the code in directly. Anywhere we already send a GOAWAY, we could delete that and rely on this to do it instead. Or we need a way to call Close to indicate a GOAWAY was already sent, and so Close should not send another one.

t.mu.Unlock()
return err
}
t.mu.Unlock()
t.controlBuf.finish()
t.cancel()
Expand Down
62 changes: 62 additions & 0 deletions internal/transport/transport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,68 @@ func (s) TestServerWithMisbehavedClient(t *testing.T) {
}
}

// Test that a transport level client shutdown successfully sends a GOAWAY frame to underlying connection.
func (s) TestClientCloseSendsAGoAwayFrame(t *testing.T) {
// Create a server.
lis, err := net.Listen("tcp", "localhost:0")
if err != nil {
t.Fatalf("Error while listening: #{err}")
}
defer lis.Close()
// The success channel indicates that the server's reader goroutine received a GOAWAY frame
// from the client.
success := testutils.NewChannel()
// Launch the server.
go func() {
sconn, err := lis.Accept()
if err != nil {
t.Errorf("Error while accepting: #{err}")
}
defer sconn.Close()
if _, err := io.ReadFull(sconn, make([]byte, len(clientPreface))); err != nil {
t.Errorf("Error while reading client preface: %v", err)
return
}
sfr := http2.NewFramer(sconn, sconn)
if err := sfr.WriteSettingsAck(); err != nil {
t.Errorf("Error while writing settings ack: %v", err)
return
}
// Read frames off the wire. Should expect to see a GOAWAY frame after the client closes.
for {
frame, err := sfr.ReadFrame()
if err != nil {
return
}
switch frame.(type) {
case *http2.SettingsFrame:
// Do nothing. A settings frame is expected from client preface.
case *http2.GoAwayFrame:
// Records that server successfully received a GOAWAY frame.
success.Send(nil)
return
default:
// The client should send have sent nothing but settings and GOAWAY frame.
success.Send(errors.New("The client received a frame other than settings or GOAWAY frame"))
return
}
}
}()
// Construct a client transport here to server and immediately close it to test if successfully sent GOAWAY frame.
connectCtx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
ct, err := NewClientTransport(connectCtx, context.Background(), resolver.Address{Addr: lis.Addr().String()}, ConnectOptions{}, func() {}, func(GoAwayReason) {}, func() {})
if err != nil {
t.Fatalf("Error while creating client transport: #{err}")
}
ct.Close()
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
if e, err := success.Receive(ctx); e != nil || err != nil {
t.Fatalf("Error in frame received: %v. Error receiving from channel: %v", e, err)
}
}

func (s) TestClientWithMisbehavedServer(t *testing.T) {
// Create a misbehaving server.
lis, err := net.Listen("tcp", "localhost:0")
Expand Down