cURL から fetch / axios への移行で詰まる 7 つのポイント
執筆: ありがっさまりょうた
デジタル道具屋を個人で開発・運用しています。記事は実装時の検証結果と公式仕様を照合して執筆しています。
本記事の執筆方針と編集ポリシーについてはAbout ページをご覧ください。
導入:ドキュメントの cURL は動くのに、JavaScript から呼ぶと 401 になる理由
API ドキュメントに掲載されている cURL コマンドはそのまま実行すると成功するのに、同じ URL とヘッダーを fetch に書き換えると 401 や 403 が返る、という現象は珍しくありません。 原因の多くは、cURL と fetch のデフォルト挙動の違いにあります。
cURL の各オプションには暗黙の副作用(メソッドの自動変更、ヘッダーの自動付与、Cookie の自動送信など)があり、これらを fetch 側で明示しないと「同じリクエストのつもりで別のリクエストを送っている」状態になります。 本記事では、移行時に差が出やすい 7 つのポイントを順に解説します。
落とし穴 1:-d は「自動的に POST + form 形式」
cURL の -d オプションは 2つの暗黙の振る舞いがあります。
- HTTP メソッドを 自動的に POST に変える
Content-Type: application/x-www-form-urlencodedを 自動付与する
fetch ではこれを明示する必要があります:
# cURL
curl -d 'name=alice&age=20' https://api.example.com/users
// 等価な fetch
fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ name: 'alice', age: '20' }),
})JSON を送る場合は、Content-Type: application/json と JSON.stringify(...) をセットで指定します。 cURL 側で -H 'Content-Type: application/json' を明示している場合、-d によるフォーム形式の自動付与は上書きされるため、fetch でも同じヘッダーを明示する必要があります。
# cURL(JSON を POST)
curl -X POST \
-H 'Content-Type: application/json' \
-d '{"name":"alice","age":20}' \
https://api.example.com/users
// 等価な fetch
fetch('https://api.example.com/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'alice', age: 20 }),
})なお、-u user:pass(Basic 認証)は fetch では Authorization: Basic ヘッダーに置き換えます。 値は user:pass を Base64 エンコードした文字列で、ブラウザでは btoa('user:pass')、Node.js では Buffer.from('user:pass').toString('base64') で生成できます。
落とし穴 2:Cookie の自動送信
cURL は同一セッション内では Cookie を自動で扱います。fetch は明示的に credentials: 'include' を指定しないと Cookie を送りません。 認証 Cookie を使う API では、これを忘れると 401 / 403 になります。
fetch('https://api.example.com/me', {
credentials: 'include', // または 'same-origin'
})さらにブラウザ環境では、サーバー側で Access-Control-Allow-Credentials: true と Access-Control-Allow-Origin に具体的なオリジン(* ではダメ)を返さないと、Cookie 送信が拒否されます。
落とし穴 3:CORS は cURL では起きない
「cURL では成功するのに、ブラウザの fetch では失敗する」という場合、CORS(Cross-Origin Resource Sharing)が原因である可能性が高いです。 cURL はブラウザのセキュリティ機構を経由しないため、CORS の制約を受けません。 一方、ブラウザからクロスオリジンの API を呼ぶ場合、サーバーが適切な Access-Control-* ヘッダーを返さないとレスポンスがブロックされます。
ブラウザ環境でのみ発生する問題は主に次の 2 つです。
- CORS:プリフライト(OPTIONS)リクエストを含め、サーバー側の許可ヘッダーが必要
- Cookie の
SameSite属性:クロスサイトのリクエストではSameSite=None; Secureでないと Cookie が送信されない
対策は基本的にサーバー側で適切な Access-Control-* ヘッダーを返すこと。 自分が API を握っていない場合は、Next.js の API ルートやプロキシを挟んで自サーバー経由で呼ぶ手もあります。
落とし穴 4:リダイレクトの扱い
cURL で -L を付けないとリダイレクトを追わないのに対し、fetch はデフォルトで redirect: 'follow'(自動追従)です。 逆に「最初のリクエストだけ送ってリダイレクトの Location ヘッダーを取りたい」場合は、fetch 側で redirect: 'manual' を指定します。
落とし穴 5:HTTPメソッドと body の組み合わせ
GET リクエストに body を付けると、fetch は TypeError になります。 cURL は -X GET -d '...' でも黙って受け付けてしまうため、API ドキュメントが間違って GET + body で書かれているケースがあります。 その場合はクエリパラメータに変更するか、サーバー実装を確認しましょう。
落とし穴 6:レスポンスの読み方
fetch のレスポンスは 1度しか読めません。response.json() を呼んだ後に response.text() を呼ぶとエラーです。 生のテキストとパース後の両方が欲しい場合は、const text = await response.text(); const data = JSON.parse(text); と段階を分けます。
// 失敗するパターン
const data = await response.json();
const text = await response.text(); // → エラー
// 正しいパターン
const text = await response.text();
const data = JSON.parse(text);落とし穴 7:ステータスコードはエラーにならない
cURL は終了コードで成功/失敗を判別しますが、fetch は HTTP レベルで通信できれば 4xx / 5xx でも resolve します。try/catch だけではサーバーエラーを捕捉できないので、response.ok を必ずチェックしましょう。
const res = await fetch(url);
if (!res.ok) {
// 4xx / 5xx の処理
throw new Error(`API error: ${res.status}`);
}
const data = await res.json();axios はこの点で挙動が異なり、デフォルトで 4xx / 5xx のレスポンスを例外(reject)として扱います(validateStatus オプションで変更可能)。 fetch と axios を併用するコードベースでは、fetch 側にも上記のような response.ok によるガードを入れて、エラー処理の挙動を揃えておくと保守しやすくなります。
変換作業を機械化する
オプションが多い cURL コマンドを手作業で fetch / axios に書き換えるのは事故のもとです。 当サイトのcURL → Fetch 変換ツールでは、 cURL コマンドを貼り付けるだけで JavaScript の fetch / axios コードに変換できます。 生成されたコードに、本記事で解説した credentials・response.ok チェック・エラーハンドリングを足して仕上げてください。
関連: HTTP ステータス検索、JSON 整形(API レスポンスのデバッグに)、URL エンコード/デコード。
よくある失敗パターン
AbortController を渡していないと、画面遷移後もバックグラウンドで fetch が完了まで走り続け、 アンマウント済みのコンポーネントに対して setState が呼ばれる警告や、不要な処理の残留につながります。 React では useEffect のクリーンアップ関数で controller.abort() を呼ぶのが基本的な対策です。 React Query や SWR などのデータ取得ライブラリはこのキャンセル処理を内部で扱うため、複数の fetch を扱う場合は導入を検討する価値があります。
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then((res) => res.json())
.then(setData)
.catch((err) => {
if (err.name !== 'AbortError') throw err;
});
return () => controller.abort();
}, [url]);参考にした一次情報
本記事の内容は、以下の公式仕様や一次情報を参照して執筆しています。
- Fetch Standard
WHATWG
https://fetch.spec.whatwg.org/
- curl - command line tool and library
curl project
https://curl.se/docs/manpage.html
- CORS - Cross-Origin Resource Sharing | MDN
MDN Web Docs
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS