#### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🤖 Supportability/Tests #### Test Plan - [x] ⚡ Unit test
195 lines
5.0 KiB
Go
195 lines
5.0 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++ {
|
|
ictx := clues.Add(ctx, "request_retry_iter", i)
|
|
|
|
resp, err = hw.client.Do(req)
|
|
|
|
if err == nil {
|
|
break
|
|
}
|
|
|
|
var http2StreamErr http2.StreamError
|
|
if !errors.As(err, &http2StreamErr) {
|
|
return nil, Stack(ictx, err)
|
|
}
|
|
|
|
logger.Ctx(ictx).Debug("http2 stream error")
|
|
events.Inc(events.APICall, "streamerror")
|
|
|
|
time.Sleep(3 * time.Second)
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, Stack(ctx, err)
|
|
}
|
|
|
|
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
|
|
}
|