curl コマンド メモ

  • curlのサイト
    http://curl.haxx.se/

curlの実験準備

今回はJenkinsさんのAPIを叩くことにします。

curlのインストール

最近のLinuxには初めからcurlがインストールされていると思いますが、ない場合はaptyum など、使用しているディストリビューションに沿った方法でインストールして下さい。

今回使用したcurlバージョン

7.38.0

$ curl --version
curl 7.38.0 (x86_64-pc-linux-gnu) libcurl/7.38.0 OpenSSL/1.0.1k zlib/1.2.8 libidn/1.29 libssh2/1.4.3 librtmp/2.3

Jenkinsのインストール

こちらも、aptyum などで適当にインストールします。
実験用に幾つか設定をしたり、ジョブを作りますが、その都度、簡単に説明することとします。

jqのインストール

jq (https://stedolan.github.io/jq/) は、jsonをうまいことパースしてくれるコマンドです。
今回は、Jenkinsのレスポンスをちょっと見やすくするためだけにjqを使用しているので、ここでは特に説明はしません。

GET

ただGETリクエストを投げたいだけならば、何もオプションは要りません。

$ curl localhost:8080/api/json | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   371  100   371    0     0  53489      0 --:--:-- --:--:-- --:--:-- 61833
{
  "assignedLabels": [
    {}
  ],
  "mode": "NORMAL",
  "nodeDescription": "ノード",
  "nodeName": "",
  "numExecutors": 2,
  "description": null,
  "jobs": [],
  "overallLoad": {},
  "primaryView": {
    "name": "すべて",
    "url": "http://localhost:8080/"
  },
  "quietingDown": false,
  "slaveAgentPort": 0,
  "unlabeledLoad": {},
  "useCrumbs": false,
  "useSecurity": false,
  "views": [
    {
      "name": "すべて",
      "url": "http://localhost:8080/"
    }
  ]
}

Jenkinsの一番トップのAPIを叩いています。
jq . で、Jsonを見やすい形に整列しています。
もっともJenkinsでは、jsonjson?pretty=true に変えればほぼ同じ出力になります。

ファイルに出力 -o -O

-o オプションでファイルに出力できます。

$ curl localhost:8080/api/json -o response
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   371  100   371    0     0  68538      0 --:--:-- --:--:-- --:--:-- 74200
$ cat response | jq .
{
  "assignedLabels": [
    {}
  ],
  "mode": "NORMAL",
  "nodeDescription": "ノード",
  "nodeName": "",
  "numExecutors": 2,
  "description": null,
  "jobs": [],
  "overallLoad": {},
  "primaryView": {
    "name": "すべて",
    "url": "http://localhost:8080/"
  },
  "quietingDown": false,
  "slaveAgentPort": 0,
  "unlabeledLoad": {},
  "useCrumbs": false,
  "useSecurity": false,
  "views": [
    {
      "name": "すべて",
      "url": "http://localhost:8080/"
    }
  ]
}

-O だと、リクエスト先の名前でファイルを保存します。

$ curl localhost:8080/api/json -O
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   371  100   371    0     0  64985      0 --:--:-- --:--:-- --:--:-- 74200

# 先ほど保存した response と同じ内容か比較
$ diff json response; echo $?
0

Progress Meter

curlはデフォルトで、↓のような転送情報をコンソールに出力します。

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   371  100   371    0     0  53489      0 --:--:-- --:--:-- --:--:-- 61833

-# オプションを使うと、プログレスバーのような表記に変更できます。

$ curl localhost:8080/api/json -o response -#
######################################################################## 100.0%

どちらにせよ、このプログレス情報を邪魔に思う日もあるでしょう。
stderrに出力されているので、/dev/nullにでも突っ込んでしまうのも手ですが、素直に-sオプションで制御します。

$ curl -s localhost:8080/api/json -O

ただしこの時、エラーメッセージまで消えてしまいます。
URLやPortを間違えた時に気づけないので、-Sを一緒に指定しておいた方が無難です。

# Portを間違えてしまったが、エラーメッセージがない
$ curl -s localhost:8081/api/json

# -Sの効果で、エラーメッセージが出力される。
$ curl -Ss localhost:8081/api/json
curl: (7) Failed to connect to localhost port 8081: 接続を拒否されました

HTTP Headerを確認する -I -i -v

-I で、Headerのみ取得し、出力することができます。

$ curl -I -s 'localhost:8080/api/json?'
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-Jenkins: 1.619
X-Jenkins-Session: b34411df
Content-Type: application/json;charset=UTF-8
Content-Length: 371
Server: Jetty(winstone-2.8)

-i ならば、Respnse Header, Body 両方を出力できます。

$ curl -i -s 'localhost:8080/api/json?'
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-Jenkins: 1.619
X-Jenkins-Session: b34411df
Content-Type: application/json;charset=UTF-8
Content-Length: 371
Server: Jetty(winstone-2.8)

{"assignedLabels":[{}],"mode":"NORMAL","nodeDescription":"ノード","nodeName":"","numExecutors":2,"description":null,"jobs":[],"overallLoad":{},"primaryView":{"name":"すべて","url":"http://localhost:8080/"},"quietingDown":false,"slaveAgentPort":0,"unlabeledLoad":{},"useCrumbs":false,"useSecurity":false,"views":[{"name":"すべて","url":"http://localhost:8080/"}]}

ただし、これはレスポンスのHttp Headerだけです。
curlでリクエストした時のHeaderも見たければ、-v が使用できます。

$ curl -v -s 'localhost:8080/api/json' | jq .
* Hostname was NOT found in DNS cache
*   Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> GET /api/json HTTP/1.1
> User-Agent: curl/7.38.0
> Host: localhost:8080
> Accept: */*
> 
< HTTP/1.1 200 OK
< X-Content-Type-Options: nosniff
< X-Jenkins: 1.619
< X-Jenkins-Session: b34411df
< Content-Type: application/json;charset=UTF-8
< Content-Length: 371
* Server Jetty(winstone-2.8) is not blacklisted
< Server: Jetty(winstone-2.8)
< 
{ [data not shown]
* Connection #0 to host localhost left intact
{
  "assignedLabels": [
    {}
  ],
  "mode": "NORMAL",
  "nodeDescription": "ノード",
  "nodeName": "",
  "numExecutors": 2,
  "description": null,
  "jobs": [],
  "overallLoad": {},
  "primaryView": {
    "name": "すべて",
    "url": "http://localhost:8080/"
  },
  "quietingDown": false,
  "slaveAgentPort": 0,
  "unlabeledLoad": {},
  "useCrumbs": false,
  "useSecurity": false,
  "views": [
    {
      "name": "すべて",
      "url": "http://localhost:8080/"
    }
  ]
}

もっと HTTP パケットのデータを確認する --trace --trace-ascii

--trace--trace-ascii を使用すると、HTTPリクエスト・レスポンスのデータを全て dump することができます。

まずは --trace の場合です。バイナリエディタとかでよく見る感じですね。

$ curl -sS  localhost:8080 -X POST -F "hoge=fuga" --trace trace.log -o /dev/null
$ cat trace.log  | head
== Info: Rebuilt URL to: localhost:8080/
== Info: Hostname was NOT found in DNS cache
== Info:   Trying 127.0.0.1...
== Info: Connected to localhost (127.0.0.1) port 8080 (#0)
=> Send header, 208 bytes (0xd0)
0000: 50 4f 53 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d POST / HTTP/1.1.
0010: 0a 55 73 65 72 2d 41 67 65 6e 74 3a 20 63 75 72 .User-Agent: cur
0020: 6c 2f 37 2e 33 35 2e 30 0d 0a 48 6f 73 74 3a 20 l/7.35.0..Host: 
0030: 6c 6f 63 61 6c 68 6f 73 74 3a 38 30 38 30 0d 0a localhost:8080..
0040: 41 63 63 65 70 74 3a 20 2a 2f 2a 0d 0a 43 6f 6e Accept: */*..Con

次に --trace-ascii の場合です。人間に優しい感じですね。

$ curl -sS  localhost:8080 -X POST -v -F "hoge=fuga" --trace-ascii trace-ascii.log -o /dev/null
$ cat trace-ascii.log  | head
== Info: Rebuilt URL to: localhost:8080/
== Info: Hostname was NOT found in DNS cache
== Info:   Trying 127.0.0.1...
== Info: Connected to localhost (127.0.0.1) port 8080 (#0)
=> Send header, 208 bytes (0xd0)
0000: POST / HTTP/1.1
0011: User-Agent: curl/7.35.0
002a: Host: localhost:8080
0040: Accept: */*
004d: Content-Length: 143

ちなみに、 --trace-time というオプションを付与すると、出力に日時が付与されるようになります。

$ curl -sS  localhost:8080 -X POST -v -F "hoge=fuga" --trace-ascii trace-ascii.log -o /dev/null --trace-time
$ cat trace-ascii.log  | head
08:01:57.605505 == Info: Rebuilt URL to: localhost:8080/
08:01:57.605768 == Info: Hostname was NOT found in DNS cache
08:01:57.610090 == Info:   Trying 127.0.0.1...
08:01:57.611293 == Info: Connected to localhost (127.0.0.1) port 8080 (#0)
08:01:57.612398 => Send header, 208 bytes (0xd0)
0000: POST / HTTP/1.1
0011: User-Agent: curl/7.35.0
002a: Host: localhost:8080
0040: Accept: */*
004d: Content-Length: 143

なお、-v --trace --trace-ascii は、どれか一つしか作用しません。これらを同時に指定した場合、最後に指定したオプションのみ有効になります。

POST

POSTリクエストを送るには、-X POST を付与します。

curl -sS -w '\n' -X POST 'localhost:8080/'

パラメータ付きPOST

POSTする際は、何らかのパラメータやデータを付与してリクエストすることが大多数だと思います。
--data またはその省略系である -d で、POSTで送信するデータを記述できます。
実際にJenkinsにPOSTし、ジョブの作成を試してみます。

# Jobを作成するPOST
$ curl -w '\n' 'http://localhost:8080/createItem' --data 'name=sample&mode=hudson.model.FreeStyleProject&Submit=OK' -XPOST

# 作成されたことを確認
$ curl -sS 'http://localhost:8080/api/json' | jq .jobs
[
  {
    "name": "sample",
    "url": "http://localhost:8080/job/sample/",
    "color": "notbuilt"
  }
]

パラメータ付きPOSTとURLエンコード

--data はURLエンコードしてくれませんので、あらかじめ自分でエンコードしておく必要があります。
試しに、上で作成したジョブ「sample」に、ファイルパラメータの設定を追加してみます。

# URLエンコード済みのパラメータを付与して、ジョブの設定を更新するPOST
$ curl -w '\n' 'http://localhost:8080/job/sample/configSubmit' --data 'json=%7b%22properties%22%3a+%7b%22hudson-model-ParametersDefinitionProperty%22%3a+%7b%22parameterized%22%3a+%7b%22parameter%22%3a+%7b%22name%22%3a+%22FileParameter%22%2c+%22description%22%3a+%22%22%2c+%22stapler-class%22%3a+%22hudson.model.FileParameterDefinition%22%2c+%22%24class%22%3a+%22hudson.model.FileParameterDefinition%22%7d%7d%7d%7d%7d%0d%0a&Submit=Save' -XPOST

# ジョブの中身を確認し、更新されていることを確認
$ curl -sS 'http://localhost:8080/job/sample/api/json' | jq .actions
[
  {
    "parameterDefinitions": [
      {
        "defaultParameterValue": null,
        "description": "",
        "name": "FileParameter",
        "type": "FileParameterDefinition"
      }
    ]
  }
]

自前でURLエンコードするのは面倒ですし、ぱっと見て何かわかりにくいです。
--data-urlencode を使うと、curlがURLエンコードしてくれるので、そのまま書いてしまうことができます。
試しに、上で追加したパラメータにDescriptionを追加してみます。

# URLエンコードはcurlに任せてPOST
$ curl -w '\n' 'http://localhost:8080/job/sample/configSubmit' --data-urlencode 'json={"properties": {"hudson-model-ParametersDefinitionProperty": {"parameterized": {"parameter": {"name": "FileParameter", "description": "Upload file to Jenkins.", "stapler-class": "hudson.model.FileParameterDefinition", "$class": "hudson.model.FileParameterDefinition"}}}}}' -d 'Submit=Save' -XPOST

# descriptionが "Upload file to Jenkins." に更新されてることを確認
$ curl -sS 'http://localhost:8080/job/sample/api/json' | jq .actions
[
  {
    "parameterDefinitions": [
      {
        "defaultParameterValue": null,
        "description": "Upload file to Jenkins.",
        "name": "FileParameter",
        "type": "FileParameterDefinition"
      }
    ]
  }
]

この時、&で複数のパラメータを定義しようとしても、肝心の&がエンコードされてしまいます。そのためSubmit=Saveは別途-dで付与しています。

ファイルをUploadするPOST

次は、POSTでJenkinsジョブをビルドしてみます。
JenkinsでPOSTと言ったらジョブのビルドですからね。

Jenkinsのファイルパラメータは、ローカルのファイルをJenkinsにUploadしてビルドすることができる機能です。
そのため、curlでファイルをUploadするPOSTをコマンドで表現する必要があります。

--form もしくは -F を使用します。

# Upload用ファイルを用意
$ echo "Sample file" > sample.txt

# Fileパラメータにsample.txtを指定してビルド実行
$ curl -sS 'http://localhost:8080/job/sample/build' -X POST -F "[email protected]" -F 'json={"parameter": [{"name":"FileParameter", "file":"file"}]}'

# ビルドが実行され、ファイルがUploadできていることを確認
$ curl -sS 'http://localhost:8080/job/sample/api/json' | jq .builds
[
  {
    "number": 1,
    "url": "http://localhost:8080/job/sample/1/"
  }
]
$ curl -sS 'http://localhost:8080/job/sample/1/parameters/parameter/FileParameter/sample.txt'
Sample file

@以降で、ファイルを相対パスで指定します。

ちなみにJenkinsのparameterizedビルドのcurlの使用例はJenkinsのHPに書いてあります。
下記ページ Submitting jobs の部分です。
https://wiki.jenkins-ci.org/display/JENKINS/Remote+access+API

-F-d は併用できません

$ curl localhost:8080 -X POST -F "hoge=fuga" -d "fuga=piyo"
Warning: You can only select one HTTP request!

こんな感じでエラーになります。同様に --data-urlencode も、-F とは併用できません。
-F を使いつつ、curlにURLエンコードを任せてPOSTするにはどうすれば。。。)

それぞれのオプションを付与した際のHTTPリクエストを観察すると、
-F では Content-Type: multipart/form-data で、
-d では Content-Type: application/x-www-form-urlencoded となります。
-F でマルチパートでPOSTすると設定しているのに、-d で違うContent-Typeで送ろうとしているから実行できない、という説明で良いでしょうか。。。もっと適切な表現があるとは思いますが。

ちなみに、 -F-d を同時に使った場合のエラーメッセージは、-S では表示されません。

$ curl -sS  localhost:8080 -X POST -F "hoge=fuga" --data-urlencode "fuga=piyo"
 # warning 行が表示されない
# 終了コードは 0 ではないです
$ echo $?
4

エラー時に気付くために -S というのに。。

認証とcurl

今までは、アカウント管理のない真っ裸なJenkinsにアクセスしていました。
ユーザアカウントとパスワードが必要な場合のcurlも試してみます。

Jenkinsの設定

詳細は割愛しますが、
Jenkinsのセキュリティ設定のMatrix-based securityで、
特定のユーザ(yasuhiroki)以外からはJenkinsにアクセスできないようにしています。

この設定をした状態でcurlを叩くと下のようになります。

$ curl -sS 'http://localhost:8080/api/json' -I
HTTP/1.1 403 Forbidden
X-Content-Type-Options: nosniff
Set-Cookie: JSESSIONID.827aac98=qv6zzok692pe1397gebsgms19;Path=/;HttpOnly
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: text/html;charset=UTF-8
X-Hudson: 1.395
X-Jenkins: 1.619
X-Jenkins-Session: 85e6322e
X-Hudson-CLI-Port: 52325
X-Jenkins-CLI-Port: 52325
X-Jenkins-CLI2-Port: 52325
X-You-Are-Authenticated-As: anonymous
X-You-Are-In-Group: 
X-Required-Permission: hudson.model.Hudson.Read
X-Permission-Implied-By: hudson.security.Permission.GenericRead
X-Permission-Implied-By: hudson.model.Hudson.Administer
Content-Length: 813
Server: Jetty(winstone-2.8)

想定通り、アカウント制限が機能しています。

アカウント名とパスワードを指定してリクエスト -u

いわゆるBASIC認証です。
今回は、ユーザ名はyasuhiroki、パスワードはsampleで作成したので、
-u yasuhiroki:sample と付与することになります。
見ての通り、コンソール上にパスワードがもろ残ってしまいます。

$ curl -sS 'http://localhost:8080/api/json' -I -u yasuhiroki:sample
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-Jenkins: 1.619
X-Jenkins-Session: 85e6322e
Content-Type: application/json;charset=UTF-8
Content-Length: 444
Server: Jetty(winstone-2.8)

コマンドにベタ書きしなければ、curl実行後に入力を求められます。

$ curl -sS 'http://localhost:8080/api/json' -I -u yasuhiroki
Enter host password for user 'yasuhiroki':
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-Jenkins: 1.619
X-Jenkins-Session: 85e6322e
Content-Type: application/json;charset=UTF-8
Content-Length: 444
Server: Jetty(winstone-2.8)

別に-uオプションを使わなくても、http://user:pass@hostnameでやってしまう方法もあります。

$ curl -sS 'http://yasuhiroki:sample@localhost:8080/api/json' -I
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-Jenkins: 1.619
X-Jenkins-Session: 85e6322e
Content-Type: application/json;charset=UTF-8
Content-Length: 444
Server: Jetty(winstone-2.8)

ちなみにJenkins自体に、パスワードとは別に使えるAPIトークンがあるので、そちらを使う手もあります。(むしろ、そちらを使うべき)
トークンはJenkinsのアカウントの設定で確認、変更ができます。

# JenkinsのAPIトークンを使用した例
$ curl -sS 'http://localhost:8080/user/yasuhiroki/api/json' -I -u yasuhiroki:a752ad2b9fc7b314562f386b603fc11f
HTTP/1.1 200 OK
X-Content-Type-Options: nosniff
X-Jenkins: 1.619
X-Jenkins-Session: 85e6322e
Content-Type: application/json;charset=UTF-8
Content-Length: 214
Server: Jetty(winstone-2.8)

SessionをCookieから取得して使用 -c -b

Sessionを保持して使用する必要がある場合、curlを単発で叩いては実現が難しい動作もあるでしょう。
-c cookie.txt で、レスポンスのCookieをcookie.txtに保存し、
-b cookie.txt で、cookie.txtの中身をリクエストのCookieに含めることができます。

Jenkinsの操作をSessionでどうにかできないか試しましたが、
何となくできそうな気がするものの、時間が掛かりそうなので割愛します...。
いつかリベンジしたいですね。

ところで、認証に失敗した時の終了コードは? --fail -f

たとえば、-u で渡すパスワードを間違えたとします。

$ curl -sS 'http://localhost:8080/api/json' -I -u yasuhiroki:miss
HTTP/1.1 401 Invalid password/token for user: yasuhiroki
X-Content-Type-Options: nosniff
WWW-Authenticate: Basic realm="Jenkins"
Content-Type: text/html;charset=ISO-8859-1
Cache-Control: must-revalidate,no-cache,no-store
Content-Length: 1441
Server: Jetty(winstone-2.8)

$ echo $?
0

終了コードは0なのです。
もし、何かエラーがあった時は、終了コードを 0 以外のを返して欲しい場合は、 --fail (-f) オプションが利用できます。

$ curl -sS 'http://localhost:8080/api/json' -u yasuhiroki:miss -f
curl: (22) The requested URL returned error: 401
$ echo $?
22

ただし、 401 407 エラーのような、認証関連の場合は確実性に欠けているそうです。

This method is not fail-safe and there are occasions where non-successful response codes will slip through,
especially when authentication is involved (response codes 401 and 407).

他のTips

-w でいろんな情報を表示する

curlの実行結果に、追加してさらに情報を出力することが可能です。

$ curl -sSO 'http://localhost:8080/api/json' -u yasuhiroki:a752ad2b9fc7b314562f386b603fc11f -w 'hogefuga'
hogefuga

-w では、特定の変数を使用すると、その変数が持つ値を表示できます。

例えば、HTTPステータスだけを表示したいなら、-w '%{http_code}\n' でOKです。改行はお好みで。

$ curl -sS -w '%{http_code}\n' 'http://localhost:8080/' -o /dev/null
200

ちなみに、manを探ったところ、27の変数が見つりました。

URLエンコードするだけ

-w '%{url_effective}' --data-urlencode -Gを使用して、文字列をURLエンコードして表示するだけのシェルスクリプトです。

-G (--get) は、-d--data-urlencode で指定した値を、クエリーとして自動的に付与しなおしてくれるオプションです。
例えば、-d 'val=A' -G としておけば、リクエストURLに、host/?val=A などとクエリーとして付与したうえで送信してくれます。
これと、-w を組み合わせることで、curlがURLエンコードしてくれた結果を出力することができます。/?がくっつくのが玉に瑕です。

$ curl -s -w '%{url_effective}\n' --data-urlencode 'じぇんきんす' -G ''
/?%E3%81%98%E3%81%87%E3%82%93%E3%81%8D%E3%82%93%E3%81%99

/?が邪魔な場合は、例えばこんな感じでしょうか。

$ urlencoded_str=$(curl -s -w '%{url_effective}\n' --data-urlencode 'じぇんきんす' -G '')
$ urlencoded_str=${urlencoded_str:2}
$ echo ${urlencoded_str}
%E3%81%98%E3%81%87%E3%82%93%E3%81%8D%E3%82%93%E3%81%99

他によく使うオプション

-k SSL証明書を無視する。主に、オレオレ証明書を使っているWebサーバーにアクセスする時に。

-x Proxyを指定。

-H Request Headerを追加する。Content-Typeを指定する時など
- 例: -H "Content-Type: application/json"

-L リダイレクト先までアクセスする。

おまけ

発音、何て読む?

初めて見た時から「カール」と呼んでいましたが、公式サイトでは、

The fact it can also be pronounced 'see URL' also helped
- http://curl.haxx.se/docs/faq.html#What_is_cURL

'see URL' と同じ、つまり、「シーユーアールエル」だそうです。
と、いいつつドキュメントの続きをよく読むと、

We pronounce curl with an initial k sound. It rhymes with words like girl and earl.

なんて書いてあり、おまけに発音例を .wav ファイルで提供しており、「カール」と呼んでいます。
(じゃあもう最初から カール で良いじゃないか。。。)

User-Agentを偽装する

Webサイトによっては、curl コマンドによるアクセスを制御している場合があるそうです。
そういう時は、 -A オプションを指定して、User-Agentを空にするとうまく行くかもしれません。

$ curl -s -w '%{http_code}\n' http://www.amazon.co.jp/dp/B00JEYPPOE/ -o /dev/null 
503
$ curl -s -w '%{http_code}\n' http://www.amazon.co.jp/dp/B00JEYPPOE/ -o /dev/null  -A ''
200

参考) http://jarp.does.notwork.org/diary/201508c.html#20150825

Optionの数

どれくらいあるのか、ざっくり調べたところ、170ありました。

$ man curl | egrep -- '^[[:space:]]{7}-' | wc -l
170
$ curl -h | egrep -- '^[[:space:]]+-' | wc -l
168
# helpには、 --environment と --proxy-header がなかった

もっと便利そうな http コマンド (httpie)

この記事を書き始めた後、 httpie というものを知りました。
https://github.com/jkbrzt/httpie

例えば、json形式のファイルをPOST時に渡せば勝手に Content-Type: application/json を設定してくれるなど、curl よりも気が利いています。