◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-05-29中級

Rork で『Network request failed』が Android エミュレータでだけ出るときの調べ方と直し方

iOS シミュレータでは通る fetch が、Android エミュレータでだけ『Network request failed』で落ちる現象の原因と、ローカル API・社内 API・本番 API を切り分けて直していく手順を、実機検証の順番までまとめます。

Rork585React Native239Android55NetworkTroubleshooting

iOS シミュレータでは普通に取れているレスポンスが、Android エミュレータに切り替えた瞬間に TypeError: Network request failed で落ちる。Rork でアプリを書いていると、最初の社内 API 連携や、ローカルで立てた Node の開発サーバーに fetch() した直後に、必ず一度は通る道です。

私自身、2014年から個人でアプリを開発してきて、累計5,000万ダウンロード規模の壁紙・癒し・引き寄せ系アプリを運営する中で、何度も同じエラーに当たっています。原因は毎回違うのに、表示されるメッセージは同じです。だからこそ、最初に切り分け順を決めておくと、後で詰まったときに迷いません。同じ落とし穴に当たっている方の時間を少しでも縮められたらと思い、私が普段踏んでいる切り分けの順番を、現場の感覚そのままで残しておきます。

何が起きているのかを正しく言い直す

Network request failed は、ネットワーク層で何かが切れたときに React Native が投げる、かなり粒度の粗いエラーです。中身を読むと、だいたい次のどれかに当たります。

  1. リクエスト先のホストにそもそも到達できていない(DNS / IP / ポート)
  2. ホストには届いているが、TLS が成立していない(証明書 / 平文 HTTP の遮断)
  3. リクエストは届いて返ってきたが、Android の cleartext ポリシーで弾かれた
  4. プロキシ・VPN・社内ファイアウォール・Charles 等の中継で落ちた

iOS シミュレータと違ってつまずきやすいのは、Android エミュレータが「自分自身を Mac とは別の Linux 端末として動いている」という点です。ここを忘れると、対症療法を 30 分繰り返してしまいます。

まずやるべき切り分け:到達できているかどうか

エラーが起きた fetch のターゲットを、Android エミュレータ内のブラウザでまず開きます。Chrome を起動して URL を直接入力するだけです。

  • 開ければ「アプリ側の問題」(fetch のヘッダ・body・タイムアウト・モジュールの読み込み順)
  • 開けなければ「ネットワーク到達性の問題」(次節の localhost 周り)

この一手間で、調査時間がだいたい半分になります。私はチーム外の方のサポートに入ったときも、最初にこの確認を一緒にやることをお願いしています。

原因1:localhost を直接叩いている

Android エミュレータから見た localhost や 127.0.0.1 は「エミュレータ自身」を指します。Mac で動いている開発サーバー(Node, Rails, FastAPI, Supabase ローカル等)には届きません。これが、Rork でローカル開発するときの定番の落とし穴です。

Android Emulator から Mac 側のホストに到達するには、以下のいずれかを使います。

// utils/apiBase.ts
import { Platform } from 'react-native';
 
const LOCAL_PORT = 8787;
 
export const getApiBase = () => {
  // 本番
  if (!__DEV__) return 'https://api.example.com';
 
  // 開発: プラットフォーム別に切り替える
  if (Platform.OS === 'android') {
    // Android Emulator が Mac の localhost を指す特別な IP
    return `http://10.0.2.2:${LOCAL_PORT}`;
  }
  // iOS シミュレータは Mac とネットワーク空間を共有するのでそのままでよい
  return `http://localhost:${LOCAL_PORT}`;
};

実機の Android で動かす場合は 10.0.2.2 も効きません。Mac の LAN 内 IP(例:192.168.1.21)を環境変数から渡して、Wi-Fi 経由で接続するか、adb reverse tcp:8787 tcp:8787 で USB ポートフォワードを張ります。

# 実機 Android を USB 接続したまま、Mac 側のポートをエミュレータ的に見せる
adb reverse tcp:8787 tcp:8787

adb reverse は USB を抜くと切れるので、CI で常用するのは避けて、ローカル検証専用に留めるのがお勧めです。

原因2:HTTP(平文)通信が Android で遮断されている

Android 9 以降、デフォルトで平文 HTTP は遮断されます。http://10.0.2.2:8787 のような開発用 URL でも例外ではなく、Network request failed で落ちます。

Rork(Expo)の場合は、app.json で開発時のみ cleartext を許可するのが楽です。

{
  "expo": {
    "android": {
      "usesCleartextTraffic": true
    }
  }
}

ただし、これを本番ビルドにそのまま入れて出すのは避けるべきです。AdMob・Firebase・自社 API が混在している環境だと、攻撃面が広がります。私のアプリでは、開発用の app config (app.dev.json) と本番用 (app.json) を分けて、cleartext は開発限定にしています。

より制御を効かせたい場合は、network security config を書き、許可するホストだけ列挙します。

<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
  <domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="true">10.0.2.2</domain>
    <domain includeSubdomains="true">192.168.1.21</domain>
  </domain-config>
</network-security-config>

「とりあえず usesCleartextTraffic: true にする → リリース時に外し忘れる」は実際に踏みやすい事故なので、開発用フラグから外したら App Bundle で必ず確認します。

原因3:自己署名 / 期限切れの証明書

社内 API や、mkcert で立てた開発用 HTTPS を叩いている場合に多い症状です。iOS シミュレータでは Mac のキーチェーンを信頼してくれることがあり、http→https で落ち始めて気づきます。

Android エミュレータのトラストストアは Mac とは独立しているため、自己署名は基本的に信頼されません。短期的には次のどちらかになります。

  • 開発期間中は usesCleartextTraffic で HTTP にフォールバック
  • Android 側に CA を読ませる(adb push + network_security_config.xml でユーザー CA を許可)

長期的には、Cloudflare Tunnel や ngrok のような正規証明書を吐く中継を一枚噛ませて、Android からも素直に HTTPS で取れる状態にしておくと、後から本番 API に差し替えるときに揉めません。

原因4:プロキシ・VPN・Charles の中継で落ちている

Charles Proxy を起動したまま、CA をインストールしていない状態で fetch() すると、Android では即 Network request failed です。原因として一番見落としやすいのがこれで、画面共有しながら一緒に調べると「Charles 立てっぱなしでした…」というケースに毎回出会います。

確認の順番は次の通りです。

  1. システム環境設定の Wi-Fi → プロキシ設定を確認
  2. Charles / Proxyman を一度終了して再試行
  3. 会社 VPN を切って再試行
  4. それでも落ちるなら Chrome で直接開く(前述)

私は Crashlytics + Remote Config で AdMob の頻度を絞る検証中、自分の Charles 経由で AdMob のリクエストが落ちて「夜だけ広告が出ない」と勘違いした経験があります。原因がアプリ内ではなく中継だと気づくまでに半日溶かしたので、ここはまず外して試すことを徹底しています。

アプリ側で見るべき2つの実装ミス

到達性が問題ない場合は、最後にコード側を見ます。Android で落ちて iOS で通る、というケースで頻出するのが次の2つです。

Content-Type を抜いて FormData を送っている

// ❌ Android だと境界文字列が付かず Network request failed になる
await fetch(url, {
  method: 'POST',
  headers: { 'Content-Type': 'multipart/form-data' }, // ← 自分で指定してはいけない
  body: formData,
});
 
// ✅ Content-Type は React Native に任せる
await fetch(url, {
  method: 'POST',
  body: formData,
});

multipart/form-data の boundary はランタイムが自動付与します。iOS は寛容に処理してくれることがあるのに対して、Android は boundary が欠けた瞬間に切ります。

タイムアウトを設けていない fetch

Android エミュレータが裏でディスクスワップしている、Wi-Fi が一瞬切れた、といった状況で、fetch() は無限に待ち続けることがあります。AbortController で 10〜15 秒を上限に切るだけで、ユーザー体験も調査もぐっと楽になります。

const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), 12_000);
try {
  const res = await fetch(url, { signal: ctrl.signal });
  return await res.json();
} catch (e: any) {
  if (e.name === 'AbortError') throw new Error('timeout');
  throw e;
} finally {
  clearTimeout(timer);
}

次のアクション

エラーが出たときの順番として、まずは「Android エミュレータの Chrome で開けるか」を 30 秒で確認するところから始めてください。そこから、localhost / cleartext / 証明書 / プロキシ の4つを順番に潰していけば、ほとんどのケースで原因まで辿り着けます。

私自身まだ学びの途中ですが、同じ場面に当たっている方の参考になれば嬉しいです。お読みいただき、ありがとうございました。

シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⬡ 開発ツール2026-10-07
日本語の見出しだけ太字にならない — Android で fontWeight が効かないときの切り分けと直し方
Rork で作ったアプリで、iPhone では太字なのに Android では日本語の見出しが細いままになる症状の切り分け記事です。原因の見分け方、fontFamily を重みごとに分ける直し方、確認用コードを載せています。
⬡ 開発ツール2026-09-10
画面を暗くする機能を足したら、アプリを閉じたあとも iPhone が暗いままでした
expo-brightness の setBrightnessAsync は、Android では現在のアクティビティにだけ効き、iOS では端末の明るさそのものを変えます。戻す責任が片側にしかない理由と、AppState で後始末をするフックの実装をまとめました。
⬡ 開発ツール2026-09-04
EAS の secret は「アプリに入れない」設定ではありません — 接頭辞と可視性を別々に決める
EXPO_PUBLIC_ の接頭辞は成果物に入るかを決め、EAS の可視性は誰が読めるかを決めます。二つを重ねたときに OTA 更新でだけ値が消える筋道と、出荷前に自分の手で確かめる手順をまとめました。