grafana/k6 · error

invalid WebSocket tags option: %w

Error message

invalid WebSocket tags option: %w

What it means

new WebSocket(url, { tags: {...} }) validates each tag through common.ApplyCustomUserTags before connecting. If a tag value is not a string, boolean, or number (e.g. an object, array, or nested structure), the apply fails and k6 wraps it as 'invalid WebSocket tags option'. Only flat, primitive-valued tags are representable in k6's metric engine.

Source

Thrown at internal/js/modules/k6/websockets/params.go:59

	params := raw.ToObject(rt)
	for _, k := range params.Keys() {
		switch k {
		case "headers":
			headersV := params.Get(k)
			if common.IsNullish(headersV) {
				continue
			}
			headersObj := headersV.ToObject(rt)
			if headersObj == nil {
				continue
			}
			for _, key := range headersObj.Keys() {
				parsed.headers.Set(key, headersObj.Get(key).String())
			}
		case "tags":
			if err := common.ApplyCustomUserTags(rt, parsed.tagsAndMeta, params.Get(k)); err != nil {
				return nil, fmt.Errorf("invalid WebSocket tags option: %w", err)
			}
		case "jar":
			jarV := params.Get(k)
			if common.IsNullish(jarV) {
				continue
			}
			if v, ok := jarV.Export().(*httpModule.CookieJar); ok {
				parsed.cookieJar = v.Jar
			}
		case "compression":
			// deflate compression algorithm is supported - as defined in RFC7692
			// compression here relies on the implementation in gorilla/websocket package, usage is
			// experimental and may result in decreased performance. package supports
			// only "no context takeover" scenario

			algoString := strings.TrimSpace(params.Get(k).ToString().String())
			if algoString != "deflate" {
				return nil, fmt.Errorf("unsupported compression algorithm '%s', supported algorithm is 'deflate'", algoString)

View on GitHub (pinned to 93accf6570)

Solutions

  1. Flatten every tag value to a primitive: tenant: String(obj.id) or JSON.stringify for complex values
  2. Whitelist only the scalar keys you need when spreading: { tags: { name: cfg.name, env: cfg.env } } where both are strings/numbers
  3. Sanitize with a helper that drops or stringifies non-primitives before constructing the WebSocket
  4. Check the wrapped cause text — it names the offending value/type

Example fix

// before
const ws = new WebSocket(url, { tags: { user: userObj } }); // userObj is {id:1} -> invalid

// after
const ws = new WebSocket(url, { tags: { user: String(userObj.id) } });
Defensive patterns

Strategy: validation

Validate before calling

function sanitizeTags(tags) {
  const out = {};
  for (const [k, v] of Object.entries(tags || {})) {
    if (v == null) continue;
    const t = typeof v;
    if (t === 'string' || t === 'number' || t === 'boolean') out[k] = v;
    else out[k] = JSON.stringify(v);
  }
  return out;
}
const sock = new WebSocket(url, { tags: sanitizeTags(cfgTags) });

Type guard

const isPrimitiveTag = v => v == null ? false : ['string','number','boolean'].includes(typeof v);

Try / catch

try { new WebSocket(url, { tags }); } catch (e) { if (/tags option/.test(e.message)) throw new Error(`tag values must be string/number/boolean: ${e.message}`); throw e; }

Prevention

When it happens

Trigger: new WebSocket(url, { tags: { tenant: { id: 7 } } }), tags: { list: ['a','b'] }, or any tags object whose export is a map containing non-primitive values. The headers/compression cases in the same parse loop succeed; only the tags branch produces this message.

Common situations: Reusing an options object built for HTTP requests where a tag value happens to be an object; spreading a config object into tags and accidentally including nested keys; migrating scripts where tags were previously silently coerced; putting the URL or params object itself into tags.

Related errors


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