t8y2/dbx · error

ETCD_WATCH_SCOPE_INVALID

ETCD_WATCH_SCOPE_INVALID

Error message

ETCD_WATCH_SCOPE_INVALID: scope must be key or prefix

What it means

watchStart validates the 'scope' parameter before opening an etcd watch stream. Only 'key' (watch a single key) and 'prefix' (watch all keys under a prefix) are supported; any other value is rejected before a client is even acquired. This fails fast so an invalid scope never reaches the etcd server.

Source

Thrown at agents/drivers/etcd-go/watch.go:227

func (s *etcdSession) removeWatch(id string) *watchState {
	s.watchesMu.Lock()
	defer s.watchesMu.Unlock()
	state := s.watches[id]
	delete(s.watches, id)
	return state
}

func (s *etcdSession) watchStart(params map[string]json.RawMessage) (any, error) {
	if s.watchCount() >= maxWatches {
		return nil, fmt.Errorf("ETCD_WATCH_LIMIT: at most %d watches are allowed per connection", maxWatches)
	}
	key, err := keyBytesParam(params)
	if err != nil {
		return nil, err
	}
	scope := stringOrDefault(params, "scope", "key")
	if scope != "key" && scope != "prefix" {
		return nil, errors.New("ETCD_WATCH_SCOPE_INVALID: scope must be key or prefix")
	}
	client, err := s.activeClient()
	if err != nil {
		return nil, err
	}
	requestedRevision := longOrNull(params, "startRevision")
	var startedRevision int64
	if requestedRevision != nil && *requestedRevision > 0 {
		startedRevision = *requestedRevision
	} else {
		ctx, cancel := context.WithTimeout(context.Background(), rpcTimeoutSeconds*time.Second)
		// Read the revision from the same Key scope that will be watched. A global
		// range is forbidden for users that are intentionally limited to one or
		// more prefixes, even when this individual Key or prefix is readable.
		revisionOptions := []clientv3.OpOption{clientv3.WithCountOnly()}
		if scope == "prefix" {
			revisionOptions = append(revisionOptions, clientv3.WithRange(prefixEnd(key)))
		}

View on GitHub (pinned to c0390bff16)

Solutions

  1. Pass scope:'key' or scope:'prefix' exactly (lowercase)
  2. Omit 'scope' entirely to use the default of 'key'
  3. Validate user-supplied scope strings against the allowed set before calling

Example fix

// before
{ "key": "/cfg", "scope": "subtree" }
// after
{ "key": "/cfg", "scope": "prefix" }
Defensive patterns

Strategy: validation

Validate before calling

func validWatchScope(p map[string]json.RawMessage) bool {
	s, ok := p["scope"]
	if !ok { return true }
	var v string
	if json.Unmarshal(s, &v) != nil { return false }
	return v == "key" || v == "prefix"
}

Type guard

func isWatchScope(v string) bool { return v == "key" || v == "prefix" }

Try / catch

params := map[string]json.RawMessage{"key": keyJSON, "scope": scopeJSON}
if !validWatchScope(params) { return fmt.Errorf("scope %q invalid; use key or prefix", scope) }
w, err := agent.handle(ctx, "watch", params)
if err != nil && strings.Contains(err.Error(), "ETCD_WATCH_SCOPE_INVALID") { return err }

Prevention

When it happens

Trigger: Calling the watch start handler with params['scope'] set to anything other than 'key' or 'prefix', e.g. 'range', 'dir', or a typo like 'prefx'.

Common situations: Porting code from other watch APIs that use scope names like 'subtree' or 'watch_prefix'; dynamically building the scope from user input that was never validated; case mismatch like 'Key' or 'PREFIX'.

Understand the failure class

Background: "must be positive", "Invalid value": how libraries reject invalid parameter values (ValueError, ArgumentError, INVALID_PARAMETER_VALUE) — this error's family across 28 libraries.

Related errors


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