Skip to content

Commit 9aaf28d

Browse files
authored
Print count of unrecognized bytes in "buf curl" (#2586)
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.
1 parent 7b3ad25 commit 9aaf28d

3 files changed

Lines changed: 60 additions & 10 deletions

File tree

private/buf/bufcurl/invoker.go

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,11 @@ func (inv *invoker) handleResponse(data []byte, msg *dynamicpb.Message) error {
290290
protoencoding.JSONMarshalerWithEmitUnpopulated(),
291291
)
292292
}
293+
unrecognized := countUnrecognized(msg.ProtoReflect())
294+
if unrecognized > 0 {
295+
inv.printer.Printf("Response message (%s) contained %d bytes of unrecognized fields.",
296+
msg.ProtoReflect().Descriptor().FullName(), unrecognized)
297+
}
293298
outputBytes, err := protoencoding.NewJSONMarshaler(inv.res, jsonMarshalerOptions...).Marshal(msg)
294299
if err != nil {
295300
return err
@@ -437,6 +442,37 @@ func (s *streamMessageProvider) next(msg proto.Message) error {
437442
}
438443
return fmt.Errorf("%s at offset %d: %w", s.name, s.dec.InputOffset(), err)
439444
}
440-
proto.Reset(msg)
441-
return protoencoding.NewJSONUnmarshaler(s.res).Unmarshal(jsonData, msg)
445+
return protoencoding.NewJSONUnmarshaler(
446+
s.res, protoencoding.JSONUnmarshalerWithDisallowUnknown(),
447+
).Unmarshal(jsonData, msg)
448+
}
449+
450+
func countUnrecognized(msg protoreflect.Message) int {
451+
var count int
452+
msg.Range(func(field protoreflect.FieldDescriptor, val protoreflect.Value) bool {
453+
switch {
454+
case field.IsMap() && isMessageKind(field.MapValue().Kind()):
455+
// Note: Technically, each message entry could have had unrecognized field
456+
// bytes, but they are discarded by the runtime. So we can only look at
457+
// unrecognized fields in message values inside the map.
458+
mapVal := val.Map()
459+
mapVal.Range(func(_ protoreflect.MapKey, v protoreflect.Value) bool {
460+
count += countUnrecognized(v.Message())
461+
return true
462+
})
463+
case field.IsList() && isMessageKind(field.Kind()):
464+
listVal := val.List()
465+
for i, length := 0, listVal.Len(); i < length; i++ {
466+
count += countUnrecognized(listVal.Get(i).Message())
467+
}
468+
case isMessageKind(field.Kind()):
469+
count += countUnrecognized(val.Message())
470+
}
471+
return true
472+
})
473+
return count + len(msg.GetUnknown())
474+
}
475+
476+
func isMessageKind(k protoreflect.Kind) bool {
477+
return k == protoreflect.MessageKind || k == protoreflect.GroupKind
442478
}

private/pkg/protoencoding/json_unmarshaler.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,24 @@ import (
2020
)
2121

2222
type jsonUnmarshaler struct {
23-
resolver Resolver
23+
resolver Resolver
24+
disallowUnknown bool
2425
}
2526

26-
func newJSONUnmarshaler(resolver Resolver) Unmarshaler {
27-
return &jsonUnmarshaler{
27+
func newJSONUnmarshaler(resolver Resolver, options ...JSONUnmarshalerOption) Unmarshaler {
28+
jsonUnmarshaler := &jsonUnmarshaler{
2829
resolver: resolver,
2930
}
31+
for _, option := range options {
32+
option(jsonUnmarshaler)
33+
}
34+
return jsonUnmarshaler
3035
}
3136

3237
func (m *jsonUnmarshaler) Unmarshal(data []byte, message proto.Message) error {
3338
options := protojson.UnmarshalOptions{
34-
Resolver: m.resolver,
35-
// TODO: make this an option
36-
DiscardUnknown: true,
39+
Resolver: m.resolver,
40+
DiscardUnknown: !m.disallowUnknown,
3741
}
3842
return options.Unmarshal(data, message)
3943
}

private/pkg/protoencoding/protoencoding.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,18 @@ func NewWireUnmarshaler(resolver Resolver) Unmarshaler {
117117
// NewJSONUnmarshaler returns a new Unmarshaler for json.
118118
//
119119
// resolver can be nil if unknown and are only needed for extensions.
120-
func NewJSONUnmarshaler(resolver Resolver) Unmarshaler {
121-
return newJSONUnmarshaler(resolver)
120+
func NewJSONUnmarshaler(resolver Resolver, options ...JSONUnmarshalerOption) Unmarshaler {
121+
return newJSONUnmarshaler(resolver, options...)
122+
}
123+
124+
// JSONUnmarshalerOption is an option for a new JSONUnmarshaler.
125+
type JSONUnmarshalerOption func(*jsonUnmarshaler)
126+
127+
// JSONUnmarshalerWithDisallowUnknown says to disallow unrecognized fields.
128+
func JSONUnmarshalerWithDisallowUnknown() JSONUnmarshalerOption {
129+
return func(jsonUnmarshaler *jsonUnmarshaler) {
130+
jsonUnmarshaler.disallowUnknown = true
131+
}
122132
}
123133

124134
// NewTxtpbUnmarshaler returns a new Unmarshaler for txtpb.

0 commit comments

Comments
 (0)