前回の記事では、React Nativeのfetchがブラウザのfetchとどう違うかを解説しました。CORSがない、Cookieの挙動が違う、ストリーミングの対応状況が異なる、といった「What」の話でした。
この記事では「Why」に踏み込みます。なぜそうなるのか。答えは、React Nativeの通信がOSのネイティブHTTPクライアントに委譲されているからです。具体的には:
NSURLSession — Apple提供のネットワーク通信基盤APIOkHttp — Square社製のHTTPクライアントライブラリこれらが裏で何をしているかを理解すると、「なぜCookieがAndroidで消えるのか」「なぜiOSだけHTTPが拒否されるのか」といった疑問がすべて解消されます。
React Nativeのネットワーク通信は、JavaScriptからネイティブコードまで何層ものレイヤーを経由します。全体を1枚の図で見てみましょう。
注目すべきはブリッジ層です。JavaScriptのXMLHttpRequest Polyfillが、iOS側ではRCTHTTPRequestHandlerクラス、Android側ではNetworkingModuleを経由して、各OSのHTTPクライアントを呼び出しています。
RCTHTTPRequestHandlerは、JavaScriptのXMLHttpRequestをiOSのNSURLSessionにブリッジするObjective-Cクラスです。React Nativeのソースコード内(Libraries/Network/配下)に実装があり、カスタマイズも可能です。AndroidのNetworkingModuleも同様の役割を担い、OkHttpクライアントにブリッジします。
NSURLSessionはAppleが提供するiOSのネットワーク通信の基盤APIです。React Nativeだけでなく、Safari、Mail、App StoreなどApple純正アプリのすべてがこのAPIを使って通信しています。
Web開発者にとって馴染みのある例えをするなら、ブラウザにおけるChromiumのネットワークスタックに相当するものです。ただし、ブラウザではこの層に触ることはできませんが、iOSアプリではNSURLSessionを直接設定・拡張できるという大きな違いがあります。
// 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];
NSURLSessionには2種類のタイムアウトがあります。これはWebのfetchにはない概念です。
| タイムアウト | デフォルト値 | 意味 | Webの対応概念 |
|---|---|---|---|
| timeoutIntervalForRequest | 60秒 | サーバーからの次のデータチャンクを待つ最大時間。データを受信するたびにリセットされる | なし(AbortControllerで近似) |
| timeoutIntervalForResource | 7日 | リクエスト全体の合計所要時間の上限。リセットされない | なし |
React Nativeのfetchで何もタイムアウトを設定しない場合、iOSではtimeoutIntervalForRequestの60秒が適用されます。つまり、サーバーが60秒以内に1バイトもデータを返さなければタイムアウトします。ただし、少しずつデータを返し続ける場合(ストリーミング等)は、タイマーがリセットされ続けるため60秒を超えても接続は維持されます。
iOSにはHTTPCookieStorageというCookie専用のストレージシステムがあります。ブラウザが内部で持っているCookieストアに相当するものですが、アプリからプログラマブルにアクセスできる点が異なります。
// 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 に従って自動的に期限切れ処理される
重要なのは、この処理がNSURLSessionのレイヤーで自動的に行われるという点です。JavaScriptのfetchやXHRのレベルでは何も設定しなくても、iOS側が勝手にCookieを保存・送信してくれます。「iOSではCookieがなんとなく動く」のはこの仕組みのおかげです。
Web開発者がReact Nativeで最初にぶつかる壁の一つがATS(App Transport Security)です。
ATSはiOS 9(2015年)で導入された仕組みで、すべてのネットワーク通信にHTTPSを強制します。ブラウザではhttp://でもアクセスできますが、iOSアプリではデフォルトでブロックされます。
# 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."
<!-- 開発時のみ: 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/> -->
全HTTPを許可するNSAllowsArbitraryLoadsをtrueにすると、App Storeの審査で「なぜATSを無効化する必要があるのか」の説明を求められ、正当な理由がなければリジェクトされます。開発時のlocalhost例外のみに留めてください。
OkHttpはSquare社(Cash App, Retrofit等を開発)が作ったオープンソースのHTTPクライアントです。Android標準のHttpURLConnectionよりも高機能で安定しているため、React Nativeを含む多くのAndroidアプリが採用しています。
NSURLSessionがAppleのOS組み込みAPIであるのに対し、OkHttpはサードパーティライブラリです。ただし事実上のAndroid標準となっており、Android 4.4以降のHttpURLConnectionの内部実装もOkHttpベースです。
OkHttpの最大の特徴はInterceptor Chain(インターセプターチェーン)です。リクエスト/レスポンスがチェーン状の処理を順番に通過する設計で、Expressのmiddlewareに近い概念です。
| 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 |
OkHttpのCookie管理はCookieJarインターフェースを通じて行われます。ここがiOSとの最大の違いであり、多くのWeb開発者がハマるポイントです。
HTTPCookieStorage がデフォルトで有効// 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のNetworkingModuleは、OkHttpクライアントにJavaNetCookieJar(java.net.CookieManagerベース)を設定しています。これによりメモリ内でのCookie管理は行われますが、ディスクへの永続化はデフォルトでは行われません。アプリがプロセスキルされるとCookieは消えます。
| タイムアウト種別 | 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);
}
}
React Nativeでは、ネイティブ層のHTTPクライアントをカスタマイズすることが可能です。これはブラウザのfetchでは絶対にできないことです。
// 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
// 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();
}
}
| 観点 | 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 |
React Nativeのネットワーク通信で問題が起きたとき、JavaScriptのfetch/XHRレベルだけを見ていても原因がわからないことがあります。なぜなら、実際の通信処理はその下のネイティブ層で行われているからです。
この記事で解説した知識があれば:
fetchのAPIは同じでも、1つレイヤーを下げれば、まったく異なる世界が広がっている。その世界を知ることが、React Nativeで安定したネットワーク通信を実現する近道です。