用 PowerShell 与 REST API 集成 ── Invoke-RestMethod 的实务

· · PowerShell, REST API, Windows, 自动化, 业务系统, 集成, JSON, 运维改善

「从核心系统的 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 / -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、编码的处理这三点。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 的文字编码与换行符 - 乱码与 CRLF/LF 的基础》。

发送前和接收后,要通过往返来确认。从上面的 $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] 采用的是四舍五入(遇 .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 的输出流与日志设计》)。

幂等性(同一请求发送多次结果都不变的性质)的处理是第二个要点。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 的错误处理与重试设计 ── 从 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) 有时需要 -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. 代理与证书

从公司内部访问互联网上的 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 服务器。

下载示例代码(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 相关命令的行为差异、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(绕过通过 Internet 设置或环境变量配置的代理)、-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 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表