「从核心系统的 Web API 获取订单数据,导入公司内部的 Excel 报表」「每天早上调用 SaaS 考勤 API,输出当日的出勤预定」── 在 PowerShell 用于业务的各种场景中,这几年明显增多的就是 REST API 集成。买专门的工具还不至于,但靠手工又跑不动。作为填补这个空隙的工具,Invoke-RestMethod 非常强大。
另一方面,即便让它跑起来很简单,一旦投入运维就立刻变难,这也是 API 集成的特点。日语出现乱码、读不懂错误响应的内容、偶尔因 429 而失败、在代理环境中不通、只有 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、编码的处理这三点。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 的文字编码与换行符 - 乱码与 CRLF/LF 的基础》。
发送前和接收后,要通过往返来确认。从上面的 $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] 采用的是四舍五入(遇 .5 舍入到偶数),
# 因此剩余 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 的错误处理与重试设计 ── 从 try/catch 失效的陷阱到 exit code、重试的实务定式》。
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. 代理与证书
从公司内部访问互联网上的 API 时,通过代理是第一道关卡。如果不指定 -Proxy,就会使用通过 Internet 设置(Internet 选项)或环境变量配置的代理。2 也就是说,「什么都没指定」并不等于「不使用代理」。这正是结果会因执行账户而异的原因。
| 症状 | 应查看的原因 | 应对方法 |
|---|---|---|
407 Proxy Authentication Required |
代理要求进行认证 | 将 -Proxy 与 -ProxyCredential 或 -ProxyUseDefaultCredentials 一起指定2 |
| 本地能通,只有夜间批处理失败 | 拾取了因执行账户而异的 Internet 设置 | 在脚本中显式指定 -Proxy。用相同的执行账户进行验证 |
| 连公司内部 API 也被代理拦截 | 默认的代理配置也应用到了公司内部的目标 | 用 -NoProxy 显式绕过(PowerShell 6 以后)2 |
| 代理凭据的存放位置 | 直接写在脚本里 | 从 SecretManagement 的保管库中取出 |
# 显式指定代理,用登录用户的凭据进行认证
Invoke-RestMethod -Uri $uri -Proxy 'http://proxy.example.co.jp:8080' -ProxyUseDefaultCredentials
# 用专用账户通过带认证的代理。
# -ProxyCredential 要与 -Proxy 配套使用,不能与 -ProxyUseDefaultCredentials 同时使用
$proxyCred = Get-Secret -Name 'ProxyAccount' # 从保管库中取出
Invoke-RestMethod -Uri $uri -Proxy 'http://proxy.example.co.jp:8080' -ProxyCredential $proxyCred
# 面向公司内部的 API 不经过代理(PowerShell 6 以后)
Invoke-RestMethod -Uri 'https://api.internal.example.local/v1/ping' -NoProxy
-ProxyCredential 和 -ProxyUseDefaultCredentials,两者都以指定了 -Proxy 为前提,而且不能同时使用。2 Windows PowerShell 5.1 没有 -NoProxy。如果在 5.1 中只想让面向公司内部的通信绕开代理,就要通过执行账户的 Internet 设置中的例外列表(不使用代理的地址)来处理。
用任务计划程序的服务账户运行时,代理设置有时会和交互登录时不同。这是「本地能运行,只有夜间批处理失败」的典型模式。请用相同的执行账户进行验证(参见《任务计划程序的任务不执行、以 0x1 结束 ── 原因排查与安全的运维设计》)。不要把代理凭据直接写在脚本里,这一点和 API 令牌是一样的(参见《PowerShell 中凭据的安全处理方式 ── 把明文密码逐出脚本》)。
对于证书错误使用 -SkipCertificateCheck,请仅限于验证环境中的临时性规避。恒久的对处方法是,把公司内部 CA 的证书放入受信任的根证书颁发机构存储区,并正确签发服务器证书。
10. 实务定式(判断表)
| 论点 | 选项 | 判断标准 |
|---|---|---|
| 命令 | 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 相关命令的行为差异、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(绕过通过 Internet 设置或环境变量配置的代理)、-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 装机 ── 让操作手册可执行
本文整理了让新员工电脑的初始设置具备可复现性的方法,涵盖通过 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 的证书放入执行环境受信任的根证书颁发机构存储区,并正确签发服务器证书。即便是在验证环境中临时使用,也要通过配置文件或参数显式切换,避免混入生产脚本。