Skip to content

Commit

Permalink
Print count of unrecognized bytes in "buf curl" (#2586)
Browse files Browse the repository at this point in the history
This provides a cue to user's that data is in the response but
getting dropped (which suggests an incomplete or out-of-date
schema) by logging the presence of unrecognized bytes when
the `-v` flag is used.
  • Loading branch information
jhump authored Nov 14, 2023
1 parent 7b3ad25 commit 9aaf28d
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 10 deletions.
40 changes: 38 additions & 2 deletions private/buf/bufcurl/invoker.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,11 @@ func (inv *invoker) handleResponse(data []byte, msg *dynamicpb.Message) error {
protoencoding.JSONMarshalerWithEmitUnpopulated(),
)
}
unrecognized := countUnrecognized(msg.ProtoReflect())
if unrecognized > 0 {
inv.printer.Printf("Response message (%s) contained %d bytes of unrecognized fields.",
msg.ProtoReflect().Descriptor().FullName(), unrecognized)
}
outputBytes, err := protoencoding.NewJSONMarshaler(inv.res, jsonMarshalerOptions...).Marshal(msg)
if err != nil {
return err
Expand Down Expand Up @@ -437,6 +442,37 @@ func (s *streamMessageProvider) next(msg proto.Message) error {
}
return fmt.Errorf("%s at offset %d: %w", s.name, s.dec.InputOffset(), err)
}
proto.Reset(msg)
return protoencoding.NewJSONUnmarshaler(s.res).Unmarshal(jsonData, msg)
return protoencoding.NewJSONUnmarshaler(
s.res, protoencoding.JSONUnmarshalerWithDisallowUnknown(),
).Unmarshal(jsonData, msg)
}

func countUnrecognized(msg protoreflect.Message) int {
var count int
msg.Range(func(field protoreflect.FieldDescriptor, val protoreflect.Value) bool {
switch {
case field.IsMap() && isMessageKind(field.MapValue().Kind()):
// Note: Technically, each message entry could have had unrecognized field
// bytes, but they are discarded by the runtime. So we can only look at
// unrecognized fields in message values inside the map.
mapVal := val.Map()
mapVal.Range(func(_ protoreflect.MapKey, v protoreflect.Value) bool {
count += countUnrecognized(v.Message())
return true
})
case field.IsList() && isMessageKind(field.Kind()):
listVal := val.List()
for i, length := 0, listVal.Len(); i < length; i++ {
count += countUnrecognized(listVal.Get(i).Message())
}
case isMessageKind(field.Kind()):
count += countUnrecognized(val.Message())
}
return true
})
return count + len(msg.GetUnknown())
}

func isMessageKind(k protoreflect.Kind) bool {
return k == protoreflect.MessageKind || k == protoreflect.GroupKind
}
16 changes: 10 additions & 6 deletions private/pkg/protoencoding/json_unmarshaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,24 @@ import (
)

type jsonUnmarshaler struct {
resolver Resolver
resolver Resolver
disallowUnknown bool
}

func newJSONUnmarshaler(resolver Resolver) Unmarshaler {
return &jsonUnmarshaler{
func newJSONUnmarshaler(resolver Resolver, options ...JSONUnmarshalerOption) Unmarshaler {
jsonUnmarshaler := &jsonUnmarshaler{
resolver: resolver,
}
for _, option := range options {
option(jsonUnmarshaler)
}
return jsonUnmarshaler
}

func (m *jsonUnmarshaler) Unmarshal(data []byte, message proto.Message) error {
options := protojson.UnmarshalOptions{
Resolver: m.resolver,
// TODO: make this an option
DiscardUnknown: true,
Resolver: m.resolver,
DiscardUnknown: !m.disallowUnknown,
}
return options.Unmarshal(data, message)
}
14 changes: 12 additions & 2 deletions private/pkg/protoencoding/protoencoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,18 @@ func NewWireUnmarshaler(resolver Resolver) Unmarshaler {
// NewJSONUnmarshaler returns a new Unmarshaler for json.
//
// resolver can be nil if unknown and are only needed for extensions.
func NewJSONUnmarshaler(resolver Resolver) Unmarshaler {
return newJSONUnmarshaler(resolver)
func NewJSONUnmarshaler(resolver Resolver, options ...JSONUnmarshalerOption) Unmarshaler {
return newJSONUnmarshaler(resolver, options...)
}

// JSONUnmarshalerOption is an option for a new JSONUnmarshaler.
type JSONUnmarshalerOption func(*jsonUnmarshaler)

// JSONUnmarshalerWithDisallowUnknown says to disallow unrecognized fields.
func JSONUnmarshalerWithDisallowUnknown() JSONUnmarshalerOption {
return func(jsonUnmarshaler *jsonUnmarshaler) {
jsonUnmarshaler.disallowUnknown = true
}
}

// NewTxtpbUnmarshaler returns a new Unmarshaler for txtpb.
Expand Down

0 comments on commit 9aaf28d

Please sign in to comment.