In this blog post, I will show you how you can trap a specific type of error when making a graph request using the Invoke-RestMethod commandlet and pause and retry the request x number of times before quitting. This is very helpful and is actually recommended to do error handling when making requests to Microsoft Graph.

The key here is to set the Url that you’re going to be calling ( along with any body or header values ) first and then enter a ‘While’ loop. This will allow you to retry the same Url when an error happens as well as handle paging in Microsoft Graph. When no error occurs or when you want to fall out of the loop, you can set the url to null or set it to a new url when paging through the results. For more information about paging in Microsoft Graph, see this document.

This example simply outputs the JSON response from Microsoft Graph to a file and is performing the client_credentials grant flow and querying the ‘Users’ endpoint. You will need an app registration with a client_secret and the application permission for Microsoft.Graph user.read.all. You can actually use any Microsoft Graph endpoint you like, just make sure you have the proper application permission consented to.

There is a method here to acquire an access token, which would be good in scenarios where you need to get a new access token such as a long running request with many pages, a function to actually make the request and then the setup for making the request and calling the method is at the very bottom. I am not using any authentication or graph libraries.

Function Get-AccessTokenCC
 
{
    $clientSecret = ''
    $clientId = ''
    $tenantId = ''
    # Construct URI
    $uri = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
    # Construct Body
    $body = @{
        client_id = $clientId
        client_secret = $clientSecret
        scope = 'https://graph.microsoft.com/.default'
        grant_type = 'client_credentials'
    }
    # Get OAuth 2.0 Token
    $tokenRequest = Invoke-WebRequest -Method Post -Uri $uri -ContentType 'application/x-www-form-urlencoded' -Body $body -UseBasicParsing
    # Access Token
    $token = ($tokenRequest.Content | ConvertFrom-Json).access_token
    #$token = "Junk"  #uncomment this line to cause a 401 error -- you can set that status in the error handler to test the pause and retry
    #Write-Host "access_token = $token"
    return $token
}
 
Function Get-GraphQueryOutput ($Uri)
{
    write-host "uri = $Uri"
    write-host "token = $token"

    $retryCount = 0
    $maxRetries = 3
    $pauseDuration = 2
 
    $allRecords = @()
    while ($Uri -ne $null){
        Write-Host $Uri
        try {
            # todo: verify that the bearer token is still good -- hasn't expired yet -- if it has, then get a new token before making the request
 
            $query = Invoke-RestMethod -Method Get -Uri $Uri -ContentType 'application/json' -Headers @{Authorization = "Bearer $token"}
 
            $allRecords += $query.value
 
            if($query.'@odata.nextLink'){
                # set the url to get the next page of records
                $Uri = $query.'@odata.nextLink'
            } else {
                $Uri = $null
            }
 
        } catch {
            Write-Host "StatusCode: " $_.Exception.Response.StatusCode.value__
            Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
 
            if($_.ErrorDetails.Message){
                Write-Host "Inner Error: $_.ErrorDetails.Message"
            }
 
            # check for a specific error so that we can retry the request otherwise, set the url to null so that we fall out of the loop
            if($_.Exception.Response.StatusCode.value__ -eq 403 ){
                # just ignore, leave the url the same to retry but pause first
                if($retryCount -ge $maxRetries){
                    # not going to retry again
                    $Uri = $null
                    Write-Host 'Not going to retry...'
                } else {
                    $retryCount += 1
                    Write-Host "Retry attempt $retryCount after a $pauseDuration second pause..."
                    Start-Sleep -Seconds $pauseDuration
                }
 
            } else {
                # not going to retry -- set the url to null to fall back out of the while loop
                $Uri = $null
            }
        }
    }
 
    $output = $allRecords | ConvertTo-Json
    return $output
}
 
# Graph API URIs
$graph_base_uri = "https://graph.microsoft.com/beta/"
 
$users_endpoint="users"
$users_filter="`$filter=userType eq 'Guest'"
 
$users_select="`$select=id,userType,userPrincipalName,signInActivity&`$top=1"
 
$users_uri=$graph_base_uri+$users_endpoint+"?"+$users_filter+"&"+$users_select
 
# Pull Data
$token = Get-AccessTokenCC
$users_results = Get-GraphQueryOutput -Uri $users_uri

# Save output
$users_results | Out-File 'c:\\temp\\users.json'
 

If you need to capture a specific header value in the error response, such as the retry-after value with a 429 throttling response ( too many requests ) you can get that value like this: $retryAfter = $_.Exception.Response.Headers[“Retry-After”]

In conclusion, this example will show you one way of handling errors in PowerShell and I do recommend that you perform error handling when working with Microsoft Graph. This will make your script more resilient as there are many moving parts and it isn’t uncommon to have the occasional error occur along the way. It could be anything from throttling from Microsoft graph or a work-load, a networking error, load balancer issue, gateway, or the graph endpoint itself. Add the error handling, decide how many times to retry the request before giving up and add a second or 2 for a pause ( unless handling the 429 too many requests error, follow this guidance for pausing ) before retrying to make your script resilient.

One Thought to “Retry Invoke-RestMethod requests in PowerShell when an error occurs”

  1. LEE

    Excellent writeup

Leave a Comment