github.com/Jeffail/benthos/v3@v3.65.0/internal/http/client.go (about)

     1  package http
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"mime"
     9  	"mime/multipart"
    10  	"net"
    11  	"net/http"
    12  	"net/textproto"
    13  	"net/url"
    14  	"strconv"
    15  	"strings"
    16  	"sync"
    17  	"time"
    18  
    19  	"github.com/Jeffail/benthos/v3/internal/bloblang/field"
    20  	"github.com/Jeffail/benthos/v3/internal/interop"
    21  	"github.com/Jeffail/benthos/v3/internal/metadata"
    22  	"github.com/Jeffail/benthos/v3/internal/tracing"
    23  	"github.com/Jeffail/benthos/v3/lib/log"
    24  	"github.com/Jeffail/benthos/v3/lib/message"
    25  	"github.com/Jeffail/benthos/v3/lib/metrics"
    26  	"github.com/Jeffail/benthos/v3/lib/types"
    27  	"github.com/Jeffail/benthos/v3/lib/util/http/client"
    28  	"github.com/Jeffail/benthos/v3/lib/util/throttle"
    29  )
    30  
    31  // MultipartExpressions represents three dynamic expressions that define a
    32  // multipart message part in an HTTP request. Specifying one or more of these
    33  // can be used as a way of creating HTTP requests that overrides the default
    34  // behaviour.
    35  type MultipartExpressions struct {
    36  	ContentDisposition *field.Expression
    37  	ContentType        *field.Expression
    38  	Body               *field.Expression
    39  }
    40  
    41  // Client is a component able to send and receive Benthos messages over HTTP.
    42  type Client struct {
    43  	client *http.Client
    44  
    45  	backoffOn map[int]struct{}
    46  	dropOn    map[int]struct{}
    47  	successOn map[int]struct{}
    48  
    49  	url               *field.Expression
    50  	headers           map[string]*field.Expression
    51  	multipart         []MultipartExpressions
    52  	host              *field.Expression
    53  	metaInsertFilter  *metadata.IncludeFilter
    54  	metaExtractFilter *metadata.IncludeFilter
    55  
    56  	conf          client.Config
    57  	retryThrottle *throttle.Type
    58  
    59  	log   log.Modular
    60  	stats metrics.Type
    61  	mgr   types.Manager
    62  
    63  	mCount         metrics.StatCounter
    64  	mErr           metrics.StatCounter
    65  	mErrReq        metrics.StatCounter
    66  	mErrReqTimeout metrics.StatCounter
    67  	mErrRes        metrics.StatCounter
    68  	mLimited       metrics.StatCounter
    69  	mLimitFor      metrics.StatCounter
    70  	mLimitErr      metrics.StatCounter
    71  	mSucc          metrics.StatCounter
    72  	mLatency       metrics.StatTimer
    73  
    74  	mCodes   map[int]metrics.StatCounter
    75  	codesMut sync.RWMutex
    76  
    77  	oauthClientCtx    context.Context
    78  	oauthClientCancel func()
    79  }
    80  
    81  // NewClient creates a new http client that sends and receives Benthos messages.
    82  func NewClient(conf client.Config, opts ...func(*Client)) (*Client, error) {
    83  	h := Client{
    84  		conf:      conf,
    85  		log:       log.Noop(),
    86  		stats:     metrics.Noop(),
    87  		mgr:       types.NoopMgr(),
    88  		backoffOn: map[int]struct{}{},
    89  		dropOn:    map[int]struct{}{},
    90  		successOn: map[int]struct{}{},
    91  		headers:   map[string]*field.Expression{},
    92  		host:      nil,
    93  	}
    94  	h.oauthClientCtx, h.oauthClientCancel = context.WithCancel(context.Background())
    95  	h.client = conf.OAuth2.Client(h.oauthClientCtx)
    96  
    97  	if tout := conf.Timeout; len(tout) > 0 {
    98  		var err error
    99  		if h.client.Timeout, err = time.ParseDuration(tout); err != nil {
   100  			return nil, fmt.Errorf("failed to parse timeout string: %v", err)
   101  		}
   102  	}
   103  
   104  	if h.conf.TLS.Enabled {
   105  		tlsConf, err := h.conf.TLS.Get()
   106  		if err != nil {
   107  			return nil, err
   108  		}
   109  		if tlsConf != nil {
   110  			if c, ok := http.DefaultTransport.(*http.Transport); ok {
   111  				cloned := c.Clone()
   112  				cloned.TLSClientConfig = tlsConf
   113  				h.client.Transport = cloned
   114  			} else {
   115  				h.client.Transport = &http.Transport{
   116  					TLSClientConfig: tlsConf,
   117  				}
   118  			}
   119  		}
   120  	}
   121  
   122  	if h.conf.ProxyURL != "" {
   123  		proxyURL, err := url.Parse(h.conf.ProxyURL)
   124  		if err != nil {
   125  			return nil, fmt.Errorf("failed to parse proxy_url string: %v", err)
   126  		}
   127  		if h.client.Transport != nil {
   128  			if tr, ok := h.client.Transport.(*http.Transport); ok {
   129  				tr.Proxy = http.ProxyURL(proxyURL)
   130  			} else {
   131  				return nil, fmt.Errorf("unable to apply proxy_url to transport, unexpected type %T", h.client.Transport)
   132  			}
   133  		} else {
   134  			h.client.Transport = &http.Transport{
   135  				Proxy: http.ProxyURL(proxyURL),
   136  			}
   137  		}
   138  	}
   139  
   140  	for _, c := range conf.BackoffOn {
   141  		h.backoffOn[c] = struct{}{}
   142  	}
   143  	for _, c := range conf.DropOn {
   144  		h.dropOn[c] = struct{}{}
   145  	}
   146  	for _, c := range conf.SuccessfulOn {
   147  		h.successOn[c] = struct{}{}
   148  	}
   149  
   150  	for _, opt := range opts {
   151  		opt(&h)
   152  	}
   153  
   154  	var err error
   155  	if h.url, err = interop.NewBloblangField(h.mgr, conf.URL); err != nil {
   156  		return nil, fmt.Errorf("failed to parse URL expression: %v", err)
   157  	}
   158  
   159  	for k, v := range conf.Headers {
   160  		if strings.EqualFold(k, "host") {
   161  			if h.host, err = interop.NewBloblangField(h.mgr, v); err != nil {
   162  				return nil, fmt.Errorf("failed to parse header 'host' expression: %v", err)
   163  			}
   164  		} else {
   165  			if h.headers[k], err = interop.NewBloblangField(h.mgr, v); err != nil {
   166  				return nil, fmt.Errorf("failed to parse header '%v' expression: %v", k, err)
   167  			}
   168  		}
   169  	}
   170  
   171  	if h.metaInsertFilter, err = h.conf.Metadata.CreateFilter(); err != nil {
   172  		return nil, fmt.Errorf("failed to construct metadata filter: %w", err)
   173  	}
   174  
   175  	if h.metaExtractFilter, err = h.conf.ExtractMetadata.CreateFilter(); err != nil {
   176  		return nil, fmt.Errorf("failed to construct metadata extract filter: %w", err)
   177  	}
   178  
   179  	h.mCount = h.stats.GetCounter("count")
   180  	h.mErr = h.stats.GetCounter("error")
   181  	h.mErrReq = h.stats.GetCounter("error.request")
   182  	h.mErrReqTimeout = h.stats.GetCounter("request_timeout")
   183  	h.mErrRes = h.stats.GetCounter("error.response")
   184  	h.mLimited = h.stats.GetCounter("rate_limit.count")
   185  	h.mLimitFor = h.stats.GetCounter("rate_limit.total_ms")
   186  	h.mLimitErr = h.stats.GetCounter("rate_limit.error")
   187  	h.mLatency = h.stats.GetTimer("latency")
   188  	h.mSucc = h.stats.GetCounter("success")
   189  	h.mCodes = map[int]metrics.StatCounter{}
   190  
   191  	var retry, maxBackoff time.Duration
   192  	if tout := conf.Retry; len(tout) > 0 {
   193  		var err error
   194  		if retry, err = time.ParseDuration(tout); err != nil {
   195  			return nil, fmt.Errorf("failed to parse retry duration string: %v", err)
   196  		}
   197  	}
   198  	if tout := conf.MaxBackoff; len(tout) > 0 {
   199  		var err error
   200  		if maxBackoff, err = time.ParseDuration(tout); err != nil {
   201  			return nil, fmt.Errorf("failed to parse max backoff duration string: %v", err)
   202  		}
   203  	}
   204  
   205  	if conf.RateLimit != "" {
   206  		if err := interop.ProbeRateLimit(context.Background(), h.mgr, conf.RateLimit); err != nil {
   207  			return nil, err
   208  		}
   209  	}
   210  
   211  	h.retryThrottle = throttle.New(
   212  		throttle.OptMaxUnthrottledRetries(0),
   213  		throttle.OptThrottlePeriod(retry),
   214  		throttle.OptMaxExponentPeriod(maxBackoff),
   215  	)
   216  
   217  	return &h, nil
   218  }
   219  
   220  //------------------------------------------------------------------------------
   221  
   222  // OptSetLogger sets the logger to use.
   223  func OptSetLogger(log log.Modular) func(*Client) {
   224  	return func(t *Client) {
   225  		t.log = log
   226  	}
   227  }
   228  
   229  // OptSetMultiPart sets the multipart to request.
   230  func OptSetMultiPart(multipart []MultipartExpressions) func(*Client) {
   231  	return func(t *Client) {
   232  		t.multipart = multipart
   233  	}
   234  }
   235  
   236  // OptSetStats sets the metrics aggregator to use.
   237  func OptSetStats(stats metrics.Type) func(*Client) {
   238  	return func(t *Client) {
   239  		t.stats = stats
   240  	}
   241  }
   242  
   243  // OptSetManager sets the manager to use.
   244  func OptSetManager(mgr types.Manager) func(*Client) {
   245  	return func(t *Client) {
   246  		t.mgr = mgr
   247  	}
   248  }
   249  
   250  // OptSetRoundTripper sets the *client.Transport to use for HTTP requests.
   251  // NOTE: This setting will override any configured TLS options.
   252  func OptSetRoundTripper(rt http.RoundTripper) func(*Client) {
   253  	return func(t *Client) {
   254  		t.client.Transport = rt
   255  	}
   256  }
   257  
   258  //------------------------------------------------------------------------------
   259  
   260  func (h *Client) incrCode(code int) {
   261  	h.codesMut.RLock()
   262  	ctr, exists := h.mCodes[code]
   263  	h.codesMut.RUnlock()
   264  
   265  	if exists {
   266  		ctr.Incr(1)
   267  		return
   268  	}
   269  
   270  	ctr = h.stats.GetCounter(fmt.Sprintf("code.%v", code))
   271  	ctr.Incr(1)
   272  
   273  	h.codesMut.Lock()
   274  	h.mCodes[code] = ctr
   275  	h.codesMut.Unlock()
   276  }
   277  
   278  func (h *Client) waitForAccess(ctx context.Context) bool {
   279  	if h.conf.RateLimit == "" {
   280  		return true
   281  	}
   282  	for {
   283  		var period time.Duration
   284  		var err error
   285  		if rerr := interop.AccessRateLimit(ctx, h.mgr, h.conf.RateLimit, func(rl types.RateLimit) {
   286  			period, err = rl.Access()
   287  		}); rerr != nil {
   288  			err = rerr
   289  		}
   290  		if err != nil {
   291  			h.log.Errorf("Rate limit error: %v\n", err)
   292  			h.mLimitErr.Incr(1)
   293  			period = time.Second
   294  		} else if period > 0 {
   295  			h.mLimited.Incr(1)
   296  			h.mLimitFor.Incr(period.Nanoseconds() / 1000000)
   297  		}
   298  
   299  		if period > 0 {
   300  			select {
   301  			case <-time.After(period):
   302  			case <-ctx.Done():
   303  				return false
   304  			}
   305  		} else {
   306  			return true
   307  		}
   308  	}
   309  }
   310  
   311  // CreateRequest forms an *http.Request from a message to be sent as the body,
   312  // and also a message used to form headers (they can be the same).
   313  func (h *Client) CreateRequest(sendMsg, refMsg types.Message) (req *http.Request, err error) {
   314  	var overrideContentType string
   315  	var body io.Reader
   316  	if len(h.multipart) > 0 {
   317  		buf := &bytes.Buffer{}
   318  		writer := multipart.NewWriter(buf)
   319  		for _, v := range h.multipart {
   320  			var part io.Writer
   321  			mh := make(textproto.MIMEHeader)
   322  			mh.Set("Content-Type", v.ContentType.String(0, refMsg))
   323  			mh.Set("Content-Disposition", v.ContentDisposition.String(0, refMsg))
   324  			if part, err = writer.CreatePart(mh); err != nil {
   325  				return
   326  			}
   327  			if _, err = io.Copy(part, bytes.NewReader([]byte(v.Body.String(0, refMsg)))); err != nil {
   328  				return
   329  			}
   330  		}
   331  		writer.Close()
   332  		overrideContentType = writer.FormDataContentType()
   333  		body = buf
   334  	} else if sendMsg != nil && sendMsg.Len() == 1 {
   335  		if msgBytes := sendMsg.Get(0).Get(); len(msgBytes) > 0 {
   336  			body = bytes.NewBuffer(msgBytes)
   337  		}
   338  	} else if sendMsg != nil && sendMsg.Len() > 1 {
   339  		buf := &bytes.Buffer{}
   340  		writer := multipart.NewWriter(buf)
   341  
   342  		for i := 0; i < sendMsg.Len(); i++ {
   343  			contentType := "application/octet-stream"
   344  			if v, exists := h.headers["Content-Type"]; exists {
   345  				contentType = v.String(i, refMsg)
   346  			}
   347  
   348  			headers := textproto.MIMEHeader{
   349  				"Content-Type": []string{contentType},
   350  			}
   351  			_ = h.metaInsertFilter.Iter(sendMsg.Get(i).Metadata(), func(k, v string) error {
   352  				headers[k] = append(headers[k], v)
   353  				return nil
   354  			})
   355  
   356  			var part io.Writer
   357  			if part, err = writer.CreatePart(headers); err != nil {
   358  				return
   359  			}
   360  			if _, err = io.Copy(part, bytes.NewReader(sendMsg.Get(i).Get())); err != nil {
   361  				return
   362  			}
   363  		}
   364  
   365  		writer.Close()
   366  		overrideContentType = writer.FormDataContentType()
   367  
   368  		body = buf
   369  	}
   370  
   371  	url := h.url.String(0, refMsg)
   372  	if req, err = http.NewRequest(h.conf.Verb, url, body); err != nil {
   373  		return
   374  	}
   375  
   376  	for k, v := range h.headers {
   377  		req.Header.Add(k, v.String(0, refMsg))
   378  	}
   379  	if sendMsg != nil && sendMsg.Len() == 1 {
   380  		_ = h.metaInsertFilter.Iter(sendMsg.Get(0).Metadata(), func(k, v string) error {
   381  			req.Header.Add(k, v)
   382  			return nil
   383  		})
   384  	}
   385  
   386  	if h.host != nil {
   387  		req.Host = h.host.String(0, refMsg)
   388  	}
   389  	if overrideContentType != "" {
   390  		req.Header.Del("Content-Type")
   391  		req.Header.Add("Content-Type", overrideContentType)
   392  	}
   393  
   394  	err = h.conf.Config.Sign(req)
   395  	return
   396  }
   397  
   398  // ParseResponse attempts to parse an HTTP response into a 2D slice of bytes.
   399  func (h *Client) ParseResponse(res *http.Response) (resMsg types.Message, err error) {
   400  	resMsg = message.New(nil)
   401  
   402  	if res.Body != nil {
   403  		defer res.Body.Close()
   404  
   405  		contentType := res.Header.Get("Content-Type")
   406  
   407  		var mediaType string
   408  		var params map[string]string
   409  		if len(contentType) > 0 {
   410  			if mediaType, params, err = mime.ParseMediaType(contentType); err != nil {
   411  				h.log.Warnf("Failed to parse media type from Content-Type header: %v\n", err)
   412  			}
   413  		}
   414  
   415  		var buffer bytes.Buffer
   416  		if strings.HasPrefix(mediaType, "multipart/") {
   417  			mr := multipart.NewReader(res.Body, params["boundary"])
   418  			var bufferIndex int64
   419  			for {
   420  				var p *multipart.Part
   421  				if p, err = mr.NextPart(); err != nil {
   422  					if err == io.EOF {
   423  						err = nil
   424  						break
   425  					}
   426  					return
   427  				}
   428  
   429  				var bytesRead int64
   430  				if bytesRead, err = buffer.ReadFrom(p); err != nil {
   431  					h.mErrRes.Incr(1)
   432  					h.mErr.Incr(1)
   433  					h.log.Errorf("Failed to read response: %v\n", err)
   434  					return
   435  				}
   436  
   437  				index := resMsg.Append(message.NewPart(buffer.Bytes()[bufferIndex : bufferIndex+bytesRead]))
   438  				bufferIndex += bytesRead
   439  
   440  				if h.conf.CopyResponseHeaders || h.metaExtractFilter.IsSet() {
   441  					meta := resMsg.Get(index).Metadata()
   442  					for k, values := range p.Header {
   443  						normalisedHeader := strings.ToLower(k)
   444  						if len(values) > 0 && (h.conf.CopyResponseHeaders || h.metaExtractFilter.Match(normalisedHeader)) {
   445  							meta.Set(normalisedHeader, values[0])
   446  						}
   447  					}
   448  				}
   449  			}
   450  		} else {
   451  			var bytesRead int64
   452  			if bytesRead, err = buffer.ReadFrom(res.Body); err != nil {
   453  				h.mErrRes.Incr(1)
   454  				h.mErr.Incr(1)
   455  				h.log.Errorf("Failed to read response: %v\n", err)
   456  				return
   457  			}
   458  			if bytesRead > 0 {
   459  				resMsg.Append(message.NewPart(buffer.Bytes()[:bytesRead]))
   460  			} else {
   461  				resMsg.Append(message.NewPart(nil))
   462  			}
   463  			if h.conf.CopyResponseHeaders || h.metaExtractFilter.IsSet() {
   464  				meta := resMsg.Get(0).Metadata()
   465  				for k, values := range res.Header {
   466  					normalisedHeader := strings.ToLower(k)
   467  					if len(values) > 0 && (h.conf.CopyResponseHeaders || h.metaExtractFilter.Match(normalisedHeader)) {
   468  						meta.Set(normalisedHeader, values[0])
   469  					}
   470  				}
   471  			}
   472  		}
   473  	} else {
   474  		resMsg.Append(message.NewPart(nil))
   475  	}
   476  
   477  	resMsg.Iter(func(i int, p types.Part) error {
   478  		p.Metadata().Set("http_status_code", strconv.Itoa(res.StatusCode))
   479  		return nil
   480  	})
   481  	return
   482  }
   483  
   484  type retryStrategy int
   485  
   486  const (
   487  	noRetry retryStrategy = iota
   488  	retryLinear
   489  	retryBackoff
   490  )
   491  
   492  // checkStatus compares a returned status code against configured logic
   493  // determining whether the send succeeded, and if not what the retry strategy
   494  // should be.
   495  func (h *Client) checkStatus(code int) (succeeded bool, retStrat retryStrategy) {
   496  	if _, exists := h.dropOn[code]; exists {
   497  		return false, noRetry
   498  	}
   499  	if _, exists := h.backoffOn[code]; exists {
   500  		return false, retryBackoff
   501  	}
   502  	if _, exists := h.successOn[code]; exists {
   503  		return true, noRetry
   504  	}
   505  	if code < 200 || code > 299 {
   506  		return false, retryLinear
   507  	}
   508  	return true, noRetry
   509  }
   510  
   511  // SendToResponse attempts to create an HTTP request from a provided message,
   512  // performs it, and then returns the *http.Response, allowing the raw response
   513  // to be consumed.
   514  func (h *Client) SendToResponse(ctx context.Context, sendMsg, refMsg types.Message) (res *http.Response, err error) {
   515  	h.mCount.Incr(1)
   516  
   517  	var spans []*tracing.Span
   518  	if sendMsg != nil {
   519  		spans = tracing.CreateChildSpans("http_request", sendMsg)
   520  		defer func() {
   521  			for _, s := range spans {
   522  				s.Finish()
   523  			}
   524  		}()
   525  	}
   526  	logErr := func(e error) {
   527  		h.mErrRes.Incr(1)
   528  		h.mErr.Incr(1)
   529  		for _, s := range spans {
   530  			s.LogKV(
   531  				"event", "error",
   532  				"type", e.Error(),
   533  			)
   534  		}
   535  	}
   536  
   537  	var req *http.Request
   538  	if req, err = h.CreateRequest(sendMsg, refMsg); err != nil {
   539  		logErr(err)
   540  		return nil, err
   541  	}
   542  	// Make sure we log the actual request URL
   543  	defer func() {
   544  		if err != nil {
   545  			err = fmt.Errorf("%s: %w", req.URL, err)
   546  		}
   547  	}()
   548  
   549  	startedAt := time.Now()
   550  
   551  	if !h.waitForAccess(ctx) {
   552  		return nil, types.ErrTypeClosed
   553  	}
   554  
   555  	rateLimited := false
   556  	numRetries := h.conf.NumRetries
   557  
   558  	res, err = h.client.Do(req.WithContext(ctx))
   559  	if err != nil {
   560  		if err, ok := err.(net.Error); ok && err.Timeout() {
   561  			h.mErrReqTimeout.Incr(1)
   562  		}
   563  	} else {
   564  		h.incrCode(res.StatusCode)
   565  		if resolved, retryStrat := h.checkStatus(res.StatusCode); !resolved {
   566  			rateLimited = retryStrat == retryBackoff
   567  			if retryStrat == noRetry {
   568  				numRetries = 0
   569  			}
   570  			err = UnexpectedErr(res)
   571  			if res.Body != nil {
   572  				res.Body.Close()
   573  			}
   574  		}
   575  	}
   576  
   577  	i, j := 0, numRetries
   578  	for i < j && err != nil {
   579  		logErr(err)
   580  		if req, err = h.CreateRequest(sendMsg, refMsg); err != nil {
   581  			continue
   582  		}
   583  		if rateLimited {
   584  			if !h.retryThrottle.ExponentialRetryWithContext(ctx) {
   585  				return nil, types.ErrTypeClosed
   586  			}
   587  		} else {
   588  			if !h.retryThrottle.RetryWithContext(ctx) {
   589  				return nil, types.ErrTypeClosed
   590  			}
   591  		}
   592  		if !h.waitForAccess(ctx) {
   593  			return nil, types.ErrTypeClosed
   594  		}
   595  		rateLimited = false
   596  		if res, err = h.client.Do(req.WithContext(ctx)); err == nil {
   597  			h.incrCode(res.StatusCode)
   598  			if resolved, retryStrat := h.checkStatus(res.StatusCode); !resolved {
   599  				rateLimited = retryStrat == retryBackoff
   600  				if retryStrat == noRetry {
   601  					j = 0
   602  				}
   603  				err = UnexpectedErr(res)
   604  				if res.Body != nil {
   605  					res.Body.Close()
   606  				}
   607  			}
   608  		} else if err, ok := err.(net.Error); ok && err.Timeout() {
   609  			h.mErrReqTimeout.Incr(1)
   610  		}
   611  		i++
   612  	}
   613  	if err != nil {
   614  		logErr(err)
   615  		return nil, err
   616  	}
   617  
   618  	h.mLatency.Timing(int64(time.Since(startedAt)))
   619  	h.mSucc.Incr(1)
   620  	h.retryThrottle.Reset()
   621  	return res, nil
   622  }
   623  
   624  // UnexpectedErr get error body
   625  func UnexpectedErr(res *http.Response) error {
   626  	body, err := io.ReadAll(res.Body)
   627  	if err != nil {
   628  		return err
   629  	}
   630  	return types.ErrUnexpectedHTTPRes{Code: res.StatusCode, S: res.Status, Body: body}
   631  }
   632  
   633  // Send creates an HTTP request from the client config, a provided message to be
   634  // sent as the body of the request, and a reference message used to establish
   635  // interpolated fields for the request (which can be the same as the message
   636  // used for the body).
   637  //
   638  // If the request is successful then the response is parsed into a message,
   639  // including headers added as metadata (when configured to do so).
   640  func (h *Client) Send(ctx context.Context, sendMsg, refMsg types.Message) (types.Message, error) {
   641  	res, err := h.SendToResponse(ctx, sendMsg, refMsg)
   642  	if err != nil {
   643  		return nil, err
   644  	}
   645  	return h.ParseResponse(res)
   646  }
   647  
   648  // Close the client.
   649  func (h *Client) Close(ctx context.Context) error {
   650  	h.oauthClientCancel()
   651  	return nil
   652  }