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
- Construct the payload inline or via a builder that never sets back-references
- Strip parent/back links before invoking: delete node.parent
- 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
- Build request payloads fresh and flat per invoke; never reuse graphs with back/parent pointers
- Run assertAcyclic (or JSON.stringify in dev) on payloads assembled from cached structures
- Strip bookkeeping fields (parent, _root, ctx) from ORM/tree data before sending
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
- unable to normalize number strings: %w
- no gRPC connection, you must call connect first
- method to invoke cannot be empty
- request cannot be nil
- couldn't unmarshal protoset file %s: %w
AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15).
Data as JSON: /api/errors/92d8f2ec69028cdd.
Report an issue: GitHub.