Private/Invoke-PatApi.ps1
|
function Invoke-PatApi { <# .SYNOPSIS Invokes the Plex API. .DESCRIPTION Internal function that sends HTTP requests to the Plex API and returns the response. Includes automatic retry with exponential backoff for transient errors such as DNS failures, connection timeouts, and rate limiting (503/429). .PARAMETER Uri The complete URI to call .PARAMETER Method The HTTP method to use (default: Get) .PARAMETER Headers Optional headers to include in the request (default: Accept = application/json) .PARAMETER MaxRetries Maximum number of retry attempts for transient errors (default: 3) .PARAMETER BaseDelaySeconds Base delay in seconds for exponential backoff (default: 1) Actual delays will be: 1s, 2s, 4s for the default value .OUTPUTS PSCustomObject Returns the MediaContainer object from the Plex API response if present, otherwise returns the full response #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string] $Uri, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $Method = 'Get', [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [hashtable] $Headers = @{ Accept = 'application/json' }, [Parameter(Mandatory = $false)] [ValidateRange(1, 10)] [int] $MaxRetries = 3, [Parameter(Mandatory = $false)] [ValidateRange(0, 60)] [int] $BaseDelaySeconds = 1 ) # Warn if using HTTP with authentication token (only once per session to avoid spam) if ($Uri -match '^http://' -and $Headers.ContainsKey('X-Plex-Token')) { if (-not $script:HttpWarningShown) { $script:HttpWarningShown = $true Write-Warning "Sending authentication token over unencrypted HTTP connection. Consider using HTTPS." } } $apiQueryParameters = @{ Method = $Method Uri = $Uri Headers = $Headers ErrorAction = 'Stop' } Write-Debug 'Invoking Plex API with the following parameters:' $apiQueryParameters | Out-String | Write-Debug # Helper function to determine if an error is transient and should be retried function Test-TransientError { param([System.Management.Automation.ErrorRecord]$ErrorRecord) $message = $ErrorRecord.Exception.Message # DNS failures if ($message -match 'No such host|DNS|name.+not.+resolve') { return $true } # Connection/timeout issues if ($message -match 'timed out|timeout|connection.+refused|connection.+reset|unable to connect') { return $true } # Server-side transient errors (rate limiting, service unavailable) if ($message -match '503|429|temporarily unavailable|service unavailable|too many requests') { return $true } return $false } $lastError = $null for ($attempt = 1; $attempt -le $MaxRetries; $attempt++) { try { $response = Invoke-RestMethod @apiQueryParameters # Handle case where response is returned as JSON string (some servers/content-types) # Check for both JSON objects ({) and arrays ([) $trimmedResponse = if ($response -is [string]) { $response.TrimStart() } else { $null } if ($trimmedResponse -and ($trimmedResponse.StartsWith('{') -or $trimmedResponse.StartsWith('['))) { Write-Debug "Response is JSON string, parsing with -AsHashtable..." # Use -AsHashtable to handle Plex API's case-sensitive keys (e.g., "guid" and "Guid") # Then convert back to PSCustomObject for consistent property access patterns $hashtable = $response | ConvertFrom-Json -AsHashtable -Depth 100 $response = ConvertTo-PsCustomObjectFromHashtable -Hashtable $hashtable } if ($response.PSObject.Properties['MediaContainer']) { return $response.MediaContainer } return $response } catch { $lastError = $_ # Check if this is a transient error that should be retried $isTransient = Test-TransientError -ErrorRecord $_ if (-not $isTransient -or $attempt -eq $MaxRetries) { # Non-transient error or final attempt - throw immediately throw "Error invoking Plex API: $($_.Exception.Message)" } # Calculate exponential backoff delay $delay = $BaseDelaySeconds * [Math]::Pow(2, $attempt - 1) Write-Verbose "Transient error on attempt $attempt of $MaxRetries. Retrying in ${delay}s. Error: $($_.Exception.Message)" Start-Sleep -Seconds $delay } } # Should not reach here, but just in case if ($lastError) { throw "Error invoking Plex API: $($lastError.Exception.Message)" } } |