Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## [0.11.2] - 2026-04-28

### Changed

- The transient-error retry classification in `Invoke-PatApi` now recognizes the Windows `WSANO_DATA` (11004) error ("The requested name is valid, but no data of the requested type was found."), which can surface while DNS records are propagating or when only AAAA records are returned. The error is now retried with the existing exponential backoff instead of failing immediately.
- After retries are exhausted, DNS resolution failures surface a single actionable error naming `Resolve-DnsName`, `Test-NetConnection`, `Get-PatStoredServer`, and `Add-PatServer -Force` as concrete recovery steps, instead of the raw socket error wrapped through three cmdlet layers.

## [0.11.1] - 2026-04-28

### Changed
Expand Down
2 changes: 1 addition & 1 deletion PlexAutomationToolkit/PlexAutomationToolkit.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
RootModule = 'PlexAutomationToolkit.psm1'

# Version number of this module.
ModuleVersion = '0.11.1'
ModuleVersion = '0.11.2'

# Supported PSEditions
CompatiblePSEditions = @('Desktop', 'Core')
Expand Down
26 changes: 24 additions & 2 deletions PlexAutomationToolkit/Private/Invoke-PatApi.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,21 @@ function Invoke-PatApi {
Write-Debug 'Invoking Plex API with the following parameters:'
$apiQueryParameters | Out-String | Write-Debug

# DNS failures. The "no data of the requested type" wording is the
# WSANO_DATA (11004) socket error that Windows raises when a hostname
# resolves but has no record of the queried type (commonly: only AAAA
# is present and the resolver asked for A). It is often transient while
# DNS records propagate or caches recover. Defined once so the retry
# classification and the post-exhaustion guidance branch cannot drift.
$dnsErrorPattern = 'No such host|DNS|name.+not.+resolve|no data of the requested type'

# 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') {
if ($message -match $dnsErrorPattern) {
return $true
}

Expand Down Expand Up @@ -144,6 +151,21 @@ function Invoke-PatApi {
"Original error: $errorMessage")
}

# If retries were exhausted on a DNS resolution failure,
# surface a single actionable message rather than the raw
# socket error wrapped through three layers of cmdlets.
# Timeouts and connection-refused errors are also transient
# but have a different recovery story (server health, network,
# firewall) so they fall through to the generic message.
if ($isTransient -and $attempt -eq $MaxRetries -and $errorMessage -match $dnsErrorPattern) {
throw ("Plex API request failed after $MaxRetries attempts: DNS could not resolve the server hostname. " +
"To resolve: verify the hostname with 'Resolve-DnsName <host>' (try -Type A and -Type AAAA), " +
"confirm reachability with 'Test-NetConnection <host> -Port <port>', " +
"check the stored URI with 'Get-PatStoredServer', " +
"and re-add the server with 'Add-PatServer -Force' if the address has changed. " +
"Original error: $errorMessage")
}

throw "Error invoking Plex API: $errorMessage"
}

Expand Down
52 changes: 52 additions & 0 deletions tests/Unit/Private/Invoke-PatApi.tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,23 @@ Describe 'Invoke-PatApi' {
$script:callCount | Should -Be 2
}

It 'Should retry on WSANO_DATA DNS error and succeed' {
$script:callCount = 0
Mock Invoke-RestMethod {
$script:callCount++
if ($script:callCount -lt 2) {
throw 'The requested name is valid, but no data of the requested type was found.'
}
return [PSCustomObject]@{
MediaContainer = [PSCustomObject]@{ status = 'ok' }
}
}

$result = Invoke-PatApi -Uri 'http://localhost:32400/test' -MaxRetries 3 -BaseDelaySeconds 0
$result.status | Should -Be 'ok'
$script:callCount | Should -Be 2
}

It 'Should retry on timeout and succeed' {
$script:callCount = 0
Mock Invoke-RestMethod {
Expand Down Expand Up @@ -494,6 +511,41 @@ Describe 'Invoke-PatApi' {
$script:callCount | Should -Be 3
}

It 'Should surface actionable DNS guidance after retries are exhausted' {
Mock Invoke-RestMethod {
throw 'No such host is known'
}

{ Invoke-PatApi -Uri 'http://localhost:32400/test' -MaxRetries 3 -BaseDelaySeconds 0 } |
Should -Throw "*DNS could not resolve*Resolve-DnsName*Test-NetConnection*Get-PatStoredServer*Add-PatServer -Force*"
}

It 'Should surface actionable DNS guidance for WSANO_DATA after retries are exhausted' {
Mock Invoke-RestMethod {
throw 'The requested name is valid, but no data of the requested type was found.'
}

{ Invoke-PatApi -Uri 'http://localhost:32400/test' -MaxRetries 3 -BaseDelaySeconds 0 } |
Should -Throw "*DNS could not resolve*Resolve-DnsName*Test-NetConnection*"
}

It 'Should preserve the original error message in the DNS guidance' {
$originalError = 'No such host is known'
Mock Invoke-RestMethod { throw $originalError }

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

It 'Should not apply DNS guidance to non-DNS transient errors' {
Mock Invoke-RestMethod {
throw '503 Service Unavailable'
}

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

It 'Should succeed after multiple retries' {
$script:callCount = 0
Mock Invoke-RestMethod {
Expand Down