Tencent/WeKnora · error

encode stale sandbox binding: %w

Error message

encode stale sandbox binding: %w

What it means

Returned by markBindingStale when json.Marshal of the marked copy of the binding (expected with StaleAt set) fails before the conditional Lua write. Like the encode error in Create, this signals a non-JSON-serializable field or a failing custom marshaller on SessionSandboxBinding. Wrapped as 'encode stale sandbox binding: %w'.

Source

Thrown at internal/sandbox/session_binding_redis.go:273

	}
}

// markBindingStale writes the marked binding back only while the stored one
// still names the same sandbox.
func (s *RedisSessionSandboxBindingStore) markBindingStale(
	ctx context.Context,
	key SessionSandboxKey,
	expected SessionSandboxBinding,
	staleAt time.Time,
) (bool, error) {
	if err := validateBindingMatch(key, expected.Provider, expected.SandboxID); err != nil {
		return false, err
	}
	marked := expected
	marked.StaleAt = &staleAt
	payload, err := json.Marshal(marked)
	if err != nil {
		return false, fmt.Errorf("encode stale sandbox binding: %w", err)
	}
	wrote, err := markBindingStaleIfMatchScript.Run(
		ctx,
		s.client,
		[]string{s.bindingKey(key)},
		string(expected.Provider),
		expected.SandboxID,
		payload,
	).Int64()
	if err != nil {
		return false, fmt.Errorf("mark sandbox binding stale: %w", err)
	}
	return wrote != 0, nil
}

// escapeRedisGlob quotes the characters SCAN's MATCH treats as wildcards. The
// namespace is operator-supplied and only screened for braces and control
// characters, so a namespace containing "*" would otherwise widen the pattern

View on GitHub (pinned to 988cbb0330)

Solutions

  1. Read the wrapped json error for the offending type and drop or json:"-" that field
  2. Ensure StaleAt *time.Time (JSON-safe) rather than a non-standard time type
  3. Test markBindingStale/InvalidateByConfig in CI with representative bindings after any struct change

Example fix

// before
type SessionSandboxBinding struct {
    Provider  RemoteProvider
    SandboxID string
    StaleAt   *time.Time
    Conn      *grpc.ClientConn // not JSON-encodable
}
// after
type SessionSandboxBinding struct {
    Provider  RemoteProvider
    SandboxID string
    StaleAt   *time.Time
    Conn      *grpc.ClientConn `json:"-"`
}
Defensive patterns

Strategy: validation

Validate before calling

// assert the struct (with StaleAt set) remains encodable after any schema change
func TestMarkedBindingEncodable(t *testing.T) {
    stale := time.Now().UTC()
    b := sandbox.SessionSandboxBinding{Provider: provider, SandboxID: "sbx-1", StaleAt: &stale}
    if _, err := json.Marshal(b); err != nil {
        t.Fatalf("marked binding not encodable: %v", err)
    }
}

Try / catch

ok, err := invalidateConfig(ctx, store, tenantID, configID)
if err != nil && strings.Contains(err.Error(), "encode stale sandbox binding") {
    return fmt.Errorf("binding schema cannot be persisted — fix struct and redeploy: %w", err)
}

Prevention

When it happens

Trigger: Calling markBindingStale (via InvalidateByConfig) for a binding whose struct, after copying expected and setting StaleAt, cannot be marshaled — unsupported field types, cyclic references, or a MarshalJSON error.

Common situations: Schema change added a runtime/non-serializable field to SessionSandboxBinding; a binding struct value that came from an unusual code path holds an unsupported embedded type; custom marshaller chokes on the *time.Time StaleAt representation.

Related errors


AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02). Data as JSON: /api/errors/fadc6e963401cde0. Report an issue: GitHub.