Jaringan selalu bisa gagal: server sedang sibuk, koneksi terputus di tengah jalan, atau sebuah layanan mati sementara. Tanpa persiapan, program kita hanya punya dua nasib — menggantung selamanya menunggu response, atau langsung mati pada gangguan pertama. Pada Chapter ini kita memasang dua pertahanan berlapis: timeout sebagai batas kesabaran, dan retry sebagai kesempatan kedua yang dilakukan dengan sopan.
Timeout, Batas Kesabaran Klien #
Parameter timeout pada requests dan httpx sebenarnya mencakup dua fase yang berbeda. Fase connect berlaku sejak koneksi dimulai sampai server menyapa balik; fase read berlaku sejak request terkirim sampai satu byte response diterima. Keduanya bahkan bisa disetel terpisah sebagai tuple:
requests.get(url, timeout=(3, 10)) # 3 detik untuk connect, 10 detik untuk readMerasakan ReadTimeout Secara Nyata #
Teori timeout paling mudah dipahami dengan melihatnya terjadi. Server berikut sengaja tidur dua detik sebelum menjawab rute /lambat, dan gagal dua kali pertama pada rute /flaky — simpan sebagai server_lambat.py dan jalankan:
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
PANGGILAN = 0
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
global PANGGILAN
if self.path == "/lambat":
time.sleep(2)
body = b"akhirnya selesai"
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
elif self.path == "/flaky":
PANGGILAN += 1
if PANGGILAN < 3:
self.send_response(500)
self.send_header("Content-Length", "0")
self.end_headers()
return
body = b"oke pada percobaan ketiga"
self.send_response(200)
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
else:
self.send_response(404)
self.send_header("Content-Length", "0")
self.end_headers()
def log_message(self, format, *args):
pass
ThreadingHTTPServer(("127.0.0.1", 8123), Handler).serve_forever()Sekarang klien dengan kesabaran setengah detik bertemu server yang butuh dua detik:
import requests
try:
requests.get("http://127.0.0.1:8123/lambat", timeout=0.5)
except requests.exceptions.ReadTimeout as err:
print("Tipe :", type(err).__name__)
print("Info :", err)Output yang Dihasilkan:
Tipe : ReadTimeout
Info : HTTPConnectionPool(host='127.0.0.1', port=8123): Read timed out. (read timeout=0.5)Mari kita telaah baris-baris kode di atas:
- Baris 4 (
timeout=0.5): Koneksi berhasil dibuat (fase connect cepat), tetapi fase read melebihi setengah detik — server memang sedang tidur dua detik. - Baris 5 (
except requests.exceptions.ReadTimeout): Exception spesifik ini hanya menyertai kegagalan fase read, sehingga kita bisa membedakannya dari server yang tidak bisa dihubungi sama sekali. - Baris 7 (
print("Info :", err)): Pesan error dari library menyertakan host, port, dan nilai timeout — informasi yang memudahkan diagnosis saat kejadian terulang.
Retry dengan Exponential Backoff #
Server yang gagal sekali sering berhasil dicoba lagi beberapa saat kemudian — tetapi mengulang permintaan secepat kilat justru menumpuk beban. Solusinya adalah exponential backoff: jeda yang melipat dua setiap kegagalan. Function berikut merangkai pola tersebut:
import time
import requests
def get_dengan_retry(url, percobaan_maks=4):
for percobaan in range(1, percobaan_maks + 1):
try:
respons = requests.get(url, timeout=3)
respons.raise_for_status()
return respons
except requests.exceptions.RequestException:
print(f"Percobaan {percobaan} gagal, coba lagi setelah jeda...")
if percobaan == percobaan_maks:
raise
time.sleep(0.2 * 2 ** (percobaan - 1))
respons = get_dengan_retry("http://127.0.0.1:8123/flaky")
print("Isi respons:", respons.text)Output yang Dihasilkan:
Percobaan 1 gagal, coba lagi setelah jeda...
Percobaan 2 gagal, coba lagi setelah jeda...
Isi respons: oke pada percobaan ketigaMari kita bedah function get_dengan_retry():
- Baris 6 (
for percobaan in range(1, percobaan_maks + 1)): Loop berjalan maksimalpercobaan_makskali; nomor percobaan dimulai dari 1 agar jeda terhitung alami. - Baris 9-10 (
requests.get→raise_for_status()): Kegagalan jaringan maupun status 4xx/5xx keduanya ditangkap di satu tempat, karena keduanya layak dicoba ulang oleh pola ini. - Baris 12 (
except requests.exceptions.RequestException): Class dasar ini menangkap semua turunan exceptionrequests, termasukReadTimeoutdanConnectionError. - Baris 14-15 (
if percobaan == percobaan_maks: raise): Percobaan terakhir yang gagal tidak ditelan — exception aslinya diteruskan ke pemanggil, karena melaporkan kegagalan jujur lebih baik daripada mengembalikanNonesecara diam-diam. - Baris 16 (
time.sleep(0.2 * 2 ** (percobaan - 1))): Rumus backoff — jeda 0,2 detik setelah gagal pertama, lalu berlipat dua.
Alur lengkap strategi ini:
Pada nilai contoh kita, tabel jadanya sebagai berikut:
| Percobaan gagal ke- | Jeda sebelum mencoba lagi |
|---|---|
| 1 | 0,2 detik |
| 2 | 0,4 detik |
| 3 | 0,8 detik |
Idempotency, Permintaan yang Aman Diulang #
Sebelum menyalakan retry otomatis, tanyakan satu hal: apakah permintaan ini aman diulang? Method GET, PUT, dan DELETE bersifat idempotent — diulang lima kali, kondisi akhir server tetap sama, sehingga retry tidak menimbulkan efek samping. Method POST berbeda: lima kali kirim POST pembuatan pesanan berarti lima pesanan. Untuk POST, ulangi hanya bila yakin permintaan belum sampai (misalnya koneksi gagal sebelum request terkirim), atau gunakan idempotency key yang disediakan API pembayaran modern.