grafana/k6 · error

couldn't unmarshal protoset file %s: %w

Error message

couldn't unmarshal protoset file %s: %w

What it means

grpc.Client.load() read the file bytes but proto.Unmarshal could not decode them as a protobuf-serialized FileDescriptorSet; the error is wrapped as 'couldn't unmarshal protoset file <path>' (internal/js/modules/k6/grpc/client.go:118). The file exists and is readable - its bytes are simply not a protoset. The offending path is included in the message.

Source

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

	if initEnv == nil {
		return nil, errors.New("missing init environment")
	}

	absFilePath := initEnv.GetAbsFilePath(protosetPath)
	fdsetFile, err := initEnv.FileSystems["file"].Open(absFilePath)
	if err != nil {
		return nil, fmt.Errorf("couldn't open protoset: %w", err)
	}

	defer func() { _ = fdsetFile.Close() }()
	fdsetBytes, err := io.ReadAll(fdsetFile)
	if err != nil {
		return nil, fmt.Errorf("couldn't read protoset: %w", err)
	}

	fdset := &descriptorpb.FileDescriptorSet{}
	if err = proto.Unmarshal(fdsetBytes, fdset); err != nil {
		return nil, fmt.Errorf("couldn't unmarshal protoset file %s: %w", protosetPath, err)
	}

	return c.convertToMethodInfo(fdset)
}

// Note: this function was lifted from `lib/options.go`
func decryptPrivateKey(key, password []byte) ([]byte, error) {
	block, _ := pem.Decode(key)
	if block == nil {
		return nil, errors.New("failed to decode PEM key")
	}

	blockType := block.Type
	if blockType == "ENCRYPTED PRIVATE KEY" {
		return nil, errors.New("encrypted pkcs8 formatted key is not supported")
	}
	/*
	   Even though `DecryptPEMBlock` has been deprecated since 1.16.x it is still

View on GitHub (pinned to 93accf6570)

Solutions

  1. Regenerate correctly: protoc --include_imports --descriptor_set_out=service.protoset service.proto
  2. Sanity-check the file: `file service.protoset` should report data, not ASCII text
  3. Or skip the protoset and use server reflection: client.connect(addr, { reflect: true })

Example fix

# before (produces a .proto text file, not a protoset)
protoc --proto_path=. service.proto --descriptor_set_out=service.protoset

# after (binary FileDescriptorSet including imports)
protoc --include_imports --proto_path=. service.proto --descriptor_set_out=service.protoset
Defensive patterns

Strategy: validation

Validate before calling

// cheap guard: a valid protoset is binary protobuf, not .proto text
const head = open('./pb/service.protoset');
if (head.startsWith('syntax') || head.startsWith('//')) {
  throw new Error('file looks like .proto source; compile with protoc --descriptor_set_out first');
}
client.load('./pb/service.protoset');

Try / catch

try {
  client.load(p);
} catch (e) {
  if (/couldn't unmarshal protoset/.test(e.message)) {
    throw new Error(`${p} is not a FileDescriptorSet; regenerate with protoc --include_imports --descriptor_set_out`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a .proto source file (plain text) to client.load(); passing a JSON descriptor or a single FileDescriptorProto instead of a set; a protoset corrupted by text-mode transfer (base64/UTF-8 mangling, git media filters).

Common situations: Generating descriptors with the wrong protoc invocation (must be --descriptor_set_out, typically with --include_imports); renaming a .proto file to .protoset instead of compiling; binary files mangled through chat tools or copy-paste.

Related errors


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