「curl で API を叩いたら、返ってくるのが JSON じゃない」 「同じ URL をブラウザで開くと普通に表示されるのに、curl だけ結果が違う」 「エラーは後段の Python が吐く JSONDecodeError。API が壊れたと思って半日調べていた」
こういうとき、犯人が API でもネットワークでもなく curl 自身だったというケースがあります。curl は URL に含まれる角括弧を「複数 URL への展開(グロブ)」として読むため、配列添字を使うクエリを投げると、こちらが書いたのとは違うリクエストになってしまうのです。
この記事では、なぜそうなるのか、どう見分けるのか、そして1文字のオプションでどう直すのかを、そのままコピーできるコマンド付きでまとめます。
curl は URL 中の角括弧を「範囲指定」、波括弧を「列挙」として解釈し、複数のリクエストに展開する
そのため配列添字を使う API のクエリ(
apps[0].app=201 のような形)が壊れる エラーは curl ではなく後段の JSON パースで出ることが多く、原因が見えにくい
対処は
-g(--globoff)を足すだけ。API 用の curl は curl -sg を既定形にする シェルの引用符では防げない。展開しているのはシェルではなく curl 自身
目次を開く
curl の URL は「1本」とは限らない
curl には、1つの URL を複数のリクエストに展開する機能があります。使うのは2種類の記号です。
# 波括弧=列挙。3リクエストに展開される
curl -O "https://example.com/{alpha,beta,gamma}.txt"
# 角括弧=範囲。001 から 010 まで 10リクエストに展開される
curl -O "https://example.com/img[001-010].png" 連番のファイルをまとめて落とすときには便利な機能です。問題は、この展開をしているのがシェルではなく curl 自身だという点にあります。シングルクォートで囲んでもダブルクォートで囲んでも、curl まで届いた角括弧は curl が展開します。ここが最大の誤解ポイントです。
詰まりの実例:配列添字を含む API クエリ
多くの Web API は、配列パラメータを name[0]=value の形で受け取ります。kintone・GitHub・PHP や Rails 系のフォーム送信などで広く使われている書き方です。
curl "$BASE/k/v1/preview/app/deploy.json?apps[0].app=201" curl から見ると、この角括弧は範囲指定です。中身が範囲として解釈できなければリクエストを送る前にエラーになり、解釈できてしまう形なら別の URL に展開されて送られます。どちらにせよ、こちらが意図した1本のリクエストにはなりません。
なぜ「API が壊れた」と誤診するのか
スクリプトの中では、curl の出力をそのまま次の処理に渡していることがほとんどです。
curl -s "$URL" | python3 -c "import sys, json; print(json.load(sys.stdin))" -s を付けている以上、curl 側のエラーは画面に出ません。出力は JSON ではないので、落ちるのは後段の Python で、メッセージは json.decoder.JSONDecodeError になります。curl という文字がエラーのどこにも出てこないため、「API のレスポンスが変わった」「認証が切れた」と見当違いの方向を調べることになります。
直し方:-g(--globoff)を足す
グロブ機能を切るオプションが用意されています。
curl -g "$BASE/k/v1/preview/app/deploy.json?apps[0].app=201" 短縮形の -g と長い形の --globoff は同じ意味です。実務では -s(進捗を出さない)と合わせて、次の形を定型にしておくと踏まなくなります。
curl -sg -H "X-Cybozu-API-Token: $TOKEN" "$URL" API を叩く curl は `-sg` から書き始める、と決めてしまうのが一番確実です。
切り分け:-v で「実際に送られた URL」を見る
疑わしいときは、リクエストの中身を目で確認します。
curl -gv "$URL" -o /dev/null 出力の中の GET /k/v1/... で始まる行が、自分の書いたパス・クエリと一致しているかを見ます。ここがズレていれば、原因は API より手前にあります。
| 見えるもの | 判断 | 次の手 |
|---|---|---|
-g を外すとエラー、付けると通る | グロブ展開が原因で確定 | -sg を定型にする |
| どちらでも同じレスポンスが返る | curl は無関係 | 認証ヘッダ・パラメータ名を疑う |
| 送信行のクエリが自分の書いた形と違う | 手前で URL が加工されている | 変数展開・パス連結を確認する |
角括弧を残したまま通す:パーセントエンコード
-g を使わずに済ませたい場合は、角括弧そのものをエンコードしてしまう手もあります。角括弧の開きは %5B、閉じは %5D です。
curl "$BASE/k/v1/preview/app/deploy.json?apps%5B0%5D.app=201" これならグロブの対象になりません。ただし読みづらく、パラメータが増えるほど間違えやすくなるので、常用するなら -g のほうが素直です。
併用できない点に注意
-g はグロブ機能そのものを無効化します。つまり、同じコマンドで連番ダウンロードは使えません。連番取得と API 呼び出しは別のコマンドに分けてください。オプションの詳しい説明は、手元の版の curl --help や man curl で確認できます。
まとめ
- curl は URL 中の角括弧を範囲、波括弧を列挙として自分で展開する
- そのため配列添字を使う API のクエリが壊れ、症状は後段の JSON パースエラーとして現れる
- シェルの引用符では防げない。展開しているのは curl 自身
- 対処は
-g(--globoff)。API 用の curl はcurl -sgを既定形にしておく - 迷ったら
-vを付けて、実際に送られたリクエスト行を目で見るのが最短
エラーメッセージが原因と噛み合わない種類の詰まりなので、一度踏んでおくと次から数分で片付きます。
よくある質問(FAQ)
シェルの引用符で囲めば防げますか?
防げません。角括弧を展開しているのはシェルではなく curl 自身なので、シングルクォートでもダブルクォートでも結果は変わりません。防ぐには -g を付けるか、角括弧をパーセントエンコードします。
-g を付けると連番ダウンロードが使えなくなりますか?
なります。-g はグロブ機能全体を切るオプションなので、範囲指定や列挙も一緒に無効になります。連番でファイルを落とす処理と、API を叩く処理は別のコマンドに分けるのが安全です。
POST のボディに角括弧が入っている場合も壊れますか?
グロブが働くのは URL の部分だけです。-d や --data で渡すボディは影響を受けません。壊れるのは、パスやクエリ文字列に角括弧を書いたときです。
wget や Python の requests でも同じことが起きますか?
起きません。URL のグロブ展開は curl 側の機能です。「ブラウザや他のクライアントでは通るのに curl でだけ結果がおかしい」という状況なら、まずこれを疑う価値があります。
