curl コマンドを読んでコードに移す
curl コマンドをフィールドごとに正確に読み、fetch や Python の requests へ正しく移す方法、そしてほとんどの翻訳がつまずく暗黙の POST の罠を整理します。
API ドキュメントが curl の行を渡してきます。ブラウザのネットワークパネルに
「Copy as cURL」ボタンがあります。誰かがバグ報告に再現を curl ... として
貼り付けます。どの場合でも curl コマンドは HTTP リクエストの正規の記述で、作業は
同じです。それを正しく読んで fetch、Python の requests、または axios で
再現することです。翻訳の大半は機械的です。人々が間違える部分は暗黙の振る舞い、
つまり curl がフラグに明記されないまま行うことです。
リクエストの解剖
すべての HTTP リクエストは 5 つの部分を持ち、すべての curl フラグはちょうどその 1 つに対応します。
- メソッド —
GET、POST、PUTなど。-Xで設定するか、推論される。 - URL — クエリ文字列を含む裸の引数。
- ヘッダー —
-Hごとに 1 つ。 - ボディ —
-d、--data-raw、-F、または--data-urlencode。 - 認証 — HTTP Basic 用の
-u、または-Hを通じたAuthorizationヘッダー。
curl コマンドを翻訳するとは、各フラグを読み、それが 5 つの部分のどれを設定するか を決め、対象の言語で同等物を出すことです。メソッドはしばしば 暗黙 になる唯一の フィールドで、ほとんどの誤訳がそこから来ます。
フラグ早見表
| フラグ | 意味 | リクエストへの影響 |
|---|---|---|
-X, --request |
メソッド設定 | 推論されたメソッドを上書き (-X POST、-X PUT) |
-H, --header |
ヘッダー追加 | フラグごとに 1 ヘッダー、後のものが上書きしうる |
-d, --data |
リクエストボディ | POST + application/x-www-form-urlencoded を含意、複数の -d は & で連結 |
--data-raw |
ボディ、文字どおり | -d と同じだが先頭の @ をファイル参照として扱わない |
--data-urlencode |
ボディ、パーセントエンコード | 送る前に値をパーセントエンコードする |
-G, --get |
データをクエリへ移動 | -d のデータを GET とともに URL クエリ文字列で送る |
-u, --user |
HTTP Basic 認証 | Authorization: Basic <base64(user:pass)> を送る |
-F, --form |
マルチパートフィールド | multipart/form-data を設定、-F file=@path でファイルをアップロード |
-b, --cookie |
クッキー送信 | Cookie ヘッダーを追加 |
-L, --location |
リダイレクトを追う | 3xx 応答でリクエストを再発行 |
このうちいくつかには、はっきり述べておくべき鋭い角があります。-d は引数から
改行を取り除き、値が @ で始まるとファイルから読みます (だから -d @body.json
はファイルの内容を送ります)。--data-raw が @ を文字どおり送る変種です。
--data-urlencode は値をパーセントエンコードし、これはフォームフィールドが
&、=、空白を含むときに重要です。規則は
URL パーセントエンコーディング で扱ったまさに
それです。そして HTTPS のない -u は資格情報を Base64 エンコードで通信路に送り、
これはエンコードであって暗号化ではありません (詳しくは後述)。
暗黙の POST の罠
これは curl を手で翻訳するときの最もよくある間違いです。
curl https://api.example.com/items -d 'name=widget&qty=3'
ここに -X POST はありませんが、これは POST です。-d の存在がメソッドを
デフォルトの GET から POST に変え、-H で上書きしない限り
Content-Type: application/x-www-form-urlencoded を設定します。このコマンドを
翻訳する人々はしばしばクエリ文字列を持つ GET や、JSON ボディを持つ POST を
書きます。どちらも間違いです。正しい読みは POST、フォームエンコードのボディ、
name=widget&qty=3 です。
同じことが -F (multipart/form-data で POST を含意) と
-T/--upload-file (PUT を含意) にも当てはまります。明示的な -X を見たとき
は、それが勝ちます。curl -X PUT -d '...' はフォームボディを持つ PUT です。
ボディのフラグを先に読み、メソッドを推論し、それから -X に上書きさせて
ください。
実例
認証、カスタムヘッダー、JSON ボディを持つ現実的なコマンドを見てみましょう。
curl -X POST https://api.example.com/v1/orders \
-H 'Authorization: Bearer tok_abc123' \
-H 'Content-Type: application/json' \
-d '{"sku":"A-19","qty":2}'
-d が JSON を運ぶのは、ひとえに -H 'Content-Type: application/json' が
あるからだという点に注目してください。そのヘッダーがなければ、curl は同じバイトを
フォームエンコードとしてラベル付けします。ボディのコンテンツタイプは、文字列の
形ではなくヘッダーが決めます。
JavaScript の fetch ではこうです。
const res = await fetch("https://api.example.com/v1/orders", {
method: "POST",
headers: {
"Authorization": "Bearer tok_abc123",
"Content-Type": "application/json",
},
body: JSON.stringify({ sku: "A-19", qty: 2 }),
});
Python の requests ではこうです。
import requests
res = requests.post(
"https://api.example.com/v1/orders",
headers={"Authorization": "Bearer tok_abc123"},
json={"sku": "A-19", "qty": 2},
)
requests 版は json= を使い、これが Content-Type: application/json ヘッダーを
代わりに設定します。だから明示的に渡すと冗長で、例ではそれを省いています。その
便利さはまた罠でもあります。curl コマンドのコンテンツタイプが別のものだったら、
json= は間違った道具で、data= が必要だったはずです。パラメータを選ぶ前に
ヘッダーを読んでください。
ボディのコンテンツタイプ
ボディのフラグは互いに交換可能ではありません。それぞれ別のコンテンツタイプと、 クライアントの別のパラメータに対応します。
- フォームエンコード —
-d 'a=1&b=2'。タイプapplication/x-www-form-urlencoded。fetchではURLSearchParamsを組み立て、requestsではdata={"a": 1, "b": 2}を渡します。 - JSON —
-H 'Content-Type: application/json' -d '{...}'。fetchでは ボディをJSON.stringifyし、requestsではjson={...}。 - マルチパート —
-F field=value -F [email protected]。境界を持つタイプmultipart/form-data。fetchではFormDataオブジェクトを使い (Content-Typeを手動で設定し ない でください。ランタイムが境界を足します)、requestsではfiles={...}を渡します。
失敗の形はこれらを取り違えることです。フォームエンコードのボディを送りながら
application/json を宣言したり、FormData を JSON.stringify したりすること
です。サーバーはその不一致を拒否し、しばしば紛らわしい 400 で返します。
Basic 認証は Base64 であって暗号化ではない
-u user:pass は、トークンが base64("user:pass") である
Authorization: Basic <token> ヘッダーを作ります。Base64 は鍵なしで戻せます。
ヘッダーを見た者は誰でも即座に資格情報を復元します。HTTPS の上では TLS が交換
全体を暗号化するので許容でき、平文の HTTP の上ではパスワードを平文で送るのと
同じです。http:// の URL に対する -u を見たら、その資格情報を侵害されたものと
して扱ってください。
翻訳は単純です。fetch ではこうです。
headers: { "Authorization": "Basic " + btoa("user:pass") }
requests では auth=("user", "pass") を渡せば、ライブラリがヘッダーを代わりに
組み立てます。
翻訳が一対一でないとき
いくつかの curl の振る舞いには、きれいな 1 行の同等物がありません。-L
(リダイレクトを追う) は requests ではデフォルトですが、fetch では
redirect: "follow" で要求する必要があります (これもデフォルト)。そしてどちらも
デフォルトでは POST のボディをリダイレクト先に再送しません。これは
--post301/--post302 が設定されない限り、curl 自身の振る舞いと一致します。-b
のクッキーは Cookie ヘッダーになりますが、元がリクエスト間でクッキーを保持する
のに -c/--cookie-jar に依存していたなら、それを再現するにはセッション
オブジェクト (requests.Session()) が必要です。そして --compressed は、
クライアントに Accept-Encoding を交渉させることに対応し、fetch と requests
の両方が透過的に行います。
リクエストが出ると、応答のステータスコードが翻訳が合っていたかを教えてくれます。
415 はコンテンツタイプが間違っていたことを、401 は認証ヘッダーが届かなかった
ことを意味します。私たちの HTTP ステータス早見 は、各コードを
その有力な原因に対応づけます。
よくある場合なら、curl コマンドを私たちの curl 変換器 に
貼り付け、メソッドとコンテンツタイプが正しく推論された fetch、requests、
axios の同等物を得てください。手での翻訳がこれほど頻繁に間違える暗黙の POST
の場合も含めて。