Skip to content

Commit 69e8244

Browse files
Make sure we close HTTP response body (#815)
> If the returned error is nil, the [Response](https://pkg.go.dev/net/http#Response) will contain a non-nil Body which the user is expected to close. If the Body is not both read to EOF and closed, the [Client](https://pkg.go.dev/net/http#Client)'s underlying [RoundTripper](https://pkg.go.dev/net/http#RoundTripper) (typically [Transport](https://pkg.go.dev/net/http#Transport)) may not be able to re-use a persistent TCP connection to the server for a subsequent "keep-alive" request. > > *-- https://pkg.go.dev/net/http#Client.Do* But it also may leak resources 🤷 See also: - https://blevesearch.com/news/Deferred-Cleanup,-Checking-Errors,-and-Potential-Problems/ - https://stackoverflow.com/questions/47293975/should-i-error-check-close-on-a-response-body - https://manishrjain.com/must-close-golang-http-response
1 parent 7e5c85b commit 69e8244

File tree

8 files changed

+118
-11
lines changed

8 files changed

+118
-11
lines changed

client/client.go

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626

2727
"github.com/matrix-org/complement/b"
2828
"github.com/matrix-org/complement/ct"
29+
"github.com/matrix-org/complement/internal"
2930
)
3031

3132
type ctxKey string
@@ -668,6 +669,9 @@ func (c *CSAPI) MustDo(t ct.TestLike, method string, paths []string, opts ...Req
668669
// match.JSONKeyEqual("errcode", "M_INVALID_USERNAME"),
669670
// },
670671
// })
672+
//
673+
// The caller does not need to worry about closing the returned `http.Response.Body` as
674+
// this is handled automatically.
671675
func (c *CSAPI) Do(t ct.TestLike, method string, paths []string, opts ...RequestOpt) *http.Response {
672676
t.Helper()
673677
escapedPaths := make([]string, len(paths))
@@ -716,6 +720,30 @@ func (c *CSAPI) Do(t ct.TestLike, method string, paths []string, opts ...Request
716720
if err != nil {
717721
ct.Fatalf(t, "CSAPI.Do response returned error: %s", err)
718722
}
723+
// `defer` is function scoped but it's okay that we only clean up all requests at
724+
// the end. To also be clear, `defer` arguments are evaluated at the time of the
725+
// `defer` statement so we are only closing the original response body here. Our new
726+
// response body will be untouched.
727+
defer internal.CloseIO(
728+
res.Body,
729+
fmt.Sprintf(
730+
"CSAPI.Do: response body from %s %s",
731+
res.Request.Method,
732+
res.Request.URL.String(),
733+
),
734+
)
735+
736+
// Make a copy of the response body so that downstream callers can read it multiple
737+
// times if needed and don't need to worry about closing it.
738+
var resBody []byte
739+
if res.Body != nil {
740+
resBody, err = io.ReadAll(res.Body)
741+
if err != nil {
742+
ct.Fatalf(t, "CSAPI.Do failed to read response body for RetryUntil check: %s", err)
743+
}
744+
res.Body = io.NopCloser(bytes.NewBuffer(resBody))
745+
}
746+
719747
// debug log the response
720748
if c.Debug && res != nil {
721749
var dump []byte
@@ -725,19 +753,12 @@ func (c *CSAPI) Do(t ct.TestLike, method string, paths []string, opts ...Request
725753
}
726754
t.Logf("%s", string(dump))
727755
}
756+
728757
if retryUntil == nil || retryUntil.timeout == 0 {
729758
return res // don't retry
730759
}
731760

732-
// check the condition, make a copy of the response body first in case the check consumes it
733-
var resBody []byte
734-
if res.Body != nil {
735-
resBody, err = io.ReadAll(res.Body)
736-
if err != nil {
737-
ct.Fatalf(t, "CSAPI.Do failed to read response body for RetryUntil check: %s", err)
738-
}
739-
res.Body = io.NopCloser(bytes.NewBuffer(resBody))
740-
}
761+
// check the condition
741762
if retryUntil.untilFn(res) {
742763
// remake the response and return
743764
res.Body = io.NopCloser(bytes.NewBuffer(resBody))

cmd/account-snapshot/internal/sync.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import (
1212
"os"
1313
"strconv"
1414
"strings"
15+
16+
"github.com/matrix-org/complement/internal"
1517
)
1618

1719
// LoadSyncData loads sync data from disk or by doing a /sync request
@@ -75,7 +77,14 @@ func doRequest(httpCli *http.Client, req *http.Request, token string) ([]byte, e
7577
if err != nil {
7678
return nil, fmt.Errorf("failed to perform request: %w", err)
7779
}
78-
defer res.Body.Close()
80+
defer internal.CloseIO(
81+
res.Body,
82+
fmt.Sprintf(
83+
"doRequest: response body from %s %s",
84+
res.Request.Method,
85+
res.Request.URL.String(),
86+
),
87+
)
7988
if res.StatusCode != 200 {
8089
return nil, fmt.Errorf("response returned %s", res.Status)
8190
}

federation/server.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package federation
44

55
import (
6+
"bytes"
67
"context"
78
"crypto/ed25519"
89
"crypto/rand"
@@ -12,6 +13,7 @@ import (
1213
"encoding/json"
1314
"encoding/pem"
1415
"fmt"
16+
"io"
1517
"io/ioutil"
1618
"math/big"
1719
"net"
@@ -32,6 +34,7 @@ import (
3234

3335
"github.com/matrix-org/complement/config"
3436
"github.com/matrix-org/complement/ct"
37+
"github.com/matrix-org/complement/internal"
3538
)
3639

3740
// Subset of Deployment used in federation
@@ -278,6 +281,9 @@ func (s *Server) SendFederationRequest(
278281
// DoFederationRequest signs and sends an arbitrary federation request from this server, and returns the response.
279282
//
280283
// The requests will be routed according to the deployment map in `deployment`.
284+
//
285+
// The caller does not need to worry about closing the returned `http.Response.Body` as
286+
// this is handled automatically.
281287
func (s *Server) DoFederationRequest(
282288
ctx context.Context,
283289
t ct.TestLike,
@@ -297,12 +303,25 @@ func (s *Server) DoFederationRequest(
297303

298304
var resp *http.Response
299305
resp, err = httpClient.DoHTTPRequest(ctx, httpReq)
306+
defer internal.CloseIO(resp.Body, "DoFederationRequest: federation response body")
300307

301308
if httpError, ok := err.(gomatrix.HTTPError); ok {
302309
t.Logf("[SSAPI] %s %s%s => error(%d): %s (%s)", req.Method(), req.Destination(), req.RequestURI(), httpError.Code, err, time.Since(start))
303310
} else if err == nil {
304311
t.Logf("[SSAPI] %s %s%s => %d (%s)", req.Method(), req.Destination(), req.RequestURI(), resp.StatusCode, time.Since(start))
305312
}
313+
314+
// Make a copy of the response body so that downstream callers can read it multiple
315+
// times if needed and don't need to worry about closing it.
316+
var respBody []byte
317+
if resp.Body != nil {
318+
respBody, err = io.ReadAll(resp.Body)
319+
if err != nil {
320+
ct.Fatalf(t, "CSAPI.Do failed to read response body for RetryUntil check: %s", err)
321+
}
322+
resp.Body = io.NopCloser(bytes.NewBuffer(respBody))
323+
}
324+
306325
return resp, err
307326
}
308327

federation/server_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"testing"
88

99
"github.com/matrix-org/complement/config"
10+
"github.com/matrix-org/complement/internal"
1011
)
1112

1213
type fedDeploy struct {
@@ -63,10 +64,10 @@ func TestComplementServerIsSigned(t *testing.T) {
6364
return // wanted failure, got failure
6465
}
6566
}
67+
defer internal.CloseIO(resp.Body, "server response body")
6668
if !tc.wantSuccess {
6769
t.Fatalf("request succeeded when we expected it to fail")
6870
}
69-
defer resp.Body.Close()
7071

7172
if resp.StatusCode != 404 {
7273
t.Errorf("expected 404, got %d", resp.StatusCode)

internal/docker/deployer.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"time"
3232

3333
"github.com/docker/docker/client"
34+
"github.com/matrix-org/complement/internal"
3435
complementRuntime "github.com/matrix-org/complement/runtime"
3536

3637
"github.com/docker/docker/api/types/container"
@@ -668,6 +669,7 @@ func waitForContainer(ctx context.Context, docker *client.Client, hsDep *Homeser
668669
time.Sleep(50 * time.Millisecond)
669670
continue
670671
}
672+
defer internal.CloseIO(res.Body, "waitForContainer: version response body")
671673
if res.StatusCode != 200 {
672674
lastErr = fmt.Errorf("GET %s => HTTP %s", versionsURL, res.Status)
673675
time.Sleep(50 * time.Millisecond)

internal/instruction/runner.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/tidwall/gjson"
1919

2020
"github.com/matrix-org/complement/b"
21+
"github.com/matrix-org/complement/internal"
2122
)
2223

2324
// An instruction for the runner to run.
@@ -212,6 +213,15 @@ func (r *Runner) runInstructionSet(contextStr string, hsURL string, instrs []ins
212213
return err
213214
}
214215
}
216+
defer internal.CloseIO(
217+
res.Body,
218+
fmt.Sprintf(
219+
"runInstructionSet: response body from %s %s",
220+
res.Request.Method,
221+
res.Request.URL.String(),
222+
),
223+
)
224+
215225
// parse the response if we have one (if bestEffort=true then we don't return an error above)
216226
if res != nil && res.Body != nil {
217227
if i < 100 || i%200 == 0 {

internal/io.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package internal
2+
3+
import (
4+
"io"
5+
"log"
6+
)
7+
8+
// CloseIO is a little helper to close an io.Closer and log any error encountered.
9+
//
10+
// Based off of https://blevesearch.com/news/Deferred-Cleanup,-Checking-Errors,-and-Potential-Problems/
11+
//
12+
// Probably, most relevant for closing HTTP response bodies as they MUST be closed, even
13+
// if you don’t read it. https://manishrjain.com/must-close-golang-http-response
14+
//
15+
// Usage:
16+
// ```go
17+
// res, err := client.Do(req)
18+
// defer internal.CloseIO(res.Body, "request body")
19+
// ```
20+
//
21+
// Alternative to this bulky pattern:
22+
//
23+
// ```go
24+
// res, err := client.Do(req)
25+
// defer func(c io.Closer) {
26+
// if c != nil {
27+
// err := c.Close()
28+
// if err != nil {
29+
// log.Fatalf("error closing request body stream %v", err)
30+
// }
31+
// }
32+
// }(res.Body)
33+
// ```
34+
func CloseIO(c io.Closer, contextString string) {
35+
if c != nil {
36+
err := c.Close()
37+
if err != nil {
38+
// In most cases, not much we can do besides logging as we already received and
39+
// handled whatever resource this io.Closer was wrapping.
40+
log.Fatalf("error closing io.Closer (%s): %v", contextString, err)
41+
}
42+
}
43+
}

tests/federation_keys_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/tidwall/sjson"
1414

1515
"github.com/matrix-org/complement"
16+
"github.com/matrix-org/complement/internal"
1617
"github.com/matrix-org/complement/match"
1718
"github.com/matrix-org/complement/must"
1819
)
@@ -36,6 +37,7 @@ func TestInboundFederationKeys(t *testing.T) {
3637

3738
res, err := fedClient.Get("https://hs1/_matrix/key/v2/server")
3839
must.NotError(t, "failed to GET /keys", err)
40+
defer internal.CloseIO(res.Body, "server key response body")
3941

4042
var keys = map[string]ed25519.PublicKey{}
4143
var oldKeys = map[string]ed25519.PublicKey{}

0 commit comments

Comments
 (0)