t8y2/dbx · error

ETCD_V2_API_DISABLED

ETCD_V2_API_DISABLED

Error message

ETCD_V2_API_DISABLED: %s does not expose the etcd v2 API (removed in etcd 3.6+)

What it means

The etcd2 agent probes the v2 keys API (/v2/keys) to confirm it is served; etcd 3.6+ removed the v2 API, and such servers respond with HTTP 404. The agent converts that into the coded error ETCD_V2_API_DISABLED identifying the endpoint, so users get a clear diagnosis instead of confusing downstream failures.

Source

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

	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
	case http.StatusNotFound:
		return nil, fmt.Errorf("ETCD_V2_API_DISABLED: %s does not expose the etcd v2 API (removed in etcd 3.6+)", c.endpoint)
	case http.StatusUnauthorized:
		return nil, fmt.Errorf("ETCD_UNAUTHENTICATED: authentication failed against %s", c.endpoint)
	default:
		body, _ := io.ReadAll(io.LimitReader(response.Body, 4096))
		return nil, fmt.Errorf("etcd v2 probe against %s failed: HTTP %d %s", c.endpoint, response.StatusCode, strings.TrimSpace(string(body)))
	}
}

// do performs a v2 API request and returns the body. Non-2xx responses are
// converted into etcdError values carrying the server's errorCode/message.
func (c *authenticatedClient) do(ctx context.Context, method, path, body string, header map[string]string) ([]byte, *http.Response, error) {
	response, err := c.request(ctx, method, path, body, header)
	if err != nil {
		return nil, response, err
	}
	payload, readErr := io.ReadAll(response.Body)
	_ = response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {

View on GitHub (pinned to c0390bff16)

Solutions

  1. Use the v3 agent driver (agents/drivers/etcd-go) instead of the etcd2 driver against etcd 3.6+
  2. Downgrade/keep an etcd 3.5.x server if the v2 API is genuinely required
  3. On servers that still support the flag, start etcd with --enable-v2=true
  4. Check for a proxy stripping /v2/ paths and route v2 requests directly to etcd

Example fix

// before
agent := etcd2agent.New("https://etcd:2379") // etcd 3.6: no v2 API

// after
agent := etcdagent.New("https://etcd:2379") // v3 driver for etcd >= 3.6
Defensive patterns

Strategy: fallback

Validate before calling

// detect v2 API availability before connecting with the etcd2 driver
resp, err := http.Get(endpoint + "/v2/keys")
if err == nil && resp.StatusCode == http.StatusNotFound {
    return errors.New("v2 API unavailable: use the v3 agent driver")
}

Type guard

func isV2Disabled(err error) bool {
    return err != nil && strings.Contains(err.Error(), "ETCD_V2_API_DISABLED")
}

Try / catch

conn, err := etcd2agent.Connect(endpoint)
if isV2Disabled(err) {
    conn, err = etcdagent.Connect(endpoint) // fall back to v3 driver
}

Prevention

When it happens

Trigger: Connecting (probeClient -> probeV2) to an etcd server that returns HTTP 404 for /v2/keys — i.e. etcd 3.6 or newer with --enable-v2 not enabled/removed.

Common situations: Upgrading an etcd cluster from 3.5 to 3.6+ where the v2 API was removed; using the etcd2 agent driver against a modern etcd cluster by mistake; a reverse proxy returning 404 for /v2/keys because the route does not exist.

Related errors


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