grafana/k6 · error

cyclic reference to an object found

Error message

cyclic reference to an object found

What it means

Before marshaling the request, k6 deep-copies and normalizes the sobek object graph (normalizeNumberStrings) to handle int64/uint64 values. While walking objects and arrays it keeps a stack of visited objects and rejects any node that appears again, because a cyclic graph cannot be serialized.

Source

Thrown at internal/js/modules/k6/grpc/client.go:447

		return v, nil
	}

	obj := v.ToObject(runtime)

	// If it's not a real object (e.g., a string, bool, or regular number primitive),
	// after checking for special floats, we can just return it.
	// We use obj.ClassName() to distinguish real objects from primitive wrappers.
	className := obj.ClassName()
	if className != "Object" && className != "Array" {
		return v, nil
	}

	// Similar to what's done when marshaling into JSON,
	// detect and prevent infinite recursion due to circular object references.
	for _, vis := range stack {
		if obj.SameAs(vis) {
			return nil, errors.New("cyclic reference to an object found")
		}
	}
	stack = append(stack, obj)

	// It's a real object or array, so we need to deep-copy and normalize it.
	var newObj *sobek.Object
	if className == "Array" {
		newObj = runtime.NewArray()
	} else {
		newObj = runtime.NewObject()
	}

	for _, key := range obj.Keys() {
		val := obj.Get(key)
		normalizedVal, err := normalizeNumberStrings(val, runtime, stack)
		if err != nil {
			return nil, err
		}

View on GitHub (pinned to 93accf6570)

Solutions

  1. Construct the payload inline or via a builder that never sets back-references
  2. Strip parent/back links before invoking: delete node.parent
  3. For one-off sanitization, deep-clone with a replacer that skips already-seen objects (see validationCode)

Example fix

// before
const req = { id: 1 };
req.self = req; // cycle
client.invoke('/pkg.Svc/Method', req);

// after
const req = { id: 1 };
client.invoke('/pkg.Svc/Method', req);
Defensive patterns

Strategy: validation

Validate before calling

function assertAcyclic(value) {
  const seen = new Set();
  (function walk(v) {
    if (v === null || typeof v !== 'object') return;
    if (seen.has(v)) throw new Error('cyclic reference detected in gRPC request payload');
    seen.add(v);
    for (const k of Object.keys(v)) walk(v[k]);
  })(value);
  return value;
}

client.invoke(method, assertAcyclic(payload));

Prevention

When it happens

Trigger: client.invoke('/pkg.Svc/Method', a) where a.self = a; mutual references such as a.b = b and b.a = a (including via arrays); payloads assembled from data structures that carry parent/back pointers.

Common situations: Building nested request payloads from ORM-like or tree structures that store parent links; caching/interning objects that reference each other; reusing a response object (deserialized with back-references) as the next request.

Related errors


AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15). Data as JSON: /api/errors/92d8f2ec69028cdd. Report an issue: GitHub.