Skip to content

Fix clone client transport race - #48

Merged
wklken merged 1 commit into
masterfrom
codex/fix-clone-transport-race
Jul 5, 2026
Merged

wklken merged 1 commit into
masterfrom
codex/fix-clone-transport-race

Conversation

@wklken

@wklken wklken commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Summary

This fixes the clone concurrency race by changing clone ownership of HTTP client state while preserving shared transport reuse:

  • New() now binds the newly created http.Client to the agent http.Transport immediately.
  • Clone() now copies the http.Client value instead of sharing the same *http.Client pointer.
  • Clones still share the underlying *http.Transport by default, preserving connection pool reuse.
  • Clone transport mutators now use copy-on-write transport isolation and keep clone.Client.Transport aligned with clone.Transport:
    • DisableCompression()
    • TLSClientConfig() via existing safeModifyTransport() path
    • Proxy() via existing safeModifyTransport() path
    • Timeouts()
  • DisableRedirect() now isolates clone client changes through safeModifyHttpClient().
  • The request send path still honors DisableTransportSwap and direct SuperAgent.Transport assignment compatibility, but writes only to each clone's own client after Clone().
  • docs/usage.md now documents that clones copy HTTP client settings and share transport by default, with private transports created when transport settings change on a clone.

Why

Clone() is the documented reuse path for concurrent requests. Before this change, clones shared the same *http.Client; concurrent clone requests could write Client.Transport while net/http read it in Client.Do, causing go test -race -run '^TestConcurrently$' to report a data race.

Tests

  • env GOCACHE=/tmp/gorequest-go-build go test -run 'TestConfigurationSettersAndCloneIsolation|TestCloneClientSettingsDoNotLeak|TestCloneTransportSettingsDoNotLeak|TestTimeoutsUpdatesTransportAndIgnoresNonTransport' -count=1 -v ./...
  • env GOCACHE=/tmp/gorequest-go-build go test -race -run '^TestConcurrently$' -count=1 -v ./...
  • env GOCACHE=/tmp/gorequest-go-build GOLANGCI_LINT_CACHE=/tmp/gorequest-golangci-lint make lint
  • env GOCACHE=/tmp/gorequest-go-build make test
  • env GOCACHE=/tmp/gorequest-go-build go test -v ./...

Why this change was needed:
Clone is documented as the safe reuse path for concurrent requests, but clones shared one http.Client. Concurrent clone requests wrote Client.Transport on the same client while net/http read it, producing a race under go test -race.

What changed:
- Bind New clients to their transport during construction.
- Make Clone copy the http.Client while sharing the transport by default.
- Copy-on-write clone transports when clone transport settings change.
- Isolate DisableRedirect client changes from the base agent.
- Add regression coverage for clone client ownership and transport setting isolation.
- Update usage docs for the new clone ownership model.

Problem solved:
Concurrent clone requests no longer race on shared client state, while the shared transport connection pool behavior is preserved.
@cursor

cursor Bot commented Jul 5, 2026

Copy link
Copy Markdown

Bugbot is not enabled for this team, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@wklken

wklken commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

Clone client/transport flow

This is the ownership model after this PR.

Default clone reuse

flowchart TD
    Base["base SuperAgent"]
    BaseClient["base http.Client"]
    T0["shared http.Transport / connection pool"]

    A["clone A SuperAgent"]
    AClient["clone A http.Client"]

    B["clone B SuperAgent"]
    BClient["clone B http.Client"]

    Base --> BaseClient
    Base --> T0
    BaseClient --> T0

    A --> AClient
    A --> T0
    AClient --> T0

    B --> BClient
    B --> T0
    BClient --> T0
Loading

Each clone owns its own http.Client, so request-time transport binding no longer writes to a shared client. The default transport is still shared to preserve connection reuse.

Transport copy-on-write

flowchart TD
    T0["base/shared Transport"]
    T1["clone B private Transport copy"]

    Base["base SuperAgent"] --> BaseClient["base Client"] --> T0
    A["clone A"] --> AClient["clone A Client"] --> T0

    B["clone B"] --> BClient["clone B Client"] --> T1
    B -. "Proxy/TLS/Timeouts/DisableCompression" .-> T1
Loading

When a clone changes transport settings, it gets a private transport and its own client is rebound to that private transport. The base agent and other clones keep using the original shared transport.

Request execution path

sequenceDiagram
    participant G as Goroutine
    participant S as Clone SuperAgent
    participant C as Owned http.Client
    participant T as Bound Transport

    G->>S: End()
    S->>S: MakeRequest()
    S->>C: sync Client.Transport = SuperAgent.Transport
    Note over S,C: This writes only this clone's owned client
    S->>C: Do(req)
    C->>T: RoundTrip(req)
    T-->>C: response
    C-->>S: response
    S-->>G: resp, body, errs
Loading

@wklken
wklken merged commit f5bd0cd into master Jul 5, 2026
6 checks passed
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.

1 participant