@@ -5,6 +5,8 @@ function Invoke-PatApi {
55
66 . DESCRIPTION
77 Internal function that sends HTTP requests to the Plex API and returns the response.
8+ Includes automatic retry with exponential backoff for transient errors such as
9+ DNS failures, connection timeouts, and rate limiting (503/429).
810
911 . PARAMETER Uri
1012 The complete URI to call
@@ -15,6 +17,13 @@ function Invoke-PatApi {
1517 . PARAMETER Headers
1618 Optional headers to include in the request (default: Accept = application/json)
1719
20+ . PARAMETER MaxRetries
21+ Maximum number of retry attempts for transient errors (default: 3)
22+
23+ . PARAMETER BaseDelaySeconds
24+ Base delay in seconds for exponential backoff (default: 1)
25+ Actual delays will be: 1s, 2s, 4s for the default value
26+
1827 . OUTPUTS
1928 PSCustomObject
2029 Returns the MediaContainer object from the Plex API response if present, otherwise returns the full response
@@ -36,7 +45,17 @@ function Invoke-PatApi {
3645 [hashtable ]
3746 $Headers = @ {
3847 Accept = ' application/json'
39- }
48+ },
49+
50+ [Parameter (Mandatory = $false )]
51+ [ValidateRange (1 , 10 )]
52+ [int ]
53+ $MaxRetries = 3 ,
54+
55+ [Parameter (Mandatory = $false )]
56+ [ValidateRange (0 , 60 )]
57+ [int ]
58+ $BaseDelaySeconds = 1
4059 )
4160
4261 # Warn if using HTTP with authentication token
@@ -53,26 +72,72 @@ function Invoke-PatApi {
5372 Write-Debug ' Invoking Plex API with the following parameters:'
5473 $apiQueryParameters | Out-String | Write-Debug
5574
56- try {
57- $response = Invoke-RestMethod @apiQueryParameters
58-
59- # Handle case where response is returned as JSON string (some servers/content-types)
60- # Check for both JSON objects ({) and arrays ([)
61- $trimmedResponse = if ($response -is [string ]) { $response.TrimStart () } else { $null }
62- if ($trimmedResponse -and ($trimmedResponse.StartsWith (' {' ) -or $trimmedResponse.StartsWith (' [' ))) {
63- Write-Debug " Response is JSON string, parsing with -AsHashtable..."
64- # Use -AsHashtable to handle Plex API's case-sensitive keys (e.g., "guid" and "Guid")
65- # Then convert back to PSCustomObject for consistent property access patterns
66- $hashtable = $response | ConvertFrom-Json - AsHashtable - Depth 100
67- $response = ConvertTo-PsCustomObjectFromHashtable - Hashtable $hashtable
75+ # Helper function to determine if an error is transient and should be retried
76+ function Test-TransientError {
77+ param ([System.Management.Automation.ErrorRecord ]$ErrorRecord )
78+
79+ $message = $ErrorRecord.Exception.Message
80+
81+ # DNS failures
82+ if ($message -match ' No such host|DNS|name.+not.+resolve' ) {
83+ return $true
6884 }
6985
70- if ($response.PSObject.Properties [' MediaContainer' ]) {
71- return $response.MediaContainer
86+ # Connection/timeout issues
87+ if ($message -match ' timed out|timeout|connection.+refused|connection.+reset|unable to connect' ) {
88+ return $true
7289 }
73- return $response
90+
91+ # Server-side transient errors (rate limiting, service unavailable)
92+ if ($message -match ' 503|429|temporarily unavailable|service unavailable|too many requests' ) {
93+ return $true
94+ }
95+
96+ return $false
7497 }
75- catch {
76- throw " Error invoking Plex API: $ ( $_.Exception.Message ) "
98+
99+ $lastError = $null
100+
101+ for ($attempt = 1 ; $attempt -le $MaxRetries ; $attempt ++ ) {
102+ try {
103+ $response = Invoke-RestMethod @apiQueryParameters
104+
105+ # Handle case where response is returned as JSON string (some servers/content-types)
106+ # Check for both JSON objects ({) and arrays ([)
107+ $trimmedResponse = if ($response -is [string ]) { $response.TrimStart () } else { $null }
108+ if ($trimmedResponse -and ($trimmedResponse.StartsWith (' {' ) -or $trimmedResponse.StartsWith (' [' ))) {
109+ Write-Debug " Response is JSON string, parsing with -AsHashtable..."
110+ # Use -AsHashtable to handle Plex API's case-sensitive keys (e.g., "guid" and "Guid")
111+ # Then convert back to PSCustomObject for consistent property access patterns
112+ $hashtable = $response | ConvertFrom-Json - AsHashtable - Depth 100
113+ $response = ConvertTo-PsCustomObjectFromHashtable - Hashtable $hashtable
114+ }
115+
116+ if ($response.PSObject.Properties [' MediaContainer' ]) {
117+ return $response.MediaContainer
118+ }
119+ return $response
120+ }
121+ catch {
122+ $lastError = $_
123+
124+ # Check if this is a transient error that should be retried
125+ $isTransient = Test-TransientError - ErrorRecord $_
126+
127+ if (-not $isTransient -or $attempt -eq $MaxRetries ) {
128+ # Non-transient error or final attempt - throw immediately
129+ throw " Error invoking Plex API: $ ( $_.Exception.Message ) "
130+ }
131+
132+ # Calculate exponential backoff delay
133+ $delay = $BaseDelaySeconds * [Math ]::Pow(2 , $attempt - 1 )
134+ Write-Verbose " Transient error on attempt $attempt of $MaxRetries . Retrying in ${delay} s. Error: $ ( $_.Exception.Message ) "
135+ Start-Sleep - Seconds $delay
136+ }
137+ }
138+
139+ # Should not reach here, but just in case
140+ if ($lastError ) {
141+ throw " Error invoking Plex API: $ ( $lastError.Exception.Message ) "
77142 }
78143}
0 commit comments