Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WithForwardResponseOption can't cast proto.Message when response_body is defined on the rpc method #1493

Closed
talanknight opened this issue Jun 29, 2020 · 3 comments

Comments

@talanknight
Copy link
Contributor

🐛 Bug Report

The WithForwardResponseOption does not allow you to cast to the original response proto when the service has a response_body field set. This makes it very difficult to conditionally attach headers, cookies, or do other conditional logic based off of the response proto.

This is a follow up bug report from a chat with @johanbrandhorst in slack.

To Reproduce

  1. Create a grpc response proto with response_body set.
import "protoc-gen-swagger/options/annotations.proto";
import "google/api/annotations.proto";

service Service {
	rpc GetObject (GetObjectRequest) returns (GetObjectResponse) {
		option (google.api.http) = {
      get: "/object"
      response_body: "value"
    };
	}
}

message GetObjectRequest {
}

message GetObjectResponse {
	string value = 1;
}
  1. Register a WithForwardResponseOption function with test:
type fake struct {}

func (f fake) GetObject(ctx context.Context, request *services.GetObjectRequest) (*services.GetObjectResponse, error) {
	return &services.GetObjectResponse{Value: "this is the value"}, nil
}

func TestGetValue(t *testing.T) {
	type GetValuer interface {
		GetValue() string
	}
	mux := runtime.NewServeMux(runtime.WithForwardResponseOption(func(ctx context.Context, w http.ResponseWriter, m proto.Message) error {
		switch m.(type) {
		case *services.GetObjectResponse:
			t.Logf("Cast to struct ptr")
		case GetValuer:
			t.Logf("Cast to interface")
		default:
			t.Errorf("Couldn't cast response to expected format.")
		}
		return nil
	}))
	services.RegisterServiceHandlerServer(context.Background(), mux, &fake{})

	req := httptest.NewRequest("GET","http://localhost/object", nil)
	resp := httptest.NewRecorder()
	mux.ServeHTTP(resp, req)
}

Expected behavior

Either "Cast to struct ptr" or "Cast to interface" would get logged and the test would pass.
This behavior would allow headers and cookies to be conditionally attached to the response using the fields that sit alongside the "value" field in the response.

Actual Behavior

The test fails with "Couldn't cast response to expected format."

Your Environment

MacOS 10.15.5
Go 1.13

Potential solutions

The reason this cast doesn't work is because the response proto.Message is embedded in a custom internal struct when response_body is defined. See this example

A few potential solutions in order of preference:

  1. Add a method for this internal struct that returns the embedded proto.Message directly so we can then cast to the interface for the wrapped structure and extract out our response proto. Something like the following (name TBD):
func (m response_ResponseBodyService_GetResponseBody_0) GetEmbeddedProtoMessage() proto.Message {
	return m.Message
}
  1. Instead of embedding a proto.Message, have the internal struct embed the response proto type (ResponseBodyOut in this example)
@johanbrandhorst
Copy link
Collaborator

Hi Todd, thanks for raising the issue!

I wonder, in the v2 branch, is it possible to call msg.ProtoReflect on the message? It might give you everything you need that way?

@talanknight
Copy link
Contributor Author

Hi Johanbrandhorst,
It does look like with v2 we can do something like this to get the wrapped proto:

	mux := runtime.NewServeMux(runtime.WithForwardResponseOption(func(ctx context.Context, w http.ResponseWriter, m proto.Message) error {
		m = m.ProtoReflect().Interface()

		switch m := m.(type) {
		case *services.GetObjectResponse:
			t.Logf("Cast to struct ptr: %q.", m.GetValue())
		case GetValuer:
			t.Logf("Cast to interface: %q.", m.GetValue())
		default:
			t.Errorf("Couldn't cast response to expected format.")
		}
		return nil
	}))

@johanbrandhorst
Copy link
Collaborator

Perfect! I think we'll probably leave things as they are then, we do want to encourage people to upgrade to v2. It is basically stable, we're just waiting for a stable protoc-gen-go-grpc release before tagging it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants