Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion PlexAutomationToolkit/Private/Invoke-PatApi.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,22 @@ function Invoke-PatApi {

if (-not $isTransient -or $attempt -eq $MaxRetries) {
# Non-transient error or final attempt - throw immediately
throw "Error invoking Plex API: $($_.Exception.Message)"
$errorMessage = $_.Exception.Message

# 401 indicates a missing, expired, or invalid token. Surface
# actionable guidance instead of the raw HTTP error so callers
# know which cmdlets to run to recover. Match \b401\b only —
# bare "Unauthorized" is too broad (UnauthorizedAccessException,
# 403 bodies mentioning the word, etc. would misfire).
if ($errorMessage -match '\b401\b') {
throw ("Plex API returned 401 Unauthorized. The authentication token is missing, expired, or invalid. " +
"To resolve: refresh the token with 'Update-PatServerToken' (use -Name to target a non-default server), " +
"list configured servers with 'Get-PatStoredServer', " +
"or pass an explicit -Token parameter to the cmdlet you are calling. " +
"Original error: $errorMessage")
}

Comment thread
tablackburn marked this conversation as resolved.
throw "Error invoking Plex API: $errorMessage"
}

# Calculate exponential backoff delay
Expand Down
36 changes: 29 additions & 7 deletions tests/Unit/Private/Invoke-PatApi.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,38 @@ Describe 'Invoke-PatApi' {
{ Invoke-PatApi -Uri 'http://localhost:32400/test' } | Should -Throw "*Error invoking Plex API*"
}

It 'Should propagate authentication errors' {
It 'Should surface actionable guidance for 401 Unauthorized errors' {
Mock Invoke-RestMethod {
$response = [PSCustomObject]@{
StatusCode = [System.Net.HttpStatusCode]::Unauthorized
}
$exception = [System.Net.WebException]::new('Unauthorized', $null, [System.Net.WebExceptionStatus]::ProtocolError, $response)
throw $exception
throw [System.Net.WebException]::new('The remote server returned an error: (401) Unauthorized.')
}

{ Invoke-PatApi -Uri 'http://localhost:32400/test' } | Should -Throw "*Error invoking Plex API*"
{ Invoke-PatApi -Uri 'http://localhost:32400/test' } | Should -Throw "*401 Unauthorized*Update-PatServerToken*Get-PatStoredServer*"
}

It 'Should include token recovery guidance when 401 is returned' {
Mock Invoke-RestMethod {
throw 'Response status code does not indicate success: 401 (Unauthorized).'
}

{ Invoke-PatApi -Uri 'http://localhost:32400/test' -MaxRetries 1 -BaseDelaySeconds 0 } |
Should -Throw "*token is missing, expired, or invalid*Update-PatServerToken*Get-PatStoredServer*"
}

It 'Should preserve the original error message in the 401 guidance' {
$originalError = 'Response status code does not indicate success: 401 (Unauthorized).'
Mock Invoke-RestMethod { throw $originalError }

{ Invoke-PatApi -Uri 'http://localhost:32400/test' -MaxRetries 1 -BaseDelaySeconds 0 } |
Should -Throw "*Original error: $originalError*"
}

It 'Should not trigger 401 guidance for non-401 errors that mention Unauthorized' {
Mock Invoke-RestMethod {
throw 'UnauthorizedAccessException: cannot read configuration file'
}

{ Invoke-PatApi -Uri 'http://localhost:32400/test' -MaxRetries 1 -BaseDelaySeconds 0 } |
Should -Throw "*Error invoking Plex API*UnauthorizedAccessException*"
}

It 'Should handle malformed JSON responses' {
Expand Down