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 }