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:

  1. Koneksi berada dalam status idle di dalam pool.
  2. Server mencapai ambang batas idle timeout dan mengirim paket FIN untuk menutup koneksi.
  3. Sebelum paket FIN tiba di perangkat mobile (terhambat latensi jaringan atau radio seluler), client React Native mengambil koneksi idle yang sama dari pool untuk mengirim request baru.
  4. Client mengirim request HTTP payload (paket data PSH, ACK).
  5. Server menerima data pada soket yang statusnya sudah ditutup (atau setengah ditutup), lalu merespons dengan paket RST (Reset).
  6. 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=0

Server 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 keepAliveTimeout

Solusi 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 keepAliveTimeout backend > keepalive_timeout proxy > ConnectionPool client.
  • Konfigurasi Android OkHttp keepAliveDuration diturunkan 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.