JuliusBrussee/caveman · error

cacheengine: invalid scope

Error message

cacheengine: invalid scope

What it means

Returned by validatePlanRequest (used by both Plan and StartEpoch) when validIdentity(request.Scope, 4096, false) fails. Scope is a required, non-optional identity string: it must be non-empty, within 4096 bytes, and pass the identity character/structure rules the validator enforces (partitionKey=false means blank is not allowed). It names the cache scope the request belongs to.

Source

Thrown at cacheengine/engine.go:270

	if err != nil {
		return Plan{}, err
	}
	return Plan{
		Decision:       DecisionNewEpoch,
		Reason:         string(cacheguard.DecisionNewEpoch),
		ProfileID:      profile.ID,
		Mode:           profile.Mode,
		Attribution:    profile.Attribution,
		PrefixSHA256:   result.PrefixSHA256,
		EconomicsBasis: economicsBasis,
		KeyShardCount:  1,
		Warnings:       warnings,
	}, nil
}

func validatePlanRequest(request PlanRequest) error {
	if !validIdentity(request.Scope, 4096, false) {
		return errors.New("cacheengine: invalid scope")
	}
	if !validIdentity(request.Epoch, 4096, false) {
		return errors.New("cacheengine: invalid epoch")
	}
	if !validIdentity(request.PartitionKey, 4096, true) {
		return errors.New("cacheengine: invalid partition key")
	}
	if request.ExpectedCalls < 0 || request.ExpectedRequestsPerMinute < 0 {
		return errors.New("cacheengine: negative traffic expectation")
	}
	profile := normalizedProfile(request.Profile)
	if !validIdentity(profile.ID, 256, false) || !validIdentity(profile.Provider, 64, true) || !validIdentity(profile.OptimizerID, 256, true) {
		return errors.New("cacheengine: invalid profile identity")
	}
	if profile.Mode != ModeUnsupported && profile.Mode != ModeImplicit && profile.Mode != ModeAffinity && profile.Mode != ModeExplicit {
		return fmt.Errorf("cacheengine: unknown mode %q", profile.Mode)
	}
	if profile.Mode != ModeUnsupported {

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Set PlanRequest.Scope to a stable, non-empty identifier within 4096 bytes (e.g. 'tenant:42:session:abc' trimmed)
  2. Hash or truncate programmatically derived scopes: use a SHA-256 hex of oversized inputs
  3. Check the other identity fields in the same validator (Epoch, PartitionKey, profile) — they fail with their own messages

Example fix

// before
req := cacheengine.PlanRequest{Epoch: "e1"} // Scope left empty

// after
req := cacheengine.PlanRequest{Scope: "tenant:42:chat", Epoch: "e1"}
Defensive patterns

Strategy: validation

Validate before calling

func safeScope(scope string) string {
	if len(scope) > 4096 {
		sum := sha256.Sum256([]byte(scope))
		return hex.EncodeToString(sum[:])
	}
	return scope
}

if strings.TrimSpace(req.Scope) == "" {
	return errors.New("scope required")
}

Try / catch

if _, err := eng.Plan(req); err != nil {
	if err.Error() == "cacheengine: invalid scope" {
		return fmt.Errorf("scope %q invalid; use non-empty <=4096B identifier", req.Scope)
	}
	return Plan{}, err
}

Prevention

When it happens

Trigger: Calling Plan or StartEpoch with an empty Scope, one exceeding 4096 bytes, or containing characters the identity validator rejects (e.g. control characters or invalid UTF-8, per validIdentity's rules).

Common situations: Deriving scope from user IDs or URLs without sanitizing/truncating; forgetting to set Scope when building PlanRequest via struct literal; multi-tenant keys concatenating long tenant metadata past 4096 bytes.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/815b2ea1feb2d57a. Report an issue: GitHub.