From dad46a0ea5540cf8300da6b8ea91a7c07a27fe9c Mon Sep 17 00:00:00 2001 From: Michael Kaplan Date: Thu, 6 Nov 2025 18:27:22 -0500 Subject: [PATCH 1/6] Ensure full response body is always read to avoid unnecessary RST_STREAM and PING frames --- api/metrics/client.go | 13 +++++++++++++ tests/fixture/tmpnet/check_monitoring.go | 7 ++++++- tests/fixture/tmpnet/monitor_processes.go | 7 ++++++- tests/fixture/tmpnet/node.go | 7 ++++++- utils/dynamicip/ifconfig_resolver.go | 7 ++++++- utils/rpc/json.go | 13 +++++++++++++ 6 files changed, 50 insertions(+), 4 deletions(-) diff --git a/api/metrics/client.go b/api/metrics/client.go index e00ae1be1aaa..4681dab43a07 100644 --- a/api/metrics/client.go +++ b/api/metrics/client.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "fmt" + "io" "net/http" "net/url" @@ -52,6 +53,10 @@ func (c *Client) GetMetrics(ctx context.Context) (map[string]*dto.MetricFamily, // Return an error for any non successful status code if resp.StatusCode < 200 || resp.StatusCode > 299 { + // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. + // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive + _, _ = io.Copy(io.Discard, resp.Body) + // Drop any error during close to report the original error _ = resp.Body.Close() return nil, fmt.Errorf("received status code: %d", resp.StatusCode) @@ -60,9 +65,17 @@ func (c *Client) GetMetrics(ctx context.Context) (map[string]*dto.MetricFamily, var parser expfmt.TextParser metrics, err := parser.TextToMetricFamilies(resp.Body) if err != nil { + // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. + // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive + _, _ = io.Copy(io.Discard, resp.Body) + // Drop any error during close to report the original error _ = resp.Body.Close() return nil, err } + + // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. + // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive + _, _ = io.Copy(io.Discard, resp.Body) return metrics, resp.Body.Close() } diff --git a/tests/fixture/tmpnet/check_monitoring.go b/tests/fixture/tmpnet/check_monitoring.go index b306d20a1011..ebef511e9a9e 100644 --- a/tests/fixture/tmpnet/check_monitoring.go +++ b/tests/fixture/tmpnet/check_monitoring.go @@ -114,7 +114,12 @@ func queryLoki( if err != nil { return 0, stacktrace.Errorf("failed to execute request: %w", err) } - defer resp.Body.Close() + defer func() { + // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. + // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() // Read and parse response body, err := io.ReadAll(resp.Body) diff --git a/tests/fixture/tmpnet/monitor_processes.go b/tests/fixture/tmpnet/monitor_processes.go index b0d891a8b7e4..de87b0abfc20 100644 --- a/tests/fixture/tmpnet/monitor_processes.go +++ b/tests/fixture/tmpnet/monitor_processes.go @@ -591,7 +591,12 @@ func checkReadiness(ctx context.Context, url string) (bool, string, error) { if err != nil { return false, "", stacktrace.Errorf("request failed: %w", err) } - defer resp.Body.Close() + defer func() { + // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. + // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() body, err := io.ReadAll(resp.Body) if err != nil { diff --git a/tests/fixture/tmpnet/node.go b/tests/fixture/tmpnet/node.go index e572cd4bfdfa..86f9687c46a8 100644 --- a/tests/fixture/tmpnet/node.go +++ b/tests/fixture/tmpnet/node.go @@ -224,7 +224,12 @@ func (n *Node) SaveMetricsSnapshot(ctx context.Context) error { if err != nil { return stacktrace.Wrap(err) } - defer resp.Body.Close() + defer func() { + // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. + // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() body, err := io.ReadAll(resp.Body) if err != nil { return stacktrace.Wrap(err) diff --git a/utils/dynamicip/ifconfig_resolver.go b/utils/dynamicip/ifconfig_resolver.go index dccbbcbdc7a0..a38aa828857d 100644 --- a/utils/dynamicip/ifconfig_resolver.go +++ b/utils/dynamicip/ifconfig_resolver.go @@ -31,7 +31,12 @@ func (r *ifConfigResolver) Resolve(ctx context.Context) (netip.Addr, error) { if err != nil { return netip.Addr{}, err } - defer resp.Body.Close() + defer func() { + // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. + // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + }() ipBytes, err := io.ReadAll(resp.Body) if err != nil { diff --git a/utils/rpc/json.go b/utils/rpc/json.go index 62fc90169bbd..95f747829393 100644 --- a/utils/rpc/json.go +++ b/utils/rpc/json.go @@ -7,6 +7,7 @@ import ( "bytes" "context" "fmt" + "io" "net/http" "net/url" @@ -49,15 +50,27 @@ func SendJSONRequest( // Return an error for any non successful status code if resp.StatusCode < 200 || resp.StatusCode > 299 { + // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. + // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive + _, _ = io.Copy(io.Discard, resp.Body) + // Drop any error during close to report the original error _ = resp.Body.Close() return fmt.Errorf("received status code: %d", resp.StatusCode) } if err := rpc.DecodeClientResponse(resp.Body, reply); err != nil { + // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. + // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive + _, _ = io.Copy(io.Discard, resp.Body) + // Drop any error during close to report the original error _ = resp.Body.Close() return fmt.Errorf("failed to decode client response: %w", err) } + + // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. + // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive + _, _ = io.Copy(io.Discard, resp.Body) return resp.Body.Close() } From 3b7c18207d2857fa6baafab533ab77b51ec683f0 Mon Sep 17 00:00:00 2001 From: Michael Kaplan Date: Thu, 6 Nov 2025 22:16:48 -0500 Subject: [PATCH 2/6] Close helper --- api/metrics/client.go | 21 ++++++------------ tests/fixture/tmpnet/check_monitoring.go | 8 ++----- tests/fixture/tmpnet/monitor_processes.go | 8 ++----- tests/fixture/tmpnet/node.go | 9 +++----- utils/dynamicip/ifconfig_resolver.go | 8 ++----- utils/rpc/json.go | 26 +++++++++++------------ 6 files changed, 27 insertions(+), 53 deletions(-) diff --git a/api/metrics/client.go b/api/metrics/client.go index 4681dab43a07..ac0c72b4689c 100644 --- a/api/metrics/client.go +++ b/api/metrics/client.go @@ -7,12 +7,13 @@ import ( "bytes" "context" "fmt" - "io" "net/http" "net/url" "github.com/prometheus/common/expfmt" + "github.com/ava-labs/avalanchego/utils/rpc" + dto "github.com/prometheus/client_model/go" ) @@ -46,6 +47,7 @@ func (c *Client) GetMetrics(ctx context.Context) (map[string]*dto.MetricFamily, return nil, fmt.Errorf("failed to create request: %w", err) } + //nolint:bodyclose // Body is closed via rpc.CleanlyCloseBody in all code paths resp, err := http.DefaultClient.Do(request) if err != nil { return nil, fmt.Errorf("failed to issue request: %w", err) @@ -53,29 +55,18 @@ func (c *Client) GetMetrics(ctx context.Context) (map[string]*dto.MetricFamily, // Return an error for any non successful status code if resp.StatusCode < 200 || resp.StatusCode > 299 { - // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. - // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive - _, _ = io.Copy(io.Discard, resp.Body) - // Drop any error during close to report the original error - _ = resp.Body.Close() + _ = rpc.CleanlyCloseBody(resp.Body) return nil, fmt.Errorf("received status code: %d", resp.StatusCode) } var parser expfmt.TextParser metrics, err := parser.TextToMetricFamilies(resp.Body) if err != nil { - // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. - // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive - _, _ = io.Copy(io.Discard, resp.Body) - // Drop any error during close to report the original error - _ = resp.Body.Close() + _ = rpc.CleanlyCloseBody(resp.Body) return nil, err } - // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. - // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive - _, _ = io.Copy(io.Discard, resp.Body) - return metrics, resp.Body.Close() + return metrics, rpc.CleanlyCloseBody(resp.Body) } diff --git a/tests/fixture/tmpnet/check_monitoring.go b/tests/fixture/tmpnet/check_monitoring.go index ebef511e9a9e..0d0d2d587658 100644 --- a/tests/fixture/tmpnet/check_monitoring.go +++ b/tests/fixture/tmpnet/check_monitoring.go @@ -24,6 +24,7 @@ import ( "github.com/ava-labs/avalanchego/tests/fixture/stacktrace" "github.com/ava-labs/avalanchego/utils/logging" + "github.com/ava-labs/avalanchego/utils/rpc" ) type getCountFunc func() (int, error) @@ -114,12 +115,7 @@ func queryLoki( if err != nil { return 0, stacktrace.Errorf("failed to execute request: %w", err) } - defer func() { - // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. - // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() - }() + defer func() { _ = rpc.CleanlyCloseBody(resp.Body) }() // Read and parse response body, err := io.ReadAll(resp.Body) diff --git a/tests/fixture/tmpnet/monitor_processes.go b/tests/fixture/tmpnet/monitor_processes.go index de87b0abfc20..7a06e04e81a5 100644 --- a/tests/fixture/tmpnet/monitor_processes.go +++ b/tests/fixture/tmpnet/monitor_processes.go @@ -24,6 +24,7 @@ import ( "github.com/ava-labs/avalanchego/tests/fixture/stacktrace" "github.com/ava-labs/avalanchego/utils/logging" "github.com/ava-labs/avalanchego/utils/perms" + "github.com/ava-labs/avalanchego/utils/rpc" ) const ( @@ -591,12 +592,7 @@ func checkReadiness(ctx context.Context, url string) (bool, string, error) { if err != nil { return false, "", stacktrace.Errorf("request failed: %w", err) } - defer func() { - // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. - // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() - }() + defer func() { _ = rpc.CleanlyCloseBody(resp.Body) }() body, err := io.ReadAll(resp.Body) if err != nil { diff --git a/tests/fixture/tmpnet/node.go b/tests/fixture/tmpnet/node.go index 86f9687c46a8..554b9b771abc 100644 --- a/tests/fixture/tmpnet/node.go +++ b/tests/fixture/tmpnet/node.go @@ -24,6 +24,7 @@ import ( "github.com/ava-labs/avalanchego/tests/fixture/stacktrace" "github.com/ava-labs/avalanchego/utils/constants" "github.com/ava-labs/avalanchego/utils/crypto/bls/signer/localsigner" + "github.com/ava-labs/avalanchego/utils/rpc" "github.com/ava-labs/avalanchego/vms/platformvm/signer" ) @@ -224,12 +225,8 @@ func (n *Node) SaveMetricsSnapshot(ctx context.Context) error { if err != nil { return stacktrace.Wrap(err) } - defer func() { - // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. - // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() - }() + defer func() { _ = rpc.CleanlyCloseBody(resp.Body) }() + body, err := io.ReadAll(resp.Body) if err != nil { return stacktrace.Wrap(err) diff --git a/utils/dynamicip/ifconfig_resolver.go b/utils/dynamicip/ifconfig_resolver.go index a38aa828857d..e8ee7e15104c 100644 --- a/utils/dynamicip/ifconfig_resolver.go +++ b/utils/dynamicip/ifconfig_resolver.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/ava-labs/avalanchego/utils/ips" + "github.com/ava-labs/avalanchego/utils/rpc" ) var _ Resolver = (*ifConfigResolver)(nil) @@ -31,12 +32,7 @@ func (r *ifConfigResolver) Resolve(ctx context.Context) (netip.Addr, error) { if err != nil { return netip.Addr{}, err } - defer func() { - // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. - // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive - _, _ = io.Copy(io.Discard, resp.Body) - _ = resp.Body.Close() - }() + defer func() { _ = rpc.CleanlyCloseBody(resp.Body) }() ipBytes, err := io.ReadAll(resp.Body) if err != nil { diff --git a/utils/rpc/json.go b/utils/rpc/json.go index 95f747829393..1b30aa98e411 100644 --- a/utils/rpc/json.go +++ b/utils/rpc/json.go @@ -14,6 +14,14 @@ import ( rpc "github.com/gorilla/rpc/v2/json2" ) +// CleanlyCloseBody avoids sending unnecessary RST_STREAM and PING frames by ensuring +// the whole body is read before being closed. +// See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive +func CleanlyCloseBody(body io.ReadCloser) error { + _, _ = io.Copy(io.Discard, body) + return body.Close() +} + func SendJSONRequest( ctx context.Context, uri *url.URL, @@ -43,6 +51,7 @@ func SendJSONRequest( request.Header = ops.headers request.Header.Set("Content-Type", "application/json") + //nolint:bodyclose // body is closed via CleanlyCloseBody in all code paths resp, err := http.DefaultClient.Do(request) if err != nil { return fmt.Errorf("failed to issue request: %w", err) @@ -50,27 +59,16 @@ func SendJSONRequest( // Return an error for any non successful status code if resp.StatusCode < 200 || resp.StatusCode > 299 { - // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. - // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive - _, _ = io.Copy(io.Discard, resp.Body) - // Drop any error during close to report the original error - _ = resp.Body.Close() + _ = CleanlyCloseBody(resp.Body) return fmt.Errorf("received status code: %d", resp.StatusCode) } if err := rpc.DecodeClientResponse(resp.Body, reply); err != nil { - // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. - // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive - _, _ = io.Copy(io.Discard, resp.Body) - // Drop any error during close to report the original error - _ = resp.Body.Close() + _ = CleanlyCloseBody(resp.Body) return fmt.Errorf("failed to decode client response: %w", err) } - // Avoid sending unnecessary RST_STREAM and PING frames by ensuring the whole body is read. - // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive - _, _ = io.Copy(io.Discard, resp.Body) - return resp.Body.Close() + return CleanlyCloseBody(resp.Body) } From 5e980d759b14c5dcf8d143cc45f30d722353091c Mon Sep 17 00:00:00 2001 From: Michael Kaplan Date: Thu, 6 Nov 2025 22:20:26 -0500 Subject: [PATCH 3/6] nit --- api/metrics/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/metrics/client.go b/api/metrics/client.go index ac0c72b4689c..cbaff955bcfe 100644 --- a/api/metrics/client.go +++ b/api/metrics/client.go @@ -47,7 +47,7 @@ func (c *Client) GetMetrics(ctx context.Context) (map[string]*dto.MetricFamily, return nil, fmt.Errorf("failed to create request: %w", err) } - //nolint:bodyclose // Body is closed via rpc.CleanlyCloseBody in all code paths + //nolint:bodyclose // body is closed via rpc.CleanlyCloseBody in all code paths resp, err := http.DefaultClient.Do(request) if err != nil { return nil, fmt.Errorf("failed to issue request: %w", err) From 1994de4932d8dbfc49f1f746abdb5d300f1f3573 Mon Sep 17 00:00:00 2001 From: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> Date: Fri, 7 Nov 2025 11:58:09 -0500 Subject: [PATCH 4/6] nits Signed-off-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> --- utils/rpc/json.go | 10 +++-- utils/rpc/requester.go | 23 +++++++++++- utils/rpc/requester_test.go | 73 +++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 utils/rpc/requester_test.go diff --git a/utils/rpc/json.go b/utils/rpc/json.go index 1b30aa98e411..cc9665288112 100644 --- a/utils/rpc/json.go +++ b/utils/rpc/json.go @@ -12,17 +12,18 @@ import ( "net/url" rpc "github.com/gorilla/rpc/v2/json2" + "errors" ) - // CleanlyCloseBody avoids sending unnecessary RST_STREAM and PING frames by ensuring // the whole body is read before being closed. // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive func CleanlyCloseBody(body io.ReadCloser) error { - _, _ = io.Copy(io.Discard, body) - return body.Close() + _, err := io.Copy(io.Discard, body) + return errors.Join(err, body.Close()) } func SendJSONRequest( + client client, ctx context.Context, uri *url.URL, method string, @@ -51,8 +52,9 @@ func SendJSONRequest( request.Header = ops.headers request.Header.Set("Content-Type", "application/json") + // TODO interface //nolint:bodyclose // body is closed via CleanlyCloseBody in all code paths - resp, err := http.DefaultClient.Do(request) + resp, err := client.Send(request) if err != nil { return fmt.Errorf("failed to issue request: %w", err) } diff --git a/utils/rpc/requester.go b/utils/rpc/requester.go index ed616b2fdfba..9de8d72c9de5 100644 --- a/utils/rpc/requester.go +++ b/utils/rpc/requester.go @@ -6,20 +6,26 @@ package rpc import ( "context" "net/url" + "net/http" ) -var _ EndpointRequester = (*avalancheEndpointRequester)(nil) +var ( + _ EndpointRequester = (*avalancheEndpointRequester)(nil) + _ client = (*httpClient)(nil) +) type EndpointRequester interface { SendRequest(ctx context.Context, method string, params interface{}, reply interface{}, options ...Option) error } type avalancheEndpointRequester struct { - uri string + client client + uri string } func NewEndpointRequester(uri string) EndpointRequester { return &avalancheEndpointRequester{ + client: &httpClient{c: http.DefaultClient}, uri: uri, } } @@ -37,6 +43,7 @@ func (e *avalancheEndpointRequester) SendRequest( } return SendJSONRequest( + e.client, ctx, uri, method, @@ -45,3 +52,15 @@ func (e *avalancheEndpointRequester) SendRequest( options..., ) } + +type client interface { + Send(req *http.Request) (*http.Response, error) +} + +type httpClient struct { + c *http.Client +} + +func (h httpClient) Send(req *http.Request) (*http.Response, error) { + return h.c.Do(req) +} diff --git a/utils/rpc/requester_test.go b/utils/rpc/requester_test.go new file mode 100644 index 000000000000..c49a860b6ec3 --- /dev/null +++ b/utils/rpc/requester_test.go @@ -0,0 +1,73 @@ +// Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +package rpc + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" +) + +var _ client = (*testClient)(nil) + +// TestEndpointRequesterLongResponse tests that [EndpointRequester.SendRequest] +// respects context cancellation when draining a long response +func TestEndpointRequesterLongResponse(t *testing.T) { + server := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"foobar"}`)) + w.(http.Flusher).Flush() + + // Try to keep sending data even after the client has received the + // response to try to block them while draining the http body. + for { + w.Write([]byte("foo")) + w.(http.Flusher).Flush() + } + }), + ) + + gotResponse := make(chan struct{}) + client := avalancheEndpointRequester{ + client: testClient{ + client: httpClient{ + c: http.DefaultClient, + }, + gotResponse: gotResponse, + }, + uri: server.URL, + } + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + eg := errgroup.Group{} + eg.Go(func() error { + return client.SendRequest(ctx, "foo", nil, new(any)) + }) + + // Block after we receive the response to check that context cancellation is + // respected while draining the response body. + <-gotResponse + cancel() + + err := eg.Wait() + require.ErrorIs(t, err, context.Canceled) +} + +type testClient struct { + client + gotResponse chan struct{} +} + +func (t testClient) Send(req *http.Request) (*http.Response, error) { + response, err := t.client.Send(req) + close(t.gotResponse) + + return response, err +} From 4c06cab3cab38f1c9beafb56dc17712eb16e3a97 Mon Sep 17 00:00:00 2001 From: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:01:55 -0500 Subject: [PATCH 5/6] add test that draining respects cancellation Signed-off-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> --- utils/rpc/json.go | 3 ++- utils/rpc/requester.go | 4 ++-- utils/rpc/requester_test.go | 6 ++++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/utils/rpc/json.go b/utils/rpc/json.go index cc9665288112..34c152a9d71b 100644 --- a/utils/rpc/json.go +++ b/utils/rpc/json.go @@ -6,14 +6,15 @@ package rpc import ( "bytes" "context" + "errors" "fmt" "io" "net/http" "net/url" rpc "github.com/gorilla/rpc/v2/json2" - "errors" ) + // CleanlyCloseBody avoids sending unnecessary RST_STREAM and PING frames by ensuring // the whole body is read before being closed. // See https://blog.cloudflare.com/go-and-enhance-your-calm/#reading-bodies-in-go-can-be-unintuitive diff --git a/utils/rpc/requester.go b/utils/rpc/requester.go index 9de8d72c9de5..f0c08ce1ab50 100644 --- a/utils/rpc/requester.go +++ b/utils/rpc/requester.go @@ -5,8 +5,8 @@ package rpc import ( "context" - "net/url" "net/http" + "net/url" ) var ( @@ -26,7 +26,7 @@ type avalancheEndpointRequester struct { func NewEndpointRequester(uri string) EndpointRequester { return &avalancheEndpointRequester{ client: &httpClient{c: http.DefaultClient}, - uri: uri, + uri: uri, } } diff --git a/utils/rpc/requester_test.go b/utils/rpc/requester_test.go index c49a860b6ec3..9bf91c96c5b6 100644 --- a/utils/rpc/requester_test.go +++ b/utils/rpc/requester_test.go @@ -8,6 +8,7 @@ import ( "net/http" "net/http/httptest" "testing" + "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" ) @@ -18,7 +19,7 @@ var _ client = (*testClient)(nil) // respects context cancellation when draining a long response func TestEndpointRequesterLongResponse(t *testing.T) { server := httptest.NewServer( - http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":"foobar"}`)) @@ -43,7 +44,8 @@ func TestEndpointRequesterLongResponse(t *testing.T) { }, uri: server.URL, } - ctx, cancel := context.WithCancel(context.Background()) + + ctx, cancel := context.WithCancel(t.Context()) defer cancel() eg := errgroup.Group{} From b111f11538da21a24feccaf8b66c4eac11c297cb Mon Sep 17 00:00:00 2001 From: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:06:19 -0500 Subject: [PATCH 6/6] clean Signed-off-by: Joshua Kim <20001595+joshua-kim@users.noreply.github.com> --- utils/rpc/json.go | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/rpc/json.go b/utils/rpc/json.go index 34c152a9d71b..0a3e752f6ab7 100644 --- a/utils/rpc/json.go +++ b/utils/rpc/json.go @@ -53,7 +53,6 @@ func SendJSONRequest( request.Header = ops.headers request.Header.Set("Content-Type", "application/json") - // TODO interface //nolint:bodyclose // body is closed via CleanlyCloseBody in all code paths resp, err := client.Send(request) if err != nil {