Private/Resolve-LMException.ps1

<#
.SYNOPSIS
    Enhanced function to handle exceptions that occur during LogicMonitor API requests.

.DESCRIPTION
    The Resolve-LMException function provides comprehensive error handling for LogicMonitor API requests,
    including rate limiting, transient errors, authentication issues, and network problems.
    Returns structured information about whether the request should be retried.

.PARAMETER LMException
    The exception object that is thrown during the API request.

.PARAMETER EnableDebugLogging
    Switch to enable detailed debug logging for troubleshooting.

.EXAMPLE
    $result = Resolve-LMException -LMException $exception
    if ($result.ShouldRetry) {
        # Retry logic here
    }

.NOTES
    Returns a structured object with the following properties:
    - ShouldRetry: Boolean indicating if the request should be retried
    - WaitSeconds: Number of seconds to wait before retry (if applicable)
    - ErrorType: Classification of the error (RateLimit, ServerError, etc.)
    - StatusCode: HTTP status code (if available)
    - Message: Human-readable error message

.OUTPUTS
    Returns a PSCustomObject with retry and error information.
#>

function Resolve-LMException {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ErrorRecord]$LMException,

        [Switch]$EnableDebugLogging
    )

    # Initialize return object
    $result = [PSCustomObject]@{
        ShouldRetry = $false
        WaitSeconds = 0
        ErrorType   = 'Unknown'
        StatusCode  = $null
        Message     = ''
    }

    # Debug logging
    if ($EnableDebugLogging) {
        $debugInfo = @{
            Timestamp     = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
            ExceptionType = $LMException.Exception.GetType().FullName
            Message       = $LMException.Exception.Message
        }

        if ($LMException.Exception.Response) {
            $debugInfo.StatusCode = $LMException.Exception.Response.StatusCode.value__
        }

        Write-Debug "Exception Details: $($debugInfo | ConvertTo-Json -Compress)"
    }

    # Check if we have a response object (HTTP error vs network error)
    if (-not $LMException.Exception.Response) {
        # Network-level error (timeout, DNS resolution, connection refused, etc.)
        $result.ShouldRetry = $true
        $result.WaitSeconds = 0  # Let caller handle backoff
        $result.ErrorType = 'NetworkError'
        $result.Message = $LMException.Exception.Message

        if ($EnableDebugLogging) {
            Write-Debug "Network error detected: $($result.Message)"
        }

        return $result
    }

    # HTTP response available - check status code
    $statusCode = $LMException.Exception.Response.StatusCode.value__
    $result.StatusCode = $statusCode

    switch ($statusCode) {
        429 {
            # Rate Limit Exceeded
            $result.ErrorType = 'RateLimit'
            $result.ShouldRetry = $true

            try {
                $headers = $LMException.Exception.Response.Headers
                $rateLimitWindow = if ($headers['x-rate-limit-window']) { [int]$headers['x-rate-limit-window'] } else { 60 }
                $rateLimitSize = $headers['x-rate-limit-limit']

                $result.WaitSeconds = $rateLimitWindow
                $result.Message = "Rate limit exceeded ($rateLimitSize requests per $rateLimitWindow seconds)"

                Write-Warning "$($result.Message). Waiting $rateLimitWindow seconds before retry..."
                Start-Sleep -Seconds $rateLimitWindow

                if ($EnableDebugLogging) {
                    Write-Debug "Rate limit handled - waited $rateLimitWindow seconds"
                }
            }
            catch {
                # Fallback if headers are missing or malformed
                $result.WaitSeconds = 60
                $result.Message = "Rate limit exceeded (using default 60s wait)"

                Write-Warning "Rate limit detected but unable to parse headers. Waiting 60 seconds..."
                Start-Sleep -Seconds 60

                if ($EnableDebugLogging) {
                    Write-Debug "Rate limit fallback - waited 60 seconds"
                }
            }

            return $result
        }

        { $_ -in @(502, 503, 504) } {
            # Bad Gateway, Service Unavailable, Gateway Timeout
            $result.ShouldRetry = $true
            $result.WaitSeconds = 0  # Let caller handle exponential backoff
            $result.ErrorType = 'ServerError'
            $result.Message = "Server temporarily unavailable (HTTP $statusCode)"

            if ($EnableDebugLogging) {
                Write-Debug "Transient server error detected: $($result.Message)"
            }

            return $result
        }

        { $_ -ge 500 } {
            # Other server errors (500, 505, etc.)
            $result.ShouldRetry = $false
            $result.ErrorType = 'ServerError'
            $result.Message = "Server error (HTTP $statusCode)"

            # Try to get more specific error message
            $errorMessage = Get-LMExceptionMessage -LMException $LMException
            if ($errorMessage) {
                $result.Message = $errorMessage
            }

            # Error writing is now handled in Invoke-LMRestMethod
            return $result
        }

        401 {
            # Unauthorized
            $result.ShouldRetry = $false
            $result.ErrorType = 'AuthenticationError'
            $result.Message = 'Authentication failed. Please check your API credentials and try connecting again.'

            # Error writing is now handled in Invoke-LMRestMethod
            return $result
        }

        403 {
            # Forbidden
            $result.ShouldRetry = $false
            $result.ErrorType = 'AuthorizationError'
            $result.Message = 'Access denied. Your API credentials do not have sufficient permissions for this operation.'

            # Error writing is now handled in Invoke-LMRestMethod
            return $result
        }

        404 {
            # Not Found - for GET operations, this might be expected behavior
            $result.ShouldRetry = $false
            $result.ErrorType = 'NotFound'

            # Try to get specific error message from response
            $errorMessage = Get-LMExceptionMessage -LMException $LMException
            $result.Message = if ($errorMessage) { $errorMessage } else { "Resource not found (HTTP 404)" }

            # Error writing is now handled in Invoke-LMRestMethod
            return $result
        }

        { $_ -in @(400, 405, 409, 422) } {
            # Bad Request, Method Not Allowed, Conflict, Unprocessable Entity
            $result.ShouldRetry = $false
            $result.ErrorType = 'ClientError'

            # Try to get specific error message from response
            $errorMessage = Get-LMExceptionMessage -LMException $LMException
            $result.Message = if ($errorMessage) { $errorMessage } else { "Client error (HTTP $statusCode)" }

            # Error writing is now handled in Invoke-LMRestMethod
            return $result
        }

        default {
            # Unexpected status code
            $result.ShouldRetry = $false
            $result.ErrorType = 'UnknownError'

            $errorMessage = Get-LMExceptionMessage -LMException $LMException
            $result.Message = if ($errorMessage) { $errorMessage } else { "Unexpected error (HTTP $statusCode)" }

            # Error writing is now handled in Invoke-LMRestMethod
            return $result
        }
    }
}

