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つです。
- 認証情報をコマンドラインに書かない:
-u user:passと書くと、実行中にpsコマンドで他のユーザーから見える場合があります。-K -は設定を標準入力から読む指定で、ヒアドキュメントでuser = "..."を渡せばコマンドラインには現れません。--netrc-fileで権限 600 のファイルから読む方法もあります。 - JSON は jq で組み立てる:
"{\"title\": \"$title\"}"のように文字列を連結すると、タイトルに"が入った時点で壊れます。jq -n --argなら必要なエスケープを jq が行います。 - **
--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を見てページ送りし、終了コードから原因を切り分ける。