grafana/k6 · error

unable to normalize number strings: %w

Error message

unable to normalize number strings: %w

What it means

Before serializing a request, k6 deep-walks it with normalizeNumberStrings (converting NaN and +/-Infinity to strings so JSON encoding works); the walk aborts with 'cyclic reference to an object found' when any object participates in a reference cycle, surfaced as 'unable to normalize number strings: ...' (internal/js/modules/k6/grpc/client.go:388). Even one cycle in a deeply nested field triggers it.

Source

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

	if err != nil {
		return grpcReq, fmt.Errorf("invalid GRPC's client.invoke() parameters: %w", err)
	}

	// k6 GRPC Invoke's default timeout is 2 minutes
	if p.Timeout == time.Duration(0) {
		p.Timeout = 2 * time.Minute
	}

	if req == nil {
		return grpcReq, errors.New("request cannot be nil")
	}

	object := req.ToObject(c.vu.Runtime())

	var stack []*sobek.Object
	normalized, err := normalizeNumberStrings(object, c.vu.Runtime(), stack)
	if err != nil {
		return grpcReq, fmt.Errorf("unable to normalize number strings: %w", err)
	}

	b, err := normalized.ToObject(c.vu.Runtime()).MarshalJSON()
	if err != nil {
		return grpcReq, fmt.Errorf("unable to serialise request object: %w", err)
	}

	p.SetSystemTags(state, c.addr, method)

	return grpcext.InvokeRequest{
		Method:                 method,
		MethodDescriptor:       methodDesc,
		Timeout:                p.Timeout,
		DiscardResponseMessage: p.DiscardResponseMessage,
		Message:                b,
		TagsAndMeta:            &p.TagsAndMeta,
		Metadata:               p.Metadata,
	}, nil

View on GitHub (pinned to 93accf6570)

Solutions

  1. Break the cycle: build a fresh plain object containing only the message fields
  2. Use JSON.parse(JSON.stringify(req)) as a diagnostic - it throws on the same cycle and points at the offending path
  3. Keep shared or cyclic state out of the actual message payload

Example fix

// before
const req = { id: 1 };
const container = { name: 'root', req };
req.parent = container; // cycle
client.invoke('/svc/Do', req);

// after
client.invoke('/svc/Do', { id: req.id });
Defensive patterns

Strategy: type-guard

Validate before calling

// optional early check: JSON.stringify throws on the same cycles
try { JSON.stringify(req); } catch (e) { throw new Error(`request is not serializable: ${e.message}`); }

Type guard

function isAcyclic(value, seen = new WeakSet()) {
  if (value === null || typeof value !== 'object') return true;
  if (seen.has(value)) return false;
  seen.add(value);
  return Object.values(value).every(v => isAcyclic(v, seen));
}
if (!isAcyclic(req)) throw new Error('request contains a cyclic reference');

Try / catch

try { client.invoke(m, req); } catch (e) { if (/cyclic reference|unable to normalize number strings/.test(e.message)) { /* rebuild req as a fresh plain object without back-references */ } throw e; }

Prevention

When it happens

Trigger: client.invoke(m, req) where req or any nested value references itself: req.self = req, parent/child objects pointing at each other, or a cyclic module-level shared structure passed as the message.

Common situations: Building request trees with back-references (audit entries with parent pointers); passing memoized/cached object graphs as protobuf messages; ORM-ish objects with owner links.

Related errors


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