Client HTTP yang baik menetapkan batas waktu, mencoba ulang kegagalan sesaat yang wajar terjadi, dan memeriksa status respons sebelum isinya dipakai. Topik ini menunjukkan ketiganya secara nyata, yaitu batas waktu koneksi dan permintaan, percobaan ulang dengan jeda yang bertambah untuk permintaan idempoten, serta validasi status dan isi. Semua contoh dijalankan terhadap server uji yang dibangun agar lambat dan fluktuatif sesuai skenario.
Server uji yang lambat dan fluktuatif#
Server uji berikut menyediakan dua perilaku yang biasa dijumpai pada API nyata. Jalur ?mode=lambat menahan respons selama 3 detik, sedangkan jalur ?mode=fluktuatif menjawab 503 untuk dua permintaan pertama lalu 200, dengan penghitung yang disimpan di berkas sementara agar urutannya pasti.
<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
// jalur ?mode=lambat menahan respons selama 3 detik untuk menguji batas waktu
if (($_GET['mode'] ?? '') === 'lambat') {
sleep(3);
}
// jalur ?mode=fluktuatif menjawab 503 untuk dua permintaan pertama, lalu 200
if (($_GET['mode'] ?? '') === 'fluktuatif') {
$hitung = 0;
if (is_file('/tmp/api-lambat-fluktuasi.txt')) {
$hitung = (int) file_get_contents('/tmp/api-lambat-fluktuasi.txt');
}
$hitung++;
file_put_contents('/tmp/api-lambat-fluktuasi.txt', (string) $hitung);
if ($hitung <= 2) {
http_response_code(503);
echo json_encode(['pesan' => 'Server sedang sibuk'], JSON_UNESCAPED_UNICODE), "\n";
return;
}
}
echo json_encode(['status' => 'siap'], JSON_UNESCAPED_UNICODE), "\n";Dijalankan tanpa parameter, server langsung menjawab status siap, dan jawaban inilah yang dibandingkan dengan perilaku lambat dan fluktuatif.
{"status":"siap"}Perhatikan is_file() sebelum file_get_contents(). Tanpa pemeriksaan itu, permintaan pertama memicu warning pada log server, dan keluaran warning tersebut ikut tercetak ke dalam body respons sehingga menimpa header status yang ingin dikirim.
Batas waktu koneksi dan permintaan#
Batas waktu pada curl terbagi dua dengan makna berbeda. CURLOPT_CONNECTTIMEOUT membatasi durasi membuka koneksi, sedangkan CURLOPT_TIMEOUT membatasi seluruh durasi permintaan sampai respons utuh diterima. Nilai yang dipakai client di bawah ini adalah 2 detik untuk koneksi dan 1 detik untuk permintaan, sedangkan server menahan respons 3 detik, sehingga permintaan pasti terputus. Client ini dijalankan oleh blok bash pada bagian Percobaan ulang dengan jeda, bukan langsung dengan php, karena permintaannya ditujukan ke server uji.
// tidak-dijalankan
// client dimuat lewat blok bash setelah server dinyalakan pada port 8160
<?php
declare(strict_types=1);
$ch = curl_init('http://localhost:8160/?mode=lambat');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 2,
// klien menunggu 1 detik, sedangkan server menahan respons 3 detik
CURLOPT_TIMEOUT => 1,
]);
$isi = curl_exec($ch);
$nomorError = curl_errno($ch);
$pesanError = curl_error($ch);
curl_close($ch);
if ($isi === false) {
if ($nomorError === CURLE_OPERATION_TIMEDOUT) {
echo "Permintaan gagal (error $nomorError): batas waktu terlampaui sebelum respons diterima", "\n";
} else {
echo "Permintaan gagal (error $nomorError): $pesanError", "\n";
}
return;
}
echo "Respons diterima: $isi", "\n";Tanpa batas waktu, satu permintaan ke server yang macet menahan skrip berjam-jam dan menguras sumber daya proses. Nilai batasnya ditentukan kontrak layanan yang dipanggil, bukan disalin dari contoh, karena API yang menghitung laporan berat memang dapat memerlukan waktu lebih lama daripada API daftar singkat.
Percobaan ulang dengan jeda#
Kegagalan 503 sering bersifat sesaat karena layanan sedang sibuk, sehingga mencoba ulang beberapa saat kemudian sering berhasil. Client berikut mencoba maksimal tiga kali dengan jeda yang bertambah, yaitu 1 detik setelah percobaan pertama dan 2 detik setelah percobaan kedua, pola yang lazim disebut backoff. Seperti client sebelumnya, eksekusinya dilakukan lewat blok bash terhadap server uji.
Jeda yang bertambah melindungi kedua pihak. Server yang sibuk mendapat waktu untuk pulih sebelum permintaan berikutnya tiba, sedangkan client tidak ikut menambah kepadatan lalu lintas pada saat yang sama. Percobaan ulang tanpa jeda dari banyak client sekaligus justru memperparah keadaan, karena gelombang permintaan ulang semacam itu membuat layanan yang sempat membaik menjadi kewalahan kembali.
// tidak-dijalankan
// client dimuat lewat blok bash setelah server dinyalakan pada port 8160
<?php
declare(strict_types=1);
const MAKS_PERCOBAAN = 3;
$alamat = 'http://localhost:8160/?mode=fluktuatif';
for ($percobaan = 1; $percobaan <= MAKS_PERCOBAAN; $percobaan++) {
$ch = curl_init($alamat);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 2,
CURLOPT_TIMEOUT => 5,
]);
$isi = curl_exec($ch);
$kode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($isi !== false && $kode === 200) {
echo "Percobaan $percobaan: berhasil, respons: $isi";
break;
}
echo "Percobaan $percobaan: gagal (HTTP $kode), menunggu sebelum mencoba ulang", "\n";
if ($percobaan < MAKS_PERCOBAAN) {
sleep($percobaan);
}
}Kedua client dijalankan terhadap server yang sama dalam satu rangkaian, dengan penghitung fluktuasi dihapus lebih dulu agar skenario selalu berjalan dari keadaan bersih.
# hapus catatan fluktuasi agar skenario selalu mulai dari keadaan bersih
rm -f /tmp/api-lambat-fluktuasi.txt
# jalankan server uji pada port 8160
php -S localhost:8160 api-lambat.php >/dev/null 2>&1 &
sleep 1
# uji batas waktu terhadap jalur lambat
php klien-batas-waktu.php
# uji percobaan ulang terhadap jalur fluktuatif
php klien-ulang.php
kill %1Permintaan gagal (error 28): batas waktu terlampaui sebelum respons diterima
Percobaan 1: gagal (HTTP 503), menunggu sebelum mencoba ulang
Percobaan 2: gagal (HTTP 503), menunggu sebelum mencoba ulang
Percobaan 3: berhasil, respons: {"status":"siap"}Keluaran menunjukkan kedua disiplin bekerja bersama. Batas waktu memangkas permintaan yang tak berujung dalam 1 detik, sedangkan percobaan ulang mengubah dua kegagalan 503 menjadi keberhasilan pada percobaan ketiga.
Validasi respons#
Keberhasilan transport tidak menjamin isi yang benar, sehingga respons tetap divalidasi sebelum dipakai. Pemeriksaan minimum berupa kode status yang sama dengan 200, header Content-Type yang menyebut application/json, dan body yang dapat diurai dengan json_decode() bersama JSON_THROW_ON_ERROR. Client pada topik ini mempraktikkan urutan yang sama, yaitu curl_exec(), lalu status, lalu isi.
Batas percobaan ulang juga bagian dari validasi keseluruhan, karena berhenti setelah tiga percobaan lebih baik daripada terus mengulang permintaan ke server yang bermasalah. Batas atas mencegah permintaan beruntun menambah beban layanan yang sedang kewalahan, sekaligus membuat kegagalan terlihat cepat pada pemantauan.
Untuk permintaan POST yang benar-benar wajib diulang, ekosistem menyediakan jalan keluar berupa idempotency key, yaitu pengenal unik yang dikirim bersama permintaan. Dengan pengenal itu, server dapat mengenali percobaan ulang dan menjawab dengan hasil yang sama tanpa menyimpan data baru. Layanan pembayaran umum menggunakan pola ini. Client pada topik ini tidak memerlukannya karena hanya membaca data, tetapi pola tersebut perlu dikenali sebelum kebutuhannya muncul.
Kesalahan umum#
- Tidak menyetel batas waktu sama sekali. Satu server yang macet menahan seluruh pekerjaan, dan kegagalannya baru terlihat setelah batas eksekusi skrip terlampaui.
- Mengulang POST tanpa pengaman. Data baru yang sama terkirim dua kali pada percobaan yang dianggap satu. Batasi percobaan ulang untuk GET, atau gunakan idempotency key bila POST wajib diulang.
- Menganggap kode status satu-satunya validitas. Respons 200 dengan body rusak tetap mungkin terjadi, sehingga penguraian JSON juga harus diberi penanganan kegagalan.
- Mencoba ulang tanpa jeda dan tanpa batas. Permintaan beruntun tanpa jeda menambah beban server yang sedang sibuk, dan tanpa batas atas, perulangannya tidak pernah selesai.
Ringkasan#
CURLOPT_CONNECTTIMEOUTmembatasi pembukaan koneksi danCURLOPT_TIMEOUTmembatasi keseluruhan permintaan, dan keduanya wajib disetel pada setiap client.- Percobaan ulang dengan jeda bertambah (backoff) dan batas atas membuat kegagalan sesaat pulih dengan sendirinya tanpa menambah beban berlebih.
- Percobaan ulang hanya untuk permintaan idempoten, karena POST yang diulang berisiko menyimpan data ganda.
- Validasi respons berurutan dari transport, kode status, lalu penguraian body, sehingga sumber kegagalan selalu terlihat jelas.
Referensi#
- PHP: curl_setopt — Manual
— Makna
CURLOPT_CONNECTTIMEOUT,CURLOPT_TIMEOUT, dan opsi terkait. - PHP: curl_errno — Manual
— Nomor error curl, termasuk
CURLE_OPERATION_TIMEDOUTyang bernilai 28. - Idempotent — MDN Web Docs Glossary — Definisi permintaan idempoten yang menjadi dasar aturan percobaan ulang.