fix: always close response body#1276
Conversation
WalkthroughDrains and closes HTTP upgrade response bodies and defers net.Conn closure on dial/init errors; consolidates connection-init message handling and error propagation; adds tests for WebSocket upgrade failures and invalid Sec-WebSocket-Accept; fixes test response-body leaks. No public API signature changes. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go (2)
8349-8354: Prefer t.Cleanup/defer to guarantee body closure even on assertion failuresClosing the body is correct. To ensure it’s always closed even if the preceding require fails (or future changes add early returns), register the close with t.Cleanup right after the Do() succeeds.
- resp, err := httpClient.Do(req) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - require.NoError(t, resp.Body.Close()) + resp, err := httpClient.Do(req) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, resp.Body.Close()) }) + require.Equal(t, http.StatusOK, resp.StatusCode)
8477-8482: Mirror the cleanup pattern for consistent, robust resource handlingSame comment as above: use t.Cleanup (or defer) so the response body is closed even if an assertion aborts the test.
- resp, err := httpClient.Do(req) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) - require.NoError(t, resp.Body.Close()) + resp, err := httpClient.Do(req) + require.NoError(t, err) + t.Cleanup(func() { require.NoError(t, resp.Body.Close()) }) + require.Equal(t, http.StatusOK, resp.StatusCode)
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go(2 hunks)v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go(1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Build and test (go 1.23 / windows-latest)
- GitHub Check: Build and test (go 1.23 / ubuntu-latest)
This prevents memory leaks.
9fad3d5 to
9b0acbc
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
🔭 Outside diff range comments (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go (1)
489-548: Fix deferred-close guard: connection can leak on invalid subprotocol returnThe deferred close relies on the local variable err to be non-nil. However, the default branch in the subprotocol switch returns a new error literal without assigning it to err. In that path, the deferred close won't run and the TCP connection leaks.
Concretely, the default branch around Line 547 returns without touching err. Assign the error to err before returning so the defer fires.
Apply this diff to make the deferred close effective for that path:
switch subProtocol { case ProtocolGraphQLWS: return newGQLWSConnectionHandler(requestContext, engineContext, conn, options, updater, c.log), nil case ProtocolGraphQLTWS: return newGQLTWSConnectionHandler(requestContext, engineContext, conn, options, updater, c.log), nil default: - return nil, NewInvalidWsSubprotocolError(subProtocol) + err = NewInvalidWsSubprotocolError(subProtocol) + return nil, err }Optional (safer): Convert the function to use a named return error and check that in the defer to cover all return sites without manual assignments.
🧹 Nitpick comments (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go (2)
591-597: Reconsider closing Transport-owned net.Conn on Do errorWhen httpClient.Do(req) returns an error, net/http’s Transport is responsible for connection lifecycle. Closing info.Conn yourself can fight the Transport (e.g., racing with its own teardown or preventing reuse in some edge cases). It’s generally safer to let Transport manage it.
Apply this diff to leave connection management to net/http:
upgradeResponse, err := c.httpClient.Do(req) if err != nil { - if conn != nil { - conn.Close() - } return nil, "", err }If you’ve observed a concrete resource leak without this manual Close, please share details; otherwise I recommend relying on Transport here.
599-606: Optionally drain non-101 response before closing to enable connection reuseFor non-upgrade responses, draining the body before Close helps net/http reuse the TCP connection. This is optional but can improve performance for repeated attempts.
Apply this diff:
// On failed upgrade, we close the connection here without transferring ownership to the callee. if upgradeResponse.StatusCode != http.StatusSwitchingProtocols { - upgradeResponse.Body.Close() + // Drain to EOF to allow connection reuse by net/http. + _, _ = io.Copy(io.Discard, upgradeResponse.Body) + upgradeResponse.Body.Close() return nil, "", &UpgradeRequestError{ URL: u, StatusCode: upgradeResponse.StatusCode, } }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these settings in your CodeRabbit configuration.
📒 Files selected for processing (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go(2 hunks)v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- v2/pkg/engine/datasource/graphql_datasource/graphql_datasource_test.go
🧰 Additional context used
🧬 Code Graph Analysis (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go (1)
pkg/execution/datasource_config.go (1)
Header(353-356)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Build and test (go 1.23 / ubuntu-latest)
- GitHub Check: Build and test (go 1.23 / ubuntu-latest)
- GitHub Check: Build and test (go 1.23 / windows-latest)
🔇 Additional comments (3)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go (3)
509-527: Init message construction is correct and avoids mutating shared stateUsing getConnectionInitMessage and then overlaying InitialPayload and Extensions on the local initMsg is clean and avoids mutating the global template. Good use of jsonparser.Set.
529-539: Error propagation style LGTMSwitching to explicit err assignment before returns keeps the defer-based cleanup reliable on failure paths. No issues here.
610-612: LGTM: close on invalid Sec-WebSocket-AcceptClosing the body on handshake validation failure correctly tears down the upgraded TCP connection and prevents leaks.
ea888fa to
fb7b556
Compare
There was a problem hiding this comment.
Actionable comments posted: 0
🧹 Nitpick comments (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go (1)
509-527: initMsg rename avoids shadowing; add a copy to avoid aliasing the global buffergetConnectionInitMessage can return the global connectionInitMessage slice. Mutating initMsg via jsonparser.Set could share the underlying array if capacity permits. Safer to copy before any Set calls.
Apply this minimal copy after retrieving initMsg:
initMsg, err := c.getConnectionInitMessage(requestContext, options.URL, options.Header) if err != nil { return nil, err } +// Ensure we don't accidentally mutate a shared/global backing array. +initMsg = append([]byte(nil), initMsg...) if len(options.InitialPayload) > 0 { initMsg, err = jsonparser.Set(initMsg, options.InitialPayload, "payload") if err != nil { return nil, err } } if options.Body.Extensions != nil { initMsg, err = jsonparser.Set(initMsg, options.Body.Extensions, "payload", "extensions") if err != nil { return nil, err } }Alternatively, modify getConnectionInitMessage to always return a fresh copy when no payload is supplied:
func (c *subscriptionClient) getConnectionInitMessage(ctx context.Context, url string, header http.Header) ([]byte, error) { if c.onWsConnectionInitCallback == nil { return append([]byte(nil), connectionInitMessage...), nil } // ... and likewise when len(payload) == 0 }v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (1)
2348-2461: Great coverage of upgrade failure matrix; consider asserting URL on error (nit)Table-driven tests cover a wide range of non-101 responses and assert the custom error type. As a minor enhancement, also assert that UpgradeRequestError.URL is set to the HTTP URL used internally (http://...) to lock in behavior.
Example assertion (after ErrorAs):
- require.Contains(t, upgradeErr.URL, "http://")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go(4 hunks)v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go(3 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go (2)
WithLogger(122-126)UpgradeRequestError(479-482)v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go (2)
GraphQLSubscriptionOptions(1928-1941)GraphQLBody(1943-1948)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go (3)
v2/pkg/engine/datasource/httpclient/httpclient.go (1)
URL(20-20)pkg/execution/datasource_config.go (1)
Header(353-356)v2/pkg/graphqlerrors/errors.go (1)
Extensions(141-143)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
- GitHub Check: Build and test (go 1.23 / windows-latest)
- GitHub Check: Build and test (go 1.23 / ubuntu-latest)
- GitHub Check: Build and test (go 1.23 / windows-latest)
- GitHub Check: Build and test (go 1.23 / ubuntu-latest)
🔇 Additional comments (10)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go (7)
489-491: Good defensive pattern: track err for deferred cleanupDeclaring a local err to coordinate deferred cleanup is correct and keeps control flow explicit.
501-508: Leak prevention on post-dial failures looks correctDeferring conn.Close() only when err != nil ensures ownership is released on all init failures without impacting the success path.
528-536: Write deadline and error propagation are correctSetting a write deadline before init and propagating errors via the shared err variable is consistent and robust.
537-539: Ack wait handling is correctwaitForAck usage and error handling look good and consistent with the read/write timeout model.
591-595: Accurate note on httptrace semanticsComment correctly documents GotConn not being invoked on Do error, clarifying why conn might be nil.
597-603: Correct: drain and close body on non-101 to enable reuseDraining to EOF and closing the body on failed upgrade avoids leaks and enables connection reuse by net/http Transport.
611-614: Correct: drain and close on invalid Sec-WebSocket-AcceptEnsures the Transport can reuse/tear down the connection properly on handshake validation failure.
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (3)
5-5: Import addition is appropriateencoding/base64 is needed for malformed accept-key scenarios; good addition.
2016-2018: Flakiness reduction after ping is sensibleBrief sleep after receiving ping gives the server time to push an additional payload; pragmatic fix for timing-sensitive test.
2463-2557: Good negative tests for Sec-WebSocket-Accept handlingValidates missing, malformed, and incorrect accept headers. The HIJACK+close ensures the client doesn’t hang on 101 paths. Looks solid.
ecbe56f to
e458547
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🔭 Outside diff range comments (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (1)
1903-1971: Avoid closing signaling channels; use buffered + non-blocking send to prevent panics on multiple pingsIn the server loop, if the client sends more than one ping before the client’s complete arrives, closing pingReceived again will panic ("close of closed channel"). Same for payloadSend if multiple sends happen. Prefer a buffered chan with a non-blocking send (or sync.Once) instead of close.
Apply this diff to make signaling idempotent and safe:
- pingReceived := make(chan struct{}) // closed when the server receives a ping - payloadSend := make(chan struct{}) // closed when the server sends a payload + // buffered channels + non-blocking send to avoid double-close panics if events repeat + pingReceived := make(chan struct{}, 1) // signaled when the server receives a ping + payloadSend := make(chan struct{}, 1) // signaled when the server sends a payloadAnd replace closing with non-blocking sends when signaling:
- receivedPing = true - close(pingReceived) + receivedPing = true + select { + case pingReceived <- struct{}{}: + default: + } ... - close(payloadSend) + select { + case payloadSend <- struct{}{}: + default: + }
♻️ Duplicate comments (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (1)
2021-2027: Nice: wait on payload signal instead of sleeping (addresses earlier flakiness)Waiting for payloadSend before unsubscribing removes timing sensitivity and aligns with the previous review suggestion to replace sleeps with channels.
🧹 Nitpick comments (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (1)
2497-2503: "Correct length but wrong content" case isn't 20 bytes; craft a 20-byte valueSec-WebSocket-Accept is a base64 of a 20-byte SHA-1. This case currently encodes a 27-byte string. It will still fail as invalid (so the test passes), but the case description doesn't match. Use a 20-byte value to reflect the intent.
Apply this diff:
- { - name: "Correct length but wrong content", - acceptKeyHandler: func(challengeKey string) string { - return base64.StdEncoding.EncodeToString([]byte("wrong-content-here-20-bytes")) - }, - expectError: true, - errorContains: "invalid Sec-WebSocket-Accept", - }, + { + name: "Correct length but wrong content", + acceptKeyHandler: func(challengeKey string) string { + // 20 bytes (not the SHA-1 of challengeKey+GUID), so length is correct but content is wrong + return base64.StdEncoding.EncodeToString([]byte("12345678901234567890")) + }, + expectError: true, + errorContains: "invalid Sec-WebSocket-Accept", + },
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go(4 hunks)v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go(8 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go
🧰 Additional context used
🧬 Code Graph Analysis (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (3)
v2/pkg/engine/resolve/context.go (2)
Request(148-151)NewContext(153-160)v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go (3)
NewGraphQLSubscriptionClient(216-272)WithLogger(122-126)UpgradeRequestError(479-482)v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go (2)
GraphQLSubscriptionOptions(1928-1941)GraphQLBody(1943-1948)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Build and test (go 1.23 / windows-latest)
- GitHub Check: Build and test (go 1.23 / ubuntu-latest)
- GitHub Check: Build and test (go 1.23 / windows-latest)
🔇 Additional comments (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (2)
5-5: Import looks necessary and scoped to new testsThe base64 import is used by TestInvalidWebSocketAcceptKey and is appropriate.
2149-2154: Good synchronization on pong receptionThe explicit wait for pongReceived with timeout makes the test much less flaky.
There was a problem hiding this comment.
Actionable comments posted: 0
🔭 Outside diff range comments (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (1)
2208-2211: Fix double-close of serverDone (panic risk)serverDone is closed in a defer; closing it again in the error branch will panic. Remove the inner close.
- defer func() { - _ = conn.Close(websocket.StatusNormalClosure, "server done") - close(serverDone) - }() + defer func() { + _ = conn.Close(websocket.StatusNormalClosure, "server done") + close(serverDone) + }() @@ - // Signal that the server is done (connection closed) - close(serverDone) - return // Exit handler goroutine + // Connection closed; defer will close serverDone + return // Exit handler goroutineAlso applies to: 2256-2257
♻️ Duplicate comments (2)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (2)
2433-2476: Capture tc per iteration to future-proof subtestsRange-var capture can bite if these subtests become parallel later. Shadow tc inside the loop.
- for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) {
2514-2570: Also capture tc per iteration hereSame reasoning as above; add a per-iteration capture.
- for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) {
🧹 Nitpick comments (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (1)
1947-1958: Avoid fixed-iteration read loop; rely on condition and context timeout insteadLimiting to 5 iterations risks missing a late message under load. Prefer looping until both conditions are met or the read context times out.
Apply this diff:
- // Process up to 5 messages or until we see both a ping and a complete - for i := 0; i < 5; i++ { - if receivedPing && receivedComplete { - break - } - - _, data, err = conn.Read(readCtx) + // Process messages until we see both a ping and a complete or the read context times out + for !(receivedPing && receivedComplete) { + _, data, err = conn.Read(readCtx) if err != nil { // Connection closed or timeout t.Logf("Connection read ended: %v", err) break }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go(9 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (3)
v2/pkg/engine/resolve/context.go (3)
Context(16-35)Request(148-151)NewContext(153-160)v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client.go (3)
NewGraphQLSubscriptionClient(216-272)WithLogger(122-126)UpgradeRequestError(479-482)v2/pkg/engine/datasource/graphql_datasource/graphql_datasource.go (2)
GraphQLSubscriptionOptions(1928-1941)GraphQLBody(1943-1948)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: Build and test (go 1.23 / ubuntu-latest)
- GitHub Check: Build and test (go 1.23 / windows-latest)
- GitHub Check: Build and test (go 1.23 / windows-latest)
🔇 Additional comments (1)
v2/pkg/engine/datasource/graphql_datasource/graphql_subscription_client_test.go (1)
1902-1906: Event-driven sync over sleeps: nice improvementUsing pingReceived/payloadSend channels with non-blocking sends removes timing flakiness and avoids double-close/panic risks. This will make the test more deterministic and faster.
Also applies to: 1966-1977
🤖 I have created a release *beep* *boop* --- ## [2.0.0-rc.221](v2.0.0-rc.220...v2.0.0-rc.221) (2025-08-21) ### Bug Fixes * always close response body ([#1276](#1276)) ([9069cc9](9069cc9)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - Chores - Bumped v2 release candidate to 2.0.0-rc.221; other version fields unchanged. No functional changes included in this PR. - Documentation - Updated the changelog with a new entry for 2.0.0-rc.221, noting a bug fix: always close the response body. Included comparison details to the previous release candidate for transparency on what changed. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Close properly body in the "newWSConnectionHandler" and "dial" functions.
Bonus part:
Summary by CodeRabbit
Bug Fixes
Tests