diff --git a/src/internal/m365/graph/errors.go b/src/internal/m365/graph/errors.go index 7c51f74b7..91dc01bb0 100644 --- a/src/internal/m365/graph/errors.go +++ b/src/internal/m365/graph/errors.go @@ -26,6 +26,7 @@ import ( type errorCode string const ( + applicationThrottled errorCode = "ApplicationThrottled" // this auth error is a catch-all used by graph in a variety of cases: // users without licenses, bad jwts, missing account permissions, etc. AuthenticationError errorCode = "AuthenticationError" @@ -81,6 +82,10 @@ const ( ) var ( + // ErrApplicationThrottled occurs if throttling retries are exhausted and completely + // fails out. + ErrApplicationThrottled = clues.New("application throttled") + // The folder or item was deleted between the time we identified // it and when we tried to fetch data for it. ErrDeletedInFlight = clues.New("deleted in flight") @@ -116,6 +121,11 @@ var ( ErrResourceOwnerNotFound = clues.New("resource owner not found in tenant") ) +func IsErrApplicationThrottled(err error) bool { + return hasErrorCode(err, applicationThrottled) || + errors.Is(err, ErrApplicationThrottled) +} + func IsErrAuthenticationError(err error) bool { return hasErrorCode(err, AuthenticationError) } diff --git a/src/internal/m365/graph/errors_test.go b/src/internal/m365/graph/errors_test.go index e8049a067..606eb23a5 100644 --- a/src/internal/m365/graph/errors_test.go +++ b/src/internal/m365/graph/errors_test.go @@ -94,6 +94,40 @@ func (suite *GraphErrorsUnitSuite) TestIsErrConnectionReset() { } } +func (suite *GraphErrorsUnitSuite) TestIsErrApplicationThrottled() { + table := []struct { + name string + err error + expect assert.BoolAssertionFunc + }{ + { + name: "nil", + err: nil, + expect: assert.False, + }, + { + name: "non-matching", + err: assert.AnError, + expect: assert.False, + }, + { + name: "non-matching oDataErr", + err: odErr("fnords"), + expect: assert.False, + }, + { + name: "applicationThrottled oDataErr", + err: odErr(string(applicationThrottled)), + expect: assert.True, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + test.expect(suite.T(), IsErrApplicationThrottled(test.err)) + }) + } +} + func (suite *GraphErrorsUnitSuite) TestIsErrAuthenticationError() { table := []struct { name string diff --git a/src/internal/m365/graph/http_wrapper.go b/src/internal/m365/graph/http_wrapper.go index b810c7aa3..fdffb583f 100644 --- a/src/internal/m365/graph/http_wrapper.go +++ b/src/internal/m365/graph/http_wrapper.go @@ -113,6 +113,10 @@ func (hw httpWrapper) Request( break } + if IsErrApplicationThrottled(err) { + return nil, Stack(ictx, clues.Stack(ErrApplicationThrottled, err)) + } + var http2StreamErr http2.StreamError if !errors.As(err, &http2StreamErr) { return nil, Stack(ictx, err) diff --git a/src/internal/m365/graph/service.go b/src/internal/m365/graph/service.go index 7b276b771..eedda99df 100644 --- a/src/internal/m365/graph/service.go +++ b/src/internal/m365/graph/service.go @@ -360,6 +360,10 @@ func (aw *adapterWrap) Send( break } + if IsErrApplicationThrottled(err) { + return nil, clues.Stack(ErrApplicationThrottled, err).WithTrace(1).WithClues(ictx) + } + if !IsErrConnectionReset(err) && !connectionEnded.Compare(err.Error()) { return nil, clues.Stack(err).WithTrace(1).WithClues(ictx) } diff --git a/src/pkg/errs/errs.go b/src/pkg/errs/errs.go index ce6e9c58a..8d5d38edd 100644 --- a/src/pkg/errs/errs.go +++ b/src/pkg/errs/errs.go @@ -13,20 +13,22 @@ import ( type errEnum string const ( - RepoAlreadyExists errEnum = "repository-already-exists" + ApplicationThrottled errEnum = "application-throttled" BackupNotFound errEnum = "backup-not-found" - ServiceNotEnabled errEnum = "service-not-enabled" + RepoAlreadyExists errEnum = "repository-already-exists" ResourceOwnerNotFound errEnum = "resource-owner-not-found" + ServiceNotEnabled errEnum = "service-not-enabled" ) // map of enums to errors. We might want to re-use an enum for multiple // internal errors (ex: "ServiceNotEnabled" may exist in both graph and // non-graph producers). var internalToExternal = map[errEnum][]error{ - RepoAlreadyExists: {repository.ErrorRepoAlreadyExists}, + ApplicationThrottled: {graph.ErrApplicationThrottled}, BackupNotFound: {repository.ErrorBackupNotFound}, - ServiceNotEnabled: {graph.ErrServiceNotEnabled}, + RepoAlreadyExists: {repository.ErrorRepoAlreadyExists}, ResourceOwnerNotFound: {graph.ErrResourceOwnerNotFound}, + ServiceNotEnabled: {graph.ErrServiceNotEnabled}, } // Internal returns the internal errors which match to the public error category.