t8y2/dbx · error

unrecognized etcd /version response from %s: %w

Error message

unrecognized etcd /version response from %s: %w

What it means

fetchVersion GETs the /version endpoint and unmarshals the JSON body into fields etcdserver/etcdcluster. If the body is not JSON in etcd's expected shape (json.Unmarshal fails), the agent wraps the parse error into this message identifying the endpoint — meaning the server at that address is not speaking the etcd version API.

Source

Thrown at agents/drivers/etcd2-go/client.go:247

	return client, probe, nil
}

type etcdVersion struct {
	etcdserver  string
	etcdcluster string
}

func (c *authenticatedClient) fetchVersion(ctx context.Context) (etcdVersion, error) {
	body, _, err := c.do(ctx, http.MethodGet, "/version", "", nil)
	if err != nil {
		return etcdVersion{}, err
	}
	var parsed struct {
		Etcdserver  string `json:"etcdserver"`
		Etcdcluster string `json:"etcdcluster"`
	}
	if err := json.Unmarshal(body, &parsed); err != nil {
		return etcdVersion{}, fmt.Errorf("unrecognized etcd /version response from %s: %w", c.endpoint, err)
	}
	return etcdVersion{etcdserver: parsed.Etcdserver, etcdcluster: parsed.Etcdcluster}, nil
}

// probeV2 checks that the v2 keys API is actually served. A 403 proves the
// channel and credentials reached etcd, mirroring the v3 agent's
// PERMISSION_DENIED handling for restricted users.
func (c *authenticatedClient) probeV2(ctx context.Context) (map[string]any, error) {
	response, err := c.request(ctx, http.MethodGet, "/v2/members", "", nil)
	if err != nil {
		return nil, err
	}
	defer drainClose(response.Body)
	switch response.StatusCode {
	case http.StatusOK:
		return map[string]any{"ok": true, "endpoint": c.endpoint}, nil
	case http.StatusForbidden:
		return map[string]any{"ok": true, "endpoint": c.endpoint, "limited": true}, nil

View on GitHub (pinned to c0390bff16)

Solutions

  1. Verify the endpoint points at a real etcd server (curl the /version path and confirm JSON with etcdserver/etcdcluster fields)
  2. Fix the endpoint/port in the connection config if it targets a proxy or the wrong service
  3. Bypass or correctly configure intermediary proxies so /version reaches etcd unchanged

Example fix

// before
client := newClient("https://lb.example.com:443") // LB returns HTML error pages

// after
client := newClient("https://etcd-0.internal:2379") // direct etcd endpoint
// verify: curl https://etcd-0.internal:2379/version -> {"etcdserver":"3.5.x",...}
Defensive patterns

Strategy: try-catch

Validate before calling

// preflight: confirm /version returns etcd-shaped JSON before using the endpoint
resp, err := http.Get(endpoint + "/version")
if err != nil { return err }
var v struct{ Etcdserver string `json:"etcdserver"` }
body, _ := io.ReadAll(resp.Body)
if json.Unmarshal(body, &v) != nil || v.Etcdserver == "" {
    return fmt.Errorf("%s is not an etcd endpoint", endpoint)
}

Type guard

func isBadVersionResponse(err error) bool {
    return err != nil && strings.Contains(err.Error(), "unrecognized etcd /version response")
}

Try / catch

v, err := client.fetchVersion(ctx)
if isBadVersionResponse(err) {
    return fmt.Errorf("endpoint %s did not return etcd /version JSON; check proxy/port config", endpoint)
}

Prevention

When it happens

Trigger: probeClient/fetchVersion against an endpoint whose /version returns HTML (a proxy or web server error page), an empty body, a non-JSON error, or a JSON object without the expected keys.

Common situations: The endpoint URL actually points to a load balancer/ingress returning HTML 502 pages; a wrong port hitting a different service; a proxy intercepting requests and returning a captive/consent page; TLS terminating proxy serving its own error JSON schema.

Related errors


AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05). Data as JSON: /api/errors/0942e938fe001d33. Report an issue: GitHub.