# Helper function to extract error messages from LogicMonitor API responses
function Get-LMExceptionMessage {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorRecord]$LMException
    )

    try {
        if ($LMException.ErrorDetails.Message) {
            $errorDetails = $LMException.ErrorDetails.Message | ConvertFrom-Json -ErrorAction Stop
            $errorMessage = if ($errorDetails.errorMessage) { 
                $errorDetails.errorMessage 
            }
            elseif ($errorDetails.message) { 
                $errorDetails.message 
            }
            elseif ($errorDetails.error) { 
                $errorDetails.error 
            }
            else { 
                $null 
            }

            if ($errorMessage) {
                # Sanitize error message to remove potential sensitive data
                $sanitized = $errorMessage -replace '(?i)(key|token|password|secret|credential)=[^&\s]+', '$1=***REDACTED***'
                return $sanitized
            }
        }
    }
    catch {
        # JSON parsing failed, try to use raw message
        if ($LMException.ErrorDetails.Message) {
            $sanitized = $LMException.ErrorDetails.Message -replace '(?i)(key|token|password|secret|credential)=[^&\s]+', '$1=***REDACTED***'
            return $sanitized
        }
    }

    # Final fallback to exception message
    if ($LMException.Exception.Message) {
        $sanitized = $LMException.Exception.Message -replace '(?i)(key|token|password|secret|credential)=[^&\s]+', '$1=***REDACTED***'
        return $sanitized
    }

    return $null
}