「シミュレータでは普通に動いていた API 通信が、TestFlight に出した瞬間に何の警告もなく無反応になる」 — これは私自身、2014年から個人でアプリを出し続けて累計5,000万DLに届いた頃に、ある広告ネットワークの計測SDKを差し替えた当日に直面した症状でした。アプリは落ちず、ローディングも止まらず、ただレスポンスだけが返ってきません。CS問い合わせが3日で200件積み上がり、原因を追ったら iOS の App Transport Security (ATS) が静かに HTTP 通信を遮断していた、という結末です。
この沈黙のエラーは、Rork のように React Native + EAS でビルドする環境で特にやっかいです。Web 開発の感覚で http://api.example.com をそのまま叩こうとすると、シミュレータの一部バージョンではログに警告が出るのに、実機ビルドではログにすら載らないことがあります。ここでは、ATS が何をブロックしているのかを切り分ける順番と、Apple の審査に通る最小権限の Info.plist 設定を、私が現場で運用している基準でまとめます。
なぜ「無音で失敗する」のか — ATS のデフォルト挙動
ATS は iOS 9 以降、すべての iOS アプリに対してデフォルトで以下を強制しています。
- 通信先は HTTPS であること
- TLS 1.2 以上であること
- 証明書チェーンが完全に検証できること
- Forward Secrecy をサポートする暗号スイートであること
これに違反した通信を OS レベルで遮断します。fetch も axios も中身は URLSession ですから、OS が握り潰した時点でアプリ側のコードには エラーオブジェクトすら返らない ことがあります。Rork で try / catch を仕込んでも、ネットワーク到達前に切られているため catch が発火しないケースがあるわけです。
私が祖父たちから受け継いだ感覚として、宮大工の現場では「最初の鉋がけが斜めだと、その後の工程で必ず歪みが出る」と教わってきました。ネットワーク層の前提が間違っていると、上のレイヤーで何時間デバッグしても見つからないのは同じ構造です。
原因の切り分け順序 — 上から潰していく
ログにエラーが出ない以上、推測で Info.plist を変更する前に、通信そのものの性質を確定 させる必要があります。私の現場では次の順で見ます。
1. URL のスキームとポート
# プロジェクトルートで叩いている API 文字列を全部洗う
grep -rE 'https?://[^"]+' src/ app/ services/ 2>/dev/null | grep -v '\.test\.'http:// で始まる URL が1つでもあれば、ほぼそこが原因です。localhost も同様で、Expo Dev Client から実機テストしている場合は ATS の例外設定が必要になります。
2. TLS バージョンと暗号スイートの実測
URL 自体が HTTPS でも、サーバ側の TLS 設定が古いと弾かれます。Mac のターミナルから次のコマンドで実測します。
# サーバの TLS 1.2 対応を確認
openssl s_client -connect api.example.com:443 -tls1_2 < /dev/null
# サーバが提示する暗号スイート一覧(要 nmap)
nmap --script ssl-enum-ciphers -p 443 api.example.comCipher is (NONE) や handshake failure が返るなら、サーバ側が ATS の暗号スイート要件を満たしていません。これは Rork 側でいくら Info.plist をいじっても解決しないので、サーバの TLS 設定を更新するか、別ドメインに切り替える方が早道です。
3. ATS 例外がアプリで有効になっているか
EAS でビルドしたバイナリの中身を確認すると、Info.plist の最終形が分かります。
# 直近の TestFlight ビルドをダウンロードして展開
unzip -p "App.ipa" "Payload/*.app/Info.plist" | plutil -convert xml1 -o - -NSAppTransportSecurity キーが意図した通り入っているかを目視で確認します。Rork の場合、app.config.ts での設定が EAS のキャッシュで反映されていないことが時々あります。
最小権限で ATS 例外を入れる Info.plist の書き方
ここからが本題です。「全許可(NSAllowsArbitraryLoads = true)」は審査リスクが高いだけでなく、ユーザーの通信を MITM 攻撃に晒す設計上の欠陥でもあります。私は子どもたちに技術を伝えていく立場として、「動けば良い」では済ませたくない部分です。Apple の審査でも、明確な正当化理由なく NSAllowsArbitraryLoads を true にしているとリジェクトの根拠になります。
パターンA: 特定ドメインのみ HTTP を許可する
決済プロバイダや古い社内 API など、HTTPS 化が物理的に難しい1ドメインだけを通したい場合の設定です。
<!-- Info.plist (app.config.ts の ios.infoPlist 経由で注入) -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>legacy-api.example.com</key>
<dict>
<!-- HTTP 通信を許可 -->
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<!-- このドメインのみ TLS 要件を緩める(任意)-->
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<!-- サブドメインも対象にしたい場合のみ true -->
<key>NSIncludesSubdomains</key>
<false/>
</dict>
</dict>
</dict>Rork の app.config.ts から注入する場合は次の書き方になります。
// app.config.ts
export default {
expo: {
ios: {
infoPlist: {
NSAppTransportSecurity: {
NSExceptionDomains: {
"legacy-api.example.com": {
NSExceptionAllowsInsecureHTTPLoads: true,
NSExceptionMinimumTLSVersion: "TLSv1.0",
NSIncludesSubdomains: false,
},
},
},
},
},
},
};期待出力(expo prebuild 後の ios/<App>/Info.plist)に上記キーが転記されていれば成功です。
パターンB: 動画ストリーミング用にメディア通信のみ緩和する
m3u8 や HLS をプログレッシブダウンロードする場合、NSAllowsArbitraryLoadsInMedia で動画系だけ ATS を緩められます。AdMob の動画広告で過去に詰まった経験から、この設定は API 通信より明確に副作用が小さいので推奨します。
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInMedia</key>
<true/>
</dict>パターンC: WebView だけ緩める
アプリ本体は HTTPS、WebView で開く外部サイトだけ HTTP を許す場合です。
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
</dict>WebView で表示する HTML 内で読み込む <img src="http://..."> などのサブリソースは、この設定で通るようになります。
審査リジェクトを避けるための補足
Apple の審査では、NSAllowsArbitraryLoads を true にしているアプリに対して「なぜ HTTPS でないのか」を説明する必要があります。プライバシーマニフェストの追加が義務化されて以降、レビュアーが ATS 設定を見る頻度も体感では上がっています(Rork でプライバシーマニフェストの API 宣言エラーを直す手順 でこの周辺をまとめています)。
正当化が必要な代表的なケースは以下です。
- 自社管理外の古いサーバとの通信(移行スケジュールを示せると通りやすい)
- 標準化されていない地域のテレビ放送 API など、HTTPS 提供がない公共インフラ
- 認証局を信頼できない閉域網内のテストサーバ(提出時は本番では無効化されるべき)
「動画再生で必要だから」という曖昧な理由は通りにくく、可能なら NSAllowsArbitraryLoadsInMedia のような限定キーに置き換えるよう求められることが多いです。
デバッグ用の小さなスニペット
ATS の例外設定が正しく反映されているかを実機で確認するためのコードを置いておきます。React Native の fetch だけでなく、URLSession レベルの挙動も同時に観察するために、Reachability 系ライブラリを使わず素朴に2方向のリクエストを比較します。
// utils/atsProbe.ts
// HTTPS と HTTP の両方を叩いて、どちらが ATS で落ちているか可視化する
export async function atsProbe() {
const targets = [
{ label: "HTTPS", url: "https://httpbin.org/get" },
{ label: "HTTP", url: "http://httpbin.org/get" },
];
for (const t of targets) {
const t0 = Date.now();
try {
const res = await fetch(t.url, { method: "GET" });
console.log(`[ATS Probe] ${t.label} ok=${res.ok} status=${res.status} ms=${Date.now() - t0}`);
} catch (e: any) {
// ATS でブロックされた場合、e.message は "Network request failed" のことが多い
console.log(`[ATS Probe] ${t.label} threw: ${e?.message ?? e} ms=${Date.now() - t0}`);
}
}
}期待動作は次のとおりです。
- ATS 例外なし: HTTPS は成功し、HTTP は
Network request failedで例外 - パターンA で
httpbin.orgを例外指定: HTTPS / HTTP 共に成功 - パターンB(メディア限定): HTTP の
fetchは変わらず失敗
ログが「HTTPS は通るのに HTTP だけ落ちる」となっていれば、ATS が正しく仕事をしているサインです。逆に「HTTPS も落ちる」場合は、ATS ではなくプロキシ・DNS・証明書ピン留めが原因なので、別の切り分け(Rork のネットワーク・API リクエストをデバッグする手順)に進む方が早いでしょう。
私が現場で取っている運用ルール
最後に、累計5,000万DLの運用で痛い目を見ながら固めた、ATS まわりの自分用ルールを共有します。
- 新規アプリは原則
NSAllowsArbitraryLoadsを入れません。バックエンドが Cloudflare や Supabase 経由なら HTTPS で十分まかなえる - 旧 API 互換のために例外を入れた場合、リリースノートにも内部 README にも「ATS 例外の対象ドメインと撤去予定日」を必ず書く
- 例外を入れたまま放置されたドメインは、半年に1度の棚卸しで強制的に剥がす
- TLS 設定の検証は、サーバ移行や CDN 切替の翌日に必ずもう一度行う
ATS 由来のトラブルは、起きると大きい割に、避けるためのコストは「数行の設定と15分の検証」で済みます。同じく iOS の沈黙系エラーで詰まりやすいRork iOS アプリのデータベースバックエンド接続エラーを切り分ける手順 も合わせて読むと、ネットワーク層全体の挙動が立体的に見えてきます。
アプリ事業を10年以上続けて初めて気づいたのですが、ATS の設定一つで「ユーザーへの最小限の誠実さ」をコードに残せるかどうかが分かれる感覚があります。
お読みいただきありがとうございました。