Skip to content

panic: defaultHTTPClient does unsafe type assertion on http.DefaultTransport (breaks otelhttp wrapping) #334

@simonferquel

Description

@simonferquel

Bug

defaultHTTPClient() in default_http_client.go does an unsafe type assertion on http.DefaultTransport:

func defaultHTTPClient() *http.Client {
    transport := http.DefaultTransport.(*http.Transport).Clone()
    transport.ResponseHeaderTimeout = defaultResponseHeaderTimeout
    return &http.Client{Transport: transport}
}

http.DefaultTransport is declared as http.RoundTripper, not *http.Transport. It is common (and recommended by the OpenTelemetry community) to wrap it for distributed tracing:

http.DefaultTransport = otelhttp.NewTransport(http.DefaultTransport)

When any application that does this calls anthropic.NewClient(...), the SDK panics:

panic: interface conversion: http.RoundTripper is *otelhttp.Transport, not *http.Transport
    github.com/anthropics/anthropic-sdk-go@v1.41.0/default_http_client.go:21

Why option.WithHTTPClient doesn't help

NewClient always invokes DefaultClientOptions() (which calls defaultHTTPClient()) before appending caller options. So the panic happens during client construction, even when the caller fully replaces the HTTP client via option.WithHTTPClient(...). The default client built by defaultHTTPClient() is then immediately discarded — meaning the SDK is panicking while constructing a client it never uses.

Affected versions

Introduced in v1.39.0 (commit 507cbe5d, 2026-04-24, "feat(go): add default http client with timeout"). Still present on main and in v1.41.0 (latest as of 2026-05-07). v1.38.0 and earlier are unaffected.

Reproduction

package main

import (
    "net/http"

    "github.com/anthropics/anthropic-sdk-go"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func main() {
    http.DefaultTransport = otelhttp.NewTransport(http.DefaultTransport)
    _ = anthropic.NewClient() // panics
}

Suggested fix

Replace the unchecked assertion with a safe one and fall back to a fresh *http.Transport (or to wrapping the existing RoundTripper) when the caller has installed a custom transport:

func defaultHTTPClient() *http.Client {
    var transport *http.Transport
    if t, ok := http.DefaultTransport.(*http.Transport); ok {
        transport = t.Clone()
    } else {
        // http.DefaultTransport has been wrapped (e.g. by otelhttp).
        // Fall back to a fresh *http.Transport so we can still set
        // ResponseHeaderTimeout without panicking.
        transport = &http.Transport{
            Proxy:                 http.ProxyFromEnvironment,
            DialContext:           (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
            ForceAttemptHTTP2:     true,
            MaxIdleConns:          100,
            IdleConnTimeout:       90 * time.Second,
            TLSHandshakeTimeout:   10 * time.Second,
            ExpectContinueTimeout: 1 * time.Second,
        }
    }
    transport.ResponseHeaderTimeout = defaultResponseHeaderTimeout
    return &http.Client{Transport: transport}
}

(Note: this file is generated by Stainless, so the fix likely needs to be applied at the generator level rather than directly in the repo.)

Workarounds for affected users

The only workarounds today require not wrapping http.DefaultTransport globally and instead wrapping each http.Client.Transport individually — which defeats the simplicity of the global-otel-instrumentation pattern. There is no SDK-level option to skip defaultHTTPClient(): WithoutEnvironmentDefaults skips credential autoload but is documented for a different purpose, and using it to dodge the panic also disables env-based credential resolution as a side effect.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions