diff --git a/Core/Helpers/ExecuteQueryRetryHelper.cs b/Core/Helpers/ExecuteQueryRetryHelper.cs index 332924e..4b349be 100644 --- a/Core/Helpers/ExecuteQueryRetryHelper.cs +++ b/Core/Helpers/ExecuteQueryRetryHelper.cs @@ -84,10 +84,29 @@ public static class ExecuteQueryRetryHelper return new InvalidOperationException(enriched, ex); } - private static bool IsAccessDenied(Exception ex) => - ex is ServerUnauthorizedAccessException || - (ex is ServerException se && - string.Equals(se.ServerErrorTypeName, "System.UnauthorizedAccessException", StringComparison.Ordinal)); + // Access-denied reaches us in two shapes: a typed CSOM ServerException + // (ServerErrorTypeName = System.UnauthorizedAccessException), and — notably on + // Microsoft 365 Group / Teams-connected sites — a bare HTTP (403) FORBIDDEN + // WebException carrying "Access is denied ... 0x80070005 (E_ACCESSDENIED)". + // Both are ownership issues elevation can fix, so classify either as access-denied. + private static bool IsAccessDenied(Exception ex) + { + for (Exception? cur = ex; cur is not null; cur = cur.InnerException) + { + if (cur is ServerUnauthorizedAccessException) + return true; + if (cur is ServerException se && + string.Equals(se.ServerErrorTypeName, "System.UnauthorizedAccessException", StringComparison.Ordinal)) + return true; + if (cur is WebException we && we.Response is HttpWebResponse resp && + resp.StatusCode == HttpStatusCode.Forbidden) + return true; + if (cur.Message.Contains("0x80070005", StringComparison.OrdinalIgnoreCase) || + cur.Message.Contains("E_ACCESSDENIED", StringComparison.OrdinalIgnoreCase)) + return true; + } + return false; + } internal static bool IsThrottleException(Exception ex) {