github.com/micro/go-micro/v2@v2.9.1/client/grpc/grpc.go (about)

     1  // Package grpc provides a gRPC client
     2  package grpc
     3  
     4  import (
     5  	"context"
     6  	"crypto/tls"
     7  	"fmt"
     8  	"net"
     9  	"reflect"
    10  	"strings"
    11  	"sync/atomic"
    12  	"time"
    13  
    14  	"github.com/micro/go-micro/v2/broker"
    15  	"github.com/micro/go-micro/v2/client"
    16  	"github.com/micro/go-micro/v2/client/selector"
    17  	raw "github.com/micro/go-micro/v2/codec/bytes"
    18  	"github.com/micro/go-micro/v2/errors"
    19  	"github.com/micro/go-micro/v2/metadata"
    20  	"github.com/micro/go-micro/v2/registry"
    21  	pnet "github.com/micro/go-micro/v2/util/net"
    22  
    23  	"google.golang.org/grpc"
    24  	"google.golang.org/grpc/credentials"
    25  	"google.golang.org/grpc/encoding"
    26  	gmetadata "google.golang.org/grpc/metadata"
    27  )
    28  
    29  type grpcClient struct {
    30  	opts client.Options
    31  	pool *pool
    32  	once atomic.Value
    33  }
    34  
    35  func init() {
    36  	encoding.RegisterCodec(wrapCodec{jsonCodec{}})
    37  	encoding.RegisterCodec(wrapCodec{protoCodec{}})
    38  	encoding.RegisterCodec(wrapCodec{bytesCodec{}})
    39  }
    40  
    41  // secure returns the dial option for whether its a secure or insecure connection
    42  func (g *grpcClient) secure(addr string) grpc.DialOption {
    43  	// first we check if theres'a  tls config
    44  	if g.opts.Context != nil {
    45  		if v := g.opts.Context.Value(tlsAuth{}); v != nil {
    46  			tls := v.(*tls.Config)
    47  			creds := credentials.NewTLS(tls)
    48  			// return tls config if it exists
    49  			return grpc.WithTransportCredentials(creds)
    50  		}
    51  	}
    52  
    53  	// default config
    54  	tlsConfig := &tls.Config{}
    55  	defaultCreds := grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))
    56  
    57  	// check if the address is prepended with https
    58  	if strings.HasPrefix(addr, "https://") {
    59  		return defaultCreds
    60  	}
    61  
    62  	// if no port is specified or port is 443 default to tls
    63  	_, port, err := net.SplitHostPort(addr)
    64  	// assuming with no port its going to be secured
    65  	if port == "443" {
    66  		return defaultCreds
    67  	} else if err != nil && strings.Contains(err.Error(), "missing port in address") {
    68  		return defaultCreds
    69  	}
    70  
    71  	// other fallback to insecure
    72  	return grpc.WithInsecure()
    73  }
    74  
    75  func (g *grpcClient) next(request client.Request, opts client.CallOptions) (selector.Next, error) {
    76  	service, address, _ := pnet.Proxy(request.Service(), opts.Address)
    77  
    78  	// return remote address
    79  	if len(address) > 0 {
    80  		return func() (*registry.Node, error) {
    81  			return &registry.Node{
    82  				Address: address[0],
    83  			}, nil
    84  		}, nil
    85  	}
    86  
    87  	// get next nodes from the selector
    88  	next, err := g.opts.Selector.Select(service, opts.SelectOptions...)
    89  	if err != nil {
    90  		if err == selector.ErrNotFound {
    91  			return nil, errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
    92  		}
    93  		return nil, errors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error())
    94  	}
    95  
    96  	return next, nil
    97  }
    98  
    99  func (g *grpcClient) call(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error {
   100  	var header map[string]string
   101  
   102  	address := node.Address
   103  
   104  	header = make(map[string]string)
   105  	if md, ok := metadata.FromContext(ctx); ok {
   106  		header = make(map[string]string, len(md))
   107  		for k, v := range md {
   108  			header[strings.ToLower(k)] = v
   109  		}
   110  	} else {
   111  		header = make(map[string]string)
   112  	}
   113  
   114  	// set timeout in nanoseconds
   115  	header["timeout"] = fmt.Sprintf("%d", opts.RequestTimeout)
   116  	// set the content type for the request
   117  	header["x-content-type"] = req.ContentType()
   118  
   119  	md := gmetadata.New(header)
   120  	ctx = gmetadata.NewOutgoingContext(ctx, md)
   121  
   122  	cf, err := g.newGRPCCodec(req.ContentType())
   123  	if err != nil {
   124  		return errors.InternalServerError("go.micro.client", err.Error())
   125  	}
   126  
   127  	maxRecvMsgSize := g.maxRecvMsgSizeValue()
   128  	maxSendMsgSize := g.maxSendMsgSizeValue()
   129  
   130  	var grr error
   131  
   132  	grpcDialOptions := []grpc.DialOption{
   133  		grpc.WithTimeout(opts.DialTimeout),
   134  		g.secure(address),
   135  		grpc.WithDefaultCallOptions(
   136  			grpc.MaxCallRecvMsgSize(maxRecvMsgSize),
   137  			grpc.MaxCallSendMsgSize(maxSendMsgSize),
   138  		),
   139  	}
   140  
   141  	if opts := g.getGrpcDialOptions(); opts != nil {
   142  		grpcDialOptions = append(grpcDialOptions, opts...)
   143  	}
   144  
   145  	cc, err := g.pool.getConn(address, grpcDialOptions...)
   146  	if err != nil {
   147  		return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err))
   148  	}
   149  	defer func() {
   150  		// defer execution of release
   151  		g.pool.release(address, cc, grr)
   152  	}()
   153  
   154  	ch := make(chan error, 1)
   155  
   156  	go func() {
   157  		grpcCallOptions := []grpc.CallOption{
   158  			grpc.ForceCodec(cf),
   159  			grpc.CallContentSubtype(cf.Name())}
   160  		if opts := g.getGrpcCallOptions(); opts != nil {
   161  			grpcCallOptions = append(grpcCallOptions, opts...)
   162  		}
   163  		err := cc.Invoke(ctx, methodToGRPC(req.Service(), req.Endpoint()), req.Body(), rsp, grpcCallOptions...)
   164  		ch <- microError(err)
   165  	}()
   166  
   167  	select {
   168  	case err := <-ch:
   169  		grr = err
   170  	case <-ctx.Done():
   171  		grr = errors.Timeout("go.micro.client", "%v", ctx.Err())
   172  	}
   173  
   174  	return grr
   175  }
   176  
   177  func (g *grpcClient) stream(ctx context.Context, node *registry.Node, req client.Request, rsp interface{}, opts client.CallOptions) error {
   178  	var header map[string]string
   179  
   180  	address := node.Address
   181  
   182  	if md, ok := metadata.FromContext(ctx); ok {
   183  		header = make(map[string]string, len(md))
   184  		for k, v := range md {
   185  			header[k] = v
   186  		}
   187  	} else {
   188  		header = make(map[string]string)
   189  	}
   190  
   191  	// set timeout in nanoseconds
   192  	if opts.StreamTimeout > time.Duration(0) {
   193  		header["timeout"] = fmt.Sprintf("%d", opts.StreamTimeout)
   194  	}
   195  	// set the content type for the request
   196  	header["x-content-type"] = req.ContentType()
   197  
   198  	md := gmetadata.New(header)
   199  	ctx = gmetadata.NewOutgoingContext(ctx, md)
   200  
   201  	cf, err := g.newGRPCCodec(req.ContentType())
   202  	if err != nil {
   203  		return errors.InternalServerError("go.micro.client", err.Error())
   204  	}
   205  
   206  	var dialCtx context.Context
   207  	var cancel context.CancelFunc
   208  	if opts.DialTimeout >= 0 {
   209  		dialCtx, cancel = context.WithTimeout(ctx, opts.DialTimeout)
   210  	} else {
   211  		dialCtx, cancel = context.WithCancel(ctx)
   212  	}
   213  	defer cancel()
   214  
   215  	wc := wrapCodec{cf}
   216  
   217  	grpcDialOptions := []grpc.DialOption{
   218  		grpc.WithTimeout(opts.DialTimeout),
   219  		g.secure(address),
   220  	}
   221  
   222  	if opts := g.getGrpcDialOptions(); opts != nil {
   223  		grpcDialOptions = append(grpcDialOptions, opts...)
   224  	}
   225  
   226  	cc, err := grpc.DialContext(dialCtx, address, grpcDialOptions...)
   227  	if err != nil {
   228  		return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error sending request: %v", err))
   229  	}
   230  
   231  	desc := &grpc.StreamDesc{
   232  		StreamName:    req.Service() + req.Endpoint(),
   233  		ClientStreams: true,
   234  		ServerStreams: true,
   235  	}
   236  
   237  	grpcCallOptions := []grpc.CallOption{
   238  		grpc.ForceCodec(wc),
   239  		grpc.CallContentSubtype(cf.Name()),
   240  	}
   241  	if opts := g.getGrpcCallOptions(); opts != nil {
   242  		grpcCallOptions = append(grpcCallOptions, opts...)
   243  	}
   244  
   245  	// create a new cancelling context
   246  	newCtx, cancel := context.WithCancel(ctx)
   247  
   248  	st, err := cc.NewStream(newCtx, desc, methodToGRPC(req.Service(), req.Endpoint()), grpcCallOptions...)
   249  	if err != nil {
   250  		// we need to cleanup as we dialled and created a context
   251  		// cancel the context
   252  		cancel()
   253  		// close the connection
   254  		cc.Close()
   255  		// now return the error
   256  		return errors.InternalServerError("go.micro.client", fmt.Sprintf("Error creating stream: %v", err))
   257  	}
   258  
   259  	codec := &grpcCodec{
   260  		s: st,
   261  		c: wc,
   262  	}
   263  
   264  	// set request codec
   265  	if r, ok := req.(*grpcRequest); ok {
   266  		r.codec = codec
   267  	}
   268  
   269  	// setup the stream response
   270  	stream := &grpcStream{
   271  		context: ctx,
   272  		request: req,
   273  		response: &response{
   274  			conn:   cc,
   275  			stream: st,
   276  			codec:  cf,
   277  			gcodec: codec,
   278  		},
   279  		stream: st,
   280  		conn:   cc,
   281  		cancel: cancel,
   282  	}
   283  
   284  	// set the stream as the response
   285  	val := reflect.ValueOf(rsp).Elem()
   286  	val.Set(reflect.ValueOf(stream).Elem())
   287  	return nil
   288  }
   289  
   290  func (g *grpcClient) poolMaxStreams() int {
   291  	if g.opts.Context == nil {
   292  		return DefaultPoolMaxStreams
   293  	}
   294  	v := g.opts.Context.Value(poolMaxStreams{})
   295  	if v == nil {
   296  		return DefaultPoolMaxStreams
   297  	}
   298  	return v.(int)
   299  }
   300  
   301  func (g *grpcClient) poolMaxIdle() int {
   302  	if g.opts.Context == nil {
   303  		return DefaultPoolMaxIdle
   304  	}
   305  	v := g.opts.Context.Value(poolMaxIdle{})
   306  	if v == nil {
   307  		return DefaultPoolMaxIdle
   308  	}
   309  	return v.(int)
   310  }
   311  
   312  func (g *grpcClient) maxRecvMsgSizeValue() int {
   313  	if g.opts.Context == nil {
   314  		return DefaultMaxRecvMsgSize
   315  	}
   316  	v := g.opts.Context.Value(maxRecvMsgSizeKey{})
   317  	if v == nil {
   318  		return DefaultMaxRecvMsgSize
   319  	}
   320  	return v.(int)
   321  }
   322  
   323  func (g *grpcClient) maxSendMsgSizeValue() int {
   324  	if g.opts.Context == nil {
   325  		return DefaultMaxSendMsgSize
   326  	}
   327  	v := g.opts.Context.Value(maxSendMsgSizeKey{})
   328  	if v == nil {
   329  		return DefaultMaxSendMsgSize
   330  	}
   331  	return v.(int)
   332  }
   333  
   334  func (g *grpcClient) newGRPCCodec(contentType string) (encoding.Codec, error) {
   335  	codecs := make(map[string]encoding.Codec)
   336  	if g.opts.Context != nil {
   337  		if v := g.opts.Context.Value(codecsKey{}); v != nil {
   338  			codecs = v.(map[string]encoding.Codec)
   339  		}
   340  	}
   341  	if c, ok := codecs[contentType]; ok {
   342  		return wrapCodec{c}, nil
   343  	}
   344  	if c, ok := defaultGRPCCodecs[contentType]; ok {
   345  		return wrapCodec{c}, nil
   346  	}
   347  	return nil, fmt.Errorf("Unsupported Content-Type: %s", contentType)
   348  }
   349  
   350  func (g *grpcClient) Init(opts ...client.Option) error {
   351  	size := g.opts.PoolSize
   352  	ttl := g.opts.PoolTTL
   353  
   354  	for _, o := range opts {
   355  		o(&g.opts)
   356  	}
   357  
   358  	// update pool configuration if the options changed
   359  	if size != g.opts.PoolSize || ttl != g.opts.PoolTTL {
   360  		g.pool.Lock()
   361  		g.pool.size = g.opts.PoolSize
   362  		g.pool.ttl = int64(g.opts.PoolTTL.Seconds())
   363  		g.pool.Unlock()
   364  	}
   365  
   366  	return nil
   367  }
   368  
   369  func (g *grpcClient) Options() client.Options {
   370  	return g.opts
   371  }
   372  
   373  func (g *grpcClient) NewMessage(topic string, msg interface{}, opts ...client.MessageOption) client.Message {
   374  	return newGRPCEvent(topic, msg, g.opts.ContentType, opts...)
   375  }
   376  
   377  func (g *grpcClient) NewRequest(service, method string, req interface{}, reqOpts ...client.RequestOption) client.Request {
   378  	return newGRPCRequest(service, method, req, g.opts.ContentType, reqOpts...)
   379  }
   380  
   381  func (g *grpcClient) Call(ctx context.Context, req client.Request, rsp interface{}, opts ...client.CallOption) error {
   382  	if req == nil {
   383  		return errors.InternalServerError("go.micro.client", "req is nil")
   384  	} else if rsp == nil {
   385  		return errors.InternalServerError("go.micro.client", "rsp is nil")
   386  	}
   387  	// make a copy of call opts
   388  	callOpts := g.opts.CallOptions
   389  	for _, opt := range opts {
   390  		opt(&callOpts)
   391  	}
   392  
   393  	next, err := g.next(req, callOpts)
   394  	if err != nil {
   395  		return err
   396  	}
   397  
   398  	// check if we already have a deadline
   399  	d, ok := ctx.Deadline()
   400  	if !ok {
   401  		// no deadline so we create a new one
   402  		var cancel context.CancelFunc
   403  		ctx, cancel = context.WithTimeout(ctx, callOpts.RequestTimeout)
   404  		defer cancel()
   405  	} else {
   406  		// got a deadline so no need to setup context
   407  		// but we need to set the timeout we pass along
   408  		opt := client.WithRequestTimeout(time.Until(d))
   409  		opt(&callOpts)
   410  	}
   411  
   412  	// should we noop right here?
   413  	select {
   414  	case <-ctx.Done():
   415  		return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
   416  	default:
   417  	}
   418  
   419  	// make copy of call method
   420  	gcall := g.call
   421  
   422  	// wrap the call in reverse
   423  	for i := len(callOpts.CallWrappers); i > 0; i-- {
   424  		gcall = callOpts.CallWrappers[i-1](gcall)
   425  	}
   426  
   427  	// return errors.New("go.micro.client", "request timeout", 408)
   428  	call := func(i int) error {
   429  		// call backoff first. Someone may want an initial start delay
   430  		t, err := callOpts.Backoff(ctx, req, i)
   431  		if err != nil {
   432  			return errors.InternalServerError("go.micro.client", err.Error())
   433  		}
   434  
   435  		// only sleep if greater than 0
   436  		if t.Seconds() > 0 {
   437  			time.Sleep(t)
   438  		}
   439  
   440  		// select next node
   441  		node, err := next()
   442  		service := req.Service()
   443  		if err != nil {
   444  			if err == selector.ErrNotFound {
   445  				return errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
   446  			}
   447  			return errors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error())
   448  		}
   449  
   450  		// make the call
   451  		err = gcall(ctx, node, req, rsp, callOpts)
   452  		g.opts.Selector.Mark(service, node, err)
   453  		if verr, ok := err.(*errors.Error); ok {
   454  			return verr
   455  		}
   456  
   457  		return err
   458  	}
   459  
   460  	ch := make(chan error, callOpts.Retries+1)
   461  	var gerr error
   462  
   463  	for i := 0; i <= callOpts.Retries; i++ {
   464  		go func(i int) {
   465  			ch <- call(i)
   466  		}(i)
   467  
   468  		select {
   469  		case <-ctx.Done():
   470  			return errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
   471  		case err := <-ch:
   472  			// if the call succeeded lets bail early
   473  			if err == nil {
   474  				return nil
   475  			}
   476  
   477  			retry, rerr := callOpts.Retry(ctx, req, i, err)
   478  			if rerr != nil {
   479  				return rerr
   480  			}
   481  
   482  			if !retry {
   483  				return err
   484  			}
   485  
   486  			gerr = err
   487  		}
   488  	}
   489  
   490  	return gerr
   491  }
   492  
   493  func (g *grpcClient) Stream(ctx context.Context, req client.Request, opts ...client.CallOption) (client.Stream, error) {
   494  	// make a copy of call opts
   495  	callOpts := g.opts.CallOptions
   496  	for _, opt := range opts {
   497  		opt(&callOpts)
   498  	}
   499  
   500  	next, err := g.next(req, callOpts)
   501  	if err != nil {
   502  		return nil, err
   503  	}
   504  
   505  	// #200 - streams shouldn't have a request timeout set on the context
   506  
   507  	// should we noop right here?
   508  	select {
   509  	case <-ctx.Done():
   510  		return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
   511  	default:
   512  	}
   513  
   514  	// make a copy of stream
   515  	gstream := g.stream
   516  
   517  	// wrap the call in reverse
   518  	for i := len(callOpts.CallWrappers); i > 0; i-- {
   519  		gstream = callOpts.CallWrappers[i-1](gstream)
   520  	}
   521  
   522  	call := func(i int) (client.Stream, error) {
   523  		// call backoff first. Someone may want an initial start delay
   524  		t, err := callOpts.Backoff(ctx, req, i)
   525  		if err != nil {
   526  			return nil, errors.InternalServerError("go.micro.client", err.Error())
   527  		}
   528  
   529  		// only sleep if greater than 0
   530  		if t.Seconds() > 0 {
   531  			time.Sleep(t)
   532  		}
   533  
   534  		node, err := next()
   535  		service := req.Service()
   536  		if err != nil {
   537  			if err == selector.ErrNotFound {
   538  				return nil, errors.InternalServerError("go.micro.client", "service %s: %s", service, err.Error())
   539  			}
   540  			return nil, errors.InternalServerError("go.micro.client", "error selecting %s node: %s", service, err.Error())
   541  		}
   542  
   543  		// make the call
   544  		stream := &grpcStream{}
   545  		err = g.stream(ctx, node, req, stream, callOpts)
   546  
   547  		g.opts.Selector.Mark(service, node, err)
   548  		return stream, err
   549  	}
   550  
   551  	type response struct {
   552  		stream client.Stream
   553  		err    error
   554  	}
   555  
   556  	ch := make(chan response, callOpts.Retries+1)
   557  	var grr error
   558  
   559  	for i := 0; i <= callOpts.Retries; i++ {
   560  		go func(i int) {
   561  			s, err := call(i)
   562  			ch <- response{s, err}
   563  		}(i)
   564  
   565  		select {
   566  		case <-ctx.Done():
   567  			return nil, errors.New("go.micro.client", fmt.Sprintf("%v", ctx.Err()), 408)
   568  		case rsp := <-ch:
   569  			// if the call succeeded lets bail early
   570  			if rsp.err == nil {
   571  				return rsp.stream, nil
   572  			}
   573  
   574  			retry, rerr := callOpts.Retry(ctx, req, i, err)
   575  			if rerr != nil {
   576  				return nil, rerr
   577  			}
   578  
   579  			if !retry {
   580  				return nil, rsp.err
   581  			}
   582  
   583  			grr = rsp.err
   584  		}
   585  	}
   586  
   587  	return nil, grr
   588  }
   589  
   590  func (g *grpcClient) Publish(ctx context.Context, p client.Message, opts ...client.PublishOption) error {
   591  	var options client.PublishOptions
   592  	for _, o := range opts {
   593  		o(&options)
   594  	}
   595  
   596  	md, ok := metadata.FromContext(ctx)
   597  	if !ok {
   598  		md = make(map[string]string)
   599  	}
   600  	md["Content-Type"] = p.ContentType()
   601  	md["Micro-Topic"] = p.Topic()
   602  
   603  	cf, err := g.newGRPCCodec(p.ContentType())
   604  	if err != nil {
   605  		return errors.InternalServerError("go.micro.client", err.Error())
   606  	}
   607  
   608  	var body []byte
   609  
   610  	// passed in raw data
   611  	if d, ok := p.Payload().(*raw.Frame); ok {
   612  		body = d.Data
   613  	} else {
   614  		// set the body
   615  		b, err := cf.Marshal(p.Payload())
   616  		if err != nil {
   617  			return errors.InternalServerError("go.micro.client", err.Error())
   618  		}
   619  		body = b
   620  	}
   621  
   622  	if !g.once.Load().(bool) {
   623  		if err = g.opts.Broker.Connect(); err != nil {
   624  			return errors.InternalServerError("go.micro.client", err.Error())
   625  		}
   626  		g.once.Store(true)
   627  	}
   628  
   629  	topic := p.Topic()
   630  
   631  	// get the exchange
   632  	if len(options.Exchange) > 0 {
   633  		topic = options.Exchange
   634  	}
   635  
   636  	return g.opts.Broker.Publish(topic, &broker.Message{
   637  		Header: md,
   638  		Body:   body,
   639  	}, broker.PublishContext(options.Context))
   640  }
   641  
   642  func (g *grpcClient) String() string {
   643  	return "grpc"
   644  }
   645  
   646  func (g *grpcClient) getGrpcDialOptions() []grpc.DialOption {
   647  	if g.opts.CallOptions.Context == nil {
   648  		return nil
   649  	}
   650  
   651  	v := g.opts.CallOptions.Context.Value(grpcDialOptions{})
   652  
   653  	if v == nil {
   654  		return nil
   655  	}
   656  
   657  	opts, ok := v.([]grpc.DialOption)
   658  
   659  	if !ok {
   660  		return nil
   661  	}
   662  
   663  	return opts
   664  }
   665  
   666  func (g *grpcClient) getGrpcCallOptions() []grpc.CallOption {
   667  	if g.opts.CallOptions.Context == nil {
   668  		return nil
   669  	}
   670  
   671  	v := g.opts.CallOptions.Context.Value(grpcCallOptions{})
   672  
   673  	if v == nil {
   674  		return nil
   675  	}
   676  
   677  	opts, ok := v.([]grpc.CallOption)
   678  
   679  	if !ok {
   680  		return nil
   681  	}
   682  
   683  	return opts
   684  }
   685  
   686  func newClient(opts ...client.Option) client.Client {
   687  	options := client.NewOptions()
   688  	// default content type for grpc
   689  	options.ContentType = "application/grpc+proto"
   690  
   691  	for _, o := range opts {
   692  		o(&options)
   693  	}
   694  
   695  	rc := &grpcClient{
   696  		opts: options,
   697  	}
   698  	rc.once.Store(false)
   699  
   700  	rc.pool = newPool(options.PoolSize, options.PoolTTL, rc.poolMaxIdle(), rc.poolMaxStreams())
   701  
   702  	c := client.Client(rc)
   703  
   704  	// wrap in reverse
   705  	for i := len(options.Wrappers); i > 0; i-- {
   706  		c = options.Wrappers[i-1](c)
   707  	}
   708  
   709  	return c
   710  }
   711  
   712  func NewClient(opts ...client.Option) client.Client {
   713  	return newClient(opts...)
   714  }