corso/src/internal/m365/graph/http_wrapper.go
Keepers f37e58ee5c
graph debug logging improvements (#4368)
#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🤖 Supportability/Tests
- [x] 🧹 Tech Debt/Cleanup
2023-09-26 22:01:00 +00:00

201 lines
5.1 KiB
Go

package graph
import (
"context"
"io"
"net/http"
"time"
"github.com/alcionai/clues"
khttp "github.com/microsoft/kiota-http-go"
"github.com/pkg/errors"
"golang.org/x/net/http2"
"github.com/alcionai/corso/src/internal/events"
"github.com/alcionai/corso/src/internal/version"
"github.com/alcionai/corso/src/pkg/logger"
)
// ---------------------------------------------------------------------------
// constructors
// ---------------------------------------------------------------------------
type Requester interface {
Request(
ctx context.Context,
method, url string,
body io.Reader,
headers map[string]string,
) (*http.Response, error)
}
// NewHTTPWrapper produces a http.Client wrapper that ensures
// calls use all the middleware we expect from the graph api client.
//
// Re-use of http clients is critical, or else we leak OS resources
// and consume relatively unbound socket connections. It is important
// to centralize this client to be passed downstream where api calls
// can utilize it on a per-download basis.
func NewHTTPWrapper(opts ...Option) *httpWrapper {
var (
cc = populateConfig(opts...)
rt = customTransport{
n: pipeline{
middlewares: internalMiddleware(cc),
transport: defaultTransport(),
},
}
redirect = func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
}
hc = &http.Client{
CheckRedirect: redirect,
Timeout: defaultHTTPClientTimeout,
Transport: rt,
}
)
cc.apply(hc)
return &httpWrapper{hc, cc}
}
// NewNoTimeoutHTTPWrapper constructs a http wrapper with no context timeout.
//
// Re-use of http clients is critical, or else we leak OS resources
// and consume relatively unbound socket connections. It is important
// to centralize this client to be passed downstream where api calls
// can utilize it on a per-download basis.
func NewNoTimeoutHTTPWrapper(opts ...Option) *httpWrapper {
opts = append(opts, NoTimeout())
return NewHTTPWrapper(opts...)
}
// ---------------------------------------------------------------------------
// requests
// ---------------------------------------------------------------------------
// Request does the provided request.
func (hw httpWrapper) Request(
ctx context.Context,
method, url string,
body io.Reader,
headers map[string]string,
) (*http.Response, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, clues.Wrap(err, "new http request")
}
for k, v := range headers {
req.Header.Set(k, v)
}
//nolint:lll
// Decorate the traffic
// See https://learn.microsoft.com/en-us/sharepoint/dev/general-development/how-to-avoid-getting-throttled-or-blocked-in-sharepoint-online#how-to-decorate-your-http-traffic
req.Header.Set("User-Agent", "ISV|Alcion|Corso/"+version.Version)
var resp *http.Response
// stream errors from http/2 will fail before we reach
// client middleware handling, therefore we don't get to
// make use of the retry middleware. This external
// retry wrapper is unsophisticated, but should only
// retry in the event of a `stream error`, which is not
// a common expectation.
for i := 0; i < hw.config.maxConnectionRetries+1; i++ {
ctx = clues.Add(ctx, "request_retry_iter", i)
resp, err = hw.client.Do(req)
if err == nil {
break
}
if IsErrApplicationThrottled(err) {
return nil, Stack(ctx, clues.Stack(ErrApplicationThrottled, err))
}
var http2StreamErr http2.StreamError
if !errors.As(err, &http2StreamErr) {
return nil, Stack(ctx, err)
}
logger.Ctx(ctx).Debug("http2 stream error")
events.Inc(events.APICall, "streamerror")
time.Sleep(3 * time.Second)
}
if err != nil {
return nil, Stack(ctx, err)
}
logResp(ctx, resp)
return resp, nil
}
// ---------------------------------------------------------------------------
// constructor internals
// ---------------------------------------------------------------------------
type (
httpWrapper struct {
client *http.Client
config *clientConfig
}
customTransport struct {
n nexter
}
pipeline struct {
transport http.RoundTripper
middlewares []khttp.Middleware
}
)
// RoundTrip kicks off the middleware chain and returns a response
func (ct customTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return ct.n.Next(req, 0)
}
// Next moves the request object through middlewares in the pipeline
func (pl pipeline) Next(req *http.Request, idx int) (*http.Response, error) {
if idx < len(pl.middlewares) {
return pl.middlewares[idx].Intercept(pl, idx+1, req)
}
return pl.transport.RoundTrip(req)
}
func defaultTransport() http.RoundTripper {
defaultTransport := http.DefaultTransport.(*http.Transport).Clone()
defaultTransport.ForceAttemptHTTP2 = true
return defaultTransport
}
func internalMiddleware(cc *clientConfig) []khttp.Middleware {
mw := []khttp.Middleware{
&RetryMiddleware{
MaxRetries: cc.maxRetries,
Delay: cc.minDelay,
},
khttp.NewRetryHandler(),
khttp.NewRedirectHandler(),
&LoggingMiddleware{},
&throttlingMiddleware{newTimedFence()},
&RateLimiterMiddleware{},
&MetricsMiddleware{},
}
if len(cc.appendMiddleware) > 0 {
mw = append(mw, cc.appendMiddleware...)
}
return mw
}