Brave(スマホ)で fetch() が「Request cannot be constructed from a URL that includes credentials」で失敗する
まとめ
user:pass@host形式の URL でページを開くと、fetch()の相対パスが認証情報を引き継いでしまうfetch('/api/...')が “Request cannot be constructed from a URL that includes credentials” で死ぬlocation.originを先頭に付けた絶対 URL にすれば解決- Chrome(デスクトップ)は通してしまうので PC では気づかない
ローカルネットワークの Web アプリをスマホ(Brave)から操作していたとき、ボタンを押すと画面がわずかに光るのに状態が変わらない、という症状が出た。サーバーログにはアクセスの記録すらない。
調査
まず alert() でエラーを可視化するデバッグコードを仕込んだ。出てきたのがこれ。
FAV NETWORK ERROR: Failed to execute 'fetch' on 'Window':
Request cannot be constructed from a URL that includes credentials:
/api/resource/fav?site_id=xxx「URL に認証情報が含まれている」と言っている。でも URL は /api/... という相対パスで、認証情報なんて書いていない。意味がわからなかった。
原因
ページを開いたときの URL が原因だった。
http://username:password@192.168.2.40:8000/Basic 認証をブラウザに覚えさせるため、user:pass@host 形式の URL を使っていた。このとき window.location.href は認証情報を含んだ状態になる。
fetch('/api/...') は相対 URL を現在のページ URL を基準に解決するので、実際に fetch に渡る URL は http://username:password@192.168.2.40:8000/api/... になる。Fetch 仕様では認証情報入りの URL は TypeError を投げることになっており、Brave はこれを厳格に適用する。
Chrome(デスクトップ)は同じ状況でも通してしまうので、PC では問題なく動いていた。スマホだけ壊れてる状態が長い間気づかれなかった。
修正
location.origin を使う。
// 修正前(相対URL → 認証情報を引き継ぐ)
const resp = await fetch(`/api/resource/${id}/fav?site_id=${siteId}`, { ... });
// 修正後(location.origin は認証情報なしの origin を返す)
const resp = await fetch(`${location.origin}/api/resource/${id}/fav?site_id=${siteId}`, { ... });location.origin は http://192.168.2.40:8000 を返す(認証情報は含まない)。URL の origin は scheme + host + port だけで構成されるため、user:pass@ の部分は落ちる。
補足
fetch() の話だったが、同じ理由で XMLHttpRequest でも発生する。user:pass@host 形式の URL でページを提供するなら、全ての fetch 呼び出しを location.origin ベースの絶対 URL にしておくのが安全。
調査の糸口になったのは alert() でエラーを吐かせることだった。当初はエラーが silent fail していて原因が掴めなかった。モバイルのデバッグは手間がかかるので、こういうローレベルな可視化が効く。