Skip to content

fix(terraform): stop provider Configure mutating the global http.DefaultClient - #21

Open
byron-lambda wants to merge 5 commits into
speakeasy-api:mainfrom
byron-lambda:fix-terraform-default-client
Open

byron-lambda wants to merge 5 commits into
speakeasy-api:mainfrom
byron-lambda:fix-terraform-default-client

Conversation

@byron-lambda

@byron-lambda byron-lambda commented Sep 16, 2026

Copy link
Copy Markdown

Why

The generated terraform provider's Configure does:

httpClient := http.DefaultClient
httpClient.Transport = NewProviderHTTPTransport(providerHTTPTransportOpts)

http.DefaultClient is a *http.Client, so the first line aliases the package-global rather than copying it, and the second line rewrites the global's Transport on every Configure. A normal provider process configures once, so this goes unnoticed, but it has two real consequences: any other user of http.DefaultClient in the process silently inherits the provider's transport, and when several provider instances share one process (in-process testing with terraform-plugin-testing's ProtoV6ProviderFactories) concurrent Configures are a data race on the global, which fails any test run under -race.

What changed

templates/templates/terraform/provider/provider.go.stmpl builds a dedicated client instead:

httpClient := &http.Client{Transport: NewProviderHTTPTransport(providerHTTPTransportOpts)}

Review flagged the sibling bug in the same wiring: when a generator config opts into a tlsSkipVerify provider attribute, the generated block mutates the TLS configuration of the transport it is handed, which was http.DefaultTransport, leaking InsecureSkipVerify to every other user of that global in the process. Rather than cloning at the mutation site, Configure now takes ownership of its transport at the single point the default enters: it clones http.DefaultTransport (guarded, so a process that replaced the global with a custom RoundTripper keeps today's behavior) and wires the clone into the transport opts, so every downstream helper mutates state Configure owns. Each provider instance gets its own connection pool, the correct trade for never writing configuration into process globals.

Generated output changes by a few lines in every terraform provider's Configure. Behavior is identical for a single provider instance; the globals http.DefaultClient and http.DefaultTransport are simply no longer touched. Changeset added via make changelog core terraform.

Testing

Applied the same one-line change to the generated provider.go of an existing Speakeasy-generated terraform provider and ran its terraform-plugin-testing e2e suite (17 resource lifecycle tests, t.Parallel, against a local test server) under go test -race: the suite fails with a data race report on http.DefaultClient.Transport before the change and passes after it. For the transport clone, verified the emitted Go by reading the template output (the terraform target has no per-change test harness for this block). Did not run TARGET=review make build-terraform.

Public-safety check

  • This change contains no credentials, customer documents, private repository URLs, private filesystem paths, or unredacted private logs.
  • Title, body, comments, and commit messages name no customers or customer-derived identifiers, private paths or trackers, or workflow provenance, and are understandable without private context (.claude/skills/public-repo-communication/SKILL.md).
  • Generated fixtures and review SDK changes are public-safe.
  • I reviewed git diff --check.

…ultClient

Configure aliased http.DefaultClient (a pointer, where a copy was clearly
intended) and set its Transport, so every Configure rewrote the
process-global client. In a normal provider process this is invisible, but
when several provider instances share a process (in-process test frameworks
such as terraform-plugin-testing) concurrent Configures race on the global
under the race detector, and the last configured provider's transport leaks
to any other http.DefaultClient user in the process. Build a fresh
http.Client instead.
@byron-lambda
byron-lambda requested a review from a team as a code owner September 16, 2026 05:24
@github-actions

github-actions Bot commented Sep 16, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@github-actions github-actions Bot added the terraform Trigger (12) snapshot tests for terraform label Sep 16, 2026
@byron-lambda

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

github-actions Bot added a commit that referenced this pull request Sep 16, 2026

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread templates/templates/terraform/provider/provider.go.stmpl
The tlsSkipVerify block mutated the TLS configuration of the transport it
was handed, which is http.DefaultTransport, leaking InsecureSkipVerify to
every other user of the global in the process. Clone it, mutate the clone,
and hand the clone to the provider transport. Same class of bug as the
http.DefaultClient aliasing this PR fixes; flagged by review.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 2 files (changes from recent commits).

Confidence score: 4/5

  • The committed review-SDK fixtures in zSDKs/sdk-terraform/internal/provider/provider.go and zSDKs/terraform-provider-testing/internal/provider/provider.go remain stale and still mutate http.DefaultTransport, so the fixtures may not reflect the updated provider behavior—refresh them to match templates/templates/terraform/includes/provider.ts.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="templates/templates/terraform/includes/provider.ts">

<violation number="1" location="templates/templates/terraform/includes/provider.ts:776">
P2: The committed review-SDK fixtures for this template change are stale. zSDKs/sdk-terraform/internal/provider/provider.go and zSDKs/terraform-provider-testing/internal/provider/provider.go still mutate http.DefaultTransport directly (no Clone) and still rewire http.DefaultClient, so they keep the exact global-mutation/data-race behavior this PR eliminates and don't demonstrate the intended generated-output change. Per CONTRIBUTING.md, the relevant zSDKs should be regenerated (e.g. TARGET=review make build-terraform) and included in this PR.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

// Clone before mutating: the transport here is http.DefaultTransport, and
// writing its TLS configuration would leak InsecureSkipVerify to every
// other user of the global in the process.
result.push(`transport = transport.Clone()`);

@cubic-dev-ai cubic-dev-ai Bot Sep 16, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The committed review-SDK fixtures for this template change are stale. zSDKs/sdk-terraform/internal/provider/provider.go and zSDKs/terraform-provider-testing/internal/provider/provider.go still mutate http.DefaultTransport directly (no Clone) and still rewire http.DefaultClient, so they keep the exact global-mutation/data-race behavior this PR eliminates and don't demonstrate the intended generated-output change. Per CONTRIBUTING.md, the relevant zSDKs should be regenerated (e.g. TARGET=review make build-terraform) and included in this PR.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At templates/templates/terraform/includes/provider.ts, line 776:

<comment>The committed review-SDK fixtures for this template change are stale. zSDKs/sdk-terraform/internal/provider/provider.go and zSDKs/terraform-provider-testing/internal/provider/provider.go still mutate http.DefaultTransport directly (no Clone) and still rewire http.DefaultClient, so they keep the exact global-mutation/data-race behavior this PR eliminates and don't demonstrate the intended generated-output change. Per CONTRIBUTING.md, the relevant zSDKs should be regenerated (e.g. TARGET=review make build-terraform) and included in this PR.</comment>

<file context>
@@ -770,12 +770,17 @@ function templateProviderHTTPTransportTlsSkipVerify(
+  // Clone before mutating: the transport here is http.DefaultTransport, and
+  // writing its TLS configuration would leak InsecureSkipVerify to every
+  // other user of the global in the process.
+  result.push(`transport = transport.Clone()`);
   result.push(`if transport.TLSClientConfig == nil {`);
   result.push(`transport.TLSClientConfig = &tls.Config{}`);
</file context>
Fix with cubic

Rather than cloning inside the tlsSkipVerify block, give Configure
ownership of its transport at the point http.DefaultTransport enters: the
downstream helpers (headers, TLS skip verification) can then mutate the
transport they are handed without touching process globals.
@byron-lambda

Copy link
Copy Markdown
Author

A possible follow-up worth a maintainer opinion: the transport clone could instead be cleanhttp.DefaultPooledTransport() from hashicorp/go-cleanhttp, which is already an indirect dependency of every generated provider via the terraform-plugin stack. It returns a concrete *http.Transport (no type assertion or guard needed), never reads process globals by design, and matches what hand-written terraform providers typically use, at the cost of tracking stdlib transport defaults in that library rather than inheriting them via Clone(). Happy to switch this PR to it if you prefer that direction for the terraform target.

TARGET=review output for terraform-provider-testing, regenerated so the
fixture demonstrates the Configure change: a cloned transport wired into
the provider transport opts and a fresh http.Client, with the process
globals untouched.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

terraform Trigger (12) snapshot tests for terraform

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant