Tentang Konfigurasi RestTemplate
Temen-temen yang sudah biasa pakai Spring Boot pasti sudah nggak asing lagi dengan class yang satu ini, RestTemplate. RestTemplate adalah sebuah class di dalam Spring Boot yang digunakan untuk consume REST API. Untuk menggunakannya, kita dapat sesimple meng-inject Spring bean-nya RestTemplate ke dalam class yang kita miliki.
Biasanya sih, untuk konsumsi REST API yang standard saja, konfigurasinya sangat mudah. Tinggal buat bean dengan type RestTemplate dan didalamnya tinggal instansiasi object RestTemplate dan return instance tersebut. Tapi ternyata, ngga jarang konfigurasi yang mudah dan sederhana ini mendatangkan masalah performance aplikasi yang buruk.
Konfigurasi RestTemplate Standard
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplateDefault() {
return new RestTemplate();
}
}
Potongan kode diatas merupakan potongan kode konfigurasi RestTemplate sederhana yang pada umumnya digunakan. Seperti yang dapat dilihat, cukup dengan sebuah method dengan satu baris kode saja, RestTemplate berhasil dibuat dan siap di-inject ke dalam class kita untuk kemudian digunakan.
Permasalahan
Berbicara tentang konfigurasi RestTemplate, saya pribadi sudah sering berhadapan dengan berbagai macam permasalahannya. Dari pertama kali pakai Spring Boot, sampai sekarang. Dari satu project ke project lainnya. Nggak habis-habis.
Sampai pada beberapa waktu lalu. Ceritanya saya dapat tugas untuk melakukan performance tuning pada sebuah aplikasi. Aplikasinya punya traffic yang cukup tinggi dan cukup sering kena poor performance, terutama pada saat akhir bulan – habis gajian pas transaksi lagi tinggi-tingginya – dan pada saat ada campaign.
Untuk mereplikasi performance issue tersebut, saya mencoba melakukan performance test pada environment testing. Strategi yang saya pakai biasanya, ya saya hit aja API-nya sebanyak-banyaknya pakai JMeter. Lalu kemudian monitor performance-nya lalu analisa hasil thread dump-nya.
Biasanya, hasil analisa nya mengerucut pada dua hal: ketika proses outgoing call suatu API, tidak terdapat konfigurasi time-out dan tidak digunakannya connection pooling pada HTTP Client. Kenapa dua hal tersebut paling sering kejadian? Tentu saja karena konfigurasi RestTemplate-nya yang masih default.
Default Time-Out = Tanpa Time-Out
Kalau kita lebih teliti lagi, kita bisa tahu berapa configurasi time-out default-nya RestTemplate. Kita perhatikan potongan kode dibagian sebelumnya di atas tadi. Pada saat pembuatan bean RestTemplate, digunakan default constructor RestTemplate.
return new RestTemplate();
Kemudian, apabila kita perhatikan secara detail, class RestTemplate meng-extend class InterceptingHttpAccessor. Lalu kemudian InterceptingHttpAccessor meng-extend class HttpAccessor. Berikut dibawah adalah potongan kode dari ketiga class tersebut.
public class RestTemplate extends InterceptingHttpAccessor implements RestOperations {
//class body..
}
public abstract class InterceptingHttpAccessor extends HttpAccessor {
//class body..
}
public abstract class HttpAccessor {
...
private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
public HttpAccessor() {
}
...
}
Perhatikan potongan code dari class HttpAccessor diatas. Pada bagian awal class, terdapat object requestFactory dengan tipe ClientHttpRequestFactory yang diinstansiasikan menggunakan class SimpleClientHttpRequestFactory.
Penjelasannya, ClientHttpRequestFactory merupakan sebuah interface yang digunakan untuk membuka koneksi dan mengirimkan request. Kemudian SimpleClientHttpRequestFactory adalah class implementasi dari interface tersebut. Dengan kata lain, untuk membuka koneksi dan mengirimkan request, RestTemplate by default akan menggunakan class SimpleClientHttpRequestFactory.
Sekarang Mari kita perhatikan class SimpleClientHttpRequestFactory pada potongan kode dibawah
public class SimpleClientHttpRequestFactory implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory {
//other code..
private int connectTimeout = -1;
private int readTimeout = -1;
//other code..
public SimpleClientHttpRequestFactory() {
}
//other method..
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
if (this.connectTimeout >= 0) {
connection.setConnectTimeout(this.connectTimeout);
}
if (this.readTimeout >= 0) {
connection.setReadTimeout(this.readTimeout);
}
//other code..
}
}
Seperti yang dapat dilihat, by default, connectTimeout dan readTimeout nilainya adalah -1. Kemudian pada method prepareConnection terdapat seleksi, apabila connectTimeout dan readTimeout lebih dari sama dengan 0, maka masing-masing timeout akan di set nilainya.
Karena connectTimeout dan readTimeout nilainya adalah -1 maka tidak ada timeout yang di set. Artinya, aplikasi akan menunggu selama mungkin untuk membuka koneksi dan menerima response, sampai di kill oleh network ataupun firewall.
Artinya, sekali lagi, pada case RestTemplate ini, default timeout adalah tanpa timeout.
Konfigurasi Time-Out
Untuk dapat melakukan konfigurasi time-out, alih-alih menggunakan default constructor dari class RestTemplate, kita dapat menggunakan class RestTemplateBuilder yang telah disediakan oleh Spring Boot.
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplateWithTimeOut() {
return new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.build();
}
}
Terdapat dua tipe timeout pada konfigurasi di atas, yaitu Connect Time Out dan Read Time Out. Connect Time Out merupakan waktu tunggu untuk membuka koneksi ke sebuah remote resource, sementara Read Time Out merupakan waktu tunggu untuk aplikasi mendapatkan response dari sebuah remote resource setelah sudah berhasil membuka koneksi.
Sederhananya, pada saat aplikasi kita mengakses suatu URL, pertama-tama aplikasi akan melakukan open connection terlebih dahulu. Pada saat proses inilah konfigurasi Connect Time Out digunakan sebagai batas waktu tunggu pembukaan koneksi.
Setelah berhasil open connection, aplikasi mengirimkan request ke URL tersebut dan menunggu response dari URL tersebut. Pada saat proses inilah konfigurasi Read Time Out digunakan sebagai batas waktu tunggu pembacaan response.
Apabila masing-masing proses diatas membutuhkan waktu lebih dari timeout yang disebutkan pada konfigurasi, maka java.net.SocketTimeoutException akan di throw.
Testing konfigurasi timeout
Setelah melakukan konfigurasi RestTemplate, kita akan membuat unit test untuk mengetes apakah timeout yang kita definisikan sudah benar-benar terkonfigurasi. Pada potongan kode konfigurasi di bagian sebelumnya, kita menggunakan timeout 5 detik untuk connect dan read timeout.
Untuk mengetest timeout, saya akan pakai online dummy slow api. Bisa dilihat di dokumentasi-nya, kita tinggal panggil URL nya dengan mengisi parameter seconds dengan value yang kita inginkan. Apabila kita isi dengan angka 5, maka response akan kita terima dalam 5 detik. Berikut apabila kita test curl dengan parameter seconds 1:
curl --location --request GET 'https://hub.dummyapis.com/delay?seconds=1'
This response has been delayed for 1 seconds
Test connect timeout
Untuk test connect timeout, kita bisa test hit ke URL yang benar namun ke dalam port yang kira-kira tidak tersedia. Untuk tau apakah port nya tersedia kita bisa eksekusi command telnet terlebih dahulu. Standard port untuk https adalah 443. Contoh potongan kode dibawah menunjukkan bahwa kita berhasil konek ke port 443.
telnet hub.dummyapis.com 443
Trying 92.53.241.39...
Connected to hub.dummyapis.com.
Escape character is '^]'.
Namun apabila kita coba konek ke suatu port yang tidak tersedia, hasilnya akan tampil seperti berikut tanpa ada pesan apapun dibawahnya. Untuk keluar dari telnet silakan tekan ctrl + c.
telnet hub.dummyapis.com 81
Trying 92.53.241.39...
Dari hasil pengecekan telnet, dapat kita simpulkan bahwa port 81 tidak dibuka atau digunakan pada URL tersebut. Kita kemudian dapat menggunakan port 81 untuk mengetes konfigurasi connect timeout pada RestTemplate.
Kita akan buat unit test untuk connect timeout. Kita inject bean RestTemplate yang telah kita konfigurasi timeout nya ke dalam class test nya, kemudian kita hit url API nya dengan port 81. Expected-nya, test akan throw java.net.SocketTimeoutException dengan message connect timeout.
@SpringBootTest
@Slf4j
public class TestTimeout {
@Autowired
@Qualifier("restTemplateWithTimeOut")
private RestTemplate restTemplate;
@Test
void testHitSlowAPIConnectTimeOut() {
try {
log.info("test connect timeout begin");
restTemplate.getForEntity("https://hub.dummyapis.com:81/delay?seconds=1", Object.class);
} catch (Exception e) {
log.info("test connect timeout end");
Assertions.assertTrue(e instanceof ResourceAccessException);
Assertions.assertTrue(e.getCause() instanceof SocketTimeoutException);
Assertions.assertEquals("connect timed out", e.getCause().getMessage());
}
}
}

Dari hasil pengetesan diatas, dapat dilihat pada log, bahwa exception connect timeout akan di throw setelah detik ke 5. Ini berarti konfigurasinya sudah benar. Untuk memastikan lagi, berikut adalah log tes apabila connect timeout kita kecilkan menjadi 3 detik.

Test read timeout
Untuk test read timeout, kita bisa langsung gunakan URL dummyapis dengan parameter seconds lebih besar dari read timeout yang sudah kita konfigurasikan. Read timeout yang sudah kita set adalah 5 detik. Maka dalam unit test kita hit URL dummyapis dengan parameter seconds 7 dengan ekspektasi URL akan mengembalikan response dalam waktu 7 detik dan method unit test kita akan return error setelah detik ke 5.
https://hub.dummyapis.com/delay?seconds=7
@SpringBootTest
@Slf4j
public class TestTimeout {
@Autowired
@Qualifier("restTemplateWithTimeOut")
private RestTemplate restTemplate;
@Test
void testHitSlowAPIReadTimeOut() {
try {
log.info("test read timeout begin");
restTemplate.getForEntity("https://hub.dummyapis.com/delay?seconds=7", Object.class);
} catch (Exception e) {
log.info("test read timeout end");
Assertions.assertTrue(e instanceof ResourceAccessException);
Assertions.assertTrue(e.getCause() instanceof SocketTimeoutException);
Assertions.assertEquals("Read timed out", e.getCause().getMessage());
}
}
}

Kesimpulan
Dengan mengkonfigurasi timeout pada RestTemplate, kita dapat membuat aplikasi atau API kita tidak terlalu lama mengembalikan response ketika ada outgoing API yang dipanggil dari aplikasi kita yang lambat ataupun mati. Kita kemudian dapat menangkap timeout exception nya dan mengembalikan response message yang informatif kepada klien API kita.
Misalnya, aplikasi kita menggunakan API perbankan untuk memproses suatu transaksi. Ketika ada masalah dengan API perbankan tersebut, alih-alih menunggu response yang lama yang menyebabkan klien menunggu terlalu lama, kita dapat mengeset timeout dan mengembalikan response error “Saat ini sedang ada masalah dengan koneksi server perbankan, harap coba beberapa saat lagi”.
Hal ini dapat membuat experience klien atau user dapat menjadi lebih baik dan tidak panik ketika terjadi kegagalan transaksi yang disebabkan oleh external API.
Contoh Source Code
Source code dari artikel ini sudah saya publish ke github saya. Silakan teman-teman bisa mampir ke github saya pada repo rest-client-example. Jangan lupa follow github saya dan subscribe channel youtube saya untuk artikel dan konten informatif lainnya.
Terima Kasih!