「從核心系統的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/-ResponseHeadersVariable。23 - 認證以標頭傳遞是最通用的方式。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這種非冪等的要求自動重試。冪等指的是同一個要求無論送出幾次,結果都不會改變的性質,GET、PUT、DELETE是冪等的,而POST是非冪等的。即使發生通訊錯誤或5xx,伺服器端也有可能已經處理完成,重送就會造成重複登錄。 Link標頭方式的分頁,可以用-FollowRelLink自動化。游標方式則需要自行寫迴圈。2- Windows PowerShell 5.1有其特有的陷阱。包括是否需要
-UseBasicParsing、明確啟用TLS 1.2,以及編碼的處理方式這3點。1 -SkipCertificateCheck不應用於長期維運。正確的做法是讓系統信任公司內部CA。
2. Invoke-RestMethod 與 Invoke-WebRequest
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層的 customer 與 lines 內容就不會展開,不會變成這種形式。傳送前直接把 $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的輸出串流與日誌設計」)。
冪等性(同一要求無論送出幾次,結果都不變的性質)的處理是第二個重點。GET 或 PUT 即使同一個要求收到兩次,結果也不會改變,但 POST 不同。尤其是「伺服器端已成功登錄,但回應在送回之前通訊就中斷了」這種情況下,若單純重送就會造成重複登錄。上面的實作中,只有在方法本身是冪等的、或是API支援冪等鍵(Idempotency-Key)的情況下,才允許自動重試,其他情況則會明確顯示「請確認是否已處理完成」並中止。
當 Retry-After 以HTTP日期格式回傳時,請把剩餘時間無條件進位。因為轉型成 [int] 時採用的不是無條件捨去,而是四捨五入(端數恰為0.5時採偶數捨入),所以扣除回應送達所花時間後,若剩餘不到0.5秒就會變成 0。Start-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) 有時需要 -UseBasicParsing。5.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伺服器。
本文的範例,實際以PowerShell 7.6執行並完成驗證(Pester 21項測試)。只要執行zip中所含的 Invoke-SampleTests.ps1,就能在您自己的環境中重現相同的驗證。
# 語法解析 + 靜態分析 + Pester測試
./Invoke-SampleTests.ps1
zip中含有驗證用的HTTP伺服器,因此不需要呼叫外部API,就能在自己的環境中重現往返過程。請直接試試看§5(錯誤本文)、§6(429重試時的警告)、§7(取得件數)中提到的確認重點。在實際對接API之前先確認這3點,就比較不容易在正式環境中陷入「不知道為什麼不動」的窘境。
設定值(路徑、伺服器名稱、租用戶ID等)僅為範例。請勿直接在正式環境中執行,務必依照貴公司的環境自行調整。
相關文章
- PowerShell 的錯誤處理與重新執行設計 ── 從 try/catch 失效的陷阱到 exit code、重試的實務定石
- PowerShell 中安全處理認證資訊 ── 把明文密碼逐出腳本
- Windows PowerShell 5.1 與 PowerShell 7 的差異 ── 公司內部腳本遷移實務指南
- PowerShell 腳本太慢時該檢查的地方 ── 陣列・管線・比對的訣竅
- Microsoft Graph PowerShell 入門 ── AzureAD・MSOnline 廢止後的 Microsoft 365 維運
- 不要用 using 包住 HttpClient ── C# 商用應用程式的 HTTP 通訊實務(生成模式・逾時・重試)
相關諮詢領域
合同會社小村軟體處理使用核心系統或SaaS API的公司內部串接之設計與實作、把既有手動作業替換為API串接的自動化,以及通訊相關的不具狀況調查。
參考連結
-
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
-
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
-
Microsoft Learn,Invoke-WebRequest。關於將回應以WebResponseObject回傳、可存取StatusCode・Headers・Content,以及Windows PowerShell 5.1預設使用Internet Explorer引擎解析HTML、可用-UseBasicParsing迴避,而PowerShell 6以後不再相依IE、-UseBasicParsing會被忽略。 ↩ ↩2 ↩3
-
Microsoft Learn,ConvertTo-Json。關於-Depth的預設值為2、超過該深度的階層不會被轉換,以及-Compress可去除空白字元。 ↩ ↩2 ↩3
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
用 winget + PowerShell 自動化 PC 配置 ── 讓操作手冊變得可執行
本文整理讓新進員工 PC 的環境建置可重現的方法,涵蓋以 winget 進行應用程式導入與 export/import、WinGet Configuration 的宣告式組態、以 PowerShell 補充的設定,以及無人執行時的注意事項。
PowerShell 腳本太慢時該檢查的地方 ── 陣列・管線・比對的訣竅
整理 PowerShell 腳本變慢的常見原因。從實務角度解說陣列 += 造成 O(n^2) 的原因、管線與 foreach 的差異、比對的雜湊表化、檔案 I/O 的改善,以及正確的測量方式。
告別 Write-Host ── PowerShell 的輸出資料流與日誌設計
本文整理 PowerShell 六個輸出資料流的使用區分、Write-Host 所存在的問題與正確用途、函式傳回值被污染的原因、透過 -Verbose 與 -InformationVariable 由呼叫端進行控制的方法,以及結構化日誌的留存方式。
PowerShell 的並行處理 ── ForEach-Object -Parallel 與工作(Job)的選用之道
從實務角度整理 ForEach-Object -Parallel、Start-ThreadJob、Start-Job 的差異與選用方式,$using: 與執行緒安全性,ThrottleLimit 的決定方法,以及反而變慢的情況。
從 PowerShell 正確呼叫外部 exe ── 引數的引號、結束代碼、亂碼陷阱
從 PowerShell 呼叫 robocopy 或公司內部 EXE 時,引數會被破壞、拿不到結束代碼、輸出出現亂碼。本文從實務角度整理 PowerShell 7.3 的引數傳遞變更、停止剖析權杖 --%、以及 Start-Process 的使用時機。
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
常見問題
整理諮詢這個主題時常見的問題。
- 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的憑證放入執行環境的受信任根憑證授權單位存放區,並正確地發行伺服器憑證。即使是在驗證環境中暫時使用,也請透過設定檔或參數明確切換,避免混入正式環境的腳本中。