curl コマンドの使い方 - HTTP リクエストの送り方とデバッグに使うオプション

curl コマンドの使い方 - HTTP リクエストの送り方とデバッグに使うオプション

作成日:
読了:約70分
更新日:

当サイトは Amazon.co.jp を宣伝しリンクすることで紹介料を得る手段を提供する、Amazonアソシエイト・プログラムの参加者です。価格・在庫はリンク先の最新情報をご確認ください。

APIの動作確認、本番サーバーの疎通チェック、CIの中でのファイル取得。Web開発をしていると、ブラウザを使わずにHTTPリクエストを送りたい場面が毎日のようにあります。そのときに最初に手が伸びるのがcurlです。

curlはオプションが非常に多く、検索して出てきたコマンドをそのまま貼り付けて使っている人も多いと思います。ただ、-X POSTを毎回付けていたり、-dでファイルを送って改行が消えていたり、エラー応答なのにスクリプトが成功扱いで進んでいたりと、仕組みを知らないと気づきにくい落とし穴がいくつもあります。

この記事では、curlの基本的な使い方から、POSTやファイル送信、認証、デバッグに使うオプション、スクリプトに組み込むときの注意、よくあるつまずきまでを、実行結果付きで説明します。

NOTE

2026年11月13日に、curlのmanページ、Everything curl、curlの変更履歴、curlの歴史で仕様と導入バージョンを確認しました。コマンドの実行結果は、特に断りがなければmacOS 27.2、Homebrewのcurl 8.23.0、zshのものです。執筆時点(2026-11-13)の最新の安定版は、curl.seの表示で8.23.0(2026年10月14日公開)です。リクエストの送り先には、example.comと、リクエストの内容をJSONで返してくれるテスト用サービスのhttpbin.orgを使っています。実行結果に出てくる接続元のIPアドレスは、文書用のアドレス(203.0.113.10など)に書き換えています。

先に結論: よく使う書き方の早見表

まず、使う頻度が高いものを並べます。それぞれの意味は後の章で順に説明します。

やりたいこと書き方
GETして本文を表示するcurl https://example.com/
ファイルに保存するcurl -o page.html https://example.com/
URLのファイル名で保存するcurl -O https://example.com/file.zip
リダイレクトを追いかけるcurl -L https://example.com/
レスポンスヘッダーも表示するcurl -i https://example.com/
レスポンスヘッダーだけ見るcurl -I https://example.com/
やり取りの詳細を見るcurl -v https://example.com/
JSONをPOSTするcurl --json '{"name":"curl"}' URL
フォーム形式でPOSTするcurl -d 'name=curl' URL
ファイルをアップロードするcurl -F 'file=@photo.png' URL
ヘッダーを追加するcurl -H 'X-Request-Id: 123' URL
Basic認証curl -u user:password URL
ステータスコードだけ出すcurl -s -o /dev/null -w '%{http_code}\n' URL
HTTPエラーを失敗として扱うcurl -f URL

スクリプトに書くときの定番の組み合わせは次のとおりです。

スクリプトでよく使う組み合わせ
# 進捗は出さない、エラーは出す、4xx/5xxは失敗にする、リダイレクトは追う
curl -fsSL https://example.com/install.sh -o install.sh
 
# タイムアウトとリトライを付ける
curl -fsS --connect-timeout 5 --max-time 30 --retry 3 https://example.com/api/health

事故を防ぐために覚えておきたいことも4つ挙げておきます。

  • curlは、HTTPのステータスコードが404や500でも終了コード0で終わります。失敗として扱いたいときは-fか--fail-with-bodyを付けます。
  • -dを付ければPOSTになります。-X POSTは要りません。
  • -d @fileは、ファイルの中の改行を取り除いて送ります。そのまま送りたいときは--data-binary @fileか--json @fileを使います。
  • -kは証明書の検証をやめるオプションです。原因を調べずに常用してはいけません。

curlとは

コマンドとライブラリ

curlは、URLで指定したサーバーとデータを送受信するためのコマンドラインツールです。manページの冒頭には、対応するプロトコルとして次のものが挙げられています。

manページに書かれている対応プロトコル
DICT, FILE, FTP, FTPS, GOPHER, GOPHERS, HTTP, HTTPS, IMAP, IMAPS, LDAP, LDAPS,
MQTT, MQTTS, POP3, POP3S, RTSP, SCP, SFTP, SMTP, SMTPS, TELNET, TFTP, WS and WSS

HTTPとHTTPSで使うことがほとんどですが、FTPやSFTP、メールの送信、WebSocketまで扱えます。この記事ではHTTPとHTTPSに絞って説明します。

curlの転送処理の本体は、libcurlというライブラリです。curlコマンドは、libcurlをコマンドラインから使うためのプログラムという位置づけになります。libcurlはPHPのcURL関数など、多くの言語やソフトウェアから利用されています。curlの歴史のページには、PHPが早い時期にlibcurlを採用し、PHP 4.0.2からサポートされるモジュールになったと書かれています。

自分の環境のcurlが何に対応しているかは、curl --versionで分かります。

curl --version
実行結果(Homebrewのcurl 8.23.0。長い行は折り返しています)
curl 8.23.0 (aarch64-apple-darwin25.6.0) libcurl/8.23.0 OpenSSL/4.1.0 zlib/1.2.12
  brotli/1.2.1 zstd/1.5.7 AppleIDN libpsl/0.23.3 libssh2/1.11.1 nghttp2/1.70.0
  ngtcp2/1.25.0 nghttp3/1.18.0 ...
Release-Date: 2026-10-14
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps
  mqtt mqtts pop3 pop3s rtsp scp sftp smtp smtps telnet tftp ws wss
Features: alt-svc AppleSecTrust AsynchDNS brotli GSS-API HSTS HTTP2 HTTP3 HTTPS-proxy
  IDN IPv6 Kerberos Largefile libz PSL SPNEGO SSL threadsafe UnixSockets zstd

1行目にcurlとlibcurlのバージョン、使っているTLSライブラリなどが出ます。FeaturesにHTTP3があればHTTP/3が使え、brotliやzstdがあればその圧縮形式を展開できます。対応する機能はビルドによって違うので、手元とサーバーで結果が違うときは、まずここを比べます。

なお、同じマシンでも、macOS 27.2に付属している/usr/bin/curlは8.7.1でした。後で出てくる--follow(8.16.0で追加)のような新しいオプションは、古いcurlでは使えません。記事中では、オプションごとに導入されたバージョンを添えています。

名前と歴史

Everything curlの名前の説明によると、curlの「c」はClientを表し、「Client」と「URL」でcURLです。「see URL」(URLを見る)と読めることも後押しになった、と書かれています。表記はcURLでもcurlでもよく、プロジェクトでは小文字のcurlと書くことが多いそうです。

歴史のページで確認できた範囲の流れは次のとおりです。

  • 1996年11月11日に、Rafael Sagula氏がHttpGet 0.1を公開した
  • Daniel Stenberg氏がこれを拡張し、まもなくメンテナーになった
  • FTPのダウンロードに対応した1997年8月に、名前がurlget(2.0)に変わった
  • アップロードにも対応して名前が合わなくなり、1998年3月20日にcurl 4として公開された(バージョン番号は前の名前から引き継がれた)

その後、内部が大きく作り直されてライブラリとして使えるようになり、7.1がlibcurlの最初の正式なリリースになったと書かれています。

基本: GETと出力の扱い

URLを渡すだけでGETになる

オプションなしでURLを渡すと、GETリクエストを送り、レスポンスの本文を標準出力に書き出します。

curl 'https://httpbin.org/get?name=curl'
実行結果
{
  "args": {
    "name": "curl"
  },
  "headers": {
    "Accept": "*/*",
    "Host": "httpbin.org",
    "User-Agent": "curl/8.23.0"
  },
  "method": "GET",
  "origin": "203.0.113.10",
  "url": "https://httpbin.org/get?name=curl"
}

httpbin.orgは受け取ったリクエストをそのままJSONで返すので、curlが何を送ったのかが分かります。何も指定しなくても、Accept: */*とUser-Agent: curl/8.23.0が付いていることが読み取れます。

URLはシングルクォートで囲む癖を付けておくと安全です。理由は「よくあるつまずき」の章で説明します。

-oと-O: ファイルに保存する

-o(--output)は、出力先のファイル名を指定します。-O(--remote-name)は、URLの最後の部分をファイル名として、カレントディレクトリに保存します。

# 名前を指定して保存する
curl -o robots-copy.txt https://httpbin.org/robots.txt
 
# URLのファイル名(robots.txt)で保存する
curl -O https://httpbin.org/robots.txt
 
# 保存先のディレクトリが無ければ作る
curl -o out/dir/a.txt --create-dirs https://httpbin.org/robots.txt

本文を捨てたいときは-o /dev/nullと書きます。ステータスコードや所要時間だけを知りたいときに、後で出てくる-wと組み合わせて使います。

シェルのリダイレクト(> file)でも保存はできますが、-oを使うと、リトライのときにcurlが書きかけの出力を片付けてくれます。リダイレクトの仕組みはパイプとリダイレクトの記事で説明しています。

-L: リダイレクトを追いかける

curlは、既定ではリダイレクトを追いかけません。301や302が返ってきたら、その応答をそのまま表示して終わります。-L(--location)を付けると、Locationヘッダーの行き先にリクエストを送り直します。

# -L なし: 302 と、行き先のURLが分かる
curl -s -o /dev/null -w '%{http_code} -> %{redirect_url}\n' \
  'https://httpbin.org/redirect-to?url=/get&status_code=302'
 
# -L あり: 3回リダイレクトした後の最終結果
curl -s -L -o /dev/null -w '%{http_code} %{num_redirects} %{url_effective}\n' \
  https://httpbin.org/redirect/3
実行結果
302 -> https://httpbin.org/get
200 3 https://httpbin.org/get

「curlで取得したら中身が空だった」「短いHTMLしか返ってこない」というときは、リダイレクトされていることがよくあります。-Iでステータスコードを確かめるか、-Lを付けてみてください。リダイレクトのステータスコードごとの意味はHTTPステータスコードの記事にまとめています。

-sと-S: 進捗とエラーメッセージ

出力をファイルやパイプに向けると、curlは進捗メーターを標準エラー出力に表示します。スクリプトのログでは邪魔になるので、-s(--silent)で消します。

ただし、-sは進捗だけでなくエラーメッセージも消します。失敗したのに何も表示されないと原因が分からないので、-S(--show-error)を一緒に付けて、エラーメッセージだけは出すようにします。これが-sSという組み合わせです。

進捗メーターだけを消して、警告などはそのまま出したいときは、--no-progress-meter(7.67.0で追加)が使えます。

-f: HTTPエラーを失敗にする

curlは、HTTPのステータスコードが404でも500でも、レスポンスを最後まで受け取れたなら「成功」として終了コード0を返します。

curl -s https://httpbin.org/status/404; echo "exit=$?"
curl -sS -f https://httpbin.org/status/404; echo "exit=$?"
実行結果
exit=0
curl: (22) The requested URL returned error: 404
exit=22

-f(--fail)を付けると、ステータスコードが400以上のときに本文を出力せず、終了コード22で終わります。シェルスクリプトやCIでset -eと組み合わせるなら、-fはほぼ必須です。

-fは本文を捨てるので、APIが返したエラーの詳細も見えなくなります。本文も欲しいときは、後で説明する--fail-with-bodyを使います。なお、manページには、-fは完全ではなく、認証が絡む場合(401と407)などにすり抜けることがあると書かれています。

-iと-I: レスポンスヘッダーを見る

-i(--show-headers)は、レスポンスヘッダーを本文の前に付けて出力します。-I(--head)はHEADメソッドでリクエストを送り、ヘッダーだけを表示します。

curl -si https://httpbin.org/status/204
実行結果
HTTP/2 204
access-control-allow-credentials: true
access-control-allow-origin: *
access-control-expose-headers: Retry-After, RateLimit, RateLimit-Policy, WWW-Authenticate
content-type: text/html; charset=utf-8
x-content-type-options: nosniff
date: Thu, 12 Nov 2026 20:03:08 GMT
 

1行目がステータス行、その後がレスポンスヘッダーで、空行の後に本文が続きます(204なので本文はありません)。-iの長いオプション名は以前は--includeでしたが、8.10.0から--show-headersになりました。古い名前も引き続き使えます。

-IはHEADメソッドを使うので、サーバーによってはGETと違う応答を返すことがあります。GETのときのヘッダーを確かめたい場合は、-iか、-s -o /dev/null -D -(ヘッダーを標準出力に書き出す)を使うほうが確実です。

送信したリクエストのヘッダーは-iでは見えません。そちらを見たいときは、次に説明する-vを使います。

POST・PUT・DELETEとリクエストボディ

-d: フォーム形式で送る

-d(--data)は、ボディを付けてPOSTリクエストを送ります。HTMLのフォームを送信したときと同じ形式で、Content-Typeはapplication/x-www-form-urlencodedになります。複数回指定すると、&でつながれます。

curl -s -d 'name=curl' -d 'lang=ja' https://httpbin.org/post \
  | jq -c '{form, ct: .headers["Content-Type"]}'
実行結果
{"form":{"lang":"ja","name":"curl"},"ct":"application/x-www-form-urlencoded"}

-dは、値をURLエンコードしません。manページにも、データは指定されたとおりにサーバーへ渡すと書かれています。空白や&、日本語を含む値を送るときは、--data-urlencodeを使います。

curl -s --data-urlencode 'q=a b&c=日本語' https://httpbin.org/post | jq -c '.form'
実行結果
{"q":"a b&c=日本語"}

name=contentの形で書くと、contentの部分だけがエンコードされます。&を含んだ値が、1つのフィールドqとして届いていることが分かります。パーセントエンコーディングの仕組みはURLエンコードの記事で説明しています。

-G(--get)を付けると、-dや--data-urlencodeで指定したデータを、ボディではなくクエリ文字列としてURLの後ろに付けてGETで送ります。クエリ文字列を自分でエンコードしなくて済むので便利です。

curl -s -G --data-urlencode 'q=curl 使い方' https://httpbin.org/get | jq -r .url
実行結果
https://httpbin.org/get?q=curl+%E4%BD%BF%E3%81%84%E6%96%B9

--json: JSONを送る

JSONを送るときは、--jsonを使うのが一番短く書けます。7.82.0で追加されたオプションで、manページによると次の3つを指定したのと同じ意味になります。

--json が置き換える3つのオプション
--data-binary [arg]
--header "Content-Type: application/json"
--header "Accept: application/json"
curl -s --json '{"name":"curl","tags":["http","cli"]}' https://httpbin.org/post
実行結果
{
  "args": {},
  "headers": {
    "Accept": "application/json",
    "Content-Length": "37",
    "Content-Type": "application/json",
    "Host": "httpbin.org",
    "User-Agent": "curl/8.23.0"
  },
  "method": "POST",
  "origin": "203.0.113.10",
  "url": "https://httpbin.org/post",
  "data": "{\"name\":\"curl\",\"tags\":[\"http\",\"cli\"]}",
  "files": {},
  "form": {},
  "json": {
    "name": "curl",
    "tags": [
      "http",
      "cli"
    ]
  }
}

--jsonは、渡された文字列が正しいJSONかどうかを検査しません。ファイルから読むときは--json @body.json、標準入力から読むときは--json @-と書きます。

7.82.0より古いcurlでは、-Hと-dを組み合わせます。古い解説記事でよく見る書き方です。

curl -s -H 'Content-Type: application/json' -d '{"name":"curl"}' https://httpbin.org/post

ここで-H 'Content-Type: application/json'を忘れると、-dの既定であるapplication/x-www-form-urlencodedで送られます。実際に試すと、サーバー側ではJSON全体が1つのフォームのキーとして解釈されました。

curl -s -d '{"name":"curl"}' https://httpbin.org/post | jq -c '{json, form}'
実行結果
{"json":null,"form":{"{\"name\":\"curl\"}":""}}

「JSONを送っているのにAPIが400や415を返す」というときは、まずContent-Typeを疑ってください。

-dの仲間: --data-rawと--data-binary

-dには似た名前のオプションがいくつかあります。違いは、@の扱いと改行の扱いです。

オプション@fileで読み込むかファイルの改行URLエンコード
-d / --data読み込む取り除くしない
--data-raw読み込まない(@も文字として送る)-しない
--data-binary読み込むそのまましない
--data-urlencode読み込むそのまま(エンコードされる)する
--json読み込むそのまましない

-d @fileは、ファイルの中の復帰(CR)、改行(LF)、ヌルバイトを取り除いて送ります。3行のテキストファイルで確かめます。

printf 'line1\nline2\nline3\n' > lines.txt
 
curl -s -d @lines.txt https://httpbin.org/post | jq -c '.form'
curl -s --data-binary @lines.txt -H 'Content-Type: text/plain' https://httpbin.org/post | jq -c '.data'
実行結果
{"line1line2line3":""}
"line1\nline2\nline3\n"

-dでは3行がつながってline1line2line3になっています。CSVやNDJSON、改行を含むテキストを送るときは、--data-binaryを使ってください。JSONは改行が無くても意味が変わらないことが多いので気づきにくいのですが、--json @fileを使っておけば改行はそのまま送られます。

--data-rawは、値が@で始まっていてもファイル名として扱いません。「@user」のような文字列をそのまま送りたいときに使います。

-X: メソッドを変える

PUT、PATCH、DELETEを送るときは、-X(--request)でメソッドを指定します。

# PUT でJSONを送る
curl -s -X PUT --json '{"name":"curl2"}' https://httpbin.org/put
 
# PATCH
curl -s -X PATCH --json '{"name":"curl3"}' https://httpbin.org/patch
 
# DELETE
curl -s -X DELETE https://httpbin.org/delete

-Xは、リクエストに書くメソッドの文字列を置き換えるだけのオプションです。manページには「通常このオプションは必要ない。GET、HEAD、POST、PUTは、それぞれ専用のオプションで行う」という趣旨の説明があります。-dを付ければPOSTになり、-Iを付ければHEADになります。-Xが必要になるのは、PUTやPATCH、DELETEのように、専用のオプションでは表せないメソッドを送るときです。

ファイルをそのままPUTで送るなら、-T(--upload-file)も使えます。curl -T backup.tar.gz https://example.com/upload/のように書くと、PUTでファイルの中身を送ります。

メソッドの使い分けや冪等性の考え方はREST API設計の記事で扱っています。

-F: ファイルをアップロードする

-F(--form)は、multipart/form-dataでPOSTします。HTMLのフォームでファイルを選んで送信するのと同じ形式です。値の先頭に@を付けると、そのファイルを添付します。

printf 'hello\n' > hello.txt
 
curl -s -F 'title=sample' -F 'file=@hello.txt;type=text/plain' https://httpbin.org/post \
  | jq -c '{form, files, ct: .headers["Content-Type"]}'
実行結果
{"form":{"title":"sample"},"files":{"file":"hello\n"},"ct":"multipart/form-data; boundary=------------------------xJeciqkw4p4iIYyfDj5998"}

Content-Typeのboundaryは、curlが自動で作ります。ここで-H 'Content-Type: multipart/form-data'を自分で指定すると、boundaryが付かなくなり、サーバーがボディを解釈できなくなります。-Fを使うときはContent-Typeを指定しないでください。

-Fの値には、@のほかに<も使えます。@はファイルとして添付し、<はファイルの中身をテキストのフィールドの値として送ります。また、-dと-Fは同時には使えません。

-H: ヘッダーを追加する

-H(--header)は、リクエストヘッダーを追加します。何回でも指定できます。

curl -s -H 'Accept-Language: ja' -H 'X-Request-Id: test-001' https://httpbin.org/headers

curlが自動で付けるヘッダーと同じ名前を指定すると、指定した値に置き換わります。-H 'Host: other.example.com'のように書けばHostヘッダーを変えられます。コロンの右側を空にすると(-H 'Accept:')、そのヘッダーを送らなくなります。

User-Agentは-A(--user-agent)でも指定できます。一部のサイトは、curlの既定のUser-Agentからのアクセスに、ブラウザとは違う応答を返します。

認証とCookie

-u: Basic認証

-u(--user)にユーザー名:パスワードを渡すと、Basic認証のAuthorizationヘッダーを付けて送ります。

curl -s -u demo:secret https://httpbin.org/basic-auth/demo/secret
curl -s -u demo:secret https://httpbin.org/headers | jq -r '.headers.Authorization'
実行結果
{
  "authenticated": true,
  "user": "demo",
  "authorized": true
}
Basic ZGVtbzpzZWNyZXQ=

ZGVtbzpzZWNyZXQ=はdemo:secretをBase64にしただけの文字列で、暗号化ではありません。HTTPSでない通信でBasic認証を使うと、パスワードがそのまま読める形で流れます。

ユーザー名だけを指定すると(-u demo)、curlがパスワードを尋ねてきます。manページには、コマンドラインに書いたパスワードは、プロセスの一覧から他のユーザーに見える瞬間があるという注意があります。シェルの履歴にも残るので、共有のサーバーでは、パスワードの入力を求めさせるか、次に説明する設定ファイルから読ませるほうが安全です。

Bearerトークン

APIの認証でよく使うBearerトークンは、-HでAuthorizationヘッダーを書くのが一般的です。

curl -s -H 'Authorization: Bearer dummy-token-123' https://httpbin.org/bearer
実行結果
{
  "authenticated": true,
  "token": "dummy-token-123"
}

--oauth2-bearer dummy-token-123と書いても、同じ結果になりました。トークンを環境変数に入れている場合は、-H "Authorization: Bearer $API_TOKEN"のようにダブルクォートで囲んで、シェルに変数を展開させます。トークンの中身がJWTの場合の読み方はJWTの記事を参照してください。

-Lと認証を組み合わせるときの挙動も知っておくと安心です。manページによると、-uや--oauth2-bearerで指定した認証情報は、最初のホストにだけ送られ、リダイレクト先が別のホストなら送られません。別のホストにも送りたいときのために--location-trustedがありますが、リダイレクト先に認証情報を渡すことになるので、行き先を信頼できる場合に限って使います。

Cookieを送るのが-b(--cookie)、受け取ったCookieをファイルに保存するのが-c(--cookie-jar)です。

# 値を直接指定して送る
curl -s -b 'theme=dark; lang=ja' https://httpbin.org/cookies
 
# サーバーが Set-Cookie で返したCookieをファイルに保存する
curl -s -c cookies.txt -o /dev/null 'https://httpbin.org/cookies/set?session=abc123'
cat cookies.txt
 
# 保存したCookieを次のリクエストで送る
curl -s -b cookies.txt https://httpbin.org/cookies
実行結果
{
  "cookies": {
    "lang": "ja",
    "theme": "dark"
  }
}
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
 
httpbin.org	FALSE	/	TRUE	0	session	abc123
{
  "cookies": {
    "session": "abc123"
  }
}

-bの引数に=が含まれていれば、Cookieの値そのものとして扱われます。=が無ければファイル名として扱われ、そのファイルからCookieを読み込みます。

ログインが必要なページを順にたどるときは、-b cookies.txt -c cookies.txtと両方を付けて、読み込みと保存に同じファイルを使います。ログインのリクエストで受け取ったセッションCookieが、次のリクエストで送られます。Cookieの属性やセッションの仕組みはCookieとセッションの記事で説明しています。

デバッグに使うオプション

-v: やり取りを全部見る

-v(--verbose)は、名前解決、接続、TLSのハンドシェイク、送ったリクエスト、受け取ったレスポンスヘッダーを表示します。「何が起きているのか分からない」ときに、最初に付けるオプションです。

curl -v -o /dev/null https://example.com
実行結果(抜粋。途中のTLSの行と進捗表示は省略しています)
* Host example.com:443 was resolved.
* IPv6: (none)
* IPv4: 104.20.23.154, 172.66.147.243
*   Trying 104.20.23.154:443...
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
...
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
*   subject: CN=example.com
*   start date: Sep 26 22:49:11 2026 GMT
*   expire date: Dec 25 22:56:35 2026 GMT
*   issuer: C=US; O=SSL Corporation; CN=Cloudflare TLS Issuing ECC CA 3
*   subjectAltName: "example.com" matches cert's "example.com"
* SSL certificate verified via OpenSSL.
* Established connection to example.com (104.20.23.154 port 443) from 192.0.2.10 port 60904
* using HTTP/2
> GET / HTTP/2
> Host: example.com
> User-Agent: curl/8.23.0
> Accept: */*
>
* Request completely sent off
< HTTP/2 200
< date: Thu, 12 Nov 2026 20:03:32 GMT
< content-type: text/html; charset=utf-8
< server: cloudflare
< alt-svc: h3=":443"; ma=86400
<
{ [5 bytes data]
* Connection #0 to host example.com:443 left intact

行の先頭の記号で、何の情報かが分かります。

先頭の記号意味
*curlからの補足情報(名前解決、接続、TLSなど)
>curlが送ったヘッダー
<curlが受け取ったヘッダー
}curlが送ったデータ
{curlが受け取ったデータ

読むときの順番は、上から「どのIPアドレスにつないだか」「TLSのバージョンと証明書は想定どおりか」「送ったリクエストのメソッド、パス、ヘッダーは正しいか」「返ってきたステータスとヘッダーは何か」です。証明書の有効期限や発行者、ALPNで決まったHTTPのバージョンもここで確認できます。TLSのハンドシェイクの各段階の意味はTLS 1.3の記事で説明しています。

-vの出力は標準エラー出力に出ます。ファイルに残したいときはcurl -v URL 2> debug.log、本文と一緒にパイプに流したいときは2>&1を付けます。

8.10以降では、-vv、-vvvと重ねることで出力を増やせます。manページによると、2つでタイムスタンプと転送のIDが付き、3つで送受信した内容も表示されます。

WARNING

-vや--traceの出力には、AuthorizationヘッダーやCookie、送信したボディがそのまま含まれます。manページにも、ログを他人と共有するときは注意するよう書かれています。Issueやチャットにそのまま貼り付けず、トークンやセッションIDを伏せてから共有してください。

--traceと--trace-ascii: ボディまで記録する

-vはヘッダーまでしか表示しません。送受信したボディの中身まで確かめたいときは、--trace-asciiを使います。引数は出力先のファイル名で、-を指定すると標準出力に出ます。

curl -s --trace-ascii - -o /dev/null --json '{"a":1}' https://httpbin.org/post
実行結果(送信部分の抜粋)
=> Send header, 142 bytes (0x8e)
0000: POST /post HTTP/2
0013: Host: httpbin.org
0026: User-Agent: curl/8.23.0
003f: Content-Type: application/json
005f: Accept: application/json
0079: Content-Length: 7
008c:
=> Send data, 7 bytes (0x7)
0000: {"a":1}

--traceは、同じ内容を16進ダンプ付きで出力します。文字コードや改行コード、バイナリの中身まで確かめたいときに使います。--trace-timeを付けると、各行に時刻が付きます。

-w: ステータスコードと時間を取り出す

-w(--write-out)は、転送が終わった後に、指定した書式で情報を出力します。書式の中に%{変数名}と書くと、その値に置き換わります。

一番よく使うのは、ステータスコードだけを取り出す書き方です。

curl -s -o /dev/null -w '%{http_code}\n' https://example.com
実行結果
200

-sで進捗を消し、-o /dev/nullで本文を捨て、-wでステータスコードだけを出しています。監視スクリプトや、デプロイ後の確認でよく使います。manページではresponse_codeという変数も用意されていて、http_codeは以前の名前だと説明されています。どちらも使えます。

どこに時間がかかっているかを調べるには、時間の変数を並べます。

curl -s -o /dev/null -w 'dns:      %{time_namelookup}
connect:  %{time_connect}
tls:      %{time_appconnect}
ttfb:     %{time_starttransfer}
total:    %{time_total}
http:     %{http_version}
ip:       %{remote_ip}
' https://example.com
実行結果
dns:      0.005766
connect:  0.012666
tls:      0.045578
ttfb:     0.056466
total:    0.056777
http:     2
ip:       104.20.23.154

これらの時間は、どれも開始からの経過秒数です。区間ごとの時間ではありません。TLSのハンドシェイクにかかった時間を知りたければ、time_appconnectからtime_connectを引きます。この例では約0.033秒です。

変数意味(開始からの経過時間)
time_namelookup名前解決が終わるまで
time_connectTCPの接続が終わるまで
time_appconnectTLSなどのハンドシェイクが終わるまで
time_pretransfer転送を始める直前まで
time_starttransfer最初の1バイトを受け取るまで
time_redirect最後のリクエストを始める前の、リダイレクトにかかった時間
time_total全体

time_namelookupが大きければDNS、time_connectとの差が大きければネットワークの経路、time_starttransferとtime_appconnectの差が大きければサーバー側の処理が遅い、という切り分けができます。名前解決の流れはDNSの記事で説明しています。

そのほかに、よく使う変数を挙げておきます。

変数意味追加されたバージョン
%{http_version}実際に使われたHTTPのバージョン-
%{remote_ip}接続先のIPアドレス-
%{url_effective}最後に取得したURL-
%{num_redirects}リダイレクトを追った回数-
%{redirect_url}-Lなしのとき、リダイレクト先になるはずだったURL-
%{size_download}受け取った本文のバイト数-
%{json}利用できる値をまとめたJSON7.70.0
%{exitcode}終了コード7.75.0
%{errormsg}エラーメッセージ7.75.0
%{header_json}レスポンスヘッダーをまとめたJSON7.83.0
%header{名前}指定したレスポンスヘッダーの値7.84.0

%{json}と%{header_json}はJSONを出力するので、jqで必要な項目だけを取り出せます。

curl -s -o /dev/null -w '%{json}\n' https://example.com \
  | jq -c '{http_code, http_version, time_total, exitcode, method, scheme}'
 
curl -s -o /dev/null -w '%header{content-type}\n' https://example.com
実行結果
{"http_code":200,"http_version":"2","time_total":0.076189,"exitcode":0,"method":"GET","scheme":"https"}
text/html; charset=utf-8

書式が長くなるときは、ファイルに書いておいて-w @format.txtで読み込めます。

--resolveと--connect-to: 接続先を差し替える

DNSを切り替える前に新しいサーバーの動作を確かめたい、ロードバランサーの後ろの特定の1台にだけリクエストを送りたい、という場面で使うのが--resolveです。ホスト名:ポート:IPアドレスの形で、そのホスト名の名前解決の結果を差し替えます。

curl -sS -o /dev/null -w '%{http_code} %{remote_ip}:%{remote_port}\n' \
  --resolve example.com:443:104.20.23.154 https://example.com/
実行結果
200 104.20.23.154:443

manページでは「コマンドラインで指定する/etc/hostsの代わりのようなもの」と説明されています。URLのホスト名は変えていないので、Hostヘッダーも、TLSのSNIも、証明書の検証に使う名前もexample.comのままです。IPアドレスを直接URLに書いて-H 'Host: example.com'を付ける方法では、HTTPSのときにSNIと証明書の名前が合わなくなりますが、--resolveならその問題が起きません。

--connect-toは、ホスト名とポートの組を、別のホスト名とポートの組に差し替えます。書式はHOST1:PORT1:HOST2:PORT2です。ローカルで立てたサーバーに、本番のホスト名のままリクエストを送る例を示します。

# 別の端末で: python3 -m http.server 8765 --bind 127.0.0.1
curl -sS -v -o /dev/null --connect-to example.com:80:127.0.0.1:8765 http://example.com/
実行結果(抜粋)
*   Trying 127.0.0.1:8765...
* Established connection to 127.0.0.1 (127.0.0.1 port 8765) from 127.0.0.1 port 60947
* using HTTP/1.x
> GET / HTTP/1.1
> Host: example.com
> User-Agent: curl/8.23.0
> Accept: */*

接続先は127.0.0.1:8765ですが、Hostヘッダーはexample.comのままです。manページにも、このオプションはネットワークの接続先だけを変え、TLSのSNIや証明書の検証、アプリケーションのプロトコルで使うホスト名には影響しないと書かれています。--resolveはIPアドレスしか指定できませんが、--connect-toはポートも変えられ、差し替え先をホスト名でも書けます。

-k: 証明書の検証をやめる(注意)

自己署名証明書のサーバーにHTTPSでつなぐと、curlは終了コード60で失敗します。

curl -sS https://self-signed.badssl.com/; echo "exit=$?"
実行結果
curl: (60) SSL certificate OpenSSL verify result: self-signed certificate (18)
More details here: https://curl.se/docs/sslcerts.html
 
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the webpage mentioned above.
exit=60

-k(--insecure)を付けると、証明書の検証を飛ばして通信します。確かにつながるようにはなりますが、これは「相手が本物かどうかを確かめない」という意味です。manページには、このオプションは転送を安全でないものにする、という警告が大文字で書かれています。通信の途中に割り込まれて、トークンやパスワードを盗まれても気づけません。

-kを使ってよいのは、自分で立てた開発用のサーバーのように、相手が分かっていて、流れるデータも重要でない場合に限られます。それ以外で証明書のエラーが出たら、次の順で原因を調べてください。

  • -vで、証明書のsubject、subjectAltName、有効期限、発行者を確かめる
  • 社内の認証局の証明書なら、--cacert ca.pemでその認証局の証明書を指定する
  • IPアドレスでつないで名前が合わないだけなら、--resolveでホスト名のままつなぐ

スクリプトやDockerfile、.curlrcに-kを書いたままにするのは避けてください。curlの証明書の検証については公式の解説があります。

--http2と--http3: HTTPのバージョンを指定する

HTTPSのURLでは、curlは既定でHTTP/2を試みます(manページの--http2の説明)。特定のバージョンで動作を確かめたいときは、オプションで指定します。

for opt in --http1.1 --http2 --http3 --http3-only; do
  curl -sS -o /dev/null -w "$opt: %{http_version}\n" $opt https://example.com/
done
実行結果
--http1.1: 1.1
--http2: 2
--http3: 3
--http3-only: 3

--http3(7.66.0で追加)は、HTTP/3での接続を試み、うまくいかなければ以前のバージョンに切り替えます。--http3-only(7.88.0で追加)は、切り替えずに失敗します。どちらも、curlがHTTP/3に対応する形でビルドされている必要があります。curl --versionのFeaturesにHTTP3があるかどうかで確認できます。HTTP/3とQUICの仕組みはHTTP/3の記事で説明しています。

「HTTP/2でだけ起きる不具合かどうか」を切り分けたいときは、--http1.1を付けて結果が変わるかを見るのが手軽です。

--compressed: 圧縮されたレスポンスを展開する

curlは、既定ではAccept-Encodingヘッダーを送りません。--compressedを付けると、curlが対応している圧縮形式をAccept-Encodingで伝え、受け取ったレスポンスを自動で展開します。

curl -s --compressed https://httpbin.org/gzip | jq -c '{gzipped, ae: .headers["Accept-Encoding"]}'
実行結果
{"gzipped":true,"ae":"deflate, gzip, br, zstd"}

--compressedを付けずに、サーバーが圧縮した本文を返してきた場合は、圧縮されたままのバイト列が出力されます。「curlの出力が文字化けしている」「jqがパースできない」というときは、これが原因のことがあります。-HでAccept-Encodingヘッダーを自分で指定していて、--compressedを付けていない場合に起きます。

スクリプトと運用で使うオプション

タイムアウト: --connect-timeoutと--max-time

--max-timeを指定しないcurlは、サーバーが応答を返さないと待ち続けます。cronやCIで動かすスクリプトでは、必ずタイムアウトを付けてください。

オプション制限する対象
--connect-timeout 秒接続の段階(名前解決と、TCP・TLS・QUICのハンドシェイク)にかけてよい時間
-m 秒 / --max-time 秒1回の転送全体にかけてよい時間
curl -sS --max-time 2 https://httpbin.org/delay/5; echo "exit=$?"
実行結果
curl: (28) Operation timed out after 2002 milliseconds with 0 bytes received
exit=28

どちらも小数で指定できます。大きなファイルをダウンロードする場合、--max-timeは転送全体の時間なので、短くしすぎると正常なダウンロードも途中で切れます。接続できないときだけ早く諦めたいなら--connect-timeoutを短くし、--max-timeは余裕を持たせます。

--retry: 一時的な失敗をやり直す

--retry 回数を付けると、一時的なエラーのときにリトライします。manページによると、一時的なエラーとして扱われるのは、タイムアウト、FTPの4xx、HTTPの408、429、500、502、503、504、522、524です。

待ち時間は、最初が1秒で、リトライのたびに2倍になり、10分で頭打ちになります。--retry-delay 秒を指定すると、この増え方をやめて固定の間隔になります。サーバーがRetry-Afterヘッダーを返した場合は、それに従います(7.66.0以降)。

curl -sS --retry 2 --retry-delay 1 -o /dev/null \
  -w '%{http_code} retries=%{num_retries}\n' https://httpbin.org/status/503
echo "exit=$?"
 
curl -sS --retry 2 --retry-delay 1 -o /dev/null \
  -w '%{http_code} retries=%{num_retries}\n' https://httpbin.org/status/404
実行結果
503 retries=2
exit=0
404 retries=0

503は2回リトライされ、404はリトライされていません。そして、503のまま終わっても終了コードは0です。リトライしても駄目だったときに失敗として扱うには、-fを一緒に付ける必要があります。

関連するオプションをまとめます。

オプション意味
--retry-max-time 秒リトライ全体にかけてよい時間
--retry-connrefused接続拒否(ECONNREFUSED)もリトライの対象にする
--retry-all-errorsあらゆるエラーでリトライする(7.71.0で追加)

--retry-all-errorsについて、manページは「リトライの大ハンマー」と呼び、既定で使わないよう注意しています。データを重複して送ったり受け取ったりするおそれがあるためです。

リトライには注意点が2つあります。1つ目は出力先です。curlは、-oや-Oで指定したファイルなら、リトライの前に書きかけのデータを消してくれますが、標準出力やパイプ、シェルのリダイレクトに書いたデータは消せません。本文の途中で失敗してリトライすると、データが重複することがあります。リトライを使うときは-oでファイルに書くのが安全です。

2つ目はメソッドです。POSTのように、同じリクエストを2回送ると結果が変わる操作をリトライすると、二重登録になることがあります。サーバー側が冪等性キーなどで重複を防いでいるかを確かめてから使ってください。考え方はREST API設計の記事で説明しています。

--fail-with-body: エラーの本文も受け取る

-fは、エラーのときに本文を出力しません。APIは、エラーの理由をJSONの本文で返すことが多いので、それが見えないと原因が分かりません。--fail-with-body(7.76.0で追加)は、本文を出力したうえで、終了コード22を返します。

curl -sS --fail-with-body https://httpbin.org/status/418; echo "exit=$?"
実行結果
curl: (22) The requested URL returned error: 418
 
    -=[ teapot ]=-
 
       _...._
     .'  _ _ `.
    | ."` ^ `". _,
    \_;`"---"`|//
      |       ;/
      \_     _/
        `"""`
exit=22

-fと--fail-with-bodyは同時には指定できません。CIのログにエラーの詳細を残したいなら、--fail-with-bodyのほうが向いています。

終了コード

curlは、失敗の種類ごとに違う終了コードを返します。manページに載っているもののうち、HTTPの利用でよく見るものを挙げます。

終了コード意味
0成功
1対応していないプロトコル
3URLの書式が正しくない
6ホスト名を解決できない
7ホストに接続できない
18転送が途中で終わった(ファイルの一部しか受け取れなかった)
22HTTPのステータスコードが400以上(-fか--fail-with-bodyを付けたときだけ)
23書き込みエラー(ローカルのファイルなどに書けなかった)
28タイムアウト
35TLSのハンドシェイクに失敗した
47リダイレクトの回数が上限に達した
52サーバーが何も返さなかった
56データの受信に失敗した
60サーバーの証明書を、既知の認証局の証明書で検証できなかった

実際のメッセージの例です。

終了コードとメッセージの例
curl: (6) Could not resolve host: no-such-host.invalid
curl: (7) Failed to connect to 127.0.0.1:9 after 0 ms: Could not connect to server
curl: (28) Operation timed out after 2002 milliseconds with 0 bytes received

括弧の中の数字が終了コードです。スクリプトで-wを使って終了コードとエラーメッセージを記録する書き方もあります(7.75.0以降)。

curl -s -o /dev/null -w '%{onerror}error: %{errormsg} (exit %{exitcode})\n' https://no-such-host.invalid/
実行結果
error: Could not resolve host: no-such-host.invalid (exit 6)

%{onerror}より後ろは、転送が失敗したときだけ出力されます。

-Kと.curlrc: 設定ファイル

毎回同じオプションを書くのが面倒なときは、設定ファイルにまとめて、-K(--config)で読み込みます。1行に1つのオプションを書きます。長いオプション名は先頭の--を省略でき、その場合は=や:で値を区切れます。#で始まる行はコメントです。

api.curlrc
# API 共通の設定
silent
show-error
fail-with-body
max-time = 30
header = "Accept: application/json"
user-agent = "sample-cli/1.0"
curl -K api.curlrc https://httpbin.org/headers | jq -c .headers
実行結果
{"Accept":"application/json","Host":"httpbin.org","User-Agent":"sample-cli/1.0"}

設定ファイルの中でURLを指定するときは、URLだけを書くのではなく、url = "https://example.com/"のようにurlオプションを使います。

curlは起動時に、既定の設定ファイルも探して読み込みます。manページに書かれている探索の順番のうち、Windows以外に関係するのは次の3つです。

  • $CURL_HOME/.curlrc
  • $XDG_CONFIG_HOME/curlrc(7.73.0で追加)
  • $HOME/.curlrc

~/.curlrcに書いた設定は、そのユーザーが実行するすべてのcurlに効きます。自分でコマンドを打つときには便利ですが、スクリプトが~/.curlrcの内容に左右されると、環境によって動作が変わる原因になります。スクリプトの中では、curl -q ...のように-q(--disable)を最初の引数に書いて、既定の設定ファイルを読まないようにできます。

認証情報を設定ファイルに書く場合は、ファイルのパーミッションを自分だけが読めるようにしておきます。

--variable: 変数を展開する

8.3.0で、curl自身が変数を扱う--variableが追加されました。変数を定義し、オプション名の前に--expand-を付けたオプションの中で、{{名前}}と書いて展開します。

API_TOKEN=dummy-token-123 curl -s \
  --variable %API_TOKEN \
  --variable 'q=curl 使い方' \
  --expand-header 'Authorization: Bearer {{API_TOKEN}}' \
  --expand-url 'https://httpbin.org/get?q={{q:url}}' \
  | jq -c '{args, auth: .headers.Authorization}'
実行結果
{"args":{"q":"curl 使い方"},"auth":"Bearer dummy-token-123"}

--variable %API_TOKENは、環境変数API_TOKENを取り込みます。その環境変数が設定されていなければ、curlはエラーで終了します(手元では終了コード2でした)。シェルの変数展開と違い、未設定のまま空文字列で送ってしまう事故を防げます。

{{q:url}}の:urlは、展開するときにURLエンコードする関数です。manページに載っている関数は次のとおりです。

関数意味
trim前後の空白を取り除く
jsonJSONの文字列として使えるようにエスケープする
urlURLエンコードする
b64Base64にエンコードする
64decBase64をデコードする(8.13.0で追加)

jsonを使うと、ダブルクォートを含む値をJSONに埋め込むときのエスケープをcurlに任せられます。

curl -s --variable 'msg=He said "hi"' \
  --expand-json '{"text": "{{msg:json}}"}' https://httpbin.org/post | jq -c '.json'
実行結果
{"text":"He said \"hi\""}

設定ファイルの中でも使えるので、トークンを環境変数から取り込む設定を-Kのファイルに書いておく、といった使い方ができます。詳しくはEverything curlの変数の章にあります。

-Z: 並列に転送する

複数のURLを渡すと、curlは順番に1つずつ処理します。-Z(--parallel、7.66.0で追加)を付けると、並列に転送します。2秒かかるURLを3つ取得して比べます。

time curl -s -Z -o /dev/null -o /dev/null -o /dev/null \
  https://httpbin.org/delay/2 https://httpbin.org/delay/2 https://httpbin.org/delay/2
 
time curl -s -o /dev/null -o /dev/null -o /dev/null \
  https://httpbin.org/delay/2 https://httpbin.org/delay/2 https://httpbin.org/delay/2
実行結果(所要時間の部分)
2.604 total   (-Z あり)
6.990 total   (-Z なし)

同時に実行する数の上限は--parallel-maxで指定し、既定は50です。相手のサーバーに負荷をかけすぎないよう、数を絞って使ってください。複数のファイルを保存するときは、-OをURLの数だけ書く代わりに--remote-name-allが使えます。

複数のURLを渡したとき、curlの終了コードは最後のURLの結果で決まります。途中の失敗を見逃したくないときは--fail-earlyを付けると、最初のエラーで終了します。

ブラウザのCopy as cURLとjq

Copy as cURL

ブラウザで起きた通信を、そのままcurlのコマンドとして取り出せます。Everything curlの説明によると、Chrome、Firefox、Edge、Safariのいずれにもこの機能があります。開発者ツールのネットワークのタブで、目的のリクエストを右クリックし、「Copy as cURL」を選びます。メニューの位置や表記はブラウザによって少し違います。

ブラウザでは失敗するリクエストを手元で再現したり、ヘッダーを1つずつ減らして「どのヘッダーが無いと失敗するのか」を調べたりするのに便利です。

注意点もあります。

  • コピーしたコマンドには、CookieやAuthorizationヘッダーがそのまま入っています。ログイン中のセッションを他人に渡すのと同じなので、チャットやIssueにそのまま貼ってはいけません。
  • ブラウザが送ったヘッダーがすべて含まれるので、コマンドが長くなります。Everything curlも、この機能は「まったく同じリクエストを再送する」ためのものなので、不要なヘッダーは削って使うことを勧めています。
  • curlで成功するのにブラウザで失敗する場合、CORSが原因のことがあります。CORSはブラウザが行う制限なので、curlのリクエストには影響しません。詳しくはCORSの記事を参照してください。

jqと組み合わせる

APIのレスポンスはJSONであることが多いので、curlの出力をjqに渡すのが定番です。進捗メーターが混ざらないように-sを付けます。

# 整形して表示する
curl -s https://httpbin.org/get | jq .
 
# 必要な値だけ取り出す
curl -s https://httpbin.org/get | jq -r '.headers["User-Agent"]'
2つ目の実行結果
curl/8.23.0

スクリプトでは、本文とステータスコードの両方が必要になることがあります。本文をファイルに書き、ステータスコードを-wで受け取ると、両方を扱えます。

本文とステータスコードを分けて受け取る
status=$(curl -sS -o response.json -w '%{http_code}' https://httpbin.org/status/404)
 
if [ "$status" -ge 400 ]; then
  echo "request failed: $status" >&2
  cat response.json >&2
  exit 1
fi
 
jq . response.json

逆に、jqでJSONを組み立ててcurlに渡すこともできます。値にダブルクォートや改行が含まれていても、jqが正しくエスケープしてくれます。

jq -n --arg name "$USER_NAME" '{name: $name}' \
  | curl -s --json @- https://httpbin.org/post

--json @-は、標準入力からJSONを読むという意味です。jqの使い方はjqの記事で説明しています。Webhookの受け口を作っているときに、テスト用のペイロードをこの方法で送ると便利です(Webhookの設計の記事)。

よくあるつまずき

-X POSTは要らない

curl -X POST -d ...という書き方をよく見ますが、-d、--json、-Fを付けた時点でPOSTになるので、-X POSTは不要です。

不要なだけなら害はないのですが、-Lと組み合わせると挙動が変わります。manページには、-Lと-Xを一緒に使うと、-Xで指定したメソッドがリダイレクト先のリクエストすべてに使われ、意図しない結果になることがあると書かれています。次の節で実際に確かめます。

同じ理由で、HEADを送りたいときに-X HEADと書くのも誤りです。manページは、-X HEADでは不十分で、-I(--head)を使う必要があると明記しています。-Xはメソッドの文字列を変えるだけで、curlの振る舞い(本文を待つかどうかなど)は変えないためです。

-LでPOSTがGETに変わる

-Lでリダイレクトを追うとき、POSTのリクエストは、ステータスコードによってGETに変わります。manページの説明では、301、302、303のときはGETで送り直し、それ以外の3xxでは同じメソッドのまま送り直します。httpbin.orgで、ステータスコードごとに、リダイレクト先に届いたメソッドを調べました。

ステータスコード-L -d-L -X POST -d-L -X DELETE--follow -X DELETE
301GETPOSTDELETEDELETE
302GETPOSTDELETEDELETE
303GETPOSTDELETEGET
307POSTPOSTDELETEDELETE
308POSTPOSTDELETEDELETE

読み取れることは3つあります。

  • -dだけなら、301、302、303でGETに変わる。これはブラウザと同じ動きで、manページによるとブラウザに合わせた挙動です
  • -X POSTを付けると、どのステータスコードでもPOSTのままになる。303は「GETで取り直す」という意味の応答なので、POSTを送り直すのは本来の動きではない
  • -X DELETEと-Lの組み合わせも、303でDELETEを送り直してしまう

3つ目の問題に対応するのが、8.16.0で追加された--followです。-Xで指定したメソッドを、HTTPの仕様に沿って扱います。表のとおり、303のときだけGETに変わりました。manページでは、-Xと組み合わせるなら-Lではなく--followを検討するよう勧めています。

301や302でもPOSTのまま送り直したいときは、--post301、--post302、--post303という専用のオプションがあります。

「APIにPOSTしたのに405が返る」「POSTしたはずなのにGETの結果が返ってくる」というときは、URLの末尾のスラッシュの有無や、http://からhttps://へのリダイレクトが間に挟まっていないかを-vで確かめてください。

-d @fileで改行が消える

「リクエストボディ」の章で確かめたとおり、-d @fileはファイルの改行を取り除きます。複数行のデータを送るときは--data-binary @file、JSONなら--json @fileを使います。

関連して、-dの値が@で始まると、ファイル名として解釈されることにも注意が必要です。値をそのまま送りたいときは--data-rawを使います。

シェルのクォート

curlの問題に見えて、実際にはシェルの問題であることがよくあります。

URLの?と&。zshでは、クォートしていない?はファイル名のパターンとして扱われ、一致するファイルが無いとエラーになります。

# zsh で実行
curl -s https://httpbin.org/get?name=curl
実行結果
zsh: no matches found: https://httpbin.org/get?name=curl

また、どのシェルでも、クォートしていない&は「コマンドをバックグラウンドで実行する」という意味になります。curl https://example.com/api?a=1&b=2と書くと、&の前までがcurlのコマンドになり、b=2は送られません。URLは必ずシングルクォートで囲んでください。

JSONのクォート。JSONはダブルクォートを多用するので、全体をシングルクォートで囲むのが基本です。

# OK: シングルクォートで囲む
curl --json '{"name":"curl"}' https://httpbin.org/post
 
# シェルの変数を入れたい場合は、ダブルクォートで囲んで中をエスケープするか、
curl --json "{\"name\":\"$NAME\"}" https://httpbin.org/post
 
# jq で組み立てて渡す(値に " が入っていても壊れない)
jq -n --arg name "$NAME" '{name: $name}' | curl --json @- https://httpbin.org/post

シングルクォートの中では、シェルの変数は展開されません。'{"name":"$NAME"}'と書くと、$NAMEという文字列がそのまま送られます。値を埋め込むときは、jqで組み立てるか、前の章の--variableを使うのが安全です。

Windows。manページには、Windowsのバッチファイルでは%が環境変数の展開に使われるため、-wの書式の中の%は2つ重ねる必要があるという注意があります。Windowsのコマンドプロンプトやそのほかのシェルでのクォートの違いは、この記事では確認していません。

URLの中の波括弧と角括弧

curlは、URLの中の{a,b}や[1-5]を、複数のURLに展開する記法(グロブ)として解釈します。curl 'https://example.com/file[1-3].txt'と書くと、3つのURLを順に取得します。

便利な機能ですが、クエリ文字列に角括弧や波括弧を含むURLを渡すと、エラーになったり、意図しないURLになったりします。

curl -sS 'https://httpbin.org/get?ids[a]=1'
curl -sS -o /dev/null -w '%{url_effective}\n' 'https://httpbin.org/get?tags={red,blue}'
curl -sS -g 'https://httpbin.org/get?tags={red,blue}' | jq -c .args
実行結果
curl: (3) bad range in position 29:
https://httpbin.org/get?ids[a]=1
                            ^
https://httpbin.org/get?tags=red
https://httpbin.org/get?tags=blue
{"tags":"{red,blue}"}

1つ目は範囲の書式として解釈できずに終了コード3で失敗し、2つ目は2つのURLに展開されて2回リクエストが送られています。こういうURLを扱うときは、3つ目のように-g(--globoff)を付けてこの展開を止めます。詳しくはEverything curlのグロブの説明にあります。

進捗メーターが出力に混ざる

curl URL | jq .のようにパイプにつなぐと、標準エラー出力に進捗メーターが表示されます。標準出力には混ざらないのでjqは正しく動きますが、端末の表示は乱れます。2>&1でまとめてログに書いている場合は、ログに進捗の行が入ります。スクリプトでは-sSを付けておくのが無難です。

まとめ

  • curlはURLを指定してデータを送受信するコマンドで、本体はlibcurlというライブラリ。対応する機能はcurl --versionで確かめる
  • 既定ではリダイレクトを追わず、HTTPのエラーも失敗にしない。-Lと、-fか--fail-with-bodyを必要に応じて付ける
  • -dはフォーム形式でPOSTする。JSONは--json(7.82.0以降)が短い。-X POSTは要らない
  • -d @fileは改行を取り除く。そのまま送るなら--data-binary @file
  • ファイルのアップロードは-F 'file=@path'。Content-Typeは自分で指定しない
  • デバッグは-vから始める。ボディまで見るなら--trace-ascii、ステータスコードや時間は-w
  • -wの時間の変数は、すべて開始からの経過時間。区間の時間は引き算で求める
  • 接続先だけを差し替えるなら--resolveか--connect-to。-kで検証を止める前に原因を調べる
  • スクリプトでは-fsSに加えて、--connect-timeout、--max-time、--retryを付ける。リトライしても終了コードは-fが無いと0のまま
  • -Lと-Xを一緒に使うと、リダイレクト先でもそのメソッドが使われる。8.16.0以降は--followがある

まずは-vでリクエストとレスポンスを眺めることから始め、-w '%{http_code}\n'と--jsonを覚えると、APIの確認作業の大半をcurlだけでこなせるようになります。困ったときは、curl --help allやmanページで、オプションの説明と導入されたバージョンを確かめてください。

参考リンク

jq の使い方入門 - JSON をコマンドラインで整形・抽出・変換する基本とハマりどころ

jq の使い方入門 - JSON をコマンドラインで整形・抽出・変換する基本とハマりどころ

約59分

jqはJSONを読み込み、フィルタで加工した結果を出力するコマンドです。整形の.、フィールド参照と.[]、パイプとカンマ、select・map・to_entries・group_by、--argと--argjson、-r・-c・-s・-n・-e、@csvや@base64、文字列補間、//と|=とreduceまでを、jq 1.8.1の実行結果付きで説明します。ファイルの上書きで中身が空になる失敗、NDJSONと--stream、終了ステータス、数値の精度、gojqやjaqなどの代替ツールも扱います。

lftp で SFTP サーバーに接続する - 鍵認証・ポート指定・mirror での同期まで

lftp で SFTP サーバーに接続する - 鍵認証・ポート指定・mirror での同期まで

約12分

コマンドラインの高機能ファイル転送クライアント lftp で SFTP サーバーに接続する方法を、基本の sftp:// URL、パスワード/公開鍵認証、ポート番号や踏み台の指定、ホスト鍵の確認、mirror での一括ダウンロード/アップロード、リトライやタイムアウトの設定、bash スクリプトでの自動化まで実例つきで解説します。lftp の SFTP は内部で ssh を呼ぶ(sftp:connect-program)ため、ssh の設定や鍵がそのまま効くのがポイントです。

楽天市場のおすすめPR / 楽天アフィリエイト