5 min read
Understanding HTTP Idempotency

Dalam mengembangkan sebuah aplikasi yang berkomunikasi via HTTP, seringkali kita jumpai kendala seperti misalnya ketika user tidak sengaja melakukan dua kali submit form yang sama yang dapat menyebabkan duplikasi data. Kendala tersebut akan menjadi sangat krusial diperhatikan, terutama pada aktivitas penting seperti misalnya pada process transfer uang di sistem perbankan, di mana sangat penting memastikan bahwa tidak ada pengulangan proses transfer ke tujuan yang sama.

Untuk mengatasi masalah tersebut, HTTP memperkenalkan konsep idempotency.

Apa itu Idempotency?

Idempotency adalah sebuah konsep dalam pemrograman yang merujuk pada proses di mana sebuah permintaan (request) yang dikirimkan berulang kali akan menghasilkan respons yang konsisten dan sama, tanpa menimbulkan efek samping tambahan. Idempotency sangat penting dalam pengembangan HTTP API untuk meminimalkan kesalahan, terutama ketika terjadi kegagalan atau ketidakpastian dalam pengiriman data. Misalnya, jika sebuah client mengirimkan permintaan POST dan mengalami timeout, akan muncul pertanyaan apakah sumber daya (resource) telah berhasil dibuat atau apakah timeout terjadi saat pengiriman permintaan atau saat menerima respons.

Dalam HTTP, beberapa metode bersifat idempotent, seperti “OPTIONS”, “HEAD”, “GET”, “PUT”, dan “DELETE”. Pengulangan permintaan dengan metode ini tidak akan mengubah hasil yang ada. Sebaliknya, metode seperti “POST” dan “PATCH” tidak bersifat idempotent, karena pengulangan permintaan dapat menghasilkan perubahan baru atau duplikasi data.

Implementasi Idempotency

Untuk mengimplementasikan idempotency, biasanya digunakan header HTTP seperti Idempotency-Key. Nilai dari Idempotency-Key harus unik untuk memastikan bahwa setiap permintaan dengan key yang sama hanya diproses sekali, meskipun permintaan tersebut dikirimkan berkali-kali. Server akan memeriksa apakah key tersebut sudah ada dalam sistem. Jika belum, server akan menyimpan key tersebut di memori atau Redis dan melanjutkan pemrosesan permintaan. Namun, jika key sudah ada, server akan mengembalikan respons yang sesuai, biasanya berupa pesan kesalahan atau status yang menunjukkan bahwa permintaan telah diproses sebelumnya.

Penentuan Nilai Idempotency-Key

Ada dua teknik umum untuk menentukan nilai Idempotency-Key:

  1. Menggunakan UUID (Universally Unique Identifier)

    Keuntungan menggunakan UUID adalah bahwa UUID dapat dihasilkan secara unik tanpa memerlukan banyak daya komputasi. Namun, tantangannya adalah memastikan bahwa setiap permintaan dengan UUID yang sama dikelola dengan benar di sisi client.

  2. Menggunakan Hash dari Body Permintaan

    Teknik ini lebih sederhana karena tidak memerlukan pengelolaan nilai secara manual untuk setiap permintaan. Namun, setiap perubahan kecil dalam body permintaan akan menghasilkan hash yang berbeda, sehingga setiap modifikasi akan diproses sebagai permintaan baru. Selain itu, hashing body yang besar bisa memerlukan daya komputasi yang tinggi.

Masing-masing teknik ini memiliki kelebihan dan tantangan tersendiri tergantung pada kompleksitas dan kebutuhan sistem yang dibangun.

Mari Kita buat program sederhana untuk mengimplementasi Idempotency menggunakan Go

Setup

Membuat Proyek Go

mkdir go-idempotency
cd go-idempotency
go mod init go-idempotency

Membuat HTTP Server

package main

import (
	"fmt"
	"net/http"
)

func main() {
	app := http.NewServeMux()

	app.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Success")
	})

	http.ListenAndServe(":8080", app)
}

Membuat Middleware Idempotency

package main

import (
	"net/http"
)

var store = map[string]bool{}

func idempotencyMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		idempotencyKey := r.Header.Get("Idempotency-Key")

		if idempotencyKey == "" {
			http.Error(w, "Idempotency Key Required", http.StatusBadRequest)
			return
		}

		if _, exists := store[idempotencyKey]; exists {
			http.Error(w, "Request already processed", http.StatusConflict)
			return
		}

		store[idempotencyKey] = true
		next.ServeHTTP(w, r)
	})
}

func main() {
	app := http.NewServeMux()

	app.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Success")
	})

	http.ListenAndServe(":8080", idempotencyMiddleware(app))
}

Menambahkan Hash ke Middleware Idempotency

Untuk membuat key lebih unik, kita bisa menambahkan metode atau URI sebagai bagian dari key yang disimpan di memori.


package main

import (
	"crypto/sha256"
	"encoding/hex"
	"net/http"
)

func generateHash(idempotencyKey, method, endpoint string) string {
	data := idempotencyKey + method + endpoint
	hash := sha256.Sum256([]byte(data))
	return hex.EncodeToString(hash[:])
}

func idempotencyMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		idempotencyKey := r.Header.Get("Idempotency-Key")

		if idempotencyKey == "" {
			http.Error(w, "Idempotency Key Required", http.StatusBadRequest)
			return
		}

		hashKey := generateHash(idempotencyKey, r.Method, r.URL.Path)

		if _, exists := store[hashKey]; exists {
			http.Error(w, "Request already processed", http.StatusConflict)
			return
		}

		store[hashKey] = true
		next.ServeHTTP(w, r)
	})
}

func main() {
	app := http.NewServeMux()

	app.HandleFunc("/post", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Success")
	})

	http.ListenAndServe(":8080", idempotencyMiddleware(app))
}

Jalankan aplikasi dengan perintah go run main.go

Pengujian

  1. Kirim Permintaan POST Pertama

Kirim permintaan POST pertama dengan Idempotency-Key yang unik.

curl -X POST http://localhost:8080/post -H "Idempotency-Key: unique-key-123"

Hasilnya post berhasil disimpan, lalu Idempotency-Key akan disimpan di memory

  1. Kirim Permintaan POST Kedua dengan Idempotency-Key yang Sama

Kirim permintaan POST kedua dengan Idempotency-Key yang sama seperti yang digunakan sebelumnya.

curl -X POST http://localhost:8080/post -H "Idempotency-Key: unique-key-123"

Hasilnya mandapatkan error dengan message Request already processed artinya request yang dikirim di anggap sama karena sebelumnya Idempotency-Key yang dikirim sama dengan request yang pertama.

Thank you! 😊

Referensi: RFC Draft: Idempotency Key Header