PowerShell與REST API串接 ── Invoke-RestMethod的實務

· · PowerShell, REST API, Windows, 自動化, 業務系統, 串接, JSON, 維運改善

「從核心系統的Web API取得受訂資料,寫入公司內部的Excel報表」「每天早上呼叫SaaS的出勤API,輸出當天的出勤預定」── 在PowerShell於業務中被使用的場景裡,這幾年明顯增加的就是REST API串接。還不到需要購買專用工具的程度,但靠手動作業又撐不下去。作為填補這道縫隙的工具,Invoke-RestMethod 非常強大。

另一方面,雖然做到能動的程度並不難,但一旦要投入實際維運,難度就會瞬間提高,這也是API串接的特徵。日文出現亂碼、看不懂錯誤回應的內容、偶爾因429而失敗、在Proxy環境下不通、只有Windows PowerShell 5.1被TLS擋下。這些都是「有對方存在的系統」才會遇到的問題。

本文針對在公司內部以PowerShell呼叫API的資訊系統人員與開發者,依實務上所需的順序,整理認證、JSON收發、錯誤處理、重試、分頁,以及5.1特有的陷阱。

前提環境・驗證環境

項目 內容
適用版本 Windows PowerShell 5.1與PowerShell 7兩者為對象。-SkipHttpErrorCheck-Authentication-MaximumRetryCount-NoProxy 等只能在PowerShell 6以後使用的功能,會在本文中逐一標明(5.1特有的限制整理於§8)1
範例的驗證環境 文末提供下載的範例程式碼是在PowerShell 7.6下執行驗證的(Pester 21項測試)。§8中5.1特有的敘述,則是根據5.1的規格整理而成
範例所用的API https://api.example.co.jp/... 為虛構的端點,原樣無法動作。回應JSON的範例同樣是為了說明而設置的虛構內容

1. 先講結論

  • 若是JSON/XML的API,就用 Invoke-RestMethod它會自動將回應物件化。若需要狀態碼或標頭,則使用 Invoke-WebRequest,或是 -StatusCodeVariable / -ResponseHeadersVariable23
  • 認證以標頭傳遞是最通用的方式。PowerShell 6以後也可以使用 -Authentication Bearer -Token(SecureString)。2
  • 日文JSON以UTF-8位元組陣列傳送最為可靠。-ContentType 中明確指定 charset=utf-8
  • ConvertTo-Json 的預設深度為2。巢狀較深的物件若不指定 -Depth,就會被截斷。4
  • 4xx/5xx會成為終止錯誤。PowerShell 7以後可以用 $_.ErrorDetails.Message 讀取回應本文。也可以用 -SkipHttpErrorCheck 選擇不將其轉為例外。2
  • 429會依照 Retry-After 等待。若指定 -MaximumRetryCount,內建功能會自動跟隨該指示。不過內建重試的對象是400〜599(以及304)的全部範圍,401、404也會被重新送出。只有在需要依代碼分別處理或需要指數退避時,才自行實作。2
  • 請不要對像 POST 這種非冪等的要求自動重試。冪等指的是同一個要求無論送出幾次,結果都不會改變的性質,GETPUTDELETE 是冪等的,而 POST 是非冪等的。即使發生通訊錯誤或5xx,伺服器端也有可能已經處理完成,重送就會造成重複登錄。
  • Link 標頭方式的分頁,可以用 -FollowRelLink 自動化。游標方式則需要自行寫迴圈。2
  • Windows PowerShell 5.1有其特有的陷阱。包括是否需要 -UseBasicParsing、明確啟用TLS 1.2,以及編碼的處理方式這3點。1
  • -SkipCertificateCheck 不應用於長期維運。正確的做法是讓系統信任公司內部CA。

2. Invoke-RestMethod 與 Invoke-WebRequest

首先掌握兩者的差異。23

  Invoke-RestMethod Invoke-WebRequest
回應的處理方式 自動將JSON/XML物件化 WebResponseObject(原始本文・標頭・代碼)
主要用途 REST API 取得HTML、參照狀態或標頭
狀態碼 -StatusCodeVariable 取得(PS7+) .StatusCode
標頭 -ResponseHeadersVariable 取得(PS6+) .Headers

若要呼叫API,基本上使用 Invoke-RestMethod。即使在需要標頭或代碼的場合,也可以用專用的變數參數取得。

$data = Invoke-RestMethod -Uri 'https://api.example.co.jp/v1/orders' `
        -Headers @{ Authorization = "Bearer $token" } `
        -StatusCodeVariable status -ResponseHeadersVariable headers -TimeoutSec 30

"HTTP $status / 剩餘可用請求數: $($headers['X-RateLimit-Remaining'])"
$data.items | Select-Object orderId, customerName, amount

3. 認證的傳遞方式

最通用的方法是直接寫在標頭裡,無論5.1或7都能以相同方式運作。

# (1) Bearer權杖(最常見)
$headers = @{ Authorization = "Bearer $accessToken"; Accept = 'application/json' }
Invoke-RestMethod -Uri $uri -Headers $headers

# (2) API金鑰(標頭名稱依提供方的規格而定)
$headers = @{ 'X-Api-Key' = $apiKey }

# (3) Basic認證(PowerShell 6以後可使用 -Authentication)
Invoke-RestMethod -Uri $uri -Authentication Basic -Credential $cred

# (4) 以 -Token 傳遞Bearer(PowerShell 6以後。可用SecureString處理)
Invoke-RestMethod -Uri $uri -Authentication Bearer -Token $secureToken

# (5) 用戶端憑證
Invoke-RestMethod -Uri $uri -Certificate $cert

使用 -Authentication 時,PowerShell預設會拒絕在HTTPS以外的情況下使用(可以用 -AllowUnencryptedAuthentication 迴避,但這會讓認證資訊以明文傳送,不應該使用)。2

不要把權杖或API金鑰直接寫在腳本裡是最基本的前提。使用SecretManagement的保管方法,整理於「PowerShell 中安全處理認證資訊」。

4. 傳送JSON ── 日文與-Depth的陷阱

傳送時必定會踩到的,就是亂碼巢狀被截斷這兩個問題。

ConvertTo-Json-Depth預設為2,超過這個深度的階層不會被展開,而是被替換成型別名稱字串等內容。4 若請求本文含有巢狀結構,請務必指定該參數。

$body = @{
    order = @{
        customer = @{ code = 'C001'; name = '株式会社サンプル' }   # 3階層目
        lines    = @( @{ item = 'A-100'; qty = 3 } )
    }
}

# 【NG】預設的 -Depth 2 會遺失 customer 與 lines 的內容
$json = $body | ConvertTo-Json

# 【OK】指定足夠的深度
$json = $body | ConvertTo-Json -Depth 10

亂碼對策方面,先轉換成UTF-8位元組陣列後再傳送是最可靠的做法。

$json  = $body | ConvertTo-Json -Depth 10
$bytes = [System.Text.Encoding]::UTF8.GetBytes($json)

$res = Invoke-RestMethod -Uri $uri -Method Post `
       -Headers @{ Authorization = "Bearer $token" } `
       -ContentType 'application/json; charset=utf-8' `
       -Body $bytes -TimeoutSec 60

在PowerShell 7中,即使直接傳遞字串也會以UTF-8送出,但若腳本要與5.1共用,先轉成位元組陣列就能吸收環境差異。關於Windows整體的文字編碼議題,請參閱「整理 Windows 的字元編碼與換行符」。

傳送前與接收後,務必以往返方式確認。從上面的 $body 想產生的JSON,應該是以下的形式。

{
  "order": {
    "customer": { "code": "C001", "name": "株式会社サンプル" },
    "lines": [ { "item": "A-100", "qty": 3 } ]
  }
}

若忘記指定 -Depth,相當於第3層的 customerlines 內容就不會展開,不會變成這種形式。傳送前直接把 $json 印到畫面上,用肉眼確認階層是否還在,是最快的確認方法(因為是從雜湊表建立的,鍵的排列順序取決於列舉順序。若想固定順序,請使用 [ordered]@{})。

假設對於這個請求,API的回應如下(這是虛構的範例)。

{
  "orderId": "2026-000123",
  "status": "accepted",
  "customer": { "code": "C001", "name": "株式会社サンプル" }
}

因為 Invoke-RestMethod 會自動把回應轉成物件,接收端可以這樣寫。

$res.orderId              # 2026-000123
$res.customer.name        # 株式会社サンプル ── 若這裡出現亂碼,就是接收端的問題

接收端的亂碼,可以用 $res.customer.name 是否可讀來判斷。若出現亂碼,請懷疑回應的 Content-Type 中是否正確宣告了 charset(可以用 -ResponseHeadersVariable 取得標頭)。透過這一來一回,就能區分問題出在傳送端還是接收端。

5. 錯誤處理 ── 讀不到回應本文就無法調查

Invoke-RestMethod 會把4xx/5xx的回應當作終止錯誤處理。也就是說可以用 try/catch 捕捉,但問題在於如何讀取API回傳的錯誤訊息本文

try {
    $res = Invoke-RestMethod -Uri $uri -Method Post -Body $bytes `
           -ContentType 'application/json; charset=utf-8' -TimeoutSec 30
}
catch {
    $status = $_.Exception.Response.StatusCode      # 例如: BadRequest / 400
    # PowerShell 7以後,回應本文會存在這裡(API的錯誤訊息)
    $detail = $_.ErrorDetails.Message
    Write-Warning "API 失敗 ($status):$detail"
    throw
}

確認的重點。$status 會存入HTTP狀態的列舉值(如同註解所示,會顯示成 BadRequest 這樣的形式),$detail 則會以字串原樣存入API回傳的本文。若API回傳JSON,可以用 $detail | ConvertFrom-Json 取出項目,依錯誤代碼或欄位名稱來分支處理。反之,若 $detail 一直是空的,可能是API沒有回傳本文,或是在Windows PowerShell 5.1下執行(5.1需要自行讀取回應串流。詳見§8)。

為了不在「收到HTTP 400,卻不知道為什麼被拒絕」這種調查上浪費時間,請把錯誤本文務必留存在日誌中的設計原則落實下去。

若處理邏輯需要依狀態碼分支,使用 -SkipHttpErrorCheck 停止例外化的寫法會更直觀(PowerShell 7以後)。2

$res = Invoke-RestMethod -Uri $uri -SkipHttpErrorCheck -StatusCodeVariable code -TimeoutSec 30
switch ($code) {
    200     { $res.items }
    404     { Write-Warning '找不到對象資料'; @() }
    { $_ -ge 500 } { throw "伺服器端錯誤:$code" }
    default { throw "非預期的回應:$code" }
}

6. 重試 ── 429與暫時性失敗

PowerShell 6以後的 Invoke-RestMethod 具有 -MaximumRetryCount-RetryIntervalSec,失敗時會自動重試。而且若429回應中包含 Retry-After,就會使用該標頭的值,而不是指定的間隔2 也就是說,如果只是「遇到速率限制時,依指示等待後重試」,內建功能就足夠了。

# 如果只是要因應速率限制,這樣往往就足夠了
Invoke-RestMethod -Uri $uri -Headers $headers -MaximumRetryCount 4 -RetryIntervalSec 5

不過,重試的對象並不只有429。官方文件明確定義為「收到400〜599(以及304)的失敗代碼時進行重試」。2 也就是說,像401(認證錯誤)、403(權限不足)、404(URL錯誤)這類無論送幾次都不會修好的要求,也同樣會被重試。若在權杖設定錯誤的情況下無人值守執行,在確認失敗之前會白白浪費 -MaximumRetryCount × -RetryIntervalSec 秒,同一個無效的要求也會反覆送達API。若想在遇到恒久性錯誤時立即中止,就需要接下來介紹的自行實作。

需要自行撰寫重試函式的情況,包括以下這些需求。

  • 想依狀態碼分別處理(例如400字頭立即判定失敗,只有5xx才重試)
  • 想採用指數退避(內建功能是以指定的間隔重試)
  • 失敗時想把API的錯誤本文留存在日誌中

以下就是這樣的形式。

function Invoke-KsApi {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Uri,
        [string] $Method = 'Get',
        [object] $Body,
        [hashtable] $Headers = @{},
        [ValidateRange(1, 10)] [int] $MaxAttempts = 4,
        # 若Retry-After指示的等待時間比這個值還長,就不等待,直接中止
        [ValidateRange(1, 86400)] [int] $MaxWaitSeconds = 300,
        # 只有在API支援冪等鍵的情況下,才允許重試POST之類的要求
        [string] $IdempotencyKey
    )

    # 只有在同一個要求收到兩次、結果也不會改變的情況下,才可以重試。
    # POST/PATCH存在「伺服器端已經處理成功,但回應沒有送達」的情況,
    # 若單純重送就會造成重複登錄
    $idempotentMethods = 'Get', 'Head', 'Options', 'Put', 'Delete'
    $canRetry = ($Method -in $idempotentMethods) -or $IdempotencyKey
    if ($IdempotencyKey) { $Headers = $Headers + @{ 'Idempotency-Key' = $IdempotencyKey } }

    for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
        $params = @{
            Uri                     = $Uri
            Method                  = $Method
            Headers                 = $Headers
            TimeoutSec              = 60
            SkipHttpErrorCheck      = $true          # 因為要依代碼分支,所以不轉為例外
            StatusCodeVariable      = 'code'
            ResponseHeadersVariable = 'resHeaders'
        }
        # 為了讓 $Body 的值即使是 $false、0、空字串,也能作為本文送出,
        # 判斷依據不是真偽值,而是「參數是否有被傳入」
        if ($PSBoundParameters.ContainsKey('Body')) {
            # 若用管線傳遞,空陣列 @() 會被視為「輸入0筆」而回傳 $null,
            # 因此改用 -InputObject 讓它不被列舉,直接轉換(@() 會變成 [])
            $json = ConvertTo-Json -InputObject $Body -Depth 10
            $params.Body        = [System.Text.Encoding]::UTF8.GetBytes($json)
            $params.ContentType = 'application/json; charset=utf-8'
        }

        # -SkipHttpErrorCheck 所抑制的,僅限於HTTP的錯誤回應。
        # 逾時、名稱解析失敗、連線重置、TLS錯誤等,回應本身
        # 沒有送達的通訊錯誤仍會以例外拋出,因此在這裡捕捉並重試
        try {
            $code = $null
            $res  = Invoke-RestMethod @params
        }
        catch {
            # 非冪等的要求,有可能只是回應沒有送達,而伺服器端其實已經處理成功。
            # 不自動重送,把判斷交給呼叫端
            if (-not $canRetry) {
                throw "通訊錯誤($Method 不會自動重試,請確認是否已處理完成):$($_.Exception.Message)"
            }
            if ($attempt -eq $MaxAttempts) { throw }
            $wait = [math]::Min([math]::Pow(2, $attempt), 60)
            Write-Warning "通訊錯誤:$($_.Exception.Message) ── $wait 秒後重試 ($attempt/$MaxAttempts)"
            Start-Sleep -Seconds $wait
            continue
        }

        if ($code -lt 400) { return $res }                    # 成功

        # 值得重試的只有暫時性錯誤。以許可清單的方式明確列出
        # (若重試405、415這類恒久性錯誤,不僅浪費時間,
        #  最後只會留下「已達重試上限」這種無關的訊息)
        $retryable = @(408, 429, 500, 502, 503, 504)
        if ($code -notin $retryable) {
            throw "API 錯誤 ($code):$($res | ConvertTo-Json -Compress -Depth 3)"
        }
        # 5xx有可能是「伺服器端處理之後才失敗」,
        # 因此非冪等的要求不重送。429也是同樣道理(無法保證是在處理之前就被擋下)
        if (-not $canRetry) {
            throw "API 錯誤 ($code)。$Method 不會自動重試:$($res | ConvertTo-Json -Compress -Depth 3)"
        }

        if ($attempt -eq $MaxAttempts) {
            throw "已達重試上限 ($code):$($res | ConvertTo-Json -Compress -Depth 3)"
        }

        # Retry-After不只會以「秒數」回傳,有時也會是HTTP日期格式。
        # 若直接轉型成[int],會發生例外,導致每次重試都失敗
        $wait = $null
        $retryAfter = if ($resHeaders) { $resHeaders['Retry-After'] | Select-Object -First 1 }
        if ($retryAfter) {
            $seconds = 0
            $date    = [datetime]::MinValue
            if ([int]::TryParse($retryAfter, [ref] $seconds)) {
                $wait = $seconds
            }
            elseif ([datetime]::TryParse($retryAfter,
                        [cultureinfo]::InvariantCulture,
                        [System.Globalization.DateTimeStyles]::AdjustToUniversal, [ref] $date)) {
                # 小數部分務必無條件進位。[int] 是採用四捨五入(偶數捨入),
                # 若剩餘0.4秒被捨入為0秒,就會在伺服器指定的期限之前重送
                $remaining = ($date - [datetime]::UtcNow).TotalSeconds
                $wait = if ($remaining -gt 0) { [int][math]::Ceiling($remaining) } else { 0 }
            }
        }
        if ($null -eq $wait) {
            $wait = [math]::Min([math]::Pow(2, $attempt), 60)             # 指數退避(上限60秒)
        }
        elseif ($wait -gt $MaxWaitSeconds) {
            # 若擅自縮短伺服器指示的等待時間、提早重送,只會反覆收到429,
            # 最終達到上限而已。若等不了這麼久,就連同等待時間一起回傳給呼叫端
            throw "目前處於速率限制中。伺服器指示的等待時間 $wait 秒超過上限 $MaxWaitSeconds 秒,因此中止執行。請稍後再重新執行 ($code)"
        }
        Write-Warning "HTTP $code ── $wait 秒後重試 ($attempt/$MaxAttempts)"
        Start-Sleep -Seconds $wait
    }
}

