Private/Invoke-CMCRequest.ps1
function Invoke-CMCRequest { <# .SYNOPSIS Makes HTTP requests to the CoinMarketCap API with authentication and error handling. .DESCRIPTION This is the core function that handles all API requests to CoinMarketCap. It manages: - Authentication header injection - Rate limiting and retry logic - Error handling and response validation - Automatic deserialization of JSON responses This is a private function and should not be called directly by users. .PARAMETER Endpoint The API endpoint path (without base URL). Example: "/cryptocurrency/listings/latest" .PARAMETER Method The HTTP method to use. Default is GET. .PARAMETER Parameters Hashtable of query parameters to include in the request. .PARAMETER Body Request body for POST/PUT requests. .PARAMETER MaxRetries Maximum number of retry attempts for rate-limited or failed requests. Default is 3. .PARAMETER RetryDelay Initial delay in milliseconds between retry attempts. Uses exponential backoff. .EXAMPLE Invoke-CMCRequest -Endpoint "/cryptocurrency/listings/latest" -Parameters @{ limit = 10 } Makes a GET request to retrieve the latest cryptocurrency listings. .NOTES This function is for internal use only. #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [string]$Endpoint, [Parameter()] [ValidateSet('GET', 'POST', 'PUT', 'DELETE')] [string]$Method = 'GET', [Parameter()] [hashtable]$Parameters = @{}, [Parameter()] [object]$Body, [Parameter()] [ValidateRange(0, 10)] [int]$MaxRetries = 3, [Parameter()] [ValidateRange(100, 10000)] [int]$RetryDelay = 1000 ) begin { Write-Verbose "Preparing CoinMarketCap API request to: $Endpoint" # Get API key $apiKey = Get-CMCApiKey -AsPlainText -ErrorAction Stop if (-not $apiKey) { throw "No API key configured. Use Set-CMCApiKey to configure authentication." } # Determine base URL # Initialize URLs if not set (fallback) if (-not $script:CMCBaseUrl) { $script:CMCBaseUrl = 'https://pro-api.coinmarketcap.com/v1' } if (-not $script:CMCSandboxUrl) { $script:CMCSandboxUrl = 'https://sandbox-api.coinmarketcap.com/v1' } if ($script:CMCUseSandbox) { $baseUrl = $script:CMCSandboxUrl Write-Verbose "Using sandbox environment: $baseUrl" } else { $baseUrl = $script:CMCBaseUrl Write-Verbose "Using production environment: $baseUrl" } # Ensure endpoint starts with / if (-not $Endpoint.StartsWith('/')) { $Endpoint = "/$Endpoint" } # Build full URL $baseUri = "$baseUrl$Endpoint" Write-Verbose "Base URI: $baseUri" # Add query parameters if ($Parameters.Count -gt 0) { # Load Web assembly for URL encoding if needed Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue $queryString = @() foreach ($key in $Parameters.Keys) { # Use Uri.EscapeDataString as a fallback if HttpUtility is not available $value = try { [System.Web.HttpUtility]::UrlEncode($Parameters[$key].ToString()) } catch { [Uri]::EscapeDataString($Parameters[$key].ToString()) } $queryString += "$key=$value" } $queryPart = $queryString -join '&' $uri = "${baseUri}?${queryPart}" Write-Verbose "Query string: $queryPart" } else { $uri = $baseUri } Write-Verbose "Full request URI: $uri" } process { # Implement rate limiting $now = [datetime]::Now $timeSinceLastRequest = ($now - $script:CMCLastRequestTime).TotalMilliseconds if ($timeSinceLastRequest -lt $script:CMCRequestDelay) { $waitTime = $script:CMCRequestDelay - $timeSinceLastRequest Write-Verbose "Rate limiting: Waiting $waitTime ms before request" Start-Sleep -Milliseconds $waitTime } # Prepare request headers $headers = @{ 'X-CMC_PRO_API_KEY' = $apiKey 'Accept' = 'application/json' 'Accept-Encoding' = 'deflate, gzip' } if ($Body) { $headers['Content-Type'] = 'application/json' } # Prepare request parameters for Invoke-RestMethod $requestParams = @{ Uri = $uri Method = $Method Headers = $headers ErrorAction = 'Stop' UseBasicParsing = $true } if ($Body) { if ($Body -is [hashtable] -or $Body -is [PSCustomObject]) { $requestParams['Body'] = $Body | ConvertTo-Json -Depth 10 } else { $requestParams['Body'] = $Body } } # Execute request with retry logic $attempt = 0 $currentDelay = $RetryDelay while ($attempt -le $MaxRetries) { $attempt++ try { Write-Verbose "Attempt $attempt of $($MaxRetries + 1)" # Make the request $response = Invoke-RestMethod @requestParams # Update last request time $script:CMCLastRequestTime = [datetime]::Now # Check response status if ($response.status) { if ($response.status.error_code -ne 0) { # API returned an error $errorMessage = "CoinMarketCap API Error [$($response.status.error_code)]: $($response.status.error_message)" # Check if it's a rate limit error if ($response.status.error_code -eq 1008 -or $response.status.error_code -eq 429) { if ($attempt -le $MaxRetries) { Write-Warning "Rate limit exceeded. Retrying in $currentDelay ms..." Start-Sleep -Milliseconds $currentDelay $currentDelay = $currentDelay * 2 # Exponential backoff continue } } throw $errorMessage } # Log credit usage if available if ($response.status.credit_count) { Write-Verbose "API credits used: $($response.status.credit_count)" } } # Return the data portion of the response if ($response.data) { Write-Verbose "Request successful, returning data" return $response.data } else { Write-Verbose "Request successful, returning full response" return $response } } catch [System.Net.WebException] { $statusCode = $_.Exception.Response.StatusCode.value__ $statusDescription = $_.Exception.Response.StatusDescription Write-Verbose "HTTP Error $statusCode : $statusDescription" # Handle specific HTTP errors switch ($statusCode) { 401 { throw "Authentication failed. Please check your API key." } 403 { throw "Access forbidden. Your API key may not have access to this endpoint." } 429 { # Rate limited if ($attempt -le $MaxRetries) { Write-Warning "Rate limit (HTTP 429) exceeded. Retrying in $currentDelay ms..." Start-Sleep -Milliseconds $currentDelay $currentDelay = $currentDelay * 2 continue } throw "Rate limit exceeded. Please try again later." } 500 { # Server error - retry if ($attempt -le $MaxRetries) { Write-Warning "Server error (HTTP 500). Retrying in $currentDelay ms..." Start-Sleep -Milliseconds $currentDelay $currentDelay = $currentDelay * 2 continue } throw "CoinMarketCap server error. Please try again later." } {$_ -in 502, 503, 504} { # Gateway errors - retry if ($attempt -le $MaxRetries) { Write-Warning "Gateway error (HTTP $statusCode). Retrying in $currentDelay ms..." Start-Sleep -Milliseconds $currentDelay $currentDelay = $currentDelay * 2 continue } throw "CoinMarketCap service temporarily unavailable." } default { throw "HTTP Error $statusCode : $statusDescription" } } } catch { # Generic error if ($attempt -le $MaxRetries) { Write-Warning "Request failed: $_. Retrying in $currentDelay ms..." Start-Sleep -Milliseconds $currentDelay $currentDelay = $currentDelay * 2 continue } throw $_ } } # If we get here, all retries failed throw "Failed to complete request after $($MaxRetries + 1) attempts" } end { # Clear API key from memory if ($apiKey) { Clear-Variable -Name apiKey -Force } Write-Verbose "Invoke-CMCRequest completed" } } |