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