Skip to content

fix(cmd/protoc-gen-go-http): support opaque protobuf API for named body and response_body fields#3813

Open
lhsanghcmus wants to merge 1 commit into
go-kratos:mainfrom
lhsanghcmus:fix/protoc-gen-go-http-opaque-body
Open

fix(cmd/protoc-gen-go-http): support opaque protobuf API for named body and response_body fields#3813
lhsanghcmus wants to merge 1 commit into
go-kratos:mainfrom
lhsanghcmus:fix/protoc-gen-go-http-opaque-body

Conversation

@lhsanghcmus
Copy link
Copy Markdown

@lhsanghcmus lhsanghcmus commented Apr 9, 2026

Summary

When the HTTP annotation uses a named body or response_body field (not "*"), protoc-gen-go-http generates direct struct field access like &in.Payload and reply.Books. This does not compile when using opaque protobuf types (newer protoc-gen-go editions), because fields are hidden (xxx_hidden_*) and only accessible via getter/setter methods.

This PR fixes the generated code to be compatible with both open and opaque protobuf Go API styles:

  • Server-side body binding: Uses in.ProtoReflect().Mutable(fd).Message().Interface() via proto reflection instead of &in.Field
  • Client-side body send: Uses getter method in.GetField() instead of in.Field
  • Server-side response_body return: Uses getter method reply.GetField() instead of reply.Field
  • Client-side response_body receive: Uses proto reflection for a writable reference instead of &out.Field
  • body: "*" behavior is unchanged — still uses ctx.Bind(&in) and in as before

Example: body with a named field

Given this proto definition:

service WebhookAPI {
  rpc BotHandler(BotHandlerRequest) returns (google.protobuf.Empty) {
    option (google.api.http) = {
      post: "/api/v1/bot"
      body: "payload"
    };
  }
}

message BotHandlerRequest {
  string token = 1;
  google.protobuf.Struct payload = 2;
}

With opaque protobuf generation, the message has no exported Payload field:

type BotHandlerRequest struct {
    state              protoimpl.MessageState `protogen:"opaque.v1"`
    xxx_hidden_Token   string
    xxx_hidden_Payload *structpb.Struct
    // ...
}

Before (broken with opaque types):

// Server — does not compile: in.Payload undefined
if err := ctx.Bind(&in.Payload); err != nil { ... }

// Client — does not compile: in.Payload undefined
err := c.cc.Invoke(ctx, "POST", path, in.Payload, &out, opts...)

After (works with both open and opaque types):

// Server — uses proto reflection to get a mutable reference to the field
if err := ctx.Bind(in.ProtoReflect().Mutable(
    in.ProtoReflect().Descriptor().Fields().ByName("payload"),
).Message().Interface()); err != nil { ... }

// Client — uses getter method which is available in all API styles
err := c.cc.Invoke(ctx, "POST", path, in.GetPayload(), &out, opts...)

Example: response_body with a named field

Given this proto definition:

service BookService {
  rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) {
    option (google.api.http) = {
      get: "/v1/books"
      response_body: "books"
    };
  }
}

message ListBooksResponse {
  repeated Book books = 1;
  string next_page_token = 2;
}

Before (broken with opaque types):

// Server — does not compile: reply.Books undefined
return ctx.Result(200, reply.Books)

// Client — does not compile: out.Books undefined
err := c.cc.Invoke(ctx, "GET", path, nil, &out.Books, opts...)

After (works with both open and opaque types):

// Server — uses getter method
return ctx.Result(200, reply.GetBooks())

// Client — uses proto reflection for a writable reference
err := c.cc.Invoke(ctx, "GET", path, nil,
    out.ProtoReflect().Mutable(
        out.ProtoReflect().Descriptor().Fields().ByName("books"),
    ).Message().Interface(), opts...)

Changes

  • http.go: Added toGetterAccessor helper to convert CamelCase field accessors to chained getter calls; set BodyProtoName/ResponseBodyProtoNameinbuildHTTPRule`
  • template.go: Added BodyProtoName and ResponseBodyProtoName fields to methodDesc; registered getterAccessor` template function
  • httpTemplate.tpl: Updated server and client templates to use proto reflection / getters for named body fields
  • http_test.go: Added tests for toGetterAccessor and template output for both named body and body: "*" cases
  • version.go: Bumped to v2.9.3

Test plan

  • All existing tests pass
  • New unit tests for toGetterAccessor function
  • New template tests verify generated code uses ProtoReflect().Mutable() for named body and ctx.Bind(&in) for body: "*"

…dy and response_body fields

When body or response_body is a named field (not "*"), the generated code
used direct struct field access (e.g. in.Payload, &in.Payload) which does
not compile with opaque protobuf types where fields are hidden behind
getter/setter methods.

Use proto reflection (ProtoReflect().Mutable()) for server-side body
binding and getter methods (Get<Field>()) for client-side body access
and server-side response_body access. This is compatible with both open
and opaque protobuf Go API styles.
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Apr 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant