Skip to content

fix: prevent http.DefaultClient mutation in search tools#151

Merged
asdek merged 2 commits intovxcontrol:feature/next_releasefrom
mason5052:fix/http-client-global-mutation
Mar 2, 2026
Merged

fix: prevent http.DefaultClient mutation in search tools#151
asdek merged 2 commits intovxcontrol:feature/next_releasefrom
mason5052:fix/http-client-global-mutation

Conversation

@mason5052
Copy link
Contributor

Description

Problem

Both tavily.go and traversaal.go assign http.DefaultClient to a local variable and then mutate its Transport field when configuring a proxy URL:

client := http.DefaultClient
if t.proxyURL != "" {
    client.Transport = &http.Transport{...}  // mutates global singleton
}

Since http.DefaultClient is a process-wide singleton, this creates a data race: concurrent requests from different tool instances overwrite each other's proxy configuration, and all other HTTP callers in the process inherit the mutated transport.

Solution

Create a new http.Client instance when proxy is configured instead of mutating the global. When no proxy is set, DefaultClient is used read-only (no mutation). This matches the correct pattern already used in browser.go:callScraper().

client := http.DefaultClient
if t.proxyURL != "" {
    client = &http.Client{           // new instance, no global mutation
        Transport: &http.Transport{
            Proxy: func(req *http.Request) (*url.URL, error) {
                return url.Parse(t.proxyURL)
            },
        },
    }
}

Type of Change

  • Bug fix (non-breaking change which fixes an issue)

Areas Affected

  • Core Services (Backend API / Workers)
  • External Integrations (Tavily, Traversaal search APIs)

Testing

Configuration

  • Local code review + CI validation (Go not installed locally)

Steps to Reproduce the Bug

  1. Configure two tavily tool instances with different proxy URLs
  2. Run concurrent search requests
  3. Observe that http.DefaultClient.Transport is overwritten by whichever goroutine runs last
  4. All subsequent HTTP requests in the process use the wrong proxy

Test Results

  • Added tavily_test.go: Tests that http.DefaultClient.Transport is NOT mutated after search() with proxy configured. Also covers IsAvailable() and parseHTTPResponse() error paths.
  • Added traversaal_test.go: Same mutation guard test, plus IsAvailable() and parseHTTPResponse() success/error paths.

Security Considerations

  • This fix eliminates a race condition that could cause proxy misconfiguration
  • No new dependencies or attack surface introduced
  • The fix is a minimal, targeted change to client instantiation

Checklist

  • My code follows the project's coding standards
  • I have performed a self-review of my code
  • I have added tests that prove my fix is effective
  • New and existing tests pass with my changes
  • My changes generate no new warnings
  • I have checked my code for potential security vulnerabilities
  • My changes are backward compatible

Copilot AI review requested due to automatic review settings February 27, 2026 02:24
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a critical race condition in the tavily and traversaal search tools where http.DefaultClient.Transport was being mutated directly, affecting all HTTP requests process-wide. The fix creates new http.Client instances when a proxy is configured, following the established pattern used consistently in other tools (duckduckgo, perplexity, sploitus, searxng, google, browser).

Changes:

  • Fixed tavily.go and traversaal.go to create new HTTP client instances instead of mutating the global singleton
  • Added comprehensive test coverage for both tools including mutation guards, availability checks, and response parsing

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
backend/pkg/tools/tavily.go Creates new http.Client with custom transport when proxy is configured, preventing global state mutation
backend/pkg/tools/traversaal.go Creates new http.Client with custom transport when proxy is configured, preventing global state mutation
backend/pkg/tools/tavily_test.go Adds tests for mutation guard, IsAvailable, and parseHTTPResponse with multiple error conditions
backend/pkg/tools/traversaal_test.go Adds tests for mutation guard, IsAvailable, and parseHTTPResponse with success and error paths

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +71 to +72
if http.DefaultClient.Transport != nil {
t.Error("http.DefaultClient.Transport should remain nil when no proxy is configured")
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

This assertion is incorrect. The test should verify that http.DefaultClient.Transport remains unchanged (comparing to originalTransport captured before the test), not that it remains nil. Other tests or code in the same process might have already set DefaultClient.Transport to a non-nil value. The TestTavilySearchDoesNotMutateDefaultClient test correctly captures the original value and compares against it - this test should follow the same pattern.

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +57
if http.DefaultClient.Transport != nil {
t.Error("http.DefaultClient.Transport should remain nil when no proxy is configured")
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

This assertion is incorrect. The test should verify that http.DefaultClient.Transport remains unchanged (comparing to originalTransport captured before the test), not that it remains nil. Other tests or code in the same process might have already set DefaultClient.Transport to a non-nil value. The TestTraversaalSearchDoesNotMutateDefaultClient test correctly captures the original value and compares against it - this test should follow the same pattern.

Copilot uses AI. Check for mistakes.
Comment on lines +46 to +57
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
resp := tavilySearchResult{
Answer: "test answer",
Query: "test",
Results: []tavilyResult{
{Title: "Result 1", URL: "https://example.com/1", Content: "content 1", Score: 0.95},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

The test server created here is never used since proxyURL is empty. Consider removing the test server setup (lines 46-57) to avoid confusion, or save and restore the original transport value like in TestTavilySearchDoesNotMutateDefaultClient to make the assertion more robust.

Copilot uses AI. Check for mistakes.
Comment on lines +164 to +167
if parseErr == nil {
t.Fatal("parseHTTPResponse() expected error, got nil")
}
})
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

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

The errContain field is defined in the test cases but never validated in the test logic. The test should check that the error message contains the expected string. Add validation like: if !strings.Contains(parseErr.Error(), tt.errContain) { t.Errorf("error message %q should contain %q", parseErr.Error(), tt.errContain) }

Copilot uses AI. Check for mistakes.
Both tavily.go and traversaal.go assigned http.DefaultClient to a local
variable and then mutated its Transport field when configuring a proxy.
Since DefaultClient is a process-wide singleton, this creates a data
race: concurrent requests overwrite each other's proxy configuration,
and all other HTTP callers in the process are affected.

Create a new http.Client instance when proxy is configured instead of
mutating the global. When no proxy is set, DefaultClient is used
read-only (no mutation). This matches the pattern in browser.go.

Signed-off-by: mason5052 <ehehwnwjs5052@gmail.com>
@mason5052 mason5052 force-pushed the fix/http-client-global-mutation branch from d280e8e to e36f47f Compare February 27, 2026 02:39
- Remove unused httptest.Server in TestTavilySearchWithoutProxy
  (tavily.search() targets hardcoded tavilyURL, not the test server)
- Remove unused encoding/json imports from both test files

Signed-off-by: mason5052 <ehehwnwjs5052@gmail.com>
@asdek asdek changed the base branch from master to feature/next_release March 2, 2026 15:36
@asdek asdek merged commit 8f1151c into vxcontrol:feature/next_release Mar 2, 2026
@asdek
Copy link
Contributor

asdek commented Mar 2, 2026

thank you for the PR!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants