PowerShellでREST APIと連携する ── Invoke-RestMethodの実務

· 更新日: · · PowerShell, REST API, Windows, 自動化, 業務システム, 連携, JSON, 運用改善

更新履歴(4件・最終更新 2026年08月02日)

この記事に加えた変更の記録です。アーカイブした更新前のバージョンは、DOI付きの固定URLから読めます。

記事の冒頭に「この記事の知識マップ」節を追加しました。本文で扱っている概念とその関係を、要約・図・詳細ページへのリンクにまとめたものです。本文の主張は変えていません。
外部レビュー(1283件)への対応として本文を更新しました。個々の変更内容は、この下の履歴を参照してください。
`Retry-After`がHTTP日付形式のときの待ち時間を切り上げるようにしました。`[int]`へのキャストは切り捨てではなく四捨五入(端数がちょうど0.5なら偶数側)なので、応答が届くまでの時間を差し引いた残りが0.5秒を切ると0になります。`Start-Sleep -Seconds 0`は即座に返るため、サーバーの指定した時刻より前に再送し、また429を受け取って試行回数だけを使い切ります。`[math]::Ceiling`を挟み、理由も本文に追記しました。
冪等の意味を初出の位置で1行定義し、送るJSONと返るJSONの往復例や再試行時のメッセージなど、確認のよりどころを4か所に追加しました。あわせて前提環境の表と、プロキシ環境での症状別の対処表を追加しています。
初版公開
この記事を引用する(DOI: 10.5281/zenodo.21547449)

この記事はZenodoにアーカイブされています。常に最新版へ解決されるDOIと、いま表示している版に固定されたDOIの両方を下に示します。

小村 豪(2026)「PowerShellでREST APIと連携する ── Invoke-RestMethodの実務」合同会社小村ソフト. https://doi.org/10.5281/zenodo.21547449 https://staging.comcomponent.com/blog/powershell-rest-api-invoke-restmethod/

DOI(最新版)
10.5281/zenodo.21547449
DOI(この版)
10.5281/zenodo.21733159

「基幹システムの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バイト配列で送るのが確実です。-ContentTypecharset=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を信頼させるのが正しい対処です。

この記事の知識マップ

この記事は、PowerShellからJSON APIを呼ぶ実務をInvoke-RestMethodを中心に整理する。ConvertTo-Jsonは既定の深さが2であるため入れ子の切り捨てを招きやすく、日本語の文字化けはUTF-8のバイト配列に変換してから送るのが確実だとしている。4xx/5xxは終了エラーになりエラー本文の確認が調査の鍵になるとしつつ、-MaximumRetryCountの組み込み再試行は400から599までを対象にするため、401や404のような恒久的エラーまで再送してしまう点を指摘し、コード別の扱いや指数バックオフ、POSTのような非冪等な要求を再送しない判断が必要な場面では自前のリトライ関数を推奨している。Windows PowerShell 5.1ではTLS 1.2の明示や-SkipHttpErrorCheck・-Authenticationの非対応が壁になり、恒久運用では-SkipCertificateCheckで証明書検証を無効化せず社内CAを信頼させるべきだとしている。

PowerShellによるREST API連携の知識マップInvoke-RestMethodを中心に、JSON送受信の文字化けと入れ子切り捨て対策、エラー処理、429の組み込み再試行と自前の指数バックオフ・冪等性判定、ページング、Windows PowerShell 5.1固有の非互換、プロキシと証明書の扱いが結びつく関係を示す図。利用する利用する利用する原因になり得る原因になり得る推奨される対応利用する利用する利用する用いるのは非推奨利用する利用する前提とする推奨される対応利用する利用するで構成できる前提とする両立しない両立しない利用する利用する利用する用いるのは非推奨Invoke-RestMethodREST APIPowerShellInvoke-WebRequestConvertTo-JsonJSONの入れ子の切り捨てJSON送受信時の文字化けUTF-8バイト配列化-SkipHttpErrorCheckパラメーター-MaximumRetryCount / -RetryIntervalSecパラメーターRetry-Afterヘッダー恒久的なHTTPエラー(401/403/404など)自前のリトライ関数(Invoke-KsApiパターン)指数バックオフHTTPメソッドの冪等性カーソル/オフセット方式のページング-FollowRelLinkパラメータープロキシ構成(-Proxy / -ProxyCredential / -NoProxy)Windows PowerShell 5.1TLS 1.2の明示的な有効化-Authenticationパラメータークライアント証明書-SkipCertificateCheckパラメーター

図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全24件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle

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) Bearerを -Token で渡す(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-Typecharset が正しく宣言されているかを疑ってください(-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が返した本文が文字列のまま入ります。JSONを返すAPIなら $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
    )

    # 再試行してよいのは、同じ要求を2回受け取っても結果が変わらない場合だけ。
    # 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 が1行出ます(HTTP 429 ── 30 秒後に再試行します (1/4) の形です)。見るのは3点で、待機秒数がサーバーの指示どおりか試行回数が -MaxAttempts の範囲に収まっているか401や404のような恒久的エラーで再試行せず即座に例外になっているかです。無人実行では画面が残らないので、警告ストリームもログに落としてください(「Write-Hostをやめる ── PowerShellの出力ストリームとログ設計」)。

冪等性(同じ要求を何度送っても結果が変わらない性質)の扱いが2つ目のポイントです。GETPUT は同じ要求を2回受け取っても結果が変わりませんが、POST は違います。とくに「サーバー側では登録に成功したが、応答が返る前に通信が切れた」というケースでは、素朴に再送すると二重登録になります。上の実装では、冪等なメソッドか、APIが冪等キー(Idempotency-Key)に対応している場合だけ自動再試行を許可し、それ以外は「処理済みかを確認してください」と明示して止めています。

Retry-AfterHTTP日付形式で返ってきたときは、残り時間を切り上げてください。[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が全件を一度に返すことはまずありません。方式は主に2つです。

(1) Link ヘッダー方式(GitHubなどが採用)は、-FollowRelLink で自動的に次ページをたどれます。2

# 次のページを自動でたどる(取得ページ数の上限も指定できる)
$all = Invoke-RestMethod -Uri $uri -Headers $headers -FollowRelLink -MaximumFollowRelLink 20

(2) カーソル/オフセット方式は自前でループします。次のコードは§6で定義した Invoke-KsApi を使っている点に注意してください。この節だけを試すなら、Invoke-KsApi -Uri $u -Headers $headersInvoke-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)"

$itemsList[T] を使っているのは、+= による配列の再作成を避けるためです(「PowerShellスクリプトが遅いときに見るところ」)。

確認は最後の1行で行います。取得件数: ... に出た数を、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 を指定しない場合は、インターネット設定(インターネット オプション)や環境変数で構成されたプロキシが使われます。2 つまり「何も指定していない」は「プロキシを使わない」ではありません。ここが、実行アカウントによって結果が変わる原因です。

症状 原因として見るところ 対処
407 Proxy Authentication Required プロキシが認証を要求している -Proxy と一緒に -ProxyCredential-ProxyUseDefaultCredentials を指定する2
手元では通るのに夜間バッチだけ失敗する 実行アカウントごとに違うインターネット設定を拾っている スクリプト側で -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で社内宛ての通信だけをプロキシから外したい場合は、実行アカウントのインターネット設定側にある例外リスト(プロキシを使わないアドレス)で対処することになります。

タスクスケジューラのサービスアカウントで動かすと、対話ログオン時とはプロキシ設定が違うことがあります。「手元では動くのに夜間バッチだけ失敗する」の典型パターンです。実行アカウントを合わせて検証してください(「タスクスケジューラのタスクが実行されない」)。プロキシの資格情報をスクリプトに直書きしないことは、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(インターネット設定や環境変数で構成されたプロキシを迂回すること)、-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の証明書を実行環境の信頼されたルート証明機関ストアに配置し、サーバー証明書を正しく発行することです。検証環境で一時的に使う場合でも、本番スクリプトに混入しないよう、設定ファイルやパラメーターで明示的に切り替える形にしてください。

著者プロフィール

記事の著者プロフィールページです。

小村 豪

合同会社小村ソフト 代表

Windows ソフト開発、技術相談、不具合調査を中心に、既存資産が残る案件や原因が見えにくい障害調査に強みがあります。

ブログ一覧に戻る