HTTP Preconditions untuk Update yang Aman - Biarkan Editor Lama Gagal, Bukan Menimpa Pekerjaan Baru
Dua orang membuka artikel yang sama di editor. Keduanya melihat revisi 17. Orang pertama memperbaiki bagian pembuka dan menyimpan revisi 18. Orang kedua, yang masih melihat revisi 17, mengubah kesimpulan lalu menyimpannya beberapa menit kemudian. Jika request kedua sekadar mengganti konten yang tersimpan, pekerjaan orang pertama hilang tanpa ada editor yang melihat error.
Inilah masalah lost update. Kita tidak memerlukan sistem terdistribusi berukuran besar untuk mengalaminya; dua tab browser, sebuah API client dan halaman admin, atau request mobile yang terlambat sudah cukup. Database transaction dapat menjaga konsistensi internal satu proses tulis, tetapi tidak dengan sendirinya memberi tahu server apakah client memulai dari data terkini. HTTP sudah memiliki kosakata untuk pertanyaan yang belum terjawab itu: validator dan precondition.
Client perlu menyebutkan versi yang dieditnya
Request seperti PUT /api/posts/42 menyebutkan resource yang hendak diubah dan membawa representasi baru yang diusulkan. Tanpa syarat tambahan, request itu juga dapat berarti, “terapkan ini tanpa memedulikan apa yang terjadi sejak terakhir kali aku membacanya.” Makna tersebut mungkin tepat untuk beberapa operasi, tetapi berbahaya untuk editor yang mengganti satu representasi secara utuh.
RFC 9110 Section 13 mendefinisikan conditional request sebagai request dengan header yang menyatakan precondition untuk diuji sebelum method diterapkan. Untuk update, precondition yang berguna biasanya berbunyi:
Terapkan perubahan ini hanya jika resource masih memiliki versi yang sebelumnya diterima client.
Server menyampaikan versi tersebut melalui validator. HTTP mendefinisikan tanggal modifikasi dan entity tag sebagai bentuk validator yang umum. ETag adalah nilai opaque yang dipilih origin server; nilainya tidak wajib berupa content hash, timestamp database, atau nomor revisi publik. Hal yang penting adalah apakah nilainya ikut berubah bersama representasi yang divalidasi.
Pertukaran GET dan PUT sederhana
Misalnya, client mula-mula mengambil sebuah artikel:
GET /api/posts/42 HTTP/1.1
Host: example.com
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "post-17"
{"title":"A careful title","content":"..."}
Client perlu menyimpan entity tag bersama data yang ditampilkannya. Ketika pengguna menyimpan, client mengirim kembali validator yang sama persis melalui If-Match:
PUT /api/posts/42 HTTP/1.1
Host: example.com
Content-Type: application/json
If-Match: "post-17"
{"title":"A more careful title","content":"..."}
Jika revisi 17 masih terkini, server dapat menerapkan proses tulis dan mengembalikan validator untuk representasi yang baru. Jika request lain sudah menghasilkan revisi 18, syarat tersebut bernilai false. Server tidak boleh menerapkan method yang diminta seolah-olah syarat itu tidak ada. Respons yang lazim untuk alur ini adalah:
HTTP/1.1 412 Precondition Failed
Content-Type: application/json
{"error":"The post changed after you loaded it. Fetch the current version before saving."}
RFC 9110 mendefinisikan 412 untuk syarat dalam request yang bernilai false. Kegagalan itu merupakan informasi berguna, bukan sekadar gangguan: kedua versi tetap terjaga cukup lama agar aplikasi dapat menawarkan reload, perbandingan, atau merge yang disengaja. Mengirim ulang body lama secara otomatis tanpa menyelesaikan perbedaannya lebih dulu justru akan mengulang overwrite yang dicegah oleh precondition.
If-Match memerlukan strong validator
Detail tag tersebut penting. Berdasarkan aturan If-Match dalam RFC 9110, entity tag dibandingkan dengan strong comparison function. Weak tag seperti W/"post-17" dapat menggambarkan representasi yang dianggap setara oleh server untuk kegunaan tertentu, tetapi tidak dapat memenuhi If-Match. Referensi MDN merangkum dampak praktisnya: weak entity tag tidak pernah cocok di header ini.
Revision counter hanya dapat menghasilkan strong tag yang layak jika invariant-nya cukup kuat: setiap perubahan yang terlihat pada representasi terpilih harus menghasilkan validator berbeda. Jika JSON artikel memuat judul, konten, dan status publikasi, tetapi counter hanya berubah saat field konten berubah, tag tersebut tidak mengidentifikasi representasi itu secara andal. Collision-resistant hash atas representasi final merupakan opsi lain, meski perhitungannya mungkin tidak perlu apabila aplikasi sudah memiliki revision control yang dapat dipercaya.
Tanggal kurang menarik ketika deteksi perubahan yang tepat dibutuhkan. RFC 9110 mencatat bahwa resolusi clock dapat membuat modification time menjadi weak validator jika resource mungkin berubah lebih dari sekali dalam rentang resolusi tersebut. If-Unmodified-Since tersedia bagi server yang tidak menyediakan entity tag, tetapi strong ETag yang terdefinisi dengan baik biasanya lebih mudah dipahami untuk editor yang dikendalikan aplikasi.
Pemeriksaan pada storage juga harus atomic
Header HTTP yang benar tidak memperbaiki check-then-write race di dalam aplikasi. Bayangkan PHP membaca version = 17, membandingkan header, lalu menjalankan UPDATE tanpa syarat. Request lain dapat melakukan commit revisi 18 di antara proses baca dan update. Kedua request HTTP tampak sudah melakukan pemeriksaan dengan benar, tetapi pernyataan SQL yang datang belakangan masih dapat menimpa pekerjaan terbaru.
Pendekatan ringkasnya adalah menyertakan versi yang diharapkan ke dalam proses tulis itu sendiri. Fragmen PDO ilustratif ini mengasumsikan authentication, authorization, validasi JSON, pemeriksaan keberadaan resource, dan exception handling sudah dilakukan. Contoh ini juga sengaja hanya menerima satu ETag buatan aplikasi, bukan mengimplementasikan seluruh sintaks daftar yang diizinkan HTTP:
<?php
$ifMatch = $_SERVER['HTTP_IF_MATCH'] ?? null;
if ($ifMatch === null) {
http_response_code(428);
echo json_encode(['error' => 'Send If-Match with the current post ETag.']);
exit;
}
if (!preg_match('/^"post-(\d+)"$/D', $ifMatch, $match)) {
http_response_code(400);
echo json_encode(['error' => 'Invalid If-Match value.']);
exit;
}
$expectedVersion = (int) $match[1];
$update = $pdo->prepare(
'UPDATE posts
SET title = :title,
content = :content,
version = version + 1
WHERE id = :id AND version = :expected_version'
);
$update->execute([
'title' => $validatedTitle,
'content' => $validatedContent,
'id' => $postId,
'expected_version' => $expectedVersion,
]);
if ($update->rowCount() !== 1) {
http_response_code(412);
echo json_encode(['error' => 'The post changed; fetch the current version.']);
exit;
}
$newVersion = $expectedVersion + 1;
header('ETag: "post-' . $newVersion . '"');
http_response_code(204);
Operasi yang menentukan adalah UPDATE bersyarat. Hanya baris yang masih memiliki versi yang diharapkan yang dapat berubah, dan versinya bertambah dalam pernyataan yang sama. Nol baris terdampak berarti proses tulis ini tidak menang. Endpoint nyata tetap perlu membedakan resource yang hilang atau tidak dapat diakses sesuai kebijakan API sebelum mencapai fragmen ini.
Untuk perubahan yang mencakup beberapa tabel, tempatkan semua proses tulis wajib dalam database transaction dan jadikan pemeriksaan versi sebagai bagian dari transaction itu. HTTP menyediakan precondition dari client; database tetap harus menjaga storage invariant. Keduanya adalah layer yang bekerja sama, bukan alternatif yang bersaing.
412, 428, dan 409 menjawab pertanyaan yang berbeda
Ketiga status code ini mudah tercampur:
- 412 Precondition Failed: client menyertakan precondition yang dikenali, tetapi nilainya false terhadap state resource saat ini.
- 428 Precondition Required: origin mengharuskan request bersifat conditional, tetapi syarat yang diperlukan tidak disertakan.
- 409 Conflict: request bertentangan dengan state resource saat ini dalam arti yang lebih luas dan tidak dinyatakan secara lebih tepat sebagai HTTP precondition yang gagal.
RFC 6585 mendefinisikan 428 secara khusus agar origin dapat mewajibkan conditional request, dengan pencegahan lost update sebagai contoh umumnya. Status ini opsional, sehingga API perlu mendokumentasikan apakah kontrak tersebut diberlakukan. Server yang diam-diam menerima update tanpa syarat belum mendapatkan perlindungan hanya karena respons GET-nya memuat ETag.
409 tetap berguna untuk domain conflict: mungkin transisi state yang diminta tidak cocok dengan workflow state resource saat ini. Ketika kondisi If-Match benar-benar gagal, 412 menyampaikan peristiwa protokol itu secara lebih tepat.
Buat hanya jika belum ada apa pun
Kondisi terkait, If-None-Match: *, menyatakan maksud lain yang berguna: terapkan unsafe method hanya ketika target belum memiliki representasi terkini. Kondisi ini dapat mengubah “buat nama ini” menjadi “buat nama ini, tetapi jangan mengganti apa pun yang sudah ada.” RFC 9110 secara eksplisit menjelaskannya sebagai perlindungan ketika beberapa client mungkin mencoba membuat representasi awal.
PUT /api/pages/about HTTP/1.1
Content-Type: application/json
If-None-Match: *
{"title":"About","content":"..."}
Ini bukan sekadar kelengkapan protokol teoretis. Amazon S3 mendokumentasikan conditional write dengan If-Match dan If-None-Match. Permission, perilaku versi object, dan detail error milik S3 bersifat spesifik terhadap produk itu, tetapi contohnya menunjukkan kosakata HTTP yang sama diterapkan pada operasi tulis nyata.
Hal yang tidak diselesaikan precondition
Validator yang cocok menyatakan bahwa representasi terkait belum berubah menurut kebijakan validator server. Hal itu tidak menyatakan bahwa pengguna boleh mengeditnya, bahwa field yang dikirim valid, atau bahwa request terlindungi dari cross-site request forgery. Authorization, input validation, dan kontrol keamanan browser tetap diperlukan.
Precondition juga mendeteksi proses tulis yang stale; ia tidak melakukan merge. Text editor dapat menampilkan kedua revisi dan meminta pengguna menyelaraskannya. Form pengaturan dapat melakukan reload dan mengharuskan perubahan dimasukkan kembali. Collaborative editing per karakter memerlukan model berbeda. Pemulihan yang tepat bergantung pada hal yang aman untuk digabungkan.
Terakhir, kebijakan ETag harus memperhitungkan pemilihan representasi. Jika resource yang sama memiliki representasi JSON, HTML, compressed, atau bahasa yang berbeda secara material, server memerlukan validator yang secara akurat mengidentifikasi representasi terpilih. Tag yang tampak rapi tetapi kadang tidak berubah saat ada perubahan yang terlihat lebih buruk daripada aturan revisi eksplisit yang sudah diuji.
Rencana pengujian yang terfokus
- Lakukan GET terhadap resource dan pastikan responsnya membawa strong ETag.
- Update dengan nilai yang cocok di
If-Match; pastikan berhasil dan menghasilkan validator baru. - Ulangi request lama dengan tag yang stale; pastikan responsnya 412 dan konten tersimpan tidak berubah.
- Hilangkan
If-Matchdari endpoint yang mewajibkannya; pastikan respons 428 yang terdokumentasi muncul. - Jalankan dua proses tulis dengan validator sama secara bersamaan; pastikan paling banyak satu conditional storage update berhasil.
- Ubah setiap field yang direpresentasikan respons dan pastikan setiap perubahan yang terlihat menghasilkan strong validator berbeda.
- Uji kegagalan authorization dan validation secara terpisah agar penanganan precondition tidak membocorkan state resource yang tidak dapat diakses.
Kesimpulan
Lost update tidak diselesaikan dengan mempercepat tombol simpan atau membungkus satu proses tulis tanpa syarat dalam transaction. Server perlu mengetahui state mana yang diedit client, dan klaim tersebut harus tetap terhubung dengan operasi tulis pada storage.
Strong ETag, If-Match, pemeriksaan versi yang atomic, dan respons 412 yang jelas membentuk kontrak kecil tetapi berarti: pekerjaan yang stale gagal secara terlihat alih-alih diam-diam menghapus pekerjaan yang lebih baru. Mewajibkan kontrak itu dengan 428 dapat menutup jalur tanpa syarat. Hal yang terjadi setelah conflict tetap menjadi keputusan aplikasi, tetapi menjaga agar conflict tidak hilang adalah langkah pertama untuk menyelesaikannya dengan jujur.
References
- RFC 9110: HTTP Semantics - IETF / RFC Editor, June 2022.
- RFC 6585 Section 3: 428 Precondition Required - IETF / RFC Editor, April 2012.
- If-Match header - MDN Web Docs, last modified July 4, 2025.
- How to prevent object overwrites with conditional writes - Amazon Web Services documentation.
