better parsing of graph odata errs (#4900)

parse the odataerr to a local struct that has
a more complete view of the data.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🧹 Tech Debt/Cleanup

#### Issue(s)

* #4685

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-12-21 14:39:31 -07:00 committed by GitHub
parent c65527e5c4
commit 8c0ac505a2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 233 additions and 210 deletions

View File

@ -8,6 +8,7 @@ import (
"os"
"strings"
"syscall"
"time"
"github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors"
@ -141,15 +142,15 @@ var (
func IsErrApplicationThrottled(err error) bool {
return errors.Is(err, ErrApplicationThrottled) ||
hasErrorCode(err, applicationThrottled)
parseODataErr(err).hasErrorCode(err, applicationThrottled)
}
func IsErrAuthenticationError(err error) bool {
return hasErrorCode(err, AuthenticationError)
return parseODataErr(err).hasErrorCode(err, AuthenticationError)
}
func IsErrInsufficientAuthorization(err error) bool {
return hasErrorCode(err, AuthorizationRequestDenied)
return parseODataErr(err).hasErrorCode(err, AuthorizationRequestDenied)
}
func IsErrDeletedInFlight(err error) bool {
@ -157,7 +158,7 @@ func IsErrDeletedInFlight(err error) bool {
return true
}
if hasErrorCode(
if parseODataErr(err).hasErrorCode(
err,
errorItemNotFound,
itemNotFound,
@ -169,56 +170,51 @@ func IsErrDeletedInFlight(err error) bool {
}
func IsErrItemNotFound(err error) bool {
return hasErrorCode(err, itemNotFound, errorItemNotFound)
return parseODataErr(err).hasErrorCode(err, itemNotFound, errorItemNotFound)
}
func IsErrInvalidDelta(err error) bool {
return hasErrorCode(err, syncStateNotFound, resyncRequired, syncStateInvalid)
return parseODataErr(err).hasErrorCode(err, syncStateNotFound, resyncRequired, syncStateInvalid)
}
func IsErrDeltaNotSupported(err error) bool {
return hasErrorMessage(err, ParameterDeltaTokenNotSupported)
return parseODataErr(err).hasErrorMessage(err, ParameterDeltaTokenNotSupported)
}
func IsErrQuotaExceeded(err error) bool {
return hasErrorCode(err, QuotaExceeded)
return parseODataErr(err).hasErrorCode(err, QuotaExceeded)
}
func IsErrExchangeMailFolderNotFound(err error) bool {
// Not sure if we can actually see a resourceNotFound error here. I've only
// seen the latter two.
return hasErrorCode(err, ResourceNotFound, errorItemNotFound, MailboxNotEnabledForRESTAPI)
return parseODataErr(err).hasErrorCode(err, ResourceNotFound, errorItemNotFound, MailboxNotEnabledForRESTAPI)
}
func IsErrUserNotFound(err error) bool {
if hasErrorCode(err, RequestResourceNotFound, invalidUser) {
ode := parseODataErr(err)
if ode.hasErrorCode(err, RequestResourceNotFound, invalidUser) {
return true
}
if hasErrorCode(err, ResourceNotFound) {
var odErr odataerrors.ODataErrorable
if !errors.As(err, &odErr) {
return false
}
mainMsg, _, _ := errData(odErr)
return strings.Contains(strings.ToLower(mainMsg), "user")
if ode.hasErrorCode(err, ResourceNotFound) {
return strings.Contains(strings.ToLower(ode.Main.Message), "user")
}
return false
}
func IsErrInvalidRecipients(err error) bool {
return hasErrorCode(err, ErrorInvalidRecipients)
return parseODataErr(err).hasErrorCode(err, ErrorInvalidRecipients)
}
func IsErrCannotOpenFileAttachment(err error) bool {
return hasErrorCode(err, cannotOpenFileAttachment)
return parseODataErr(err).hasErrorCode(err, cannotOpenFileAttachment)
}
func IsErrAccessDenied(err error) bool {
return hasErrorCode(err, ErrorAccessDenied) ||
return parseODataErr(err).hasErrorCode(err, ErrorAccessDenied) ||
clues.HasLabel(err, LabelStatus(http.StatusForbidden))
}
@ -240,17 +236,17 @@ func IsErrConnectionReset(err error) bool {
func IsErrUnauthorizedOrBadToken(err error) bool {
return clues.HasLabel(err, LabelStatus(http.StatusUnauthorized)) ||
hasErrorCode(err, invalidAuthenticationToken) ||
parseODataErr(err).hasErrorCode(err, invalidAuthenticationToken) ||
errors.Is(err, ErrTokenExpired)
}
func IsErrBadJWTToken(err error) bool {
return hasErrorCode(err, invalidAuthenticationToken)
return parseODataErr(err).hasErrorCode(err, invalidAuthenticationToken)
}
func IsErrItemAlreadyExistsConflict(err error) bool {
return errors.Is(err, ErrItemAlreadyExistsConflict) ||
hasErrorCode(err, nameAlreadyExists)
parseODataErr(err).hasErrorCode(err, nameAlreadyExists)
}
// LabelStatus transforms the provided statusCode into
@ -262,7 +258,7 @@ func LabelStatus(statusCode int) string {
// IsMalware is true if the graphAPI returns a "malware detected" error code.
func IsMalware(err error) bool {
return hasErrorCode(err, malwareDetected)
return parseODataErr(err).hasErrorCode(err, malwareDetected)
}
func IsMalwareResp(ctx context.Context, resp *http.Response) bool {
@ -274,22 +270,26 @@ func IsMalwareResp(ctx context.Context, resp *http.Response) bool {
}
func IsErrFolderExists(err error) bool {
return hasErrorCode(err, folderExists)
return parseODataErr(err).hasErrorCode(err, folderExists)
}
func IsErrUsersCannotBeResolved(err error) bool {
return hasErrorCode(err, noResolvedUsers) || hasErrorMessage(err, usersCannotBeResolved)
ode := parseODataErr(err)
return ode.hasErrorCode(err, noResolvedUsers) || ode.hasErrorMessage(err, usersCannotBeResolved)
}
func IsErrSiteNotFound(err error) bool {
return hasErrorMessage(err, requestedSiteCouldNotBeFound)
return parseODataErr(err).hasErrorMessage(err, requestedSiteCouldNotBeFound)
}
func IsErrResourceLocked(err error) bool {
ode := parseODataErr(err)
return errors.Is(err, ErrResourceLocked) ||
hasInnerErrorCode(err, ResourceLocked) ||
hasErrorCode(err, NotAllowed) ||
errMessageMatchesAllFilters(
ode.hasInnerErrorCode(err, ResourceLocked) ||
ode.hasErrorCode(err, NotAllowed) ||
ode.errMessageMatchesAllFilters(
err,
filters.In([]string{"the service principal for resource"}),
filters.In([]string{"this indicate that a subscription within the tenant has lapsed"}),
@ -297,105 +297,7 @@ func IsErrResourceLocked(err error) bool {
}
func IsErrSharingDisabled(err error) bool {
return hasInnerErrorCode(err, sharingDisabled)
}
// ---------------------------------------------------------------------------
// error parsers
// ---------------------------------------------------------------------------
func hasErrorCode(err error, codes ...errorCode) bool {
if err == nil {
return false
}
var oDataError odataerrors.ODataErrorable
if !errors.As(err, &oDataError) {
return false
}
code, ok := ptr.ValOK(oDataError.GetErrorEscaped().GetCode())
if !ok {
return false
}
cs := make([]string, len(codes))
for i, c := range codes {
cs[i] = string(c)
}
return filters.Equal(cs).Compare(code)
}
func hasInnerErrorCode(err error, codes ...errorCode) bool {
if err == nil {
return false
}
var oDataError odataerrors.ODataErrorable
if !errors.As(err, &oDataError) {
return false
}
inner := oDataError.GetErrorEscaped().GetInnerError()
if inner == nil {
return false
}
code, err := str.AnyValueToString("code", inner.GetAdditionalData())
if err != nil {
return false
}
cs := make([]string, len(codes))
for i, c := range codes {
cs[i] = string(c)
}
return filters.Equal(cs).Compare(code)
}
// only use this as a last resort. Prefer the code or statuscode if possible.
func hasErrorMessage(err error, msgs ...errorMessage) bool {
if err == nil {
return false
}
var oDataError odataerrors.ODataErrorable
if !errors.As(err, &oDataError) {
return false
}
msg, ok := ptr.ValOK(oDataError.GetErrorEscaped().GetMessage())
if !ok {
return false
}
cs := make([]string, len(msgs))
for i, c := range msgs {
cs[i] = string(c)
}
return filters.In(cs).Compare(msg)
}
// only use this as a last resort. Prefer the code or statuscode if possible.
func errMessageMatchesAllFilters(err error, fs ...filters.Filter) bool {
if err == nil {
return false
}
var oDataError odataerrors.ODataErrorable
if !errors.As(err, &oDataError) {
return false
}
msg, ok := ptr.ValOK(oDataError.GetErrorEscaped().GetMessage())
if !ok {
return false
}
return filters.Must(msg, fs...)
return parseODataErr(err).hasInnerErrorCode(err, sharingDisabled)
}
// Wrap is a helper function that extracts ODataError metadata from
@ -405,20 +307,20 @@ func Wrap(ctx context.Context, e error, msg string) *clues.Err {
return nil
}
var oDataError odataerrors.ODataErrorable
if !errors.As(e, &oDataError) {
return clues.WrapWC(ctx, e, msg).WithTrace(1)
ode := parseODataErr(e)
if !ode.isODataErr {
return clues.StackWC(ctx, e).WithTrace(1)
}
mainMsg, data, innerMsg := errData(oDataError)
if len(mainMsg) > 0 {
e = clues.Stack(e, clues.New(mainMsg))
if len(ode.Main.Message) > 0 {
e = clues.Stack(e, clues.New(ode.Main.Message))
}
ce := clues.WrapWC(ctx, e, msg).With(data...).WithTrace(1)
ce := clues.WrapWC(ctx, e, msg).
With("graph_api_err", ode).
WithTrace(1)
return setLabels(ce, innerMsg)
return setLabels(ce, ode)
}
// Stack is a helper function that extracts ODataError metadata from
@ -428,20 +330,20 @@ func Stack(ctx context.Context, e error) *clues.Err {
return nil
}
var oDataError *odataerrors.ODataError
if !errors.As(e, &oDataError) {
ode := parseODataErr(e)
if !ode.isODataErr {
return clues.StackWC(ctx, e).WithTrace(1)
}
mainMsg, data, innerMsg := errData(oDataError)
if len(mainMsg) > 0 {
e = clues.Stack(e, clues.New(mainMsg))
if len(ode.Main.Message) > 0 {
e = clues.Stack(e, clues.New(ode.Main.Message))
}
ce := clues.StackWC(ctx, e).With(data...).WithTrace(1)
ce := clues.StackWC(ctx, e).
With("graph_api_err", ode).
WithTrace(1)
return setLabels(ce, innerMsg)
return setLabels(ce, ode)
}
// stackReq is a helper function that extracts ODataError metadata from
@ -467,12 +369,12 @@ func stackReq(
// Checks for the following conditions and labels the error accordingly:
// * mysiteNotFound | mysiteURLNotFound
// * malware
func setLabels(err *clues.Err, msg string) *clues.Err {
func setLabels(err *clues.Err, ode oDataErr) *clues.Err {
if err == nil {
return nil
}
f := filters.Contains([]string{msg})
f := filters.Contains([]string{ode.Main.Message + ode.Main.Code})
if f.Compare(string(MysiteNotFound)) ||
f.Compare(string(MysiteURLNotFound)) {
@ -490,35 +392,6 @@ func setLabels(err *clues.Err, msg string) *clues.Err {
return err
}
func errData(err odataerrors.ODataErrorable) (string, []any, string) {
data := make([]any, 0)
// Get MainError
mainErr := err.GetErrorEscaped()
mainMsg := ptr.Val(mainErr.GetMessage())
data = appendIf(data, "odataerror_code", mainErr.GetCode())
data = appendIf(data, "odataerror_message", mainErr.GetMessage())
data = appendIf(data, "odataerror_target", mainErr.GetTarget())
msgConcat := ptr.Val(mainErr.GetMessage()) + ptr.Val(mainErr.GetCode())
for i, d := range mainErr.GetDetails() {
pfx := fmt.Sprintf("odataerror_details_%d_", i)
data = appendIf(data, pfx+"code", d.GetCode())
data = appendIf(data, pfx+"message", d.GetMessage())
data = appendIf(data, pfx+"target", d.GetTarget())
msgConcat += ptr.Val(d.GetMessage())
}
inner := mainErr.GetInnerError()
if inner != nil {
data = appendIf(data, "odataerror_inner_cli_req_id", inner.GetClientRequestId())
data = appendIf(data, "odataerror_inner_req_id", inner.GetRequestId())
}
return mainMsg, data, strings.ToLower(msgConcat)
}
func reqData(req *http.Request) map[string]any {
if req == nil {
return nil
@ -547,14 +420,6 @@ func respData(resp *http.Response) map[string]any {
return r
}
func appendIf(a []any, k string, v *string) []any {
if v == nil {
return a
}
return append(a, k, *v)
}
// ItemInfo gathers potentially useful information about a drive item,
// and aggregates that data into a map.
func ItemInfo(item *custom.DriveItem) map[string]any {
@ -593,6 +458,162 @@ func ItemInfo(item *custom.DriveItem) map[string]any {
return m
}
// ---------------------------------------------------------------------------
// error parsers
// ---------------------------------------------------------------------------
type oDataErr struct {
isODataErr bool
Details []oDataDeets `json:"details,omitempty"`
Inner innerErr `json:"inner,omitempty"`
Main mainErr `json:"main,omitempty"`
Resp apiResp `json:"resp,omitempty"`
}
type oDataDeets struct {
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Target string `json:"target,omitempty"`
}
type innerErr struct {
ClientRequestID string `json:"clientRequestID,omitempty"`
Code string `json:"code,omitempty"`
Date time.Time `json:"date,omitempty"`
OdataType string `json:"odataType,omitempty"`
RequestID string `json:"requestID,omitempty"`
}
type mainErr struct {
Code string `json:"code,omitempty"`
Message string `json:"message,omitempty"`
Target string `json:"target,omitempty"`
}
type apiResp struct {
Message string `json:"message,omitempty"`
StatusCode int `json:"statusCode,omitempty"`
}
// parses the odataerror.ODataError interface to a local
// struct for easy logging/usage.
func parseODataErr(err error) oDataErr {
if err == nil {
return oDataErr{}
}
var (
ode oDataErr
graphODE *odataerrors.ODataError
)
if !errors.As(err, &graphODE) {
return oDataErr{}
}
ode = oDataErr{
isODataErr: true,
Resp: apiResp{
Message: strings.Clone(graphODE.ApiError.Message),
StatusCode: graphODE.ApiError.ResponseStatusCode,
},
}
var (
main = graphODE.GetErrorEscaped()
details []odataerrors.ErrorDetailsable
inner odataerrors.InnerErrorable
)
if main != nil {
ode.Main = mainErr{
Code: strings.Clone(ptr.Val(main.GetCode())),
Message: strings.Clone(ptr.Val(main.GetMessage())),
Target: strings.Clone(ptr.Val(main.GetTarget())),
}
inner = main.GetInnerError()
details = main.GetDetails()
}
if inner != nil {
ode.Inner = innerErr{
ClientRequestID: strings.Clone(ptr.Val(inner.GetClientRequestId())),
Date: ptr.Val(inner.GetDate()),
OdataType: strings.Clone(ptr.Val(inner.GetOdataType())),
RequestID: strings.Clone(ptr.Val(inner.GetRequestId())),
}
code, err := str.AnyValueToString("code", inner.GetAdditionalData())
if err == nil {
ode.Inner.Code = code
}
}
ode.Details = make([]oDataDeets, 0, len(details))
for _, deet := range details {
d := oDataDeets{
Code: strings.Clone(ptr.Val(deet.GetCode())),
Message: strings.Clone(ptr.Val(deet.GetMessage())),
Target: strings.Clone(ptr.Val(deet.GetTarget())),
}
ode.Details = append(ode.Details, d)
}
return ode
}
func (ode oDataErr) hasErrorCode(err error, codes ...errorCode) bool {
if !ode.isODataErr {
return false
}
cs := make([]string, len(codes))
for i, c := range codes {
cs[i] = string(c)
}
return filters.Equal(cs).Compare(ode.Main.Code)
}
func (ode oDataErr) hasInnerErrorCode(err error, codes ...errorCode) bool {
if !ode.isODataErr {
return false
}
cs := make([]string, len(codes))
for i, c := range codes {
cs[i] = string(c)
}
return filters.Equal(cs).Compare(ode.Inner.Code)
}
// only use this as a last resort. Prefer the code or statuscode if possible.
func (ode oDataErr) hasErrorMessage(err error, msgs ...errorMessage) bool {
if !ode.isODataErr {
return false
}
cs := make([]string, len(msgs))
for i, c := range msgs {
cs[i] = string(c)
}
return filters.In(cs).Compare(ode.Main.Message)
}
// only use this as a last resort. Prefer the code or statuscode if possible.
func (ode oDataErr) errMessageMatchesAllFilters(err error, fs ...filters.Filter) bool {
if !ode.isODataErr {
return false
}
return filters.Must(ode.Main.Message, fs...)
}
// ---------------------------------------------------------------------------
// other helpers
// ---------------------------------------------------------------------------

View File

@ -9,7 +9,6 @@ import (
"github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
@ -876,16 +875,6 @@ func (suite *GraphErrorsUnitSuite) TestIsErrItemNotFound() {
}
func (suite *GraphErrorsUnitSuite) TestIsErrResourceLocked() {
innerMatch := graphTD.ODataErr("not-match")
merr := odataerrors.NewMainError()
inerr := odataerrors.NewInnerError()
inerr.SetAdditionalData(map[string]any{
"code": string(ResourceLocked),
})
merr.SetInnerError(inerr)
merr.SetCode(ptr.To("not-match"))
innerMatch.SetErrorEscaped(merr)
table := []struct {
name string
err error
@ -913,7 +902,7 @@ func (suite *GraphErrorsUnitSuite) TestIsErrResourceLocked() {
},
{
name: "matching oDataErr inner code",
err: innerMatch,
err: graphTD.ODataInner(string(ResourceLocked)),
expect: assert.True,
},
{
@ -941,16 +930,6 @@ func (suite *GraphErrorsUnitSuite) TestIsErrResourceLocked() {
}
func (suite *GraphErrorsUnitSuite) TestIsErrSharingDisabled() {
innerMatch := graphTD.ODataErr("not-match")
merr := odataerrors.NewMainError()
inerr := odataerrors.NewInnerError()
inerr.SetAdditionalData(map[string]any{
"code": string(sharingDisabled),
})
merr.SetInnerError(inerr)
merr.SetCode(ptr.To("not-match"))
innerMatch.SetErrorEscaped(merr)
table := []struct {
name string
err error
@ -973,7 +952,7 @@ func (suite *GraphErrorsUnitSuite) TestIsErrSharingDisabled() {
},
{
name: "matching oDataErr inner code",
err: innerMatch,
err: graphTD.ODataInner(string(sharingDisabled)),
expect: assert.True,
},
}

View File

@ -6,6 +6,8 @@ import (
"io"
"testing"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/google/uuid"
"github.com/microsoft/kiota-abstractions-go/serialization"
kjson "github.com/microsoft/kiota-serialization-json-go"
"github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors"
@ -33,6 +35,27 @@ func ODataErrWithMsg(code, message string) *odataerrors.ODataError {
return odErr
}
func ODataInner(innerCode string) *odataerrors.ODataError {
odErr := odataerrors.NewODataError()
inerr := odataerrors.NewInnerError()
merr := odataerrors.NewMainError()
inerr.SetAdditionalData(map[string]any{
"code": innerCode,
})
inerr.SetClientRequestId(ptr.To(uuid.NewString()))
inerr.SetOdataType(ptr.To("@odata.type"))
inerr.SetRequestId(ptr.To("req-id"))
merr.SetInnerError(inerr)
merr.SetCode(ptr.To("main code"))
merr.SetMessage(ptr.To("main message"))
odErr.SetErrorEscaped(merr)
return odErr
}
func ParseableToMap(t *testing.T, thing serialization.Parsable) map[string]any {
sw := kjson.NewJsonSerializationWriter()