Error HTTP 502 Bad Gateway yang muncul sporadis pada aplikasi React Native sering kali berakar dari race condition pada level TCP socket, bukan bug fungsional di kode backend. Kasus ini umumnya terjadi ketika aplikasi mengirimkan request menggunakan koneksi idle yang sedang ditutup sepihak oleh reverse proxy atau upstream server.
Gejala Permasalahan
Aplikasi React Native mengalami request drop secara acak, terutama saat pengguna kembali membuka aplikasi setelah mendiamkannya beberapa detik atau menit. Pada sisi client, error muncul sebagai Network Error, 502 Bad Gateway, atau IOException: unexpected end of stream on Connection.
Pemeriksaan pada access log dan error log Nginx mengindikasikan koneksi terputus di tengah jalan:
[error] 1420#1420: *84920 upstream prematurely closed connection while reading response header from upstream, client: 192.168.1.50, server: api.example.com, request: "GET /api/v1/user/profile HTTP/1.1", upstream: "http://10.0.0.5:8080/api/v1/user/profile"Atau jika Nginx bertindak langsung sebagai terminal TLS untuk client React Native:
192.168.1.50 - - [14/May/2024:10:15:32 +0700] "POST /api/v1/sync HTTP/1.1" 502 157 "-" "okhttp/4.9.2"Root Cause: Race Condition TCP Keep-Alive
HTTP Keep-Alive mengizinkan satu koneksi TCP digunakan kembali untuk banyak request HTTP guna memangkas overhead TCP 3-way handshake dan TLS negotiation. Masalah muncul ketika ada ketidaksesuaian (mismatch) durasi idle timeout antara client dan server.
Secara default, client HTTP Android pada React Native (OkHttp) mempertahankan koneksi idle dalam ConnectionPool selama 5 menit. Sebaliknya, default keepalive_timeout pada Nginx adalah 65-75 detik, dan Node.js/Go default server timeout berkisar antara 5-60 detik.
Kronologi race condition terjadi sebagai berikut:
- Koneksi berada dalam status idle di dalam pool.
- Server mencapai ambang batas idle timeout dan mengirim paket
FINuntuk menutup koneksi. - Sebelum paket
FINtiba di perangkat mobile (terhambat latensi jaringan atau radio seluler), client React Native mengambil koneksi idle yang sama dari pool untuk mengirim request baru. - Client mengirim request HTTP payload (paket data
PSH, ACK). - Server menerima data pada soket yang statusnya sudah ditutup (atau setengah ditutup), lalu merespons dengan paket
RST(Reset). - Reverse proxy menghasilkan status
502 Bad Gateway, atau layer native React Native melempar exception koneksi putus.
Analisis Paket TCP (Packet Inspection)
Inspeksi paket via tcpdump atau Wireshark membuktikan urutan crash soket tersebut:
Time Source Destination Protocol Info
0.000 10.0.0.1 10.0.0.50 TCP 8080 → 45231 [FIN, ACK] Seq=1021 Ack=501
0.012 10.0.0.50 10.0.0.1 HTTP GET /api/v1/user/profile HTTP/1.1
0.013 10.0.0.1 10.0.0.50 TCP 8080 → 45231 [RST] Seq=1022 Win=0Server telah menutup koneksi terlebih dahulu. Paket GET dari client berpapasan di jalur transmisi (in-flight).
Solusi 1: Atur Server Timeout Lebih Besar dari Client
Kaidah utama HTTP keep-alive pooling: Keep-Alive server/proxy harus selalu lebih lama dibandingkan keep-alive client. Komponen upstream harus menutup koneksi paling akhir.
Jika Nginx berada di depan upstream server (misal Node.js atau Go):
# Konfigurasi Nginx (/etc/nginx/nginx.conf)
http {
# Timeout koneksi downstream (React Native -> Nginx)
keepalive_timeout 65s;
upstream backend_cluster {
server 127.0.0.1:3000;
# Pertahankan koneksi idle ke upstream
keepalive 32;
}
server {
listen 443 ssl http2;
server_name api.example.com;
location / {
proxy_pass http://backend_cluster;
proxy_http_version 1.1;
proxy_set_header Connection "";
# Timeout membaca respon dari upstream
proxy_read_timeout 60s;
proxy_connect_timeout 10s;
}
}
}Pastikan upstream runtime (misalnya Node.js) memiliki keepAliveTimeout lebih besar dari keepalive_timeout proxy:
// Node.js HTTP Server
const server = app.listen(3000);
// Default 5000ms (5 detik). Naikkan melewati batas proxy:
server.keepAliveTimeout = 70000; // 70 detik
server.headersTimeout = 71000; // Harus lebih tinggi dari keepAliveTimeoutSolusi 2: Tuning OkHttp ConnectionPool di React Native (Android)
Di ekosistem Android, React Native menggunakan OkHttp melalui NetworkingModule. Default pool OkHttp menyimpan koneksi idle selama 5 menit. Turunkan nilai ini ke durasi yang lebih aman (misal 30 detik), sehingga client mendepak koneksi idle dari pool sebelum server mengirim FIN.
Implementasikan custom OkHttpClientFactory pada proyek Android React Native:
package com.yourapp;
import com.facebook.react.modules.network.OkHttpClientFactory;
import com.facebook.react.modules.network.OkHttpClientProvider;
import com.facebook.react.modules.network.ReactCookieJarContainer;
import java.util.concurrent.TimeUnit;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
public class CustomOkHttpClientFactory implements OkHttpClientFactory {
@Override
public OkHttpClient createNewNetworkClient() {
return new OkHttpClient.Builder()
.cookieJar(new ReactCookieJarContainer())
// Batasi idle connection hanya 30 detik (default: 5 menit)
.connectionPool(new ConnectionPool(5, 30, TimeUnit.SECONDS))
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
// ponytail: OkHttp internal retryOnConnectionFailure mengatasi sebagian socket drop,
// tetapi tidak melindungi terhadap race condition request non-idempotent.
.retryOnConnectionFailure(true)
.build();
}
}Daftarkan factory ini di dalam MainApplication.java (atau MainApplication.kt) pada lifecycle onCreate:
@Override
public void onCreate() {
super.onCreate();
// Daftarkan factory kustom sebelum React Native menginisialisasi network client
OkHttpClientProvider.setOkHttpClientFactory(new CustomOkHttpClientFactory());
}Solusi 3: Implementasi Retry Interceptor untuk Koneksi Transien
Variasi latensi jaringan seluler dapat tetap memicu race condition sesekali meski timeout telah dituning. Buat OkHttp Interceptor untuk melakukan retry otomatis secara transparan jika terdeteksi koneksi idle tertutup saat pengiriman request idempoten (GET, HEAD, OPTIONS, PUT).
package com.yourapp;
import java.io.IOException;
import java.net.SocketException;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
public class StaleConnectionRetryInterceptor implements Interceptor {
private static final int MAX_RETRIES = 2;
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
IOException lastException = null;
boolean isIdempotent = request.method().equals("GET") ||
request.method().equals("HEAD") ||
request.method().equals("PUT");
for (int attempt = 0; attempt < MAX_RETRIES; attempt++) {
try {
response = chain.proceed(request);
return response;
} catch (IOException e) {
lastException = e;
// Cek apakah error berasal dari soket yang ditutup sepihak
if (isIdempotent && isStaleConnectionException(e)) {
continue; // Coba ulang menggunakan koneksi baru dari pool
}
throw e;
}
}
throw lastException;
}
private boolean isStaleConnectionException(IOException e) {
String msg = e.getMessage() != null ? e.getMessage().toLowerCase() : "";
return e instanceof SocketException ||
msg.contains("unexpected end of stream") ||
msg.contains("connection reset");
}
}Tambahkan interceptor ke builder OkHttp pada CustomOkHttpClientFactory:
builder.addInterceptor(new StaleConnectionRetryInterceptor());Peringatan Keamanan dan Integritas Data: Jangan lakukan auto-retry tanpa batas pada request non-idempoten (seperti POST transaksi atau mutasi akun) kecuali endpoint backend mendukung idempotency key. Jika soket putus setelah server memproses request namun sebelum header respon sampai ke client, retry berisiko memicu eksekusi ganda.Daftar Verifikasi Akhir
- Pastikan
keepAliveTimeoutbackend >keepalive_timeoutproxy >ConnectionPoolclient. - Konfigurasi Android OkHttp
keepAliveDurationditurunkan ke kisaran 30-45 detik. - Pastikan downstream proxy menangani koneksi reuse upstream dengan
proxy_http_version 1.1. - Gunakan retry interceptor terbatas khusus untuk request idempoten.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!