jaegertracing/jaeger · error
invalid server URL
Error message
invalid server URL %q: %w
What it means
newRawClient parses each configured server address with net/url before building the connection pool. This error means url.Parse itself rejected the string (control characters, invalid percent-escapes, malformed IPv6 brackets, etc.), so the client cannot be constructed at all. It is returned from NewClient and surfaces at storage initialization time, not at request time.
Solutions
- Print the exact server string from the error message and inspect it for stray characters (spaces, quotes, newlines, commas).
- Fix the URL to a valid form, e.g. http://host:9200 or http://[::1]:9200 for IPv6, and URL-escape any special characters.
- If passing multiple servers, provide them as separate list elements (or split on commas) rather than one concatenated string.
- If %-escapes are intended, percent-encode them correctly (e.g. %25 for a literal %).
Example fix
// before servers: ["http://es-1:9200,http://es-2:9200"] // after servers: - http://es-1:9200 - http://es-2:9200
Defensive patterns
Strategy: validation
Validate before calling
for _, s := range servers {
u, err := url.Parse(strings.TrimSpace(s))
if err != nil {
return fmt.Errorf("invalid server URL %q: %w", s, err)
}
if u.Scheme == "" || u.Host == "" {
return fmt.Errorf("server URL %q must include scheme and host", s)
}
} Type guard
func isValidServerURL(s string) bool {
u, err := url.Parse(strings.TrimSpace(s))
return err == nil && u.Scheme != "" && u.Host != ""
} Try / catch
client, err := esclient.NewClient(...)
if err != nil {
if strings.Contains(err.Error(), "invalid server URL") {
log.Fatalf("bad ES server config: %v", err) // fail fast at startup, not at request time
}
return err
} Prevention
- Validate every server URL with url.Parse (plus scheme/host check) in config parsing, before the client is constructed.
- Trim whitespace and strip surrounding quotes from env-var/YAML config values.
- Pass multiple servers as separate list elements, never as one comma-joined string.
- Use bracketed IPv6 form http://[::1]:9200 and percent-encode special characters.
- Fail fast: surface the raw string from the error message when logging the config at startup.
When it happens
Trigger: Calling NewClient (or the tests that build raw clients) with a server string net/url cannot parse — e.g. a URL containing raw spaces or control characters, invalid %-encoding like http://host:9200/%zz, or unbalanced brackets in an IPv6 literal like http://[::1:9200. Comma-separated or quoted strings leaked in from misparsed config will also fail here.
Common situations: Env var SPAN_STORAGE_TYPE elasticsearch config with stray whitespace/newlines or surrounding quotes pasted into the servers list; Kubernetes env interpolation introducing control characters; hand-edited YAML producing 'http://es:9200,http://es2:9200' as a single element; shell quoting stripping characters like [ or ] from IPv6 addresses.
Understand the failure class
Background: "Invalid URL" / "URL cannot be empty": fix the malformed or missing URL behind request-construction failures — this error's family across 50 libraries.
Related errors
- deprecated ES rotation flags
- error creating Elasticsearch client
- failed to create Elasticsearch client
- failed to create gRPC server
- failed to discover Elasticsearch nodes (sniffing)
AI-assisted analysis of jaegertracing/jaeger@806f444784 (2026-09-01).
Data as JSON: /api/errors/27ca262c41c7fcdc.
Report an issue: GitHub.
Appendix: source
Thrown at internal/storage/elasticsearch/esclient/transport.go:65
// discoverNodes enables one-shot node discovery (sniffing) at startup.
discoverNodes bool
// logLevel selects the client log-level (debug/info/error); empty disables
// client logging. logger is the destination for those logs.
logLevel string
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 setsView on GitHub (pinned to 806f444784)