Mengatasi Isu Cross-Origin Resource Sharing (CORS)

Ceritanya kemarin saya sedang mengembangkan sebuah aplikasi web bersama teman-teman seangkatan saya. Saya kebagian in charge di tim back end. Saat itu saya sedang mengerjakan halaman “Tentang Kami” yang berisi profil-profil anggota tim pengembang.

Aplikasi ini dikembangkan dengan menggunakan WordPress. Kebetulan, profil user di WordPress ditangani oleh Globally Recognized Avatars (Gravatar) yang berarti tentunya, detail profil user yang ada seperti foto, facebook, twitter dan lainnya harus di-fetch dari situs Gravatar tersebut.

Syukurnya, Gravatar punya API yang dapat digunakan oleh publik. Jadi saya gunakan saja API tersebut untuk mengambil data.

about-us-page

Tampilan halaman “Tentang Kami” yang sedang dikerjakan.

Sebagai informasi, jumlah tim kami ada 11 orang, dengan kata lain saya harus melakukan perulangan 11 kali untuk mengambil data profil dari situs tersebut. Belum lagi untuk menampilkan profil kontributor yang nanti jumlahnya bisa banyak. Pengambilan data akan dilakukan dengan menggunakan PHP biasa. Jadi, implementasinya waktu itu seperti ini:

foreach ($users as $user) {
    $str = file_get_contents( 'http://www.gravatar.com/' . md5($user->user_email) . '.php' );
    $profile = unserialize( $str );
    if ( is_array( $profile ) && isset( $profile['entry'] ) )
        /* Action to display profile... */
}

Saat dicoba, ternyata proses loading-nya lama sekali. -_- Saya baru sadar, proses pengambilan data oleh PHP dilakukan secara berurut. Dengan kata lain, profil si user akan diambil satu-satu, dan prosesnya harus ditunggu sampai selesai baru bisa mengambil profil lainnya. Kalau misalnya ada masalah di koneksi pada saat melakukan fetching salah satu profile, tentu saja semuanya jadi ikut bermasalah. Belum lagi kalau misalnya nanti jumlah user yang ada menjadi lebih banyak seiring berjalannya waktu. Mau berapa lama loading-nya?

Karena itu, supaya aman, proses fetching informasi profile dari Gravatar harus dilakukan secara paralel. Masalahnya, PHP tidak mendukung multithread secara default. Jadi, bagaimana?

Tentu saja, JavaScript!

Dengan menggunakan jQuery, profilnya akan diambil secara asynchronous, jadi tampilan halaman akan muncul terlebih dahulu, baru kemudian profilnya akan di-append satu-satu ke halaman tersebut. Untungnya Gravatar menyediakan API yang dapat digunakan untuk mengambil data dalam format JSON. Kodenya jadi begini:

$(document).ready(function() {
    $.get('http://www.gravatar.com/' + userMd5Hash + '.json', function(data) {
        /* Operation to parse JSON (emitted). */
        $("#" + userMd5Hash + "-accounts").append(/* ... */);
    });
});

Saat dicoba dijalankan, tiba-tiba saya mendapatkan pesan error di jendela console. Tampilannya mirip seperti di bawah ini:

cors-error

Nah, ada apa lagi ini? -_-

Ternyata, operasi GET menggunakan AJAX dengan menggunakan jQuery yang saya lakukan terbentur oleh same origin policy. Ini ada apa lagi ya? o.O

Ternyata..

Jadi, saya mengerjakan halaman ini di localhost, sedangkan profil yang ingin saya ambil, ada di alamat domain gravatar.com yang tentunya berbeda lokasinya. Secara defaultweb browser tidak mengizinkan langsung operasi pengambilan data yang berbeda domain seperti ini untuk alasan keamanan.

Setelah googling beberapa saat, ternyata untuk mengatasinya, harus dilakukan penyetelan yang memungkinkan untuk melakukan Cross-Origin Resource Sharing (CORS). Untuk melakukannya, ada 2 pilihan yang dapat dilakukan:

Enable CORS di Server Tujuan

Untuk dapat melakukan CORS, pemilik server harus menambahkan header khusus di setiap response yang dikembalikan dari tiap request yang diberikan yaitu:

Access-Control-Allow-Origin: *

Jika header ini tidak didapatkan di response yang dikirimkan balik, maka browser akan memberikan pesan error seperti yang ada di atas tadi.

API di website lain seperti GitHub misalnya, sudah mengizinkan CORS secara default (lihat gambar di bawah, dan perhatikan kotak merah):

github-response-header

Sedangkan, Gravatar tidak mengizinkannya (lihat gambar di bawah):

gravatar-response-header

Karena saya tidak mungkin mengotak-atik API server milik Gravatar, ya apa boleh buat. -_- Tapi untungnya, di sisi client ada hal lain yang dapat dilakukan, yaitu:

Gunakan JSON with padding (JSONP)

Teknik yang dapat dilakukan untuk mengatasi ini adalah menggunakan JSONP. Implementasinya, cukup dengan mengganti rutin jQuery yang digunakan, dan mengubah URL request dengan menambahkan parameter callback.

$(document).ready(function() {
    $.getJSON('http://www.gravatar.com/' + userMd5Hash + '.json?callback=?', function(data) {
        /* Operation to parse JSON (emitted). */
        $("#" + userMd5Hash + "-accounts").append(/* ... */);
    });
});

Fiuh, dan akhirnya berhasil juga. 😀

Syukur sekali saya mendapatkan masalah ini. Kebetulan, aplikasi yang sedang saya kerjakan dan skripsi saya saat ini banyak berhubungan dengan RESTful API, dan isu seperti CORS seperti ini pasti akan sangat membingungkan nanti untuk orang yang menggunakan API yang saya kembangkan. Jadi, untuk pengembang REST API, sepertinya isu CORS seperti ini patut untuk diperhatikan.

Demikian cerita ini, mudah-mudahan bermanfaat untuk pembaca ya. 🙂

Advertisements

Pentingnya Modularisasi Dalam Pemrograman

Don’t Repeat Yourself (DRY). Pernah mendengar slogan ini? DRY merupakan sebuah prinsip pengembangan software yang makna utamanya adalah mengurangi repetisi semaksimal mungkin.

Mungkin repetisi yang pembaca lakukan tidak akan terlalu terasa kalau pembaca menulis software yang pendek (seperti tugas algoritma dan pemrograman di kelas misalnya). Kalau urusannya dengan software siap pakai beneran, barulah semuanya terasa. -_-

Contoh singkat, pernah menulis program dengan Java? Untuk mengeksekusi 1 query ke database MySQL saja misalnya, paling tidak harus ada kode seperti di bawah ini bukan?

import java.sql.*;

import javax.swing.JOptionPane;

public class MySQLSampleProgram {

    public static final String  DB_NAME     = "dbname";
    public static final String  DB_HOST     = "localhost";
    public static final int     DB_PORT     = 3306;
    public static final String  DB_USER     = "dbuser";
    public static final String  DB_PASSWORD = "dbpassword";

    public static Connection conn = null;
    public static Statement stat = null;

    public static void main(String[] args) {
        try {
            Class.forName("com.mysql.jdbc.Driver");
            conn = DriverManager.getConnection("jdbc:mysql://" + DB_HOST + ":" + String.valueOf(DB_PORT) + "/" + DB_NAME, DB_USER, DB_PASSWORD);
            stat = conn.createStatement();

            String query = "SELECT * FROM table;";

            ResultSet rs = stat.executeQuery(query);

            /* Continued... */
        } catch (Exception e) {
            JOptionPane.showMessageDialog(null, e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
        }
    }
}

Baru setelah melakukan eksekusi terhadap query, hasil yang didapatkan dari database bisa dipakai. Bayangkan kalau setiap form yang pembaca buat harus mencantumkan kode konfigurasi database di atas (DB_NAME dan seterusnya) dan kode yang ada di blok try di atas (Class.forName dan seterusnya). Fiuh, capek bukan?

Untuk itulah pentingnya modularisasi. Dengan mengelompokkan operasi-operasi yang sering dilakukan ke dalam suatu modul (function, procedure atau class), maka jika dibutuhkan, operasi-operasi tersebut tinggal dipanggil dengan cara yang singkat. Di sinilah pemrograman berorientasi objek menjadi menyenangkan. 😉

Dengan menganggap masalah yang hendak diproses sebagai objek, maka proses pemecahan masalah dapat dilakukan langsung tanpa harus melaksanakan serentetan operasi yang berulang-ulang. Untuk menyelesaikan contoh kasus koneksi database di Java seperti di atas misalnya, saya melakukan modularisasi dengan cara menulis class yang dapat digunakan untuk menyederhanakan eksekusi query. Coba lihat di sini: https://gist.github.com/imamhidayat92/5418582

Kalau pembaca menggunakan class yang saya tulis tersebut, mengeksekusi query hanya akan sekedar memanggil perintah sederhana seperti berikut:

import helper.MySQLConnector;

import java.sql.*;

import javax.swing.JOptionPane;

public class MySQLSampleProgram {

    public static void main(String[] args) {
        try {
            String query = "SELECT * FROM table;";

            ResultSet rs = MySQLConnector.executeQuery(query);

            /* Continued... */
        } catch (Exception e) {
            JOptionPane.showMessageDialog(null, e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE);
        }
    }
}

Hemat bukan jadinya? 😛

Melakukan sesuatu yang repetitif itu akan sangat melelahkan. Tanpa modularisasi, proses meng-update kode juga akan menjadi rumit. Bayangkan, ada berapa kode serupa yang harus diperbaiki kalau ternyata ada prosedur yang salah?

Dengan modularisasi, tentu saja menulis program juga akan menjadi lebih efisien, dan kodenya akan lebih mudah dibaca.

Semoga bermanfaat. 😉