Kode klien HTTP yang sempurna sekalipun akan bertemu jaringan yang tidak sempurna: server mati mendadak, WiFi terputus, DNS bermasalah, atau endpoint salah alamat. Tanpa strategi penanganan, satu gangguan kecil menjatuhkan seluruh program dengan traceback yang membingungkan pengguna. Chapter penutup Section ini merangkai semuanya: mengenali jenis kegagalan dari exception-nya, menangkapnya pada tingkat yang tepat, mencatatnya dengan logging, dan memutuskan kapan mencoba lagi versus gagal cepat.

Hierarki Exception pada requests #

Seluruh error yang dimunculkan library requests adalah turunan dari satu induk, requests.exceptions.RequestException. Mengenali peta keluarganya membuat kita bisa menangkap error pada tingkat presisi yang kita butuhkan:

text
requests.exceptions.RequestException        ← induk semua error requests
├── ConnectionError                          ← koneksi gagal atau terputus
├── Timeout                                  ← batas waktu terlampaui
│   ├── ConnectTimeout                      ← gagal saat menyambung
│   └── ReadTimeout                         ← server terlalu lama menjawab
└── HTTPError                                ← status 4xx/5xx dari raise_for_status()

Aturan pembacaannya: ConnectionError berarti pesan kita tidak pernah sampai ke server, Timeout berarti sampai tetapi jawabannya tidak kembali tepat waktu, dan HTTPError berarti percakapan selesai namun server menolak — tiga situasi berbeda yang menuntut reaksi berbeda. Library httpx memiliki peta serupa dengan nama yang hampir sama: httpx.ConnectError untuk kegagalan koneksi, httpx.ReadTimeout untuk timeout baca, dan httpx.HTTPStatusError dari raise_for_status().

Menangkap yang Spesifik, Bukan yang Generik #

Mari kita lihat dua kegagalan yang paling sering terjadi. Yang pertama: server tidak ada sama sekali di port tujuan:

python
import requests

try:
    requests.get("http://127.0.0.1:59999/", timeout=2)
except requests.exceptions.ConnectionError as err:
    print("Tipe :", type(err).__name__)
    print("Info :", err)

Output yang Dihasilkan:

text
Tipe : ConnectionError
Info : HTTPConnectionPool(host='127.0.0.1', port=59999): Max retries exceeded with url: / (Caused by NewConnectionError("HTTPConnection(host='127.0.0.1', port=59999): Failed to establish a new connection: [Errno 111] Connection refused"))

Pesan error tersebut panjang, tetapi bagian paling informatif ada di ujungnya: [Errno 111] Connection refused — sistem operasi menegaskan bahwa tidak ada yang mendengarkan di port itu. Yang kedua: server hidup tetapi halaman tidak ada. Nyalakan kembali server_lokal.py dari Chapter Menggunakan urllib, lalu:

python
import requests

respons = requests.get("http://127.0.0.1:8123/tidak-ada", timeout=3)
try:
    respons.raise_for_status()
except requests.exceptions.HTTPError as err:
    print("Tipe :", type(err).__name__)
    print("Info :", err)

Output yang Dihasilkan:

text
Tipe : HTTPError
Info : 404 Client Error: Not Found for url: http://127.0.0.1:8123/tidak-ada

Method raise_for_status() mengubah status 4xx/5xx menjadi exception; tanpa memanggilnya, status 404 hanya angka biasa di .status_code. Sebaliknya, padanan httpx untuk contoh pertama terlihat seperti ini:

python
import httpx

try:
    httpx.get("http://127.0.0.1:59999/", timeout=2)
except httpx.ConnectError as err:
    print("Tipe :", type(err).__name__)
    print("Info :", err)

Output yang Dihasilkan:

text
Tipe : ConnectError
Info : [Errno 111] Connection refused

Mencatat Kegagalan dengan logging #

Penanganan terbaik tetap membuang informasi bila kegagalan tidak dicatat. Module logging memastikan jejak setiap kegagalan tersimpan dengan terstruktur:

python
import logging
import sys

import requests

logging.basicConfig(
    level=logging.INFO,
    format="%(levelname)s %(name)s - %(message)s",
    stream=sys.stdout,
)
catatan = logging.getLogger("klien-api")


def ambil_data(url):
    try:
        respons = requests.get(url, timeout=3)
        respons.raise_for_status()
        return respons.json()
    except requests.exceptions.HTTPError as err:
        catatan.error("Server menolak permintaan: %s", err)
    except requests.exceptions.ConnectionError as err:
        catatan.error("Tidak bisa terhubung ke server: %s", type(err).__name__)
    return None


data = ambil_data("http://127.0.0.1:8123/tidak-ada")
print("Data diterima:", data)

Output yang Dihasilkan:

text
ERROR klien-api - Server menolak permintaan: 404 Client Error: Not Found for url: http://127.0.0.1:8123/tidak-ada
Data diterima: None

Function ambil_data() mengembalikan data saat sukses dan None saat gagal; setiap cabang gagal dicatat oleh catatan.error() dengan penyebab yang berbeda. Penulisan logger, level, dan handler dibahas tuntas pada Chapter Module logging — di sini kita memakai bentuk paling sederhana yang sudah layak untuk produksi kecil.

Strategi, Kapan Mengulang dan Kapan Menyerah #

KondisiReaksi yang tepat
ConnectionError pada server yang biasanya hidupCoba lagi dengan backoff (Chapter Timeouts dan Retries)
ReadTimeout pada endpoint yang memang lambatPerbesar timeout atau coba lagi sekali
HTTPError 4xx (401, 403, 404)Jangan diulang — perbaiki permintaan
HTTPError 429Tunggu sesuai Retry-After (Chapter Pagination dan Rate Limits)
HTTPError 5xxBoleh diulang beberapa kali
Kegagalan setelah N percobaanGagal cepat, sampaikan pesan ke pengguna

Saat kegagalan akhirnya sampai ke pengguna, terjemahkan menjadi kalimat yang bisa ditindaklanjuti: “Server sedang tidak dapat dihubungi, coba beberapa saat lagi” — bukan traceback mentah, dan bukan juga None yang diam-diam. Pola lengkapnya: permintaan dengan timeout, penangkapan exception spesifik, pencatatan dengan logging, retry untuk kegagalan sementara, dan pesan yang jujur ketika semuanya gagal. Dengan empat chapter terakhir sebagai fondasi, kita siap membangun klien API yang tetap berdiri ketika jaringan sedang tidak bersahabat.