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
- Pass scope:'key' or scope:'prefix' exactly (lowercase)
- Omit 'scope' entirely to use the default of 'key'
- 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
- Restrict scope values in your config schema to enum key|prefix
- Normalize user input with strings.ToLower before passing
- Write a table-driven test asserting only key/prefix are accepted
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
- ETCD_WATCH_NOT_FOUND
- lease, ttl, and preserveLease cannot be specified together
- ETCD_WATCH_SCOPE_INVALID
- ETCD_WATCH_LIMIT
- ETCD_INVALID_ACCESS
AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05).
Data as JSON: /api/errors/c316e69ca02b8053.
Report an issue: GitHub.