1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

curlでAPIを測ったつもりが測れていなかった。実測で確かめた8つの落とし穴

1
Posted at

先に要点だけ置きます。

  • URLに [ を直接書くと curl: (3) で落ちる。-g が要る
  • --compressed を付けると %{size_download} と実ファイルのサイズが一致しない
  • -s だけではエラーが無言で通る。終了コードも0
  • -f を足すとエラー本文が消える
  • --data@ をファイル名として読む
  • --data は改行を落として連結する。パラメータが壊れる
  • --data-urlencode+%2b にする
  • コマンド置換は末尾の改行を落とす。サイズがずれる

以下、2026年8月27日に手元で確かめた記録です。curl 8.7.1、macOS と Linux の両方で同じ結果でした。

きっかけ

あるAPIのレスポンスを軽くするために、不要なフィールドを落とすパラメータを付けました。サイズを測ったら、6.5倍に増えていました。

原因はAPI側の仕様で、渡した false が文字列として真と解釈されていたためです。それはそれで書いたのですが、調べる過程で気づいたことがありました。

**サイズを測る手順のほうにも、間違いが3つ混ざっていました。**測定が正しかったから異常に気づけたわけで、測り方を間違えていたら「効いている」と思ったまま終わっていた可能性があります。

角括弧は、書く場所で挙動が変わる

多くのAPIが request[fields][description] のような角括弧を使います。curl はこれをグロブとして解釈します。

$ curl -sS "https://api.example.com/?a[b]=1"
curl: (3) bad range in URL position 27:
https://api.example.com/?a[b]=1
                          ^

-g--globoff)で止まります。

$ curl -sS -g "https://api.example.com/?a[b]=1"

ただし、**--data-raw 経由なら -g なしでも通ります。**グロブの解釈はURL引数にだけかかるためです。

$ curl -sS --get "https://api.example.com/" --data-raw 'a[b]=1'

どちらでも動くので、わたしは -g を付けたうえで --data-raw を使っています。あとからURL直書きに書き換えたときに落ちないためです。

--compressed を付けると、測っている対象が変わる

同じURLを2回叩いて、サイズを比べました。

$ curl -sS "$URL" -o r.json -w "size_download=%{size_download}\n"
size_download=192973
$ wc -c < r.json
192973

$ curl -sS --compressed "$URL" -o r.json -w "size_download=%{size_download}\n"
size_download=43299
$ wc -c < r.json
192973

--compressed を付けると、%{size_download}転送されたバイト数(gzip後の43,299)を返し、ファイルは展開後の192,973になります。

どちらも正しい値です。回線の負荷を見たいなら転送量、パースの負荷や中身の量を見たいなら展開後。**測りたいほうを先に決めておかないと、数字だけ残って意味が消えます。**両方を並べて記録するのがいちばん誤解がありません。

これに気づかず %{size_download} だけを記録していると、--compressed を付けたり外したりした瞬間に、比較できない数字が混ざります。

-s だけだと、失敗が成功に見える

404を返すURLを叩きます。

$ curl -s "$URL_404" -o /dev/null; echo "終了コード=$?"
終了コード=0

**終了コードは0です。**HTTPのエラーは、curl にとって通信の失敗ではありません。何も出力されず、スクリプトは次へ進みます。

-f--fail)を足すと変わります。

$ curl -sS -f "$URL_404" -o /dev/null; echo "終了コード=$?"
curl: (22) The requested URL returned error: 404
終了コード=22

-S-s で消したエラー表示を戻すオプションなので、-sS は組で使います。

ただし -f は、エラー本文を捨てる

APIのエラーは、本文に理由が書いてあることがほとんどです。-f を付けると、それが読めません。

$ curl -sS "$URL_404"
{"message": "Not Found"}

$ curl -sSf "$URL_404"
(何も出ない)

理由が要るなら -f を使わず、HTTPコードを別に取ります。

CODE=$(curl -sS "$URL" -o body.json -w '%{http_code}')
if [ "$CODE" -ge 400 ]; then
  echo "HTTP $CODE"
  cat body.json     # ここに理由が書いてある
  exit 1
fi

レート制限に当たったときなど、本文を読まないと待つべきか諦めるべきかが分かりません。

--data@ をファイル名として読む

これは見つけにくいほうの罠です。

$ echo 'ファイルの中身' > at.txt

$ curl -sS -g --get "$URL" --data "@at.txt" -w '%{url_effective}\n' -o /dev/null
https://api.example.com/?ファイルの中身

$ curl -sS -g --get "$URL" --data-raw "@at.txt" -w '%{url_effective}\n' -o /dev/null
https://api.example.com/?@at.txt

パラメータに @ を含む文字列(メールアドレスなど)を渡すと、存在しないファイルを読もうとして黙って空になります。--data-raw なら文字どおり送られます。

--data は改行を落として連結する

ファイルからパラメータを読み込むとき、こうなります。

$ printf 'a=1\nb=2\n' > multi.txt
$ curl -sS -g --get "$URL" --data "@multi.txt" -w '%{url_effective}\n' -o /dev/null
https://api.example.com/?a=1b=2

a=1b=2 です。& になりません。改行がただ消えます。パラメータを行区切りのファイルで持っている場合、気づかないまま1つ目の値だけが壊れた形で送られます。

配列に持って、シェル側で & を組むほうが安全です。

DROP=(description sections screenshots ratings tags)
Q='action=query&per_page=250'
for f in "${DROP[@]}"; do Q="$Q&fields[$f]=0"; done
curl -sS -g --get "$URL" --data-raw "$Q" -o out.json

--data-urlencode+%2b にする

空白や記号を含む値には --data-urlencode を使いますが、変換の中身を把握していないと結果が変わります。

$ curl ... --data-raw "q=a b&x=1+2"
https://api.example.com/?q=a b&x=1+2

$ curl ... --data-urlencode "q=a b" --data-urlencode "x=1+2"
https://api.example.com/?q=a+b&x=1%2b2

空白は + に、+%2b になります。元から + を含む値を送るときは、この変換が入って正しくなります。逆に、すでにエンコード済みの文字列を --data-urlencode に渡すと二重エンコードになります。

--data-raw は一切変換しません。自分でエンコードする前提です。

コマンド置換は、末尾の改行を落とす

最後がいちばん引っかかりました。

$ BODY=$(curl -sS "$URL")
$ echo ${#BODY}
192972

$ curl -sS "$URL" -o v.json && wc -c < v.json
192973

**1バイト違います。**シェルのコマンド置換は、末尾の改行を取り除く仕様です。1つではなく、連続する改行すべてが落ちます。

サイズを測る目的なら、必ずファイルに落としてから wc -c で数えます。変数に入れて長さを見る方法は、JSONの中身を扱うぶんには問題ありませんが、測定には使えません。

わたしは最初これで測っていて、期待と違う数字が出た理由をしばらく別のところに探していました。

まとめの形

以上を全部入れると、こうなります。

#!/bin/zsh
set -u
API='https://api.example.com/'
UA='my-tool/1.0'

fetch() {   # fetch <出力先> <クエリ文字列>
  local out="$1" query="$2"
  local code
  code=$(curl -sS -g -A "$UA" --compressed --get "$API" \
           --data-raw "$query" -o "$out" -w '%{http_code}')
  if [ "$code" -ge 400 ]; then
    echo "HTTP $code" >&2
    cat "$out" >&2          # エラー本文を捨てない
    return 1
  fi
}

DROP=(description sections screenshots)
Q='action=query&per_page=250'
for f in $DROP; do Q="$Q&fields[$f]=0"; done

fetch page.json "$Q" || exit 1
echo "展開後 $(wc -c < page.json) bytes"

-f は使わず、HTTPコードで自分で判定しています。サイズはファイルから数えています。

最後に

このうち半分は、動いているスクリプトの中に潜んでいても気づけません。エラーにならないからです。

いま動いているスクリプトで、サイズをコマンド置換で測っていませんか。-f でエラー本文を捨てていませんか。心当たりがあれば、値をひとつ手で確かめてみてください。


ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?