2026年8月6日

2026年8月6日

シェルスクリプトでAPIを呼び出す方法(curl):認証・エラー処理・リトライ・ページ送り

シェルスクリプトから API を呼ぶときは、curl に「タイムアウト」「HTTP エラーで失敗扱いにするオプション」「認証情報をコマンドラインに出さない渡し方」を最初から付け、レスポンスは jq で処理する、という形にすると安定します。この記事では WordPress の REST API を例に、GET・POST の基本、ステータスコードによる分岐、リトライ、ページ送り、つまずきやすいエラーの読み方を順に説明します。

curlの基本形:最初に付けておくオプション

まずは、公開されている投稿一覧を取得する例です。

curl -sS --fail-with-body \
  --connect-timeout 5 --max-time 30 \
  -H 'Accept: application/json' \
  'https://example.com/wp-json/wp/v2/posts?per_page=5&_fields=id,title,link' \
  | jq -r '.[] | "\(.id)\t\(.title.rendered)"'

各オプションの意味と、付けない場合に起きることは次のとおりです。

  • **-sS**: 進捗表示を消しつつ(-s)、エラーメッセージだけは表示する(-S)。-s だけだと失敗しても何も表示されず、原因が分かりません。
  • **--fail-with-body**: HTTP ステータスが 400 以上のとき、終了コード 22 で失敗扱いにします。curl は既定では 404 や 500 でも「通信には成功した」として終了コード 0 を返すため、これがないと set -e でも止まりません。--fail(-f)との違いは、エラー時のレスポンス本文も出力する点で、API が返すエラー理由を確認できます。比較的新しい curl(7.76.0 以降)のオプションなので、古い環境では --fail を使います。
  • **--connect-timeout と --max-time**: 接続待ちと全体の時間の上限です。付けないと、相手が応答しないときにスクリプトが止まったままになり、cron が多重起動する原因になります。
  • **_fields**: WordPress REST API のパラメーターで、必要な項目だけを返させます。転送量が減り、jq の処理も単純になります。

検索語などをクエリに入れるときは、URL に直接つなげずに -G --data-urlencode を使うと、空白や日本語、& を正しくエンコードしてくれます。

curl -sS --fail-with-body -G \
  --data-urlencode "search=${keyword}" \
  --data 'per_page=10' \
  'https://example.com/wp-json/wp/v2/posts'

ステータスコードで処理を分ける

「404 ならスキップ、429 なら待ってやり直し、それ以外のエラーは停止」のように分岐したい場合は、本文をファイルに保存し、ステータスコードを -w で受け取ります。

api_get() {
  local url="$1" body code
  body="$(mktemp)"
  if ! code="$(curl -sS -o "$body" -w '%{http_code}' \
        --connect-timeout 5 --max-time 30 \
        -H 'Accept: application/json' "$url")"; then
    echo "curl failed (network or timeout): $url" >&2
    rm -f "$body"
    return 1
  fi
  case "$code" in
    2??) cat "$body" ;;
    404) echo "not found: $url" >&2 ;;
    429) echo "rate limited: $url" >&2; rm -f "$body"; return 75 ;;
    *)   echo "HTTP ${code}: $(head -c 300 "$body")" >&2; rm -f "$body"; return 1 ;;
  esac
  rm -f "$body"
}

ここでは --fail-with-body を付けていません。付けると 400 以上で curl 自体が失敗し、ステータスごとの分岐に入る前に if の失敗側へ進むためです。通信そのものに失敗したときは -w の値が 000 になるので、curl の終了コードと HTTP ステータスを分けて扱う必要があります。エラー本文の表示を先頭300バイトに絞っているのは、HTML のエラーページが丸ごとログに流れ込むのを防ぐためです。

認証付きでPOSTする(Application Passwords)

WordPress の REST API で投稿を作成・更新するには認証が要ります。シェルからは、WordPress 5.6 で追加された Application Passwords(ユーザーのプロフィール画面で発行する、API 専用のパスワード)を Basic 認証で使うのが手軽です。通常は HTTPS のサイトでのみ有効になります。ログイン用のパスワードは使わず、用途ごとに発行し、不要になったら失効させてください。

: "${WP_USER:?set WP_USER}" "${WP_APP_PASSWORD:?set WP_APP_PASSWORD}"

payload="$(jq -n --arg title "API から作成した下書き" --arg content "本文です" \
  '{title: $title, content: $content, status: "draft"}')"

curl -sS --fail-with-body --max-time 30 -K - --json "$payload" \
  'https://example.com/wp-json/wp/v2/posts' <<EOF | jq '{id, status, link}'
user = "${WP_USER}:${WP_APP_PASSWORD}"
EOF

ポイントは3つです。

  1. 認証情報をコマンドラインに書かない: -u user:pass と書くと、実行中に ps コマンドで他のユーザーから見える場合があります。-K - は設定を標準入力から読む指定で、ヒアドキュメントで user = "..." を渡せばコマンドラインには現れません。--netrc-file で権限 600 のファイルから読む方法もあります。
  2. JSON は jq で組み立てる: "{\"title\": \"$title\"}" のように文字列を連結すると、タイトルに " が入った時点で壊れます。jq -n --arg なら必要なエスケープを jq が行います。
  3. **--json**: 本文を送り、Content-Type: application/json と Accept: application/json を自動で付けるオプションです(curl 7.82.0 以降)。古い curl では -H 'Content-Type: application/json' --data-binary "$payload" で代用します。

環境変数の中身は、権限を絞ったファイル(Git の管理外、Web から見えない場所)に置き、スクリプト側では source して読み込む形にします。-v やトレースの出力には Authorization ヘッダーが含まれるため、ログに残さないよう注意してください。

リトライとレート制限

一時的なネットワーク障害やサーバーの過負荷に備えて、curl のリトライ機能を使えます。

curl -sS --fail --retry 3 --retry-max-time 120 \
  --connect-timeout 5 --max-time 30 "$url"

--retry は、タイムアウトや 429・503 などの「一時的」とされるエラーのときだけ、間隔を空けて再試行します。対象になるステータスや、サーバーの Retry-After ヘッダーの扱いは curl の版によって異なるため、利用中の版の man curl で確認してください。--max-time は1回ごと、--retry-max-time はリトライ全体の上限です。

注意したいのは POST などの更新系です。サーバー側では処理が終わっているのに応答だけが途切れた場合、リトライで同じ投稿が二重に作られることがあります。更新系は自動リトライを避けるか、作成前に同じものがないかを確認する処理を入れてください。また、大量のリクエストを送るときは sleep を挟み、相手サーバーの負荷や利用規約に配慮します。

ページ送りで全件を取得する

WordPress REST API は1回あたり最大100件(per_page=100)までしか返しません。全体のページ数はレスポンスヘッダー X-WP-TotalPages に入っているので、それを見ながら順に取得します。

#!/usr/bin/env bash
set -euo pipefail
base='https://example.com/wp-json/wp/v2/posts'
workdir="$(mktemp -d)"
trap 'rm -rf "$workdir"' EXIT

page=1
total_pages=1
while (( page <= total_pages )); do
  curl -sS --fail --connect-timeout 5 --max-time 30 \
    -D "$workdir/headers" \
    -o "$workdir/page-$(printf '%03d' "$page").json" \
    "${base}?per_page=100&page=${page}&_fields=id,slug,date"
  total_pages="$(awk -F': *' 'tolower($1) == "x-wp-totalpages" { print $2 }' "$workdir/headers" | tr -d '\r')"
  total_pages="${total_pages:-1}"
  page=$((page + 1))
  sleep 1
done

jq -s 'add' "$workdir"/page-*.json > all-posts.json
jq 'length' all-posts.json

-D でヘッダーをファイルに保存し、tr -d '\r' でヘッダー行末の CR を取り除いています。これを忘れると数値の比較でエラーになります。ファイル名をゼロ埋めしているのは、page-10 が page-2 より先に並ぶのを防ぐためです。

エラーの読み方

curl が失敗したときは、まず終了コードを確認します。よく見るものは次のとおりです。

  • 6: ホスト名を解決できない。URL の綴りや DNS 設定を確認します。
  • 7: 接続できない。ポート、ファイアウォール、相手サーバーの停止を疑います。
  • 22: --fail 系オプション使用時に HTTP 400 以上が返った。本文やステータスを見て原因を探します。WordPress なら 401 は認証情報、403 は権限や WAF、404 はパーマリンク設定や URL の誤りが多い原因です。
  • 28: タイムアウト。相手の応答が遅いのか、こちらの上限が短すぎるのかを -w '%{time_total}' で測ります。
  • 35 / 60: TLS の接続や証明書の検証に失敗した。証明書の期限切れや中間証明書の不足が典型的です。-k で検証を無効にして回避するのは、通信の安全性を失うため本番では避けます。

原因が分からないときは -v でやり取りを表示できますが、前述のとおり認証ヘッダーが含まれる点に注意してください(2026年10月現在の curl 8 系で確認できる挙動を前提にしています)。

まとめ

  • -sS・--fail-with-body・タイムアウトを基本形にして、失敗を検知できるようにする。
  • ステータスごとに分岐したいときは -o と -w '%{http_code}' で本文とコードを分ける。
  • 認証情報は -K - や netrc で渡し、JSON は jq で組み立てる。
  • リトライは一時的なエラーに限り、更新系の二重実行に注意する。
  • REST API の全件取得は X-WP-TotalPages を見てページ送りし、終了コードから原因を切り分ける。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