😣

Fetch API を使った時に redirect が発生すると default では response.ok = true になるので注意

2024/05/10に公開

TL;DR

  • JavaScript の Fetch API を使った時に redirect が発生すると default では Response.ok = true になる(こともある)
    • それどころか Response.status もリクエスト先が302だろうが200になったりする
  • これは、Fetch API は redirect オプションでリクエスト先からリダイレクトステータスが返ってきた時の挙動を指定することができ、その default 挙動が「リダイレクトに従う」だから
  • redirect オプションを指定せずデフォルト挙動のまま、Response.redirected を用いて判別してもいいし、Fetch API 実行時に redirect: 'error'redirect: 'manual' を指定することで制御してもいい

redirect が発生した時の redirect オプションごとの Response オブジェクト

まず、fetchAPIはこんな感じで実行するとする

const response = await fetch("/hoge/fuga", {
  method: "POST",
  credentials: "same-origin",
  headers: {
    "Content-Type": "application/json"
  },
  body: JSON.stringify(postBody),
  redirect: "follow"
});

※fetchAPI実行時に redirect オプションを省略すると redirect: 'follow' 扱いで実行される

redirect: 'follow'

Response: {
  body: ..., // リクエストによる
  bodyUsed: false,
  headers: ..., // リクエストによる
  ok: true,
  redirected: true, // ここが true!!
  status: 200, // 最終的なリダイレクト先の HTTP status!!
  statusText: "OK", // 最終的なリダイレクト先の HTTP status Text!!
  type: "basic",
  url: "https://<最終的なリダイレクト先のURL>"  // 最終的なリダイレクト先のURL!!
}

Response.ok 読取専用
レスポンスが成功 (200–299 の範囲のステータス) したか否かを通知する論理値が入ります。

https://developer.mozilla.org/ja/docs/Web/API/Response

と MDN にはあるが、fetch API でのリクエスト先が HTTP 302 などでリダイレクト指定がある場合は
リダイレクトが発生した後の「その先のURL」の HTTP Status が入ってくる。
リダイレクト先からさらにリダイレクトした場合、最終的なリダイレクト先のURLへリクエストした結果がresponseオブジェクトに入ってくるようだ。

最終的なリダイレクト先が HTTP 404 とかなら reponse.ok = false になりそうではある(あんまりなさそう)

redirect: 'error'

fetch API (= fetch() )自体が例外を吐くので
Response オブジェクトは取得できない

catch すると net::ERR_FAILED 302 (Found)

redirect: 'manual'

Response: {
  body: ..., // リクエストによる
  bodyUsed: false,
  headers: ..., // リクエストによる
  ok: false,
  redirected: false,
  status: 0, // マジで0
  statusText: "", // マジで空文字
  type: "opaqueredirect",
  url: "https://<fetch API のリクエスト先>"
}

"manual" – HTTP リダイレクトには従いませんが、response.url が新しい URL になり、response.redirected が true になります。必要に応じて新しい URL へ手動でリダイレクトすることができます。

https://ja.javascript.info/fetch-api

と JavaScript.info にはあるが、Google Chrome v124.0.6367.119 で試した感じ
redirected: false だった。

また、

Response.redirected 読取専用
レスポンスがリダイレクトの結果である (つまり、その URL リストには複数のエントリーがある) かどうかを示します。

と MDN にはあるが、 Google Chrome v124.0.6367.119 で試した感じ
Response.url は単一の String だしどこに URL リストがあるのかはわからなかった。
しかも Response.url に元々の fetch API のリクエスト先があるので
redirect: 'manual' と言いつつ、リダイレクト先を知り再度fetchしなおす方法すらないように感じる。
Response.headers.get('Location') してもリダイレクト先は取得できなかった。
な、何だこれは……

と思ったらこんなISSUEがあった
https://github.com/whatwg/fetch/issues/763

Cannot get next URL for redirect="manual"

That is by design

redirect: 'manual' がリダイレクト先を知れないのは仕様だそうだ。無念…

気になるところをまとめると

Response.ok Response.redirected Response.url
redirect: 'follow' true true 最終的なリダイレクト先のURL
redirect: 'error' (例外になる) - -
redirect: 'manual' false false fetch API のリクエスト先

※最終的なリダイレクト先が HTTP 404 とかなら reponse.ok = false になりそうではある(あんまりなさそう)

結局、リダイレクトをどう制御してやるのがいいか

まずはこの記事の主題である

Fetch API を使った時に redirect が発生すると default では response.ok = true になる

をとにかく知っておく必要がある。
その上で、リクエスト先がリダイレクトが発生し得ないのであれば気にしなくていい。
ただし、POST時の認証セッション切れなどでリダイレクトが発生し得るのであれば、以下の制御を行っておいた方が良さげ。

  • redirect: 'follow' (または省略) を使う
    • Response.redirected が唯一 true になり得るのがこれ
    • Response.redirected が true だったらリダイレクトが発生してるので、リダイレクトした時の制御をしてやればいい
    • Response.url には最終的なリダイレクト先のURLが格納されているので、それを使って 再度fetchしその結果の Response.text() を使って強引にリダイレクト先のDOMを使って new DOMParser().parseFromString(Response.text(), "text/html") みたいなことをして強引にリダイレクト先のDOMを使ってアレコレすることもできそうだが、あんまりそういう変な?ことはしない方がいい……と思う……
      • 詳しく知りたい人がもしいればコメントください
  • redirect: 'error' を使う
    • リダイレクトが発生したら例外になるので try-catch してやればいい
    • fetchAPI の error は基本的に通信での失敗のケースのみで発生するはずなので、catch時の制御は通信の失敗 or fetchAPI実行時のリダイレクト発生となるので、そこだけは注意
  • redirect: 'manual' を使う
    • マニュアルとか言いつつ、実態は「エラーにはしないが、リダイレクト先に追従もしない」って挙動
    • ただ、redirect: 'follow' とは違いリダイレクトが発生すると Response.ok が false になるので if (!Response.ok) みたいな条件で HTTP 200番台以外のステータスコード時の制御と一律で制御組めるのはよさそう

もっと簡単にまとめて!

  • エラーにするなら redirect: 'error'
  • Response.ok で制御するなら redirect: 'manual'
  • Response.redirected で制御するなら redirect: 'follow' (or指定なし)

が結論ってことでどうでしょうか

参考

Discussion