jaegertracing/jaeger · error

server URL %q must include a scheme and host, e.g. http://ho

Error message

server URL %q must include a scheme and host, e.g. http://host:9200

What it means

newRawClient validates each configured Elasticsearch server URL after url.Parse. Because url.Parse accepts bare hosts and host:port strings as scheme-less/path-only URLs, the client explicitly rejects any URL missing a scheme or host so the connection pool always has well-formed node endpoints. The error names the offending server string and shows the expected shape (e.g. http://host:9200).

Source

Thrown at internal/storage/elasticsearch/esclient/transport.go:70

	logger   *zap.Logger
}

// newRawClient builds a rawClient that round-robins requests across servers,
// sending each through base.
func newRawClient(ctx context.Context, base http.RoundTripper, opts rawClientOptions) (*rawClient, error) {
	if len(opts.servers) == 0 {
		return nil, errors.New("no servers specified")
	}
	urls := make([]*url.URL, 0, len(opts.servers))
	for _, server := range opts.servers {
		u, err := url.Parse(server)
		if err != nil {
			return nil, fmt.Errorf("invalid server URL %q: %w", server, err)
		}
		// url.Parse accepts host:port or bare hosts as scheme/path-only URLs; the
		// pool needs a scheme and host, so reject those up front with a clear error.
		if u.Scheme == "" || u.Host == "" {
			return nil, fmt.Errorf("server URL %q must include a scheme and host, e.g. http://host:9200", server)
		}
		urls = append(urls, u)
	}
	// Retry is disabled to preserve the current admin-client behavior; the data
	// plane can opt into the pool's read retry when it adopts rawClient in Stage B.
	transportOpts := []elastictransport.Option{
		elastictransport.WithURLs(urls...),
		elastictransport.WithTransport(base),
		elastictransport.WithDisableRetry(),
	}
	if opts.compressRequestBody {
		// Defaults to gzip.DefaultCompression, matching the level the olivere
		// client used before it was retired. The pool gzips the body and sets
		// Content-Encoding before handing the request to base, so the auth stack
		// below (notably the SigV4 signer) signs the compressed payload that
		// actually goes on the wire.
		transportOpts = append(transportOpts, elastictransport.WithCompression())
	}

View on GitHub (pinned to 806f444784)

Solutions

  1. Add the scheme to every server URL, e.g. change "localhost:9200" to "http://localhost:9200" (or "https://..." for TLS).
  2. Check the config source (CLI flag --es.servers, env var, YAML) for values missing the scheme or with empty entries.
  3. Ensure no template/env expansion produced an empty or truncated host before the scheme, e.g. "http://" alone is also rejected because Host is empty.

Example fix

// before
servers: ["es-cluster:9200"]
// after
servers: ["http://es-cluster:9200"]
Defensive patterns

Strategy: validation

Validate before calling

for _, s := range cfg.Servers {
	u, err := url.Parse(s)
	if err != nil || u.Scheme == "" || u.Host == "" {
		return fmt.Errorf("server %q must be an absolute URL, e.g. http://host:9200", s)
	}
}

Type guard

func isValidServerURL(s string) bool {
	u, err := url.Parse(s)
	return err == nil && u.Scheme != "" && u.Host != ""
}

Prevention

When it happens

Trigger: Calling NewClient (via newRawClient) with a server entry like "localhost:9200", "es-cluster", ":9200", or an empty string — any URL that parses but lacks u.Scheme or u.Host.

Common situations: Config files where SPAN_STORAGE_TYPE=elasticsearch servers are set without the http:// or https:// prefix; YAML/env config where the scheme was dropped during migration from an older client that tolerated bare hosts; template variables that rendered to an empty or partial host.

Related errors


AI-assisted analysis of jaegertracing/jaeger@806f444784 (2026-09-01). Data as JSON: /api/errors/890cc69ad862e22e. Report an issue: GitHub.