gastownhall/beads · error · issueops.ErrValidation
%w: %v
Error message
%w: %v
What it means
PlanCompareAndSetKey validates the metadata key with ValidateMetadataKey and wraps any failure as issueops.ErrValidation with the underlying message. ValidateMetadataKey rejects invalid characters (including quotes and backslashes) and empty/oversized keys, since keys are embedded in JSON path expressions and must be safe. The wrapped %v carries the specific rule that was violated.
Source
Thrown at internal/storage/metadata_cas.go:68
// PlanCompareAndSetKey validates a compare-and-set request and canonicalizes
// its values. It is the whole of the role's request validation: every
// implementation calls it before touching a substrate, so a refused request
// costs no database work anywhere.
//
// It COPIES both raw values rather than aliasing the caller's, because the
// canonical form is written into the plan and the request belongs to the caller
// for the whole call.
func PlanCompareAndSetKey(in issueops.CompareAndSetKeyRequest) (CompareAndSetKeyPlan, error) {
if in.Actor == "" {
return CompareAndSetKeyPlan{}, fmt.Errorf(
"%w: compare-and-set requires an actor to attribute the swap to", issueops.ErrValidation)
}
if in.IssueID == "" {
return CompareAndSetKeyPlan{}, fmt.Errorf(
"%w: compare-and-set requires an issue id", issueops.ErrValidation)
}
if err := ValidateMetadataKey(in.Key); err != nil {
return CompareAndSetKeyPlan{}, fmt.Errorf("%w: %v", issueops.ErrValidation, err)
}
plan := CompareAndSetKeyPlan{Actor: in.Actor, IssueID: in.IssueID, Key: in.Key}
var err error
if plan.Expected, err = CanonicalMetadataPointer(in.Expected); err != nil {
return CompareAndSetKeyPlan{}, fmt.Errorf("%w: expected value for metadata key %q: %v",
issueops.ErrValidation, in.Key, err)
}
if plan.Value, err = CanonicalMetadataPointer(in.Value); err != nil {
return CompareAndSetKeyPlan{}, fmt.Errorf("%w: new value for metadata key %q: %v",
issueops.ErrValidation, in.Key, err)
}
return plan, nil
}
// CanonicalMetadataValue returns raw's canonical encoding: the encoding two
// JSON metadata values share exactly when issueops.MetadataCAS calls them
// equal.
//View on GitHub (pinned to 71377f2769)
Solutions
- Call ValidateMetadataKey(key) yourself first and surface the specific rule violation to the user.
- Sanitize the key: lowercase kebab-case, letters/digits/hyphens, no quotes or backslashes.
- Trim whitespace and reject empty keys before constructing the request.
- Check errors.Is(err, issueops.ErrValidation) to distinguish this from storage errors.
Example fix
// before
key := "my key \"quoted\"" // illegal characters
plan, err := PlanCompareAndSetKey(req) // ErrValidation
// after
key := "my-key"
if err := ValidateMetadataKey(key); err == nil {
plan, err = PlanCompareAndSetKey(req)
} Defensive patterns
Strategy: validation
Validate before calling
if err := ValidateMetadataKey(req.Key); err != nil {
return fmt.Errorf("invalid metadata key %q: %v", req.Key, err)
} // run before PlanCompareAndSetKey Type guard
func safeKey(k string) bool { return ValidateMetadataKey(k) == nil } Try / catch
plan, err := PlanCompareAndSetKey(req)
if err != nil {
if errors.Is(err, issueops.ErrValidation) {
return fmt.Errorf("compare-and-set rejected: %v", err)
}
return err
} Prevention
- Restrict keys to kebab-case alphanumerics; reject quotes, backslashes, and whitespace at the input boundary.
- Trim and check non-empty before accepting user-supplied keys.
- Re-run ValidateMetadataKey in tests for every key format your tooling can produce.
When it happens
Trigger: Calling PlanCompareAndSetKey with in.Key containing illegal characters (quotes, backslashes, whitespace/control chars), an empty key, or a key exceeding the allowed length — e.g. a key read from user input without sanitization.
Common situations: Building keys dynamically from labels or filenames that contain spaces/quotes; copy-pasting keys with trailing whitespace; API consumers passing arbitrary user input straight through as a key.
Related errors
- %w: compare-and-set requires an actor to attribute the swap
- %w: compare-and-set requires an issue id
- %w: expected value for metadata key %q: %v
- %w: new value for metadata key %q: %v
- ExternalDoltConfig: must set Socket or (Host, Port)
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/462d5e31794eb5d4.
Report an issue: GitHub.