github.com/cloudwego/hertz@v0.9.3/pkg/protocol/client/client.go (about)

     1  /*
     2   * Copyright 2022 CloudWeGo Authors
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   *
    16   * The MIT License (MIT)
    17   *
    18   * Copyright (c) 2015-present Aliaksandr Valialkin, VertaMedia, Kirill Danshin, Erik Dubbelboer, FastHTTP Authors
    19   *
    20   * Permission is hereby granted, free of charge, to any person obtaining a copy
    21   * of this software and associated documentation files (the "Software"), to deal
    22   * in the Software without restriction, including without limitation the rights
    23   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    24   * copies of the Software, and to permit persons to whom the Software is
    25   * furnished to do so, subject to the following conditions:
    26   *
    27   * The above copyright notice and this permission notice shall be included in
    28   * all copies or substantial portions of the Software.
    29   *
    30   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    31   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    32   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    33   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    34   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    35   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    36   * THE SOFTWARE.
    37   *
    38   * This file may have been modified by CloudWeGo authors. All CloudWeGo
    39   * Modifications are Copyright 2022 CloudWeGo Authors.
    40   */
    41  
    42  package client
    43  
    44  import (
    45  	"context"
    46  	"sync"
    47  	"time"
    48  
    49  	"github.com/cloudwego/hertz/internal/bytestr"
    50  	"github.com/cloudwego/hertz/pkg/common/config"
    51  	"github.com/cloudwego/hertz/pkg/common/errors"
    52  	"github.com/cloudwego/hertz/pkg/common/timer"
    53  	"github.com/cloudwego/hertz/pkg/protocol"
    54  	"github.com/cloudwego/hertz/pkg/protocol/consts"
    55  )
    56  
    57  const defaultMaxRedirectsCount = 16
    58  
    59  var (
    60  	errTimeout          = errors.New(errors.ErrTimeout, errors.ErrorTypePublic, "host client")
    61  	errMissingLocation  = errors.NewPublic("missing Location header for http redirect")
    62  	errTooManyRedirects = errors.NewPublic("too many redirects detected when doing the request")
    63  
    64  	clientURLResponseChPool sync.Pool
    65  )
    66  
    67  type HostClient interface {
    68  	Doer
    69  	SetDynamicConfig(dc *DynamicConfig)
    70  	CloseIdleConnections()
    71  	ShouldRemove() bool
    72  	ConnectionCount() int
    73  }
    74  
    75  type Doer interface {
    76  	Do(ctx context.Context, req *protocol.Request, resp *protocol.Response) error
    77  }
    78  
    79  // DefaultRetryIf Default retry condition, mainly used for idempotent requests.
    80  // If this cannot be satisfied, you can implement your own retry condition.
    81  func DefaultRetryIf(req *protocol.Request, resp *protocol.Response, err error) bool {
    82  	// cannot retry if the request body is not rewindable
    83  	if req.IsBodyStream() {
    84  		return false
    85  	}
    86  
    87  	if isIdempotent(req, resp, err) {
    88  		return true
    89  	}
    90  
    91  	return false
    92  }
    93  
    94  func isIdempotent(req *protocol.Request, resp *protocol.Response, err error) bool {
    95  	return req.Header.IsGet() ||
    96  		req.Header.IsHead() ||
    97  		req.Header.IsPut() ||
    98  		req.Header.IsDelete() ||
    99  		req.Header.IsOptions() ||
   100  		req.Header.IsTrace()
   101  }
   102  
   103  // DynamicConfig is config set which will be confirmed when starts a request.
   104  type DynamicConfig struct {
   105  	Addr     string
   106  	ProxyURI *protocol.URI
   107  	IsTLS    bool
   108  }
   109  
   110  // RetryIfFunc signature of retry if function
   111  // Judge whether to retry by request,response or error , return true is retry
   112  type RetryIfFunc func(req *protocol.Request, resp *protocol.Response, err error) bool
   113  
   114  type clientURLResponse struct {
   115  	statusCode int
   116  	body       []byte
   117  	err        error
   118  }
   119  
   120  func GetURL(ctx context.Context, dst []byte, url string, c Doer, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) {
   121  	req := protocol.AcquireRequest()
   122  	req.SetOptions(requestOptions...)
   123  
   124  	statusCode, body, err = doRequestFollowRedirectsBuffer(ctx, req, dst, url, c)
   125  
   126  	protocol.ReleaseRequest(req)
   127  	return statusCode, body, err
   128  }
   129  
   130  func GetURLTimeout(ctx context.Context, dst []byte, url string, timeout time.Duration, c Doer, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) {
   131  	deadline := time.Now().Add(timeout)
   132  	return GetURLDeadline(ctx, dst, url, deadline, c, requestOptions...)
   133  }
   134  
   135  func GetURLDeadline(ctx context.Context, dst []byte, url string, deadline time.Time, c Doer, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) {
   136  	timeout := -time.Since(deadline)
   137  	if timeout <= 0 {
   138  		return 0, dst, errTimeout
   139  	}
   140  
   141  	var ch chan clientURLResponse
   142  	chv := clientURLResponseChPool.Get()
   143  	if chv == nil {
   144  		chv = make(chan clientURLResponse, 1)
   145  	}
   146  	ch = chv.(chan clientURLResponse)
   147  
   148  	req := protocol.AcquireRequest()
   149  	req.SetOptions(requestOptions...)
   150  
   151  	// Note that the request continues execution on errTimeout until
   152  	// client-specific ReadTimeout exceeds. This helps to limit load
   153  	// on slow hosts by MaxConns* concurrent requests.
   154  	//
   155  	// Without this 'hack' the load on slow host could exceed MaxConns*
   156  	// concurrent requests, since timed out requests on client side
   157  	// usually continue execution on the host.
   158  	go func() {
   159  		statusCodeCopy, bodyCopy, errCopy := doRequestFollowRedirectsBuffer(ctx, req, dst, url, c)
   160  		ch <- clientURLResponse{
   161  			statusCode: statusCodeCopy,
   162  			body:       bodyCopy,
   163  			err:        errCopy,
   164  		}
   165  	}()
   166  
   167  	tc := timer.AcquireTimer(timeout)
   168  	select {
   169  	case resp := <-ch:
   170  		protocol.ReleaseRequest(req)
   171  		clientURLResponseChPool.Put(chv)
   172  		statusCode = resp.statusCode
   173  		body = resp.body
   174  		err = resp.err
   175  	case <-tc.C:
   176  		body = dst
   177  		err = errTimeout
   178  	}
   179  	timer.ReleaseTimer(tc)
   180  
   181  	return statusCode, body, err
   182  }
   183  
   184  func PostURL(ctx context.Context, dst []byte, url string, postArgs *protocol.Args, c Doer, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error) {
   185  	req := protocol.AcquireRequest()
   186  	req.Header.SetMethodBytes(bytestr.StrPost)
   187  	req.Header.SetContentTypeBytes(bytestr.StrPostArgsContentType)
   188  	req.SetOptions(requestOptions...)
   189  
   190  	if postArgs != nil {
   191  		if _, err := postArgs.WriteTo(req.BodyWriter()); err != nil {
   192  			return 0, nil, err
   193  		}
   194  	}
   195  
   196  	statusCode, body, err = doRequestFollowRedirectsBuffer(ctx, req, dst, url, c)
   197  
   198  	protocol.ReleaseRequest(req)
   199  	return statusCode, body, err
   200  }
   201  
   202  func doRequestFollowRedirectsBuffer(ctx context.Context, req *protocol.Request, dst []byte, url string, c Doer) (statusCode int, body []byte, err error) {
   203  	resp := protocol.AcquireResponse()
   204  	bodyBuf := resp.BodyBuffer()
   205  	oldBody := bodyBuf.B
   206  	bodyBuf.B = dst
   207  
   208  	statusCode, _, err = DoRequestFollowRedirects(ctx, req, resp, url, defaultMaxRedirectsCount, c)
   209  
   210  	// In HTTP2 scenario, client use stream mode to create a request and its body is in body stream.
   211  	// In HTTP1, only client recv body exceed max body size and client is in stream mode can trig it.
   212  	body = resp.Body()
   213  	bodyBuf.B = oldBody
   214  	protocol.ReleaseResponse(resp)
   215  
   216  	return statusCode, body, err
   217  }
   218  
   219  func DoRequestFollowRedirects(ctx context.Context, req *protocol.Request, resp *protocol.Response, url string, maxRedirectsCount int, c Doer) (statusCode int, body []byte, err error) {
   220  	redirectsCount := 0
   221  
   222  	for {
   223  		req.SetRequestURI(url)
   224  		req.ParseURI()
   225  
   226  		if err = c.Do(ctx, req, resp); err != nil {
   227  			break
   228  		}
   229  		statusCode = resp.Header.StatusCode()
   230  		if !StatusCodeIsRedirect(statusCode) {
   231  			break
   232  		}
   233  
   234  		redirectsCount++
   235  		if redirectsCount > maxRedirectsCount {
   236  			err = errTooManyRedirects
   237  			break
   238  		}
   239  		location := resp.Header.PeekLocation()
   240  		if len(location) == 0 {
   241  			err = errMissingLocation
   242  			break
   243  		}
   244  		url = getRedirectURL(url, location)
   245  	}
   246  
   247  	return statusCode, body, err
   248  }
   249  
   250  // StatusCodeIsRedirect returns true if the status code indicates a redirect.
   251  func StatusCodeIsRedirect(statusCode int) bool {
   252  	return statusCode == consts.StatusMovedPermanently ||
   253  		statusCode == consts.StatusFound ||
   254  		statusCode == consts.StatusSeeOther ||
   255  		statusCode == consts.StatusTemporaryRedirect ||
   256  		statusCode == consts.StatusPermanentRedirect
   257  }
   258  
   259  func getRedirectURL(baseURL string, location []byte) string {
   260  	u := protocol.AcquireURI()
   261  	u.Update(baseURL)
   262  	u.UpdateBytes(location)
   263  	redirectURL := u.String()
   264  	protocol.ReleaseURI(u)
   265  	return redirectURL
   266  }
   267  
   268  func DoTimeout(ctx context.Context, req *protocol.Request, resp *protocol.Response, timeout time.Duration, c Doer) error {
   269  	if timeout <= 0 {
   270  		return errTimeout
   271  	}
   272  	// Note: it will overwrite the reqTimeout.
   273  	req.SetOptions(config.WithRequestTimeout(timeout))
   274  	return c.Do(ctx, req, resp)
   275  }
   276  
   277  func DoDeadline(ctx context.Context, req *protocol.Request, resp *protocol.Response, deadline time.Time, c Doer) error {
   278  	timeout := time.Until(deadline)
   279  	if timeout <= 0 {
   280  		return errTimeout
   281  	}
   282  	// Note: it will overwrite the reqTimeout.
   283  	req.SetOptions(config.WithRequestTimeout(timeout))
   284  	return c.Do(ctx, req, resp)
   285  }