Protopath Problems in Go

2024-09-11

Failed to compute set of methods to expose - Symbol not found

This cryptic error has reared its ugly head a few times in the past when working with grpc in Golang. I can’t remember how I’ve solved it before. Probably with some combination of stackoverflow answers and random github gists with advice that I have now forgotten. When it happened most recently, I decided to try to actually understand what was going on under the hood.

Note: For the purposes of this post I am using Golang v1.22.3, libprotoc 3.21.12, google.golang.org/grpc v1.63.2, and google.golang.org/protobuf v1.34.1.

I had gone a long time without encountering this error, only stumbling upon on it again while using the protovalidate library for validating protobuf messages. Used in an interceptor it provides a way to validate input messages on gRPCs.

According to the docs, I should use the validate.proto files by importing them into my project and referencing them in my primary proto files.

syntax = "proto3";

import "buf/validate/validate.proto";

message User {
  // User's name, must be at least 1 character long.
  string name = 1 [(buf.validate.field).string.min_len = 1];
}

I am not a fan of having to add files under arbitrary directory structures so I did not store my validate.proto (and expression.proto and some others that are dependencies of those two) under the buf directory. I stored them under the validate directory. This is what things looked like.

├── services.proto
└── validate
    ├── expression.proto
    ├── priv
    │   └── private.proto
    └── validate.proto

services.proto being my primary proto file of interest. It imported validate.proto like so:

import "validate/validate.proto";

My generation script does not generate any code from validate.proto, since we’re using the schema for its options only, that then get baked into the output for my services.proto file.

For performing the validations on these baked, generated options, I’m using github.com/bufbuild/protovalidate-go. Code example from its github repo:

package main

import (
	"fmt"
	"time"
	
	pb "github.com/path/to/generated/protos"
	"github.com/bufbuild/protovalidate-go"
	"google.golang.org/protobuf/types/known/timestamppb"
)

func main() {
	msg := &pb.Transaction{
		Id:           1234,
		Price:        "$5.67",
		PurchaseDate: timestamppb.New(time.Now()),
		DeliveryDate: timestamppb.New(time.Now().Add(time.Hour)),
	}

	v, err := protovalidate.New()
	if err != nil {
		fmt.Println("failed to initialize validator:", err)
	}

	if err = v.Validate(msg); err != nil {
		fmt.Println("validation failed:", err)
	} else {
		fmt.Println("validation succeeded")
	}
}

Compiling the protobuf into Golang works: the gRPC server registers the RPC server defined in the schema and my program runs. If I call any of the RPCs, it works fine.

It’s when I try to use the reflection server is when we run into trouble. I’ve built a sample project to demonstrate this over at https://github.com/BadgerBadgerBadgerBadger/protoreflect-error-test/tree/main/error-state.

Our proto folder looks like this:

.
├── dependencies
│   └── secondary
│       └── secondary.proto
├── gen.sh
└── primary.proto

Our gen.sh:

set -e;  
  
# Get the directory where the script is located  
scriptDir="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )";  
echo "script directory: $scriptDir";  

# ensure output directory exists
outDir="$scriptDir/lang_go";  
mkdir -p $outDir/primary;  

# build the secondary proto
protoc -I="$scriptDir/dependencies" \  
    --go_out="$outDir" --go_opt=paths=source_relative \  
    --go-grpc_out="$outDir" --go-grpc_opt=paths=source_relative \  
    secondary/secondary.proto  

# build the primary proto
protoc -I="$scriptDir" \  
    --go_out="$outDir/primary" --go_opt=paths=source_relative \  
    --go-grpc_out="$outDir/primary" --go-grpc_opt=paths=source_relative \  
    primary.proto

Our primary.proto and secondary.proto files:

syntax = "proto3";  
  
option go_package = "github.com/BadgerBadgerBadgerBadger/protoreflect-error-test/proto/lang_go/primary";  
package protoreflect.error.test.primary;  
  
import "dependencies/secondary/secondary.proto";  
  
service Main {  
  rpc GetPrimary (PrimaryRequest) returns (PrimaryResponse) {}  
}  
  
message PrimaryRequest {  
  secondary.SecondaryMessage primary_request = 1;  
}  
  
