hashicorp/nomad · error

unexpected HTTP transport: %T

Error message

unexpected HTTP transport: %T

What it means

In api/api.go:409, cloneWithTimeout clones the client's http.Client so a per-request timeout can be applied; it asserts the Transport is a concrete *http.Transport. If a user configured Config.HttpClient with a custom round-tripper that is not *http.Transport (or wrapped it), the clone cannot preserve per-request timeouts and the client returns this error.

Source

Thrown at api/api.go:409

	return config
}

// cloneWithTimeout returns a cloned httpClient with set timeout if positive;
// otherwise, returns the same client
func cloneWithTimeout(httpClient *http.Client, t time.Duration) (*http.Client, error) {
	if httpClient == nil {
		return nil, errors.New("nil HTTP client")
	} else if httpClient.Transport == nil {
		return nil, errors.New("nil HTTP client transport")
	}

	if t.Nanoseconds() < 0 {
		return httpClient, nil
	}

	tr, ok := httpClient.Transport.(*http.Transport)
	if !ok {
		return nil, fmt.Errorf("unexpected HTTP transport: %T", httpClient.Transport)
	}

	// copy all public fields, to avoid copying transient state and locks
	ntr := &http.Transport{
		Proxy:                  tr.Proxy,
		DialContext:            tr.DialContext,
		Dial:                   tr.Dial,
		DialTLS:                tr.DialTLS,
		TLSClientConfig:        tr.TLSClientConfig,
		TLSHandshakeTimeout:    tr.TLSHandshakeTimeout,
		DisableKeepAlives:      tr.DisableKeepAlives,
		DisableCompression:     tr.DisableCompression,
		MaxIdleConns:           tr.MaxIdleConns,
		MaxIdleConnsPerHost:    tr.MaxIdleConnsPerHost,
		MaxConnsPerHost:        tr.MaxConnsPerHost,
		IdleConnTimeout:        tr.IdleConnTimeout,
		ResponseHeaderTimeout:  tr.ResponseHeaderTimeout,
		ExpectContinueTimeout:  tr.ExpectContinueTimeout,

View on GitHub (pinned to 482b49bf1a)

Solutions

  1. Use *http.Transport directly as HttpClient.Transport (start from a copy of http.DefaultTransport or cleanhttp.DefaultTransport)
  2. If you need instrumentation, wrap at the http.Client level only for direct requests, or apply timeouts via context instead of cloneWithTimeout
  3. Set an explicit non-negative timeout only when needed — negative/zero duration short-circuits the clone and avoids this path
  4. If wrapping is required, embed *http.Transport fields into a real *http.Transport copy rather than a custom RoundTripper type

Example fix

// before
client.HttpClient.Transport = otelhttp.NewTransport(http.DefaultTransport)
nodeClient, err := apiClient.GetNodeClientWithTimeout(nodeID, 5*time.Second) // panics into error
// after
tr := http.DefaultTransport.(*http.Transport).Clone()
client.HttpClient.Transport = tr // use context for tracing instead
nodeClient, err := apiClient.GetNodeClientWithTimeout(nodeID, 5*time.Second)
Defensive patterns

Strategy: validation

Validate before calling

if cfg.HttpClient != nil && cfg.HttpClient.Transport != nil {
	if _, ok := cfg.HttpClient.Transport.(*http.Transport); !ok {
		return errors.New("Nomad client requires *http.Transport")
	}
}

Type guard

func isHTTPTransport(c *api.Config) bool {
	_, ok := c.HttpClient.Transport.(*http.Transport)
	return ok
}

Try / catch

nodeClient, err := client.GetNodeClientWithTimeout(nodeID, 5*time.Second)
if err != nil && strings.Contains(err.Error(), "unexpected HTTP transport") {
	return fmt.Errorf("replace custom RoundTripper with *http.Transport: %w", err)
}

Prevention

When it happens

Trigger: Calling GetNodeClient/GetNodeClientWithTimeout (or the anonymous wrapper) while Config.HttpClient.Transport is set to anything other than *http.Transport — e.g. an http2 transport, oteltracing round-tripper, or a struct embedding *http.Transport.

Common situations: Injecting an OpenTelemetry/instrumented RoundTripper into the Nomad client; using a custom transport for proxy auth or mTLS that wraps rather than extends *http.Transport; libraries like hashicorp/go-cleanhttp not used and a hand-rolled transport supplied.

Related errors


AI-assisted analysis of hashicorp/nomad@482b49bf1a (2026-09-04). Data as JSON: /api/errors/7f59e70ec8c1345b. Report an issue: GitHub.