grafana/k6 · error

must be an object with key-value pairs

Error message

must be an object with key-value pairs

What it means

newMetadata (internal/js/modules/k6/grpc/params.go:73) converts gRPC metadata from JavaScript into Go's metadata.MD. It exports the value and requires a `map[string]any`; anything else — arrays, strings, numbers, class instances — fails the assertion with "must be an object with key-value pairs". It is reached from the `metadata` field of call/stream params and the `reflectMetadata` field of connect params.

Source

Thrown at internal/js/modules/k6/grpc/params.go:84

		}
	}

	return result, nil
}

// newMetadata constructs a metadata.MD from the input value.
func newMetadata(input sobek.Value) (metadata.MD, error) {
	md := metadata.New(nil)

	if common.IsNullish(input) {
		return md, nil
	}

	v := input.Export()

	rawHeaders, ok := v.(map[string]any)
	if !ok {
		return md, errors.New("must be an object with key-value pairs")
	}

	for hk, kv := range rawHeaders {
		var val string
		// The gRPC spec defines that Binary-valued keys end in -bin
		// https://grpc.io/docs/what-is-grpc/core-concepts/#metadata
		if strings.HasSuffix(hk, "-bin") {
			var binVal []byte
			if binVal, ok = kv.([]byte); !ok {
				return md, fmt.Errorf("%q value must be binary", hk)
			}

			// https://github.com/grpc/grpc-go/blob/v1.57.0/Documentation/grpc-metadata.md#storing-binary-data-in-metadata
			val = string(binVal)
		} else if val, ok = kv.(string); !ok {
			return md, fmt.Errorf("%q value must be a string", hk)
		}

View on GitHub (pinned to 93accf6570)

Solutions

  1. Pass a plain object literal: `metadata: { 'authorization': 'token k6' }`
  2. Convert arrays/Maps to a plain object before the call
  3. Use string values for normal keys and []byte (keys ending in -bin) for binary keys

Example fix

// before
client.connect(addr, { reflectMetadata: [['api-key', 'secret']] });

// after
client.connect(addr, { reflectMetadata: { 'api-key': 'secret' } });
Defensive patterns

Strategy: validation

Validate before calling

const isPlainObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
if (!isPlainObject(metadata)) { throw new TypeError('metadata must be a plain object of key/value pairs'); }

Type guard

const isMetadataObject = (v) =>
  typeof v === 'object' && v !== null && !Array.isArray(v) &&
  Object.values(v).every((x) => typeof x === 'string');

Try / catch

try {
  client.connect(addr, { reflectMetadata: md });
} catch (e) {
  if (String(e.message).includes('must be an object with key-value pairs')) throw new Error('reflectMetadata/metadata must be { key: value }, got: ' + JSON.stringify(md));
  throw e;
}

Prevention

When it happens

Trigger: Passing `metadata: 'authorization: token'` (string), `metadata: [['k','v']]` (array), `metadata: 42`, or a non-plain object (e.g. a Map or class instance) in the params of connect/invoke/stream: `client.connect(addr, { reflectMetadata: [] })` or `new grpc.Stream(client, method, { metadata: 'k' })`.

Common situations: Porting curl-style header strings or header arrays from HTTP code into gRPC params; using a JS Map because the docs show object syntax; building metadata with a helper that returns a non-plain object.

Related errors


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