Repository: bojand/ghz (go)

Issue:
template error when using --binary-file
**Proto file(s)**
Anything with a `bytes` field. [BytesValue](https://github.com/protocolbuffers/protobuf/blob/5ab8239d4b7da3589d3660233e9a2a7e89364083/src/google/protobuf/wrappers.proto#L122) for example.

**Command line arguments / config**
`--binary-file`

**Describe the bug**

Binary protobuf messages with bytes fields cause template errors like this:

    template: call_template_data:2: unrecognized character in action: U+FFFD '�'

If the bytes happen to contain character sequences that look like `template` directives.

The problem `template` call seems to be [here](https://github.com/bojand/ghz/blob/master/runner/calldata.go#L131)

**To Reproduce**

Here's a [test](https://github.com/wjackson/ghz/blob/3bf8309d678026813fd43a501819c17008169f69/runner/run_test.go#L460]) that produces the error.

**Expected behavior**

Binary messages provided via `--binary` or `--binary-file` are not processed by `template` and work regardless of what byte patterns they happen to have.

**Environment**
 - OS: linux
 - ghz: 0.95.0

Pull request description:
do not attempt template actions with binary data. fixes #284

Patch:
diff --git a/internal/wrapped/server.go b/internal/wrapped/server.go
index 3ca4527..3bb1941 100644
--- a/internal/wrapped/server.go
+++ b/internal/wrapped/server.go
@@ -11,3 +11,9 @@ type WrappedService struct{}
 func (s *WrappedService) GetMessage(ctx context.Context, req *wrappers.StringValue) (*wrappers.StringValue, error) {
 	return &wrappers.StringValue{Value: "Hello: " + req.GetValue()}, nil
 }
+
+func (s *WrappedService) GetBytesMessage(ctx context.Context, req *wrappers.BytesValue) (*wrappers.BytesValue, error) {
+	return &wrappers.BytesValue{Value: req.GetValue()}, nil
+}
+
+func (s *WrappedService) mustEmbedUnimplementedWrappedServiceServer() {}
diff --git a/internal/wrapped/wrapped.pb.go b/internal/wrapped/wrapped.pb.go
index 13702b9..1bb1ee0 100644
--- a/internal/wrapped/wrapped.pb.go
+++ b/internal/wrapped/wrapped.pb.go
@@ -1,122 +1,87 @@
 // Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.25.0
+// 	protoc        v3.11.4
 // source: wrapped.proto
 
 package wrapped
 
 import (
-	context "context"
-	fmt "fmt"
-	math "math"
-
 	proto "github.com/golang/protobuf/proto"
 	wrappers "github.com/golang/protobuf/ptypes/wrappers"
-	grpc "google.golang.org/grpc"
-	codes "google.golang.org/grpc/codes"
-	status "google.golang.org/grpc/status"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
 )
 
-// Reference imports to suppress errors if they are not otherwise used.
-var _ = proto.Marshal
-var _ = fmt.Errorf
-var _ = math.Inf
-
-// This is a compile-time assertion to ensure that this generated file
-// is compatible with the proto package it is being compiled against.
-// A compilation error at this line likely means your copy of the
-// proto package needs to be updated.
-const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
-
-func init() { proto.RegisterFile("wrapped.proto", fileDescriptor_a9aa7f80a2b4490d) }
-
-var fileDescriptor_a9aa7f80a2b4490d = []byte{
-	// 135 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x2d, 0x2f, 0x4a, 0x2c,
-	0x28, 0x48, 0x4d, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0x87, 0x72, 0xa5, 0xe4, 0xd2,
-	0xf3, 0xf3, 0xd3, 0x73, 0x52, 0xf5, 0xc1, 0xc2, 0x49, 0xa5, 0x69, 0xfa, 0x10, 0x89, 0xa2, 0x62,
-	0x88, 0x42, 0xa3, 0x28, 0x2e, 0xbe, 0x70, 0x88, 0xd2, 0xe0, 0xd4, 0xa2, 0xb2, 0xcc, 0xe4, 0x54,
-	0x21, 0x0f, 0x2e, 0x2e, 0xf7, 0xd4, 0x12, 0xdf, 0xd4, 0xe2, 0xe2, 0xc4, 0xf4, 0x54, 0x21, 0x19,
-	0x3d, 0x88, 0x01, 0x7a, 0x30, 0x03, 0xf4, 0x82, 0x4b, 0x8a, 0x32, 0xf3, 0xd2, 0xc3, 0x12, 0x73,
-	0x4a, 0x53, 0xa5, 0xf0, 0xca, 0x3a, 0x09, 0x45, 0x09, 0x64, 0xe6, 0x95, 0xa4, 0x16, 0xe5, 0x25,
-	0xe6, 0x40, 0xad, 0x4d, 0x49, 0x62, 0x03, 0xab, 0x34, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xfd,
-	0xf7, 0x3c, 0x4d, 0xb0, 0x00, 0x00, 0x00,
-}
-
-// Reference imports to suppress errors if they are not otherwise used.
-var _ context.Context
-var _ grpc.ClientConn
-
-// This is a compile-time assertion to ensure that this generated file
-// is compatible with the grpc package it is being compiled against.
-const _ = grpc.SupportPackageIsVersion4
-
-// WrappedServiceClient is the client API for WrappedService service.
-//
-// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
-type WrappedServiceClient interface {
-	GetMessage(ctx context.Context, in *wrappers.StringValue, opts ...grpc.CallOption) (*wrappers.StringValue, error)
-}
-
-type wrappedServiceClient struct {
-	cc *grpc.ClientConn
-}
-
-func NewWrappedServiceClient(cc *grpc.ClientConn) WrappedServiceClient {
-	return &wrappedServiceClient{cc}
-}
-
-func (c *wrappedServiceClient) GetMessage(ctx context.Context, in *wrappers.StringValue, opts ...grpc.CallOption) (*wrappers.StringValue, error) {
-	out := new(wrappers.StringValue)
-	err := c.cc.Invoke(ctx, "/wrapped.WrappedService/GetMessage", in, out, opts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-// WrappedServiceServer is the server API for WrappedService service.
-type WrappedServiceServer interface {
-	GetMessage(context.Context, *wrappers.StringValue) (*wrappers.StringValue, error)
-}
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
 
-// UnimplementedWrappedServiceServer can be embedded to have forward compatible implementations.
-type UnimplementedWrappedServiceServer struct {
+// This is a compile-time assertion that a sufficiently up-to-date version
+// of the legacy proto package is being used.
+const _ = proto.ProtoPackageIsVersion4
+
+var File_wrapped_proto protoreflect.FileDescriptor
+
+var file_wrapped_proto_rawDesc = []byte{
+	0x0a, 0x0d, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
+	0x07, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65,
+	0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x32, 0xa7, 0x01, 0x0a, 0x0e, 0x57, 0x72, 0x61,
+	0x70, 0x70, 0x65, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x48, 0x0a, 0x0a, 0x47,
+	0x65, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+	0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69,
+	0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+	0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67,
+	0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x4b, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x42, 0x79, 0x74, 0x65,
+	0x73, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
+	0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73,
+	0x56, 0x61, 0x6c, 0x75, 0x65, 0x1a, 0x1b, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x42, 0x79, 0x74, 0x65, 0x73, 0x56, 0x61, 0x6c,
+	0x75, 0x65, 0x42, 0x12, 0x5a, 0x10, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x77,
+	0x72, 0x61, 0x70, 0x70, 0x65, 0x64, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
 }
 
-func (*UnimplementedWrappedServiceServer) GetMessage(ctx context.Context, req *wrappers.StringValue) (*wrappers.StringValue, error) {
-	return nil, status.Errorf(codes.Unimplemented, "method GetMessage not implemented")
+var file_wrapped_proto_goTypes = []interface{}{
+	(*wrappers.StringValue)(nil), // 0: google.protobuf.StringValue
+	(*wrappers.BytesValue)(nil),  // 1: google.protobuf.BytesValue
 }
-
-func RegisterWrappedServiceServer(s *grpc.Server, srv WrappedServiceServer) {
-	s.RegisterService(&_WrappedService_serviceDesc, srv)
+var file_wrapped_proto_depIdxs = []int32{
+	0, // 0: wrapped.WrappedService.GetMessage:input_type -> google.protobuf.StringValue
+	1, // 1: wrapped.WrappedService.GetBytesMessage:input_type -> google.protobuf.BytesValue
+	0, // 2: wrapped.WrappedService.GetMessage:output_type -> google.protobuf.StringValue
+	1, // 3: wrapped.WrappedService.GetBytesMessage:output_type -> google.protobuf.BytesValue
+	2, // [2:4] is the sub-list for method output_type
+	0, // [0:2] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
 }
 
-func _WrappedService_GetMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(wrappers.StringValue)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(WrappedServiceServer).GetMessage(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: "/wrapped.WrappedService/GetMessage",
+func init() { file_wrapped_proto_init() }
+func file_wrapped_proto_init() {
+	if File_wrapped_proto != nil {
+		return
 	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(WrappedServiceServer).GetMessage(ctx, req.(*wrappers.StringValue))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-var _WrappedService_serviceDesc = grpc.ServiceDesc{
-	ServiceName: "wrapped.WrappedService",
-	HandlerType: (*WrappedServiceServer)(nil),
-	Methods: []grpc.MethodDesc{
-		{
-			MethodName: "GetMessage",
-			Handler:    _WrappedService_GetMessage_Handler,
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_wrapped_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   0,
+			NumExtensions: 0,
+			NumServices:   1,
 		},
-	},
-	Streams:  []grpc.StreamDesc{},
-	Metadata: "wrapped.proto",
+		GoTypes:           file_wrapped_proto_goTypes,
+		DependencyIndexes: file_wrapped_proto_depIdxs,
+	}.Build()
+	File_wrapped_proto = out.File
+	file_wrapped_proto_rawDesc = nil
+	file_wrapped_proto_goTypes = nil
+	file_wrapped_proto_depIdxs = nil
 }
diff --git a/internal/wrapped/wrapped_grpc.pb.go b/internal/wrapped/wrapped_grpc.pb.go
new file mode 100644
index 0000000..18607ce
--- /dev/null
+++ b/internal/wrapped/wrapped_grpc.pb.go
@@ -0,0 +1,138 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+
+package wrapped
+
+import (
+	context "context"
+	wrappers "github.com/golang/protobuf/ptypes/wrappers"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+// WrappedServiceClient is the client API for WrappedService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type WrappedServiceClient interface {
+	GetMessage(ctx context.Context, in *wrappers.StringValue, opts ...grpc.CallOption) (*wrappers.StringValue, error)
+	GetBytesMessage(ctx context.Context, in *wrappers.BytesValue, opts ...grpc.CallOption) (*wrappers.BytesValue, error)
+}
+
+type wrappedServiceClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewWrappedServiceClient(cc grpc.ClientConnInterface) WrappedServiceClient {
+	return &wrappedServiceClient{cc}
+}
+
+func (c *wrappedServiceClient) GetMessage(ctx context.Context, in *wrappers.StringValue, opts ...grpc.CallOption) (*wrappers.StringValue, error) {
+	out := new(wrappers.StringValue)
+	err := c.cc.Invoke(ctx, "/wrapped.WrappedService/GetMessage", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *wrappedServiceClient) GetBytesMessage(ctx context.Context, in *wrappers.BytesValue, opts ...grpc.CallOption) (*wrappers.BytesValue, error) {
+	out := new(wrappers.BytesValue)
+	err := c.cc.Invoke(ctx, "/wrapped.WrappedService/GetBytesMessage", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// WrappedServiceServer is the server API for WrappedService service.
+// All implementations must embed UnimplementedWrappedServiceServer
+// for forward compatibility
+type WrappedServiceServer interface {
+	GetMessage(context.Context, *wrappers.StringValue) (*wrappers.StringValue, error)
+	GetBytesMessage(context.Context, *wrappers.BytesValue) (*wrappers.BytesValue, error)
+	mustEmbedUnimplementedWrappedServiceServer()
+}
+
+// UnimplementedWrappedServiceServer must be embedded to have forward compatible implementations.
+type UnimplementedWrappedServiceServer struct {
+}
+
+func (UnimplementedWrappedServiceServer) GetMessage(context.Context, *wrappers.StringValue) (*wrappers.StringValue, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetMessage not implemented")
+}
+func (UnimplementedWrappedServiceServer) GetBytesMessage(context.Context, *wrappers.BytesValue) (*wrappers.BytesValue, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetBytesMessage not implemented")
+}
+func (UnimplementedWrappedServiceServer) mustEmbedUnimplementedWrappedServiceServer() {}
+
+// UnsafeWrappedServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to WrappedServiceServer will
+// result in compilation errors.
+type UnsafeWrappedServiceServer interface {
+	mustEmbedUnimplementedWrappedServiceServer()
+}
+
+func RegisterWrappedServiceServer(s grpc.ServiceRegistrar, srv WrappedServiceServer) {
+	s.RegisterService(&WrappedService_ServiceDesc, srv)
+}
+
+func _WrappedService_GetMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(wrappers.StringValue)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(WrappedServiceServer).GetMessage(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/wrapped.WrappedService/GetMessage",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(WrappedServiceServer).GetMessage(ctx, req.(*wrappers.StringValue))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _WrappedService_GetBytesMessage_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(wrappers.BytesValue)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(WrappedServiceServer).GetBytesMessage(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/wrapped.WrappedService/GetBytesMessage",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(WrappedServiceServer).GetBytesMessage(ctx, req.(*wrappers.BytesValue))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// WrappedService_ServiceDesc is the grpc.ServiceDesc for WrappedService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var WrappedService_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "wrapped.WrappedService",
+	HandlerType: (*WrappedServiceServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "GetMessage",
+			Handler:    _WrappedService_GetMessage_Handler,
+		},
+		{
+			MethodName: "GetBytesMessage",
+			Handler:    _WrappedService_GetBytesMessage_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "wrapped.proto",
+}
diff --git a/runner/data.go b/runner/data.go
index 660b390..e777a02 100644
--- a/runner/data.go
+++ b/runner/data.go
@@ -99,9 +99,12 @@ func newDataProvider(mtd *desc.MethodDescriptor,
 
 	// Test if we can preseed data
 	ctd := newCallData(mtd, funcs, "", 0)
-	ha, err := ctd.hasAction(string(dp.data))
-	if err != nil {
-		return nil, err
+	ha := false
+	if !dp.binary {
+		ha, err = ctd.hasAction(string(dp.data))
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	dp.hasActions = ha
diff --git a/runner/options.go b/runner/options.go
index 1629e0a..9fb08eb 100644
--- a/runner/options.go
+++ b/runner/options.go
@@ -185,6 +185,10 @@ func NewConfig(call, host string, options ...Option) (*RunConfig, error) {
 		return nil, errors.New("host required")
 	}
 
+	if c.binary && c.streamDynamicMessages {
+		return nil, errors.New("cannot use dynamic messages with binary data")
+	}
+
 	if c.loadSchedule != ScheduleConst &&
 		c.loadSchedule != ScheduleStep &&
 		c.loadSchedule != ScheduleLine {
diff --git a/www/docs/examples.md b/www/docs/examples.md
index ee5c17b..c9e7afd 100644
--- a/www/docs/examples.md
+++ b/www/docs/examples.md
@@ -58,7 +58,7 @@ ghz --insecure \
 <a name="binary-data">
 ### Binary data
 
-Using binary data file (see [writing a message](https://developers.google.com/protocol-buffers/docs/gotutorial#writing_a_message)):
+Using binary data file (see [writing a message](https://developers.google.com/protocol-buffers/docs/gotutorial#writing-a-message)):
 
 ```sh
 ghz --proto ./greeter.proto \
@@ -97,7 +97,7 @@ message ImageSaveRequest {
 message ImageSaveResponse {}
 ```
 
-One way to create the request for a test is to use the binary data option `-B` to specify a binary file like we did in the prior exammple. Simply [serialize the whole](https://developers.google.com/protocol-buffers/docs/gotutorial#writing_a_message) message to a binary file first.
+One way to create the request for a test is to use the binary data option `-B` to specify a binary file like we did in the prior exammple. Simply [serialize the whole](https://developers.google.com/protocol-buffers/docs/gotutorial#writing-a-message) message to a binary file first.
 
 Alternatively we can [use base64 string](https://developers.google.com/protocol-buffers/docs/proto3#json) representation of the image as the value for the `data` field.
 
diff --git a/www/docs/options.md b/www/docs/options.md
index 011ad6e..47d763c 100644
--- a/www/docs/options.md
+++ b/www/docs/options.md
@@ -211,7 +211,7 @@ The path for call data JSON file. For example, `-D /home/user/file.json` or `-D
 
 The call data comes as serialized protocol buffer messages read from standard input. 
 
-We support two formats of binary data: single message and multiple count-delimited messages. See [writing a message](https://developers.google.com/protocol-buffers/docs/gotutorial#writing_a_message) on how to serialize a single message. 
+We support two formats of binary data: single message and multiple count-delimited messages. See [writing a message](https://developers.google.com/protocol-buffers/docs/gotutorial#writing-a-message) on how to serialize a single message. 
 
 For multiple messages prefix each message with its length in bytes. See [streaming multiple messages](https://developers.google.com/protocol-buffers/docs/techniques#streaming) in protobuf documentation.
 


Test patch:
diff --git a/runner/options_test.go b/runner/options_test.go
index e23a945..a9b21b8 100644
--- a/runner/options_test.go
+++ b/runner/options_test.go
@@ -275,6 +275,17 @@ func TestRunConfig_newRunConfig(t *testing.T) {
 		assert.Equal(t, c.enableCompression, false)
 	})
 
+	t.Run("with binary data from file and dynamic message", func(t *testing.T) {
+		c, err := NewConfig("call", "localhost:50050",
+			WithProtoFile("testdata/data.proto", []string{}),
+			WithBinaryDataFromFile("../testdata/hello_request_data.bin"),
+			WithStreamDynamicMessages(true),
+		)
+
+		assert.Error(t, err)
+		assert.Nil(t, c)
+	})
+
 	t.Run("with data from file", func(t *testing.T) {
 		c, err := NewConfig("call", "localhost:50050",
 			WithProtoFile("testdata/data.proto", []string{}),
diff --git a/runner/run_test.go b/runner/run_test.go
index d70fd2c..fedef90 100644
--- a/runner/run_test.go
+++ b/runner/run_test.go
@@ -10,6 +10,7 @@ import (
 	"github.com/bojand/ghz/internal"
 	"github.com/bojand/ghz/internal/helloworld"
 	"github.com/golang/protobuf/proto"
+	"github.com/golang/protobuf/ptypes/wrappers"
 	"github.com/google/uuid"
 	"github.com/jhump/protoreflect/desc"
 	"github.com/jhump/protoreflect/dynamic"
@@ -3112,6 +3113,30 @@ func TestRunWrappedUnary(t *testing.T) {
 		assert.Equal(t, report.Average, report.Fastest)
 	})
 
+	t.Run("wrapped bin data with template chars", func(t *testing.T) {
+		protoMsg := wrappers.BytesValue{Value: []byte{'\x7b', '\x7b', '\xc2', '\x7d', '\x7d'}}
+		msg, err := proto.Marshal(&protoMsg)
+		assert.NoError(t, err)
+
+		report, err := Run(
+			"wrapped.WrappedService.GetBytesMessage",
+			internal.TestLocalhost,
+			WithProtoFile("../testdata/wrapped.proto", []string{"../testdata"}),
+			WithTotalRequests(1),
+			WithConcurrency(1),
+			WithTimeout(time.Duration(20*time.Second)),
+			WithDialTimeout(time.Duration(20*time.Second)),
+			WithBinaryData(msg),
+			WithInsecure(true),
+		)
+
+		assert.NoError(t, err)
+
+		assert.NotNil(t, report)
+
+		assert.Equal(t, 1, int(report.Count))
+	})
+
 	t.Run("json string data from file", func(t *testing.T) {
 		report, err := Run(
 			"wrapped.WrappedService.GetMessage",
diff --git a/testdata/wrapped.proto b/testdata/wrapped.proto
index 42ffbbb..bf422e4 100644
--- a/testdata/wrapped.proto
+++ b/testdata/wrapped.proto
@@ -8,4 +8,5 @@ import "google/protobuf/wrappers.proto";
 
 service WrappedService {
   rpc GetMessage (google.protobuf.StringValue) returns (google.protobuf.StringValue);
+  rpc GetBytesMessage (google.protobuf.BytesValue) returns (google.protobuf.BytesValue);
 }
\ No newline at end of file