message PrimaryResponse {  
  secondary.SecondaryMessage primary_response = 1;  
}
syntax = "proto3";  
  
option go_package = "github.com/BadgerBadgerBadgerBadger/protoreflect-error-test/proto/lang_go/secondary";  
  
package protoreflect.error.test.secondary;  
  
message SecondaryMessage {  
  string value = 1;  
}

I won’t paste the go code here that runs the server since it’s of less interest to us but you can find that here.

When we run this, and then try to use grpcui to introspect the server, we get:

➜  ~ grpcui -plaintext localhost:8000
Failed to compute set of methods to expose: Symbol not found: protoreflect.error.test.primary.Main
caused by: File not found: dependencies/secondary/secondary.proto

This is the error that gave me a significant amount of headache before I fully understood not only what causes it but also what causes it in my specific situation.

I stumbled across this github issue. If you’re smart (which I am not) you will immediately understand the problem and figure out a solution. It took me a bit longer since I also like to understand how things work.

Since dependencies/secondary/secondary.proto is the culprit let’s look at two things:

  • How are we importing that dependency?
  • How are we building that dependency?

Looking at our primary.proto file, we are importing it as:

import "dependencies/secondary/secondary.proto";

And while the reflection server is trying to find it by that path, it can’t seem to.

Looking at our gen.sh script, we see:

protoc -I="$scriptDir/dependencies" \  
    --go_out="$outDir" --go_opt=paths=source_relative \  
    --go-grpc_out="$outDir" --go-grpc_opt=paths=source_relative \  
    secondary/secondary.proto

We are building our dependency with the path secondary/secondary.proto.

“But Badger”, you might ask, “Why does it matter what path we use to reference our dependency while building?” And I might answer: “I didn’t think it would, but apparently it does!”

As jhump says in his comment to that github issue I linked:

this is a problem in the Go protobuf runtime regarding how file descriptors that are compiled into your binary are “linked”.

They are linked purely by name. What that means is that the name (and relative path) used to compile a proto with protoc must exactly match how all other files will import it.

Which essentially means that the name you use while building a certain dependency gets encoded in the Golang code of the generated file as the path for that dependency. To me this feels silly, but without jumping into google.golang.org/protobuf’s code and understanding why they built it this way, it would be similarly silly to pass judgement.

While jhump’s example pointed to this file I could not find a similar example in my own code due to the differences in library and tool versions.

I looked at the Golang code generated and followed this code path:

I’m sure there’s an easier way to do this, but I did it by stepping through the code using Goland’s debugging tools.

func (r *Files) RegisterFile(file protoreflect.FileDescriptor) error is the function responsible for registering a protobuf file to the Global Registry

Debugging our way to line 125 we see that the path has been encoded as secondary/secondary.proto, which is the path that we used when building the file.

![[shows-path-of-compiled-protobuf-file.png]] It looks to be that the path used to build the file is the one that gets embedded as the path to the file. And the gRPC reflection server tries to look up the file using that path.

Since the file does not actually reside at that path, the reflection fails.

The solution is easy enough: use the same path while building the dependency as used to reference the dependency.

The corrected code is at https://github.com/BadgerBadgerBadgerBadger/protoreflect-error-test/tree/main/working-state

I hope you enjoyed the post and if you know why things were implemented this way, let me know.


More posts like this

Building Rant

2022-11-14 | #golang #humor #intepreters

Intro Sometime in mid to late 2021 (which is a period I’ve entirely made up because I don’t actually remember when any of this happened), I needed to rant on my company Slack. Something had happened to ruin my day and I needed to get my feelings out. But ranting takes more effort than you’d think. If you want your rant to have impact there needs to be just the right amount of !

Continue reading 


Dealing With Errors in Go Projects

2017-09-13 | #coding-practices #golang

Dealing With Errors in Go Projects Note: In this post I talk both about the purely technical reasons for doing certain things and some practices that have made debugging production systems easier for me. I’m still fairly new to Go and still learning so if you find any issues with my approach or have a better way to approach the problem, please leave a comment. I come from a NodeJS background and my coding habits have been heavily influenced by the paradigms most used in Node.

Continue reading 