ここではほとんど関係がないため簡潔に済ませますが、開示事項として:私たちは Geonode であり、プロキシを販売しています。 POSTリクエストにおいて、私たち側の介入は一切必要ありません。 以下で説明する内容はすべて、ご自身の接続環境からご自身のエンドポイントに対して実行されます。プロキシが関与するのはずっと後の段階であり、最後に、プロキシ経由でPOSTを行う際に唯一実際に変化する点について簡単に触れています。その変化は、多くの人が予想しているものとは異なります。
基本
curl -d "name=Ada&role=engineer" https://api.example.com/users
curlマニュアルでは、-d, --data
について次のように説明されています。「指定されたデータをサーバーに送信します。HTTP(S)の場合、これはPOSTメソッドを使用して行われ、ユーザーがHTMLフォームに入力して送信ボタンを押した際のブラウザの動作と同じです。 このオプションを指定すると、curlはapplication/x-www-form-urlencoded
というコンテンツタイプを使用してデータをサーバーに送信します。」
2つのことが自動的に行われ、どちらも重要です。
**-d
は POST を意味します。** -X POST
は必要ありません。これを追加しても、リダイレクト処理において各ホップで -X
が適用される以外、何も変わりません。
**-d
は Content-Type: application/x-www-form-urlencoded
を設定します。** これはフォーム送信の場合は正しいですが、それ以外のほとんどの場合では誤りです。
-d
を繰り返すと、curl は各部分を結合します。マニュアルには、「-d name=daniel -d skill=lousy
を使用すると、name=daniel&skill=lousy
のような POST チャンクが生成される」と記載されています。
JSONの送信
最も一般的な実際の用途であり、エラーが発生しやすい箇所でもあります。
明示的な方法:
curl -X POST https://api.example.com/users \
-H "Content-Type: application/json" \
-d '{"name":"Ada","role":"engineer"}'
ほとんどの人が知らないショートカット。 curl には専用の --json オプションがあり、ドキュメントには次のように記載されています。「指定された JSON データを POST リクエストとして HTTP サーバーに送信します。--json は、次の 3 つのオプション(--data-binary [arg]、--header "Content-Type: application/json"、--header "Accept: application/json")を指定するショートカットとして機能します。」
curl --json '{"name":"Ada","role":"engineer"}' https://api.example.com/users
3 つのオプションが 1 つにまとめられており、Accept だけでなく Content-Type も設定されます。これは通常、ユーザーが望む動作です。また、@ を使用することで、ファイルや標準入力(stdin)から読み込むこともできます:
curl --json @payload.json https://api.example.com/users
cat payload.json | curl --json @- https://api.example.com/users
マニュアルには次のような率直な注意書きがあります。「渡されたデータが実際にJSONであるか、構文が正しいかどうかの検証は行われません」。これはヘッダーを設定するだけで、検証は行いません。構文が不正な本文でも、JSONコンテンツタイプとして送信されてしまいます。その場合、サーバーからのエラーはcurlではなく、JSON自体に関するものとなります。
このツールが設定するヘッダーは「通常通り --header で上書き可能」なので、このショートカットを使い続けつつ、一部を調整することもできます。
一重引用符は重要です。 JSON ボディを一重引用符で囲むことで、シェルが $ を展開したり、内部の二重引用符を解釈したりするのを防ぎます。JSON に一重引用符が含まれている場合は、ファイルに保存してください。
5つのデータオプションとそれぞれの使用場面
curlには、特定の点で異なるいくつかの--dataのバリエーションがあるため、このセクションを読むことで、多くの疑問が解消されるでしょう。
| オプション | コンテンツタイプ | @ 特殊? | 改行 | 使用場面 |
|---|---|---|---|---|
-d / --data | form-urlencoded | はい、ファイルを読み込む | 削除 | フォーム送信データ |
--data-raw | form-urlencoded | いいえ | 削除 | @ で始まるデータ |
--data-binary | form-urlencoded | はい | 保持 | ファイル、正確なバイト数 |
--data-urlencode | form-urlencoded | はい | エンコード | 特殊文字を含む値 |
--json | application/json | はい | 保持 | JSON ボディ |
--data-raw が存在する理由はただ一つです。マニュアルによると、このメソッドは「--dataと同様にデータを送信するが、@文字に対する特別な解釈は行わない」と記載されているからです。 リテラルデータが @ で始まる場合(メールアドレス、ハンドル、メンションなど)、-d はそれをファイル名として読み取ろうとし、混乱を招くようなエラーが発生します。マニュアルの例としては curl --data-raw "@at@at@" が挙げられています。
--data-binary は、ファイルの送信に使用するものです。 マニュアルには、「指定されたとおりにデータを投稿し、一切の追加処理を行わないこと……改行やキャリッジリターンは保持され、変換は決して行われない」とあります。なお、デフォルトでは依然として application/x-www-form-urlencoded が送信されるため、任意のバイナリを投稿する場合は、マニュアルに従ってこれを上書きする必要があります:-H "Content-Type: application/octet-stream"。
これが、-d @file.json が微妙に動作しなくなる理由です。改行が削除されてしまうからです。 JSONの場合、通常これは問題になりませんが、空白が意味を持つものについては問題になります。--data-binary @file.json または --json @file.json の方が安全な形式です。
--data-urlencode は、&、=、スペース、その他フォームエンコーディングを破綻させる可能性のある文字を含む値を処理します。マニュアルにはいくつかの構文が記載されていますが、ほとんどの場合、name=content を使用するのが適切です。これはコンテンツをURLエンコードしつつ、名前はそのまま残します:
curl --data-urlencode "comment=hello & goodbye = fine" https://example.com/post
これを指定しないと、& がフィールド区切り文字として認識され、コメントが黙って切り捨てられてしまいます。また、name@filename という関数もあり、これはファイルからコンテンツを読み込み、URLエンコードを施した上で、ファイル名に = を付加します。
ファイルのアップロードとマルチパートフォーム
実際のファイルアップロードには、-F が適しており、-d とは動作が異なります。
マニュアルには次のように記載されています。「-F, --form <name=content> ... は、ユーザーが送信ボタンを押した状態の入力済みフォームをエミュレートします。これにより、curlはRFC 2388に準拠して、Content-Typeをmultipart/form-dataとしてPOSTデータを送信します。」
curl -F "file=@report.pdf" -F "title=Q3 Report" https://api.example.com/upload
@と<の違いは直感的ではないため、理解しておく価値があります。マニュアルには次のように記載されています。「'content'部分をファイルとして強制するには、ファイル名の先頭に@記号を付けます。ファイルから'content'部分を取得するには、ファイル名の先頭に<記号を付けます。 @ と < の違いは、@ がファイルをファイルアップロードとして投稿に添付するのに対し、< はテキストフィールドを作成し、そのテキストフィールドの内容をファイルから取得する点にあります。」
つまり、@ はファイルをファイルとしてアップロードし、< はファイルの内容をテキストフィールドの値として送信します。
パーツにコンテンツタイプを設定するには:
curl -F "file=@data.csv;type=text/csv" https://api.example.com/upload
また、@ や < で始まるリテラル値が必要な場合は、--form-string を使用してください。これらはどちらの文字も解釈しません。
Content-Type: multipart/form-data を自分で設定しないでください。curl は境界パラメータを含めてこれを生成するため、上書きするとサーバーが解析できないリクエストが生成されます。これは、原因不明の 400 や 415 エラーの非常に一般的な原因です。
ファイルを単純にPUTする場合、-Tの方が簡単です。「指定されたローカルファイルをリモートURLにアップロードします…このオプションをHTTP(S) URLと併用する場合、PUTメソッドが使用されます。」
認証とヘッダー
curl --json '{"a":1}' \
-H "Authorization: Bearer eyJhbG..." \
https://api.example.com/items
-H
は繰り返し実行可能であり、--json
で設定されたものを含め、curl のデフォルト設定を上書きします。
ベーシック認証の場合は、-u user:password
— あるいは -u user
のみを使用すると、curl がパスワードの入力を促すため、パスワードがシェルの履歴に残るのを防げます。 マニュアルには、「この機能が動作するシステムでは、curl は指定されたオプション引数をプロセス一覧から非表示にする」と記載されていますが、その一方で「これだけでは認証情報を保護するには不十分である」とも付け加えられています。
セッションベースの API の場合、Cookie を取得して再利用します:
curl -c jar.txt -d "user=ada&pass=secret" https://example.com/login
curl -b jar.txt --json '{"a":1}' https://example.com/api/items
動作しないPOSTリクエストのデバッグ
ほぼすべての問題を解決できる簡単な手順。
送信した内容を正確に確認する:
curl -v --json '{"a":1}' https://api.example.com/items 2>&1 | grep -E '^[<>]'
>
で始まる行がリクエスト、<
がレスポンスです。メソッド、Content-Type
、およびボディが意図した通りであることを確認してください。「APIが壊れている」という報告の驚くほど多くの割合が、ここで解決します。
ステータスコードとエラー本文を確認してください:
curl -sS -o body.txt -D headers.txt --json '{"a":1}' https://api.example.com/items
head -1 headers.txt; head -c 300 body.txt
よくあるエラーの解釈:
| ステータス | 通常は次を意味します |
|---|---|
| 400 | ボディの形式が不正、または必須フィールドが欠落している |
| 401 | 認証情報が欠落しているか無効 |
| 403 | 認証は済んでいるがアクセスが許可されていない |
| 405 | エンドポイントがPOSTを受け付けない — URLとメソッドを確認してください |
| 413 | ボディが大きすぎる |
| 415 | Content-Type |
が不正 — JSONの「-d | |
| 」という典型的なエラー | |
| 422 | コンテンツタイプは正しいが、データの検証に失敗 |
415と422の違いは、しっかりと理解しておく価値があります。415はラッパーが間違っていることを意味し、422はコンテンツに問題があることを意味します。 これについては、415ステータスコードとはで詳しく解説しました。
スクリプト内でエラーを明確に表示する:
curl --fail-with-body --silent --show-error \
--connect-timeout 5 --max-time 30 \
--json @payload.json https://api.example.com/items
--fail-with-body
は、HTTPエラーが発生した場合にゼロ以外の終了コードを返す一方で、レスポンス本文も出力します。これは、APIが有用なJSONエラーメッセージを返す場合に望ましい動作です。一方、単純な --fail
は本文を破棄してしまうため、説明情報が失われてしまいます。
ブラウザのリクエストをcurlに変換する
ブラウザでは動作するが、自分のコードでは動作しないPOSTリクエストを再現する最も手っ取り早い方法――そして、多くの人が本来ならもっと早く気づくべきだったテクニックです。
開発者ツールからコピーしましょう。 Chrome、Firefox、Safariで「ネットワーク」タブを開き、該当するリクエストを見つけて右クリックし、「cURLとしてコピー」を選択します。これにより、ブラウザが送信したすべてのヘッダー、クッキー、ボディを含む完全なコマンドが得られます。これをターミナルに貼り付ければ、同じ動作をするはずです。
これにより、ほとんどのデバッグ作業の根底にある疑問――「問題はリクエストにあるのか、それとも自分のコードにあるのか?」――が即座に解決されます。コピーしたコマンドが動作するのに自分のコードでは動作しない場合、違いは送信内容にあります。そして、両方のバージョンを並べて比較できるようになります。
次に、コマンドを絞り込みます。 コピーしたコマンドには通常30個ほどのヘッダーが含まれていますが、そのほとんどは不要です。それらを少しずつ削除し、動作しなくなるまで再実行してください。残ったものが、サーバーが実際に必要とする最小限のセットであり、それがアプリケーションに組み込むべきものです:
curl 'https://api.example.com/items' \
-H 'content-type: application/json' \
-H 'authorization: Bearer eyJhbG...' \
--data-raw '{"name":"Ada"}'
なお、ブラウザは -d ではなく --data-raw をエクスポートすることに注意してください。これは、@ で始まる本文が、そうでなければファイル名として誤認識されてしまうためです。
コピー処理で失われてしまう2つの点に注意してください。 クッキーはリテラルなヘッダーとして含まれるため、有効期限が切れてしまいます。また、ページがJavaScriptで計算した内容(CSRFトークン、署名、タイムスタンプから導出された値など)は、コピーされたコマンド内に固定文字列として埋め込まれるため、一度だけ機能してその後は動作しなくなります。 再現したリクエストが1回目は成功し、2回目で失敗する場合は、ほぼ間違いなくこれが原因です。修正するには、トークンをハードコーディングするのではなく、動的に取得するようにします。
逆に、いくつかのツールはcurlコマンドをほとんどの言語のコードに変換してくれます。これは、ヘッダーを手動で再入力することなく、動作するコマンドから動作するクライアントへと移行するための合理的な方法です。
プロキシ経由でのPOST
手短に言うと、重要な点はテクニックというよりは注意点です。
curl -x http://user:pass@proxy.example.com:9000 \
--json '{"a":1}' https://api.example.com/items
仕組み自体は変わりません。変わるのはリトライの処理であり、--retry
を追加する前に、この点についてよく検討しておく価値があります。
POSTは一般的に冪等ではありません。 2回送信すると、2つのレコードが作成される可能性があります。curlの--retry
はデフォルトでは一時的な状態でのみ発動しますが、--retry-all-errors
ではその範囲が大幅に広がります。また、プロキシを経由する場合、5xxエラーは一時的な不具合ではなく、ターゲットがリクエストを拒否したことを意味することがよくあります。 そのリクエストを再試行しても、良くて無意味であり、最悪の場合、書き込みが重複してしまいます。
タイムアウトは失敗の証拠ではありません。 サーバーがリクエストを受信した後にタイムアウトが発生した場合、エラーが表示された間に操作は完了していた可能性があります。 プロキシを経由する場合、この現象が発生する可能性のあるホップが1つ追加されます。操作が重要な場合は、安全性を確保するためにリトライロジックに頼るのではなく、冪等性キー(idempotency key)を使用してください。ほとんどの重要なAPIではこれがサポートされています。
また、どのホップで拒否されたかを確認してください。 %{http_connect}
では、ターゲットのステータスとは別に、CONNECT に対するプロキシの応答が報告されます:
curl -sS -o /dev/null -x "$PROXY" \
-w 'connect=%{http_connect} status=%{response_code}\n' \
--json '{"a":1}' https://api.example.com/items
connect=407
は、プロキシが認証情報を要求したことを意味します。connect=200 status=403
は、プロキシは正常に機能したが、ターゲット側が拒否したことを意味します。問題が異なれば、対処法も異なります。
よくある質問
curl で POST リクエストを送信するにはどうすればよいですか?
curl -d "key=value" URL。-d オプションは POST を暗黙的に指定するため、-X POST は不要です。また、このオプションは Content-Type: application/x-www-form-urlencoded を設定しますが、これはフォーム送信では正しいものの、JSON 送信では誤りとなります。
curl で JSON を POST するにはどうすればよいですか?
curl --json '{"key":"value"}' URL がショートカットです。これにより、--data-binary が設定されるほか、Content-Type および Accept ヘッダーの両方が application/json に設定されます。 より長い形式は -X POST -H "Content-Type: application/json" -d '...' です。なお、--json では JSON の検証は行われない点に注意してください。
curl で 415 Unsupported Media Type エラーが発生するのはなぜですか?
ほとんどの場合、コンテンツタイプを設定せずに -d を使用して JSON ボディを送信したことが原因です。-d はフォームエンコードで送信するため、JSON を期待している API では拒否されます。--json を使用するか、-H "Content-Type: application/json" を追加してください。
-d と --data-raw の違いは何ですか?
-d は、先頭に @ がある場合を「このファイルから読み込む」と解釈します。--data-raw はそうではないため、リテラルデータが @ で始まる場合(例えば、メールアドレスやハンドルなど)に必要です。それ以外の場合、両者の動作は同じです。
curlのPOSTでファイルをアップロードするにはどうすればよいですか?
curl -F "file=@document.pdf" URL を使用します。これにより、multipart/form-data が送信されます。ファイルを添付するには @ を、その内容をテキストフィールドの値として送信するには < を使用してください。Content-Type ヘッダーは手動で設定しないでください。curlが、必要な境界付きでこれを生成します。
ファイルからPOSTデータを送信するにはどうすればよいですか?
JSONの場合は curl --json @payload.json URL、改行を含む正確なバイトデータの場合は --data-binary @file を使用してください。空白が重要な場合は -d @file の使用を避けてください。-d は改行とキャリッジリターンを削除してしまうためです。
アンパサンド(&)を含む値をPOSTするにはどうすればよいですか?
--data-urlencode "field=value with & inside" を使用してください。通常の -d を使用すると、アンパサンドはフィールド区切り文字として認識され、その時点で値が黙って切り捨てられてしまいます。
失敗したPOSTリクエストを再試行すべきですか?
慎重に行ってください。POSTは一般的に冪等性を持たないため、再試行すると重複データが生成される可能性があります。また、タイムアウトが発生したからといって、サーバーがリクエストを処理しなかったとは限りません。APIが冪等性キーをサポートしている場合はそれを使用し、--retry-all-errors については注意してください。特にプロキシを経由する場合、5xxエラーは一時的な障害ではなく、リクエストの拒否を意味することが多いためです。
まとめ
この話題は、結局のところ「エンドポイントが求めるコンテンツタイプは何か、そしてあなたのコマンドはそれを送信しているか」という1つの問いに帰着します。
-d はフォームエンコードで送信しますが、これはフォーム送信には適していますが、JSONには不適切です。この単一の不一致が、多くの人が遭遇する415エラーの主な原因となっています。代わりに利用すべきオプションは --json ですが、これはあまり知られていません。これは、ボディの処理と両方のヘッダーを設定する1つのフラグであり、@ によるファイルや標準入力のサポートも備えています。
それ以外にも、覚えておく価値のある特定の理由に基づいて、いくつかのバリエーションが存在します。データが @ で始まる場合は --data-raw を使用します。改行が重要な場合は --data-binary を使用します。値にフォームエンコーディングを破綻させる文字が含まれている場合は --data-urlencode を使用します。実際のファイルアップロードには -F を使用し、ファイルを添付するには @、ファイルからテキストフィールドを読み取るには < を使用します。
また、何かが失敗した場合は、-v で実行し、何かを変更する前に > の行を確認してください。送信したリクエストは、自分が送信したつもりだったリクエストとは異なることがよくあり、そのギャップこそが、この分野における混乱の大部分の原因となっています。