確認是否正在重試。當這個函式進入重試,每次嘗試都會輸出一行 Write-Warning(形式如 HTTP 429 ── 30 秒後重試 (1/4))。要看的重點有3個:等待秒數是否符合伺服器的指示嘗試次數是否落在 -MaxAttempts 的範圍內,以及遇到401、404這類恒久性錯誤時,是否不重試而立即拋出例外。無人值守執行時畫面不會保留下來,因此警告串流也請一併寫入日誌(「別再用Write-Host ── PowerShell的輸出串流與日誌設計」)。

冪等性(同一要求無論送出幾次,結果都不變的性質)的處理是第二個重點。GETPUT 即使同一個要求收到兩次,結果也不會改變,但 POST 不同。尤其是「伺服器端已成功登錄,但回應在送回之前通訊就中斷了」這種情況下,若單純重送就會造成重複登錄。上面的實作中,只有在方法本身是冪等的、或是API支援冪等鍵(Idempotency-Key)的情況下,才允許自動重試,其他情況則會明確顯示「請確認是否已處理完成」並中止。

Retry-AfterHTTP日期格式回傳時,請把剩餘時間無條件進位。因為轉型成 [int] 時採用的不是無條件捨去,而是四捨五入(端數恰為0.5時採偶數捨入),所以扣除回應送達所花時間後,若剩餘不到0.5秒就會變成 0Start-Sleep -Seconds 0 會立即返回,於是就會在伺服器指定的時刻之前重送,得到的結果又是429。這樣重複用完所有嘗試次數,最後以「已達重試上限」收場。上面的實作之所以插入 [math]::Ceiling,原因就在這裡。

不要縮短 Retry-After 指示的等待時間也是重要的一點。若伺服器指示「30分鐘後再來」,卻以上限為理由在5分鐘後就重送,得到的結果只會是同樣的429。不僅浪費要求次數用光了嘗試機會,最後也只會留下「已達重試上限」的訊息。上面的實作中,若指示的等待時間超過 -MaxWaitSeconds,並不會縮短等待時間重試,而是連同等待時間一起立即判定失敗。若是批次處理,呼叫端可以接收這個例外,選擇留到下次執行,或是依指示的時間乖乖等待。

另一個重點是,以許可清單的方式,明確列出要重試的對象。若寫成「只列舉並排除恒久性錯誤」的形式,那些漏列的代碼(如405 Method Not Allowed、415 Unsupported Media Type等)就會被當成暫時性錯誤處理,反覆送出永遠不會修好的要求,最後只留下「已達重試上限」這種原因不明的訊息。只重試已知是暫時性的代碼,其餘則連同API的錯誤內容一起立即判定失敗,這才是正確做法。關於重試設計的一般性原則,請參閱「PowerShell 的錯誤處理與重新執行設計」。

7. 分頁

API幾乎不會一次回傳全部資料,方式主要有兩種。

(1) Link 標頭方式(GitHub等採用),可以用 -FollowRelLink 自動追蹤下一頁。2

# 自動追蹤下一頁(也可以指定取得頁數的上限)
$all = Invoke-RestMethod -Uri $uri -Headers $headers -FollowRelLink -MaximumFollowRelLink 20

(2) 游標/位移方式則需要自行寫迴圈。請注意以下程式碼使用了§6中定義的 Invoke-KsApi。若只想測試這一節,把 Invoke-KsApi -Uri $u -Headers $headers 換成 Invoke-RestMethod -Uri $u -Headers $headers 也能運作(只是少了重試與錯誤本文記錄的功能)。

$items    = [System.Collections.Generic.List[object]]::new()
$cursor   = $null
$page     = 0
$maxPages = 100

do {
    $page++

    # 分隔符號會依原始URI是否已經帶有查詢字串而改變。
    # 若一律加上 ?,就會變成 .../items?status=active?cursor=...,
    # 從伺服器的角度看,游標會被誤認成是status值的一部分。
    # 游標本身也是不透明的字串(可能包含 + & = # 等字元),因此務必進行編碼
    $u = if ($cursor) {
        $sep = if ($uri.Contains('?')) { '&' } else { '?' }
        "$uri$sep" + "cursor=$([uri]::EscapeDataString($cursor))"
    }
    else { $uri }

    $res = Invoke-KsApi -Uri $u -Headers $headers
    $items.AddRange([object[]]$res.items)
    $cursor = $res.nextCursor

    # 中止時務必要告知。若默默跳出,呼叫端會誤以為「已取得全部資料」
    if ($page -ge $maxPages -and $cursor) {
        Write-Warning "已達頁數上限 ($maxPages),因此中止取得。可能有遺漏未取得的資料"
        break
    }
} while ($cursor)

"取得件數: $($items.Count)"

$items 之所以使用 List[T],是為了避免因 += 而重新建立陣列(「PowerShell 腳本太慢時該檢查的地方」)。

確認以最後一行來進行。請把 取得件數:... 顯示的數字,與API端的總件數(許多API的回應中都有 total 之類的項目)或管理畫面上的件數互相核對。若對不上,先確認是否出現了中止的警告。若既沒有警告、件數卻不足,就要懷疑 nextCursor 的項目名稱是否與規格相符。

也別忘記務必設定頁數上限。伺服器持續回傳相同的游標,或是忘記把 nextCursor 清空,這類問題實際上是會發生的。若沒有上限,腳本就會在那個當下不停地送出要求。而中止這件事,也必須以警告的形式呈現出來。若默默 break,呼叫端就會誤把不完整的結果當成全部資料來處理。

8. Windows PowerShell 5.1特有的陷阱

在仍保留5.1的環境中,請先懷疑以下這3點。1

(1) TLS 1.2未啟用。若維持舊有的預設設定,就無法連線到只接受TLS 1.2以上的API,並會出現「基礎連接已關閉」之類的錯誤。

# Windows PowerShell 5.1的固定寫法(放在腳本開頭)
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

(2) 有時需要 -UseBasicParsing5.1的 Invoke-WebRequest 預設會使用Internet Explorer的引擎來解析HTML,因此在IE尚未初始化的帳戶(如服務帳戶)下會失敗。PowerShell 6以後不再有這個相依性,即使指定 -UseBasicParsing 也會被忽略。1

(3) 不存在 -SkipHttpErrorCheck-Authentication在5.1中,要讀取錯誤本文,必須自行讀取回應串流。若要正式進行API串接,導入PowerShell 7是最划算的做法(「Windows PowerShell 5.1 與 PowerShell 7 的差異」)。

9. Proxy與憑證

從公司內部呼叫網際網路上的API時,能否通過Proxy是第一道關卡。若未指定 -Proxy,就會使用網際網路設定(網際網路選項)或環境變數所設定的Proxy。2 也就是說,「什麼都沒指定」並不等於「不使用Proxy」。這正是結果會因執行帳戶而異的原因。

症狀 該檢查的原因 對策
407 Proxy Authentication Required Proxy要求認證 搭配 -Proxy 一起指定 -ProxyCredential-ProxyUseDefaultCredentials2
本機可以通,但只有夜間批次失敗 依執行帳戶不同,讀取到不同的網際網路設定 在腳本端明確指定 -Proxy。統一執行帳戶後再驗證
連公司內部API也被吃進Proxy 預設的Proxy設定連公司內部位址也一併套用 -NoProxy 明確繞過(PowerShell 6以後)2
Proxy認證資訊的存放位置 直接寫在腳本裡 從SecretManagement的保管庫中取出
# 明確指定Proxy,並以登入使用者的認證資訊進行驗證
Invoke-RestMethod -Uri $uri -Proxy 'http://proxy.example.co.jp:8080' -ProxyUseDefaultCredentials

# 以專用帳戶通過需要認證的Proxy。
# -ProxyCredential 要與 -Proxy 搭配使用。無法與 -ProxyUseDefaultCredentials 併用
$proxyCred = Get-Secret -Name 'ProxyAccount'   # 從保管庫中取出
Invoke-RestMethod -Uri $uri -Proxy 'http://proxy.example.co.jp:8080' -ProxyCredential $proxyCred

# 面向公司內部的API不經過Proxy(PowerShell 6以後)
Invoke-RestMethod -Uri 'https://api.internal.example.local/v1/ping' -NoProxy

-ProxyCredential-ProxyUseDefaultCredentials,兩者都以指定了 -Proxy 為前提,且無法同時使用2 Windows PowerShell 5.1中沒有 -NoProxy。若在5.1下只想把面向公司內部的通訊排除在Proxy之外,就必須透過執行帳戶的網際網路設定中的例外清單(不使用Proxy的位址)來處理。

以工作排程器的服務帳戶執行時,Proxy設定有時會與互動式登入時不同。這正是「本機可以動,但只有夜間批次失敗」的典型模式。請統一執行帳戶後再驗證(「工作排程器的工作不執行、以 0x1 結束」)。不要把Proxy的認證資訊直接寫在腳本裡,這一點與API權杖是相同的道理(「PowerShell 中安全處理認證資訊」)。

對於憑證錯誤使用 -SkipCertificateCheck 時,請僅限於在驗證環境中做暫時性的迴避。長久的做法,是把公司內部CA的憑證放入受信任的根憑證授權單位存放區,並正確地發行伺服器憑證。

10. 實務上的定石(判斷表)

議題 選項 判斷依據
Cmdlet Invoke-RestMethod / Invoke-WebRequest JSON API用前者。標頭、代碼以專用變數取得23
認證 直接寫在標頭 / -Authentication 若要與5.1共用,選標頭方式。值以SecretManagement保管
JSON傳送 字串 / UTF-8位元組陣列 + 明確指定charset 可以避免因環境差異而產生亂碼
ConvertTo-Json 預設 / 明確指定 -Depth 預設值為2。有巢狀結構時務必指定4
錯誤 僅用try/catch / 本文也留存到日誌 $_.ErrorDetails.Message(PS7)。能否釐清原因會因此不同2
分支較多 catch / -SkipHttpErrorCheck + 代碼分支 若要依狀態分別處理,後者較直觀2
重試 -MaximumRetryCount / 自行實作 429的Retry-After,內建功能會自動跟隨。但因會對400〜599全部重試,若想在恒久性錯誤時立即失敗,則需自行實作2
POST的重試 僅限有冪等鍵的情況 有可能只是回應沒送達,登錄其實已成功,單純重送會造成重複登錄
分頁 -FollowRelLink / 自行寫迴圈 Link 標頭方式選前者2
5.1環境 維持原樣 / 明確指定TLS 1.2 + 評估導入PS7 多數連線錯誤的原因都出在TLS設定1
憑證錯誤 -SkipCertificateCheck / 信任公司內部CA 長期維運時不要停用驗證

11. 總結

  • JSON API基本上使用 Invoke-RestMethod。標頭與狀態碼可以用 -ResponseHeadersVariable / -StatusCodeVariable 取得。
  • 日文JSON以UTF-8位元組陣列傳送,並明確指定 charset=utf-8。若漏了指定 ConvertTo-Json -Depth,會導致巢狀內容缺漏。
  • 4xx/5xx屬於終止錯誤。請以 $_.ErrorDetails.Message 讀取本文,並留存在日誌中。若分支較多,-SkipHttpErrorCheck 會更容易處理。
  • 請不要對像 POST 這種非冪等的要求自動重試。通訊錯誤或5xx有可能是「伺服器端已經處理完成」,重送就會造成重複登錄。若要重試,就需要冪等鍵的機制。
  • 429基本上依照 Retry-After 等待。若使用 -MaximumRetryCount,這項跟隨行為由內建功能執行。不過內建功能會把400〜599全部列為重試對象,若想讓401、404立即失敗,就需要自行實作重試。需要依代碼分別處理或指數退避時也是同樣道理,恒久性錯誤不重試,直接立即判定失敗。
  • 分頁方面,Link 標頭方式使用 -FollowRelLink,游標方式則自行寫迴圈。彙整時請不要使用 +=
  • 在5.1環境中,明確指定TLS 1.2、對IE引擎的相依性、功能不足這3點會成為障礙。若要持續進行API串接,導入PowerShell 7是最快的解決方案。

範例程式碼下載

本文所使用的程式碼,整理成可以直接執行的形式提供下載。內含實作了重試、冪等性、分頁的API呼叫,以及用於驗證的HTTP伺服器。

下載範例程式碼(zip)

本文的範例,實際以PowerShell 7.6執行並完成驗證(Pester 21項測試)。只要執行zip中所含的 Invoke-SampleTests.ps1,就能在您自己的環境中重現相同的驗證。

# 語法解析 + 靜態分析 + Pester測試
./Invoke-SampleTests.ps1

zip中含有驗證用的HTTP伺服器,因此不需要呼叫外部API,就能在自己的環境中重現往返過程。請直接試試看§5(錯誤本文)、§6(429重試時的警告)、§7(取得件數)中提到的確認重點。在實際對接API之前先確認這3點,就比較不容易在正式環境中陷入「不知道為什麼不動」的窘境。

設定值(路徑、伺服器名稱、租用戶ID等)僅為範例。請勿直接在正式環境中執行,務必依照貴公司的環境自行調整。

相關文章

相關諮詢領域

合同會社小村軟體處理使用核心系統或SaaS API的公司內部串接之設計與實作、把既有手動作業替換為API串接的自動化,以及通訊相關的不具狀況調查。

參考連結

  1. Microsoft Learn,Differences between Windows PowerShell 5.1 and PowerShell 7.x。關於Web相關Cmdlet的行為差異、Windows PowerShell 5.1特有的限制,以及PowerShell 7新增的功能。同時參考ServicePointManager.SecurityProtocol 屬性如何指定TLS版本。  2 3 4 5

  2. Microsoft Learn,Invoke-RestMethod。關於向REST端點傳送要求、將回應的JSON/XML轉換為PowerShell物件後回傳、-Headers / -Body / -ContentType / -Method 的指定、-Authentication(Basic / Bearer / OAuth)與-Token、預設拒絕HTTPS以外的認證、以-SkipHttpErrorCheck讓4xx/5xx不轉為例外、以-StatusCodeVariable及-ResponseHeadersVariable取得對應資訊、以-MaximumRetryCount / -RetryIntervalSec進行重試、以-FollowRelLink / -MaximumFollowRelLink對Link標頭進行分頁、-Proxy / -ProxyCredential / -ProxyUseDefaultCredentials(皆以指定-Proxy為前提,且-ProxyCredential與-ProxyUseDefaultCredentials無法併用)、PowerShell 6.0新增的-NoProxy(繞過網際網路設定或環境變數所設定的Proxy)、-SkipCertificateCheck,以及-TimeoutSec。  2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

  3. Microsoft Learn,Invoke-WebRequest。關於將回應以WebResponseObject回傳、可存取StatusCode・Headers・Content,以及Windows PowerShell 5.1預設使用Internet Explorer引擎解析HTML、可用-UseBasicParsing迴避,而PowerShell 6以後不再相依IE、-UseBasicParsing會被忽略。  2 3

  4. Microsoft Learn,ConvertTo-Json。關於-Depth的預設值為2、超過該深度的階層不會被轉換,以及-Compress可去除空白字元。  2 3

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

常見問題

整理諮詢這個主題時常見的問題。

Invoke-RestMethod 與 Invoke-WebRequest 該如何區分使用?
若要處理API的JSON或XML,就用Invoke-RestMethod。它會自動解析回應本文並轉換成PowerShell物件,不需要自行呼叫ConvertFrom-Json。Invoke-WebRequest則會把回應以HtmlWebResponseObject回傳,可以存取狀態碼、標頭、原始本文。若想查看狀態碼或標頭,或是想直接處理HTML,就選擇Invoke-WebRequest。另外,PowerShell 6以後的Invoke-RestMethod具有-ResponseHeadersVariable與-StatusCodeVariable,所以若只是需要標頭或代碼,即使繼續使用Invoke-RestMethod也能取得。
傳送含有日文的JSON時,對方那邊會出現亂碼。
這是因為本文的位元組序列與Content-Type的宣告不一致所致。可靠的做法,是把ConvertTo-Json產生的字串轉換成UTF-8位元組陣列後傳給-Body,並在-ContentType中明確指定charset=utf-8。在Windows PowerShell 5.1中,若直接傳遞字串,有時會以預設編碼傳送,因此這個對策特別有效。接收端的亂碼問題也是同樣道理,請懷疑回應的charset是否有正確宣告。
當API回傳404或500時,想讀取回應本文中的錯誤訊息。
在PowerShell 7以後,只要在catch區塊中查看$_.ErrorDetails.Message,裡面就會存有回應本文。狀態碼可以用$_.Exception.Response.StatusCode取得。此外,加上-SkipHttpErrorCheck後,即使是4xx/5xx也不會轉為例外,可以當作一般回應接收,對於想依狀態碼分支處理的邏輯來說會更容易操作。Windows PowerShell 5.1則必須自行讀取回應串流,就這一點而言也建議使用7。
API回傳了429(速率限制)。應該如何處理?
基本做法是等待回應中Retry-After標頭所指示的秒數後再重試。若沒有這個標頭,則以指數退避(2秒、4秒、8秒……)拉長間隔。PowerShell 6以後具有-MaximumRetryCount與-RetryIntervalSec,若429的回應中含有Retry-After,就會使用該標頭的值而非指定的間隔,因此若只是要因應速率限制,內建功能就足夠了。只有在想依狀態碼分別處理,或需要指數退避時,才使用自行撰寫的重試函式。另外,像POST這種非冪等的要求,重試有可能造成重複登錄,因此除非有冪等鍵的機制,否則應該避免重試。從根本上來說,減少呼叫次數本身(只取得必要的項目、使用批次取得的API)才是更可靠的做法。
公司內部API的自簽憑證出現錯誤。可以使用-SkipCertificateCheck嗎?
請避免在長期維運中使用。停止憑證驗證,意味著不確認通訊對象是否為真,即使是在公司內部網路,也會留下中間人攻擊的空間。正確的做法,是把公司內部CA的憑證放入執行環境的受信任根憑證授權單位存放區,並正確地發行伺服器憑證。即使是在驗證環境中暫時使用,也請透過設定檔或參數明確切換,避免混入正式環境的腳本中。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