curl コマンドを読んでコードに移す

curl コマンドをフィールドごとに正確に読み、fetch や Python の requests へ正しく移す方法、そしてほとんどの翻訳がつまずく暗黙の POST の罠を整理します。

API ドキュメントが curl の行を渡してきます。ブラウザのネットワークパネルに 「Copy as cURL」ボタンがあります。誰かがバグ報告に再現を curl ... として 貼り付けます。どの場合でも curl コマンドは HTTP リクエストの正規の記述で、作業は 同じです。それを正しく読んで fetch、Python の requests、または axios で 再現することです。翻訳の大半は機械的です。人々が間違える部分は暗黙の振る舞い、 つまり curl がフラグに明記されないまま行うことです。

リクエストの解剖

すべての HTTP リクエストは 5 つの部分を持ち、すべての curl フラグはちょうどその 1 つに対応します。

  • メソッドGETPOSTPUT など。-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-dataPOST を含意) と -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-urlencodedfetch では 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-datafetch では FormData オブジェクトを使い (Content-Type を手動で設定し ない でください。ランタイムが境界を足します)、requests では files={...} を渡します。

失敗の形はこれらを取り違えることです。フォームエンコードのボディを送りながら application/json を宣言したり、FormDataJSON.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 を交渉させることに対応し、fetchrequests の両方が透過的に行います。

リクエストが出ると、応答のステータスコードが翻訳が合っていたかを教えてくれます。 415 はコンテンツタイプが間違っていたことを、401 は認証ヘッダーが届かなかった ことを意味します。私たちの HTTP ステータス早見 は、各コードを その有力な原因に対応づけます。

よくある場合なら、curl コマンドを私たちの curl 変換器 に 貼り付け、メソッドとコンテンツタイプが正しく推論された fetchrequestsaxios の同等物を得てください。手での翻訳がこれほど頻繁に間違える暗黙の POST の場合も含めて。