sue@blog ~ /posts/react-native-networking-internals
$ cd ../
$ cat post.metadata

React Nativeのネットワーク通信の裏側
— NSURLSessionとOkHttpが実際にやっていること

date: 2026-03-17 React Native iOS Android Networking
React NativeでfetchやXMLHttpRequestを呼ぶと、iOS側ではNSURLSession、Android側ではOkHttpが動いている。Cookie自動管理、2段階タイムアウト、App Transport Security強制など、Web開発者が知らない「OS側で起きていること」を掘り下げる。前回の記事「React Nativeのfetchはブラウザのfetchと何が違うのか」の続編。

前提: この記事で扱うこと

$ cat --context

前回の記事では、React Nativeのfetchがブラウザのfetchとどう違うかを解説しました。CORSがない、Cookieの挙動が違う、ストリーミングの対応状況が異なる、といった「What」の話でした。

この記事では「Why」に踏み込みます。なぜそうなるのか。答えは、React Nativeの通信がOSのネイティブHTTPクライアントに委譲されているからです。具体的には:

これらが裏で何をしているかを理解すると、「なぜCookieがAndroidで消えるのか」「なぜiOSだけHTTPが拒否されるのか」といった疑問がすべて解消されます。

JS → Native の通信パス全体像

$ strace -f react-native-fetch

React Nativeのネットワーク通信は、JavaScriptからネイティブコードまで何層ものレイヤーを経由します。全体を1枚の図で見てみましょう。

flowchart TB subgraph JS["JavaScript 層"] fetch["fetch()"] --> xhrPoly["XMLHttpRequest
Polyfill"] end subgraph Bridge["ブリッジ層"] xhrPoly --> |"JSI / Bridge"| rctNet["RCTNetworking
(iOS)"] xhrPoly --> |"JSI / Bridge"| netMod["NetworkingModule
(Android)"] end subgraph iOS["iOS ネイティブ層"] rctNet --> handler["RCTHTTPRequestHandler"] handler --> session["NSURLSession"] session --> cookieStore["HTTPCookieStorage"] session --> ats["App Transport
Security"] session --> urlCache["URLCache"] ats --> network["ネットワーク"] end subgraph Android["Android ネイティブ層"] netMod --> okhttp["OkHttp"] okhttp --> cookieJar["CookieJar"] okhttp --> interceptor["Interceptor Chain"] okhttp --> cache["Cache"] interceptor --> network2["ネットワーク"] end network --> server["サーバー"] network2 --> server style JS fill:#161b22,stroke:#d2a8ff,color:#c9d1d9 style Bridge fill:#161b22,stroke:#f0883e,color:#c9d1d9 style iOS fill:#161b22,stroke:#58a6ff,color:#c9d1d9 style Android fill:#161b22,stroke:#7ee787,color:#c9d1d9

注目すべきはブリッジ層です。JavaScriptのXMLHttpRequest Polyfillが、iOS側ではRCTHTTPRequestHandlerクラス、Android側ではNetworkingModuleを経由して、各OSのHTTPクライアントを呼び出しています。

RCTHTTPRequestHandlerとは
React NativeのRCTHTTPRequestHandlerは、JavaScriptのXMLHttpRequestをiOSのNSURLSessionにブリッジするObjective-Cクラスです。React Nativeのソースコード内(Libraries/Network/配下)に実装があり、カスタマイズも可能です。AndroidのNetworkingModuleも同様の役割を担い、OkHttpクライアントにブリッジします。

iOS側: NSURLSessionの正体

$ man NSURLSession

NSURLSessionとは何か

NSURLSessionはAppleが提供するiOSのネットワーク通信の基盤APIです。React Nativeだけでなく、Safari、Mail、App StoreなどApple純正アプリのすべてがこのAPIを使って通信しています。

Web開発者にとって馴染みのある例えをするなら、ブラウザにおけるChromiumのネットワークスタックに相当するものです。ただし、ブラウザではこの層に触ることはできませんが、iOSアプリではNSURLSessionを直接設定・拡張できるという大きな違いがあります。

NSURLSession の階層構造(Objective-C)
// NSURLSession は3つの主要コンポーネントで構成される

// 1. Configuration — セッション全体の設定
NSURLSessionConfiguration *config =
    [NSURLSessionConfiguration defaultSessionConfiguration];
config.timeoutIntervalForRequest = 30;   // リクエストタイムアウト
config.timeoutIntervalForResource = 300;  // リソースタイムアウト
config.HTTPCookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];

// 2. Session — 通信の実行主体
NSURLSession *session =
    [NSURLSession sessionWithConfiguration:config];

// 3. Task — 個々のリクエスト
NSURLSessionDataTask *task =
    [session dataTaskWithURL:url
           completionHandler:^(NSData *data, NSURLResponse *resp, NSError *err) {
        // レスポンス処理
    }];
[task resume];

2段階タイムアウトの仕組み

NSURLSessionには2種類のタイムアウトがあります。これはWebのfetchにはない概念です。

sequenceDiagram participant App as アプリ participant Session as NSURLSession participant Server as サーバー App->>Session: リクエスト開始 Note over Session: timeoutIntervalForRequest 開始
(デフォルト: 60秒) Session->>Server: HTTPリクエスト送信 alt サーバーが応答しない場合 Note over Session: 60秒経過 Session-->>App: タイムアウトエラー
(NSURLErrorTimedOut) else サーバーが応答した場合 Server-->>Session: レスポンス開始(ヘッダー受信) Note over Session: timeoutIntervalForRequest リセット Note over Session: timeoutIntervalForResource 継続中
(デフォルト: 7日) Server-->>Session: ボディ受信中... Note over Session: データが来るたびに
Requestタイマーリセット Server-->>Session: ボディ受信完了 Session-->>App: 完了コールバック end
タイムアウト デフォルト値 意味 Webの対応概念
timeoutIntervalForRequest 60秒 サーバーからの次のデータチャンクを待つ最大時間。データを受信するたびにリセットされる なし(AbortControllerで近似)
timeoutIntervalForResource 7日 リクエスト全体の合計所要時間の上限。リセットされない なし
React Nativeでこのタイムアウトが効くケース

React Nativeのfetchで何もタイムアウトを設定しない場合、iOSではtimeoutIntervalForRequestの60秒が適用されます。つまり、サーバーが60秒以内に1バイトもデータを返さなければタイムアウトします。ただし、少しずつデータを返し続ける場合(ストリーミング等)は、タイマーがリセットされ続けるため60秒を超えても接続は維持されます

HTTPCookieStorage: iOSのCookie管理

iOSにはHTTPCookieStorageというCookie専用のストレージシステムがあります。ブラウザが内部で持っているCookieストアに相当するものですが、アプリからプログラマブルにアクセスできる点が異なります。

HTTPCookieStorage の仕組み
// HTTPCookieStorage はアプリ全体で共有されるシングルトン
NSHTTPCookieStorage *store =
    [NSHTTPCookieStorage sharedHTTPCookieStorage];

// Set-Cookie ヘッダーを受信すると自動的にここに保存される
// 次回のリクエストで自動的に Cookie ヘッダーに付与される

// 保存済み Cookie の確認
NSArray *cookies = [store cookiesForURL:url];

// Cookie の手動削除
for (NSHTTPCookie *cookie in store.cookies) {
    [store deleteCookie:cookie];
}

// Cookie の永続化ポリシー
store.cookieAcceptPolicy = NSHTTPCookieAcceptPolicyAlways;
// → アプリを再起動しても Cookie は保持される
// → expires/max-age に従って自動的に期限切れ処理される
flowchart LR subgraph Request["リクエスト時"] A["fetch('/api/me')"] --> B["RCTHTTPRequestHandler"] B --> C["NSURLSession"] C --> D["HTTPCookieStorage
から Cookie 取得"] D --> E["Cookie ヘッダー付きで
リクエスト送信"] end subgraph Response["レスポンス時"] F["サーバーが
Set-Cookie を返す"] --> G["NSURLSession が受信"] G --> H["HTTPCookieStorage
に自動保存"] H --> I["次回リクエストで
自動送信"] end style Request fill:#161b22,stroke:#58a6ff,color:#c9d1d9 style Response fill:#161b22,stroke:#7ee787,color:#c9d1d9

重要なのは、この処理がNSURLSessionのレイヤーで自動的に行われるという点です。JavaScriptのfetchやXHRのレベルでは何も設定しなくても、iOS側が勝手にCookieを保存・送信してくれます。「iOSではCookieがなんとなく動く」のはこの仕組みのおかげです。

App Transport Security (ATS)

Web開発者がReact Nativeで最初にぶつかる壁の一つがATS(App Transport Security)です。

ATSはiOS 9(2015年)で導入された仕組みで、すべてのネットワーク通信にHTTPSを強制します。ブラウザではhttp://でもアクセスできますが、iOSアプリではデフォルトでブロックされます。

ATSが要求する条件
# ATS が許可する通信の条件
- プロトコル: HTTPS 必須(HTTP は拒否)
- TLS バージョン: TLS 1.2 以上
- 暗号スイート: Forward Secrecy 対応のもの
- 証明書: SHA-256 以上のハッシュ、2048bit 以上の RSA キー

# ATS に違反した場合のエラー
Error Domain=NSURLErrorDomain Code=-1022
"The resource could not be loaded because the
 App Transport Security policy requires the use
 of a secure connection."
Info.plist — 開発時の ATS 例外設定
<!-- 開発時のみ: localhost への HTTP を許可 -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>localhost</key>
        <dict>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
    </dict>
</dict>

<!-- 本番では絶対にやらないこと: 全 HTTP を許可 -->
<!-- <key>NSAllowsArbitraryLoads</key><true/> -->
NSAllowsArbitraryLoads は App Store 審査で拒否される可能性あり

全HTTPを許可するNSAllowsArbitraryLoadstrueにすると、App Storeの審査で「なぜATSを無効化する必要があるのか」の説明を求められ、正当な理由がなければリジェクトされます。開発時のlocalhost例外のみに留めてください。

Android側: OkHttpの正体

$ adb logcat -s OkHttp

OkHttpとは何か

OkHttpはSquare社(Cash App, Retrofit等を開発)が作ったオープンソースのHTTPクライアントです。Android標準のHttpURLConnectionよりも高機能で安定しているため、React Nativeを含む多くのAndroidアプリが採用しています。

NSURLSessionがAppleのOS組み込みAPIであるのに対し、OkHttpはサードパーティライブラリです。ただし事実上のAndroid標準となっており、Android 4.4以降のHttpURLConnectionの内部実装もOkHttpベースです。

Interceptor Chain: OkHttpの設計思想

OkHttpの最大の特徴はInterceptor Chain(インターセプターチェーン)です。リクエスト/レスポンスがチェーン状の処理を順番に通過する設計で、Expressのmiddlewareに近い概念です。

flowchart TB A["アプリから
リクエスト"] --> I1["Application
Interceptor"] I1 --> I2["RetryAndFollowUp
Interceptor"] I2 --> I3["Bridge
Interceptor"] I3 --> I4["Cache
Interceptor"] I4 --> I5["Connect
Interceptor"] I5 --> I6["Network
Interceptor"] I6 --> N["ネットワーク
通信"] N --> I6 I6 --> I5 I5 --> I4 I4 --> I3 I3 --> I2 I2 --> I1 I1 --> B["アプリに
レスポンス"] style I1 fill:#21262d,stroke:#d2a8ff,color:#d2a8ff style I2 fill:#21262d,stroke:#58a6ff,color:#58a6ff style I3 fill:#21262d,stroke:#f0883e,color:#f0883e style I4 fill:#21262d,stroke:#7ee787,color:#7ee787 style I5 fill:#21262d,stroke:#58a6ff,color:#58a6ff style I6 fill:#21262d,stroke:#d2a8ff,color:#d2a8ff
Interceptor 役割 Web開発での類似概念
Application Interceptor アプリ層のカスタム処理(ログ、認証ヘッダ付与) Expressのmiddleware / Axiosのinterceptor
RetryAndFollowUp リダイレクト追従・リトライ fetchのredirect: 'follow'
Bridge ヘッダー補完(Content-Length, Accept-Encoding等) ブラウザの自動ヘッダー付与
Cache HTTPキャッシュの参照・保存 ブラウザキャッシュ
Connect TCP/TLS接続の確立・接続プール管理 ブラウザの接続管理(隠蔽されている)
Network Interceptor 実際のネットワーク通信直前のカスタム処理 Service Worker

CookieJar: AndroidのCookie管理

OkHttpのCookie管理はCookieJarインターフェースを通じて行われます。ここがiOSとの最大の違いであり、多くのWeb開発者がハマるポイントです。

iOS (NSURLSession)

  • HTTPCookieStorage がデフォルトで有効
  • Cookie はディスクに永続化される
  • アプリ再起動後も Cookie が残る
  • expires / max-age に従って自動期限管理

Android (OkHttp)

  • デフォルトの CookieJar は空実装
  • Cookie はメモリにのみ保持
  • アプリ再起動で Cookie が消える
  • 永続化するには自前実装が必要
OkHttp の CookieJar インターフェース(Java)
// OkHttp の CookieJar はシンプルなインターフェース
public interface CookieJar {
    // リクエスト時: この URL に送るべき Cookie を返す
    List<Cookie> loadForRequest(HttpUrl url);

    // レスポンス時: サーバーから受け取った Cookie を保存する
    void saveFromResponse(HttpUrl url, List<Cookie> cookies);
}

// デフォルト実装: NO_COOKIES(何も保存しない)
CookieJar NO_COOKIES = new CookieJar() {
    public List<Cookie> loadForRequest(HttpUrl url) {
        return Collections.emptyList();
    }
    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
        // 何もしない
    }
};
React NativeのAndroid側ではどうなっているか

React NativeのNetworkingModuleは、OkHttpクライアントにJavaNetCookieJarjava.net.CookieManagerベース)を設定しています。これによりメモリ内でのCookie管理は行われますが、ディスクへの永続化はデフォルトでは行われません。アプリがプロセスキルされるとCookieは消えます。

タイムアウト設計の違い

$ diff timeout-ios.conf timeout-android.conf
タイムアウト種別 iOS (NSURLSession) Android (OkHttp)
接続タイムアウト timeoutIntervalForRequestに含まれる connectTimeout: 10秒
読み取りタイムアウト timeoutIntervalForRequest: 60秒 readTimeout: 10秒
書き込みタイムアウト timeoutIntervalForRequestに含まれる writeTimeout: 10秒
全体タイムアウト timeoutIntervalForResource: 7日 callTimeout: なし(無制限)

ここにiOS 60秒 vs Android 10秒の差が生まれます。重い処理をするAPIエンドポイント(レポート生成、画像処理等)で応答に15秒かかる場合、iOSでは問題なく動くのにAndroidではタイムアウトする、という現象が起きます。

プラットフォーム差を吸収するタイムアウト設計
import { Platform } from 'react-native';

// プラットフォームのデフォルトタイムアウトに依存せず、
// 自前で統一的なタイムアウトを設定する
const TIMEOUT_MS = Platform.select({
  ios: 30000,      // iOS: デフォルト60秒より短く
  android: 30000,  // Android: デフォルト10秒より長く
  default: 30000,
});

async function apiFetch(url: string, options?: RequestInit) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);

  try {
    return await fetch(url, { ...options, signal: controller.signal });
  } finally {
    clearTimeout(timeoutId);
  }
}

ネイティブ層をカスタマイズする方法

$ vim native-config.m

React Nativeでは、ネイティブ層のHTTPクライアントをカスタマイズすることが可能です。これはブラウザのfetchでは絶対にできないことです。

iOS: NSURLSessionConfigurationのカスタマイズ

RCTHTTPRequestHandler+Custom.m(Objective-C)
// React Native の HTTP ハンドラーをカスタマイズする例
// ネイティブモジュールとして実装

#import <React/RCTHTTPRequestHandler.h>

@implementation RCTHTTPRequestHandler (CustomConfig)

- (NSURLSessionConfiguration *)customSessionConfiguration {
    NSURLSessionConfiguration *config =
        [NSURLSessionConfiguration defaultSessionConfiguration];

    // タイムアウトを統一
    config.timeoutIntervalForRequest = 30;

    // キャッシュポリシー
    config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;

    // HTTP/2 の有効化(デフォルトで有効だが明示)
    config.HTTPShouldUsePipelining = YES;

    return config;
}

@end

Android: OkHttpClientのカスタマイズ

CustomOkHttpFactory.java
// React Native の OkHttpClient をカスタマイズする例
public class CustomOkHttpFactory implements OkHttpClientFactory {
    public OkHttpClient createNewNetworkModuleClient() {
        return new OkHttpClient.Builder()
            // タイムアウトを統一
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)

            // Cookie永続化
            .cookieJar(new PersistentCookieJar())

            // ログインターセプター(デバッグ用)
            .addInterceptor(new HttpLoggingInterceptor())

            // 証明書ピニング
            .certificatePinner(new CertificatePinner.Builder()
                .add("api.example.com", "sha256/AAAA...")
                .build())

            .build();
    }
}
ネイティブ層カスタマイズが必要なケース

まとめ: 全体比較

$ diff --side-by-side nsurlsession.spec okhttp.spec
観点 iOS (NSURLSession) Android (OkHttp)
提供元 Apple(OS組み込み) Square社(サードパーティ、事実上の標準)
利用アプリ Safari, Mail, App Store, 全Apple純正アプリ 多数のAndroidアプリ、HttpURLConnection内部
Cookie管理 HTTPCookieStorage(ディスク永続化) CookieJar(デフォルトはメモリのみ)
タイムアウト Request: 60秒 / Resource: 7日 Connect: 10秒 / Read: 10秒 / Write: 10秒
HTTPS強制 ATS(Info.plistで例外設定) CleartextTraffic(AndroidManifestで設定)
HTTP/2 デフォルトで対応 デフォルトで対応
拡張方法 URLProtocol, SessionDelegate Interceptor Chain
RNのブリッジクラス RCTHTTPRequestHandler NetworkingModule

おわりに: 知っていると何が変わるか

$ echo "レイヤーを1つ下げると、すべてが見えてくる"

React Nativeのネットワーク通信で問題が起きたとき、JavaScriptのfetch/XHRレベルだけを見ていても原因がわからないことがあります。なぜなら、実際の通信処理はその下のネイティブ層で行われているからです。

この記事で解説した知識があれば:

fetchのAPIは同じでも、1つレイヤーを下げれば、まったく異なる世界が広がっている。その世界を知ることが、React Nativeで安定したネットワーク通信を実現する近道です。

$ _