Providers/Okta/Private/Get-OktaErrorDetail.ps1
|
function Get-OktaErrorDetail { <# .SYNOPSIS Extracts the useful part of a failed Okta API response .DESCRIPTION Okta puts the actionable text in the response body, under errorSummary and errorCauses, and PowerShell throws that body away in favour of a generic "The remote server returned an error" message. Worse, the two editions surface the body differently: PowerShell 7 populates $_.ErrorDetails.Message, Windows PowerShell often leaves it empty and only exposes the response stream. This reads whichever one is available and flattens errorCauses, which is the field that actually says which attribute was rejected and why. .PARAMETER ErrorRecord The ErrorRecord caught from Invoke-WebRequest .OUTPUTS String suitable for putting straight into an exception message .EXAMPLE catch { throw (Get-OktaErrorDetail -ErrorRecord $_) } .NOTES Author: Jeffrey Stuhr Version: 1.0.0 Last Updated: 2026-08-07 #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory = $true)] [System.Management.Automation.ErrorRecord]$ErrorRecord ) $rawBody = $null if ($ErrorRecord.ErrorDetails -and -not [string]::IsNullOrWhiteSpace($ErrorRecord.ErrorDetails.Message)) { $rawBody = $ErrorRecord.ErrorDetails.Message } elseif ($ErrorRecord.Exception.PSObject.Properties['Response'] -and $ErrorRecord.Exception.Response) { # Windows PowerShell only. GetResponseStream does not exist on the PowerShell 7 # HttpResponseMessage, hence the method probe rather than a version check. $response = $ErrorRecord.Exception.Response if ($response.PSObject.Methods['GetResponseStream']) { $reader = $null try { $stream = $response.GetResponseStream() $reader = New-Object System.IO.StreamReader($stream, [System.Text.Encoding]::UTF8) $rawBody = $reader.ReadToEnd() } catch { Write-Verbose "Could not read the error response stream: $($_.Exception.Message)" } finally { if ($reader) { $reader.Dispose() } } } } # Whatever is left when the body is absent or is not an Okta error document. A transport # failure is the common case: PowerShell 7 puts the inner exception's entire ToString, # stack trace included, into both ErrorDetails.Message and Exception.Message, so a refused # connection yields twenty lines of System.Net internals wrapped around one useful # sentence. Keep the sentence. The full record is still on $Error[0] for anyone who wants # it, and Okta always answers an API error with JSON, so nothing structured is lost here. $fallbackSource = if ([string]::IsNullOrWhiteSpace($rawBody)) { $ErrorRecord.Exception.Message } else { $rawBody } $fallback = (@($fallbackSource -split "`r?`n") | Where-Object { $_.Trim() } | Select-Object -First 1) $fallback = if ($fallback) { $fallback.Trim() } else { 'no further detail was returned' } if ([string]::IsNullOrWhiteSpace($rawBody)) { return $fallback } $parsed = $null try { $parsed = $rawBody | ConvertFrom-Json } catch { return $fallback } if (-not $parsed) { return $fallback } # The token endpoint uses the OAuth error shape, everything else uses Okta's own. if ($parsed.PSObject.Properties['error'] -and -not $parsed.PSObject.Properties['errorSummary']) { $oauthParts = @($parsed.error) if ($parsed.PSObject.Properties['error_description']) { $oauthParts += $parsed.error_description } return ($oauthParts -join ': ') } $parts = @() if ($parsed.PSObject.Properties['errorSummary']) { $parts += $parsed.errorSummary } if ($parsed.PSObject.Properties['errorCauses'] -and $parsed.errorCauses) { $parts += @($parsed.errorCauses | ForEach-Object { $_.errorSummary }) } if ($parsed.PSObject.Properties['errorCode'] -and $parsed.errorCode) { $parts += "[$($parsed.errorCode)]" } $message = ($parts | Where-Object { $_ }) -join ' ' if ([string]::IsNullOrWhiteSpace($message)) { return $fallback } return $message } |