Skip to content

x/net/http2: indefinite hangs when closing response body #48908

Open
@neild

Description

@neild

Closing a response body can block indefinitely under at least two circumstances:

Closing a body with unread data returns flow-control tokens for the unread data. If the underlying net.Conn for the request is write-blocked, the attempt to return the tokens can block either while acquiring the stream write mutex, or while writing the window update to the connection.

Closing a response body waits for the request stream to be cleaned up, or the request context to expire, whichever comes first. This ensures that there is no lingering per-request state remaining after a nil response from resp.Body.Close. (We don't guarantee this property in the package docs, but we do have tests asserting this property. For example, we assume that a request no longer counts against the concurrency limit after a nil response from Close.) If the request writer is blocked reading the request body, we currently have no way to interrupt it and resp.Body.Close will block until the request context is canceled (if ever).

My first thought was that closing the response body could close the request body, interrupting the request writer, but this will require caution: The RoundTripper contract permits RoundTrip to access fields of the request up to the point where the request body is closed. If we change the place we close the request body, we need to be very cautious that we don't access any part of the request after that point.

Test for this second case:

  func TestTransportCloseResponseBodyWhileRequestBodyHangs(t *testing.T) {
          st := newServerTester(t, func(w http.ResponseWriter, r *http.Request) {
                  w.WriteHeader(200)
                  io.Copy(io.Discard, r.Body)
          }, optOnlyServer)
          defer st.Close()

          tr := &Transport{TLSClientConfig: tlsConfigInsecure}
          defer tr.CloseIdleConnections()

          pr, pw := net.Pipe()
          req, err := http.NewRequest("GET", st.ts.URL, pr)
          if err != nil {
                  t.Fatal(err)
          }
          res, err := tr.RoundTrip(req)
          if err != nil {
                  t.Fatal(err)
          }
          res.Body.Close()
          pw.Close()
  }

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions