Fundamental Ring

Memahami handler, request, response, dan middleware sebagai fundamental Ring.

Daftar isi

Topik ini membahas empat pilar framework Ring yang menjadi dasar hampir semua layanan web Clojure, yaitu request map, response map, handler, dan middleware. Pembahasan berjalan sepenuhnya tanpa server sehingga setiap konsep dapat diuji langsung pada REPL dengan memanggil fungsi secara manual. Setelah topik ini selesai, kontrak data yang dipatuhi setiap aplikasi Ring sudah dapat dibaca, ditulis, dan disusun menjadi rantai pemrosesan yang utuh.

Handler sebagai kontrak request dan response#

Ring menetapkan satu kontrak sederhana, yaitu handler berupa fungsi satu argumen yang menerima request map dan mengembalikan response map. Request map adalah map biasa yang berisi informasi permintaan HTTP, dengan kunci utama :request-method, :uri, :query-string, dan :headers. Response map adalah map yang berisi kunci :status berupa bilangan bulat kode HTTP, :headers berupa map dari nama header ke nilainya, dan :body berupa string atau sumber data lain. Karena keduanya hanyalah map biasa, handler dapat dipanggil langsung dengan request map buatan sendiri, tanpa server dan tanpa jaringan.

Sifat berbasis data inilah yang memudahkan pengembangan layanan web Clojure pada REPL. Fungsi handler diuji seperti fungsi biasa, sehingga hasilnya dapat diperiksa dengan prn tanpa peralatan tambahan.

clojure Contoh: handler katalog dipanggil langsung tanpa server
(def request-contoh
  {:request-method :get
   :uri "/katalog"
   :query-string ""
   :headers {"host" "localhost:18080" "accept" "*/*"}})

(println "Kunci request:" (vec (keys request-contoh)))

(defn handler-katalog [request]
  (if (= "/katalog" (:uri request))
    {:status 200
     :headers {"Content-Type" "text/plain; charset=utf-8"}
     :body "katalog perpustakaan: 3 judul"}
    {:status 404
     :headers {"Content-Type" "text/plain; charset=utf-8"}
     :body "halaman tidak ditemukan"}))

(prn (handler-katalog request-contoh))
(prn (handler-katalog {:request-method :get :uri "/rusak" :query-string "" :headers {}}))
text Keluaran
Kunci request: [:request-method :uri :query-string :headers]
{:status 200, :headers {"Content-Type" "text/plain; charset=utf-8"}, :body "katalog perpustakaan: 3 judul"}
{:status 404, :headers {"Content-Type" "text/plain; charset=utf-8"}, :body "halaman tidak ditemukan"}

Handler di atas memeriksa :uri, lalu mengembalikan response 200 atau 404 sesuai jalur yang diminta. Tidak ada mekanisme tersembunyi di antara pemanggilan fungsi dan responsenya. Server kelak hanya bertugas menerjemahkan soket HTTP menjadi request map, memanggil handler, dan mengirim response map kembali sebagai data HTTP.

Menyusun response dengan ring.util.response#

Fungsi pada namespace ring.util.response berasal dari pustaka ring-core yang diunduh dari repositori Clojars melalui koordinat ring/ring-core. Dependensinya dinyatakan pada berkas deps.edn di akar direktori proyek, dan berkas deps.edn pada bundle topik ini berisi persis deklarasi tersebut. Dengan berkas itu, pemanggilan clojure -M berkas.clj dari dalam direktori topik sudah membawa ring-core pada classpath tanpa penyetelan tambahan.

edn deps.edn
{:deps {ring/ring-core {:mvn/version "1.15.6"}}}

Menulis response map secara manual sah dilakukan, tetapi pustaka ring.util.response menyediakan fungsi pembantu yang membuat penulisannya lebih ringkas. Fungsi response menerima body dan mengembalikan response map dasar dengan status 200. Fungsi status mengganti :status, fungsi content-type mengisi header Content-Type, sedangkan header menambahkan header lain. Fungsi-fungsi tersebut mengembalikan response map baru, sehingga dapat dirangkai dengan threading macro ->.

clojure Contoh: menyusun response dengan fungsi pembantu
(require '[ring.util.response :refer [response status content-type]])

(defn cari-anggota [id]
  (get {"A1" "Ali" "A2" "Budi"} id))

(defn handler-anggota [request]
  (if-let [nama (cari-anggota (get-in request [:params "id"]))]
    (-> (response nama)
        (content-type "text/plain; charset=utf-8"))
    (-> (response "anggota tidak ditemukan")
        (status 404)
        (content-type "text/plain; charset=utf-8"))))

(prn (handler-anggota {:params {"id" "A1"}}))
(prn (handler-anggota {:params {"id" "A9"}}))
text Keluaran
{:status 200, :headers {"Content-Type" "text/plain; charset=utf-8"}, :body "Ali"}
{:status 404, :headers {"Content-Type" "text/plain; charset=utf-8"}, :body "anggota tidak ditemukan"}

Cabang pertama mengembalikan response 200 berisi nama anggota, sedangkan cabang kedua menaikkan status menjadi 404 dengan status sebelum menetapkan header. Kunci :params pada request contoh diisi langsung untuk keperluan uji. Middleware pengurai query string pada bagian berikutnya mengisi nilai itu secara otomatis.

Middleware sebagai fungsi handler ke handler#

Middleware adalah fungsi yang menerima satu handler dan mengembalikan handler baru dengan perilaku tambahan. Handler baru biasanya berupa fungsi anonim yang melakukan pekerjaan tambahan sebelum atau sesudah memanggil handler aslinya. Middleware inilah mekanisme Ring untuk menangani kebutuhan yang sama pada banyak handler, seperti penguraian parameter, pencatatan log, dan penanganan sesi, tanpa menyalin kode yang sama ke setiap handler.

Contoh berikut membuat middleware wrap-jejak yang mencatat setiap permintaan masuk, lalu menggabungkannya dengan middleware bawaan wrap-params yang menguraikan query string menjadi kunci :params. Handler yang sudah berlapis tetap dipanggil secara langsung, dan urutan pencetakan memperlihatkan request yang melewati middleware terluar terlebih dahulu sebelum tiba pada handler.

clojure Contoh: middleware buatan sendiri dan wrap-params
(require '[ring.middleware.params :refer [wrap-params]])

(defn handler-cari [request]
  (let [kata (get-in request [:params "q"])]
    {:status 200
     :headers {"Content-Type" "text/plain; charset=utf-8"}
     :body (str "pencarian: " (or kata "kosong"))}))

(defn wrap-jejak [handler]
  (fn [request]
    (println "permintaan masuk:" (:request-method request) (:uri request))
    (handler request)))

(def app (-> handler-cari wrap-params wrap-jejak))

(prn (app {:request-method :get :uri "/cari" :query-string "q=jenis"}))
(prn (app {:request-method :get :uri "/cari" :query-string ""}))
text Keluaran
permintaan masuk: :get /cari
{:status 200, :headers {"Content-Type" "text/plain; charset=utf-8"}, :body "pencarian: jenis"}
permintaan masuk: :get /cari
{:status 200, :headers {"Content-Type" "text/plain; charset=utf-8"}, :body "pencarian: kosong"}

Bentuk (-> handler-cari wrap-params wrap-jejak) menghasilkan lapisan dengan wrap-jejak di posisi terluar, kemudian wrap-params, lalu handler di posisi terdalam. Permintaan melewati lapisan-lapisan itu satu per satu, dan responsenya kembali melewati lapisan tersebut dalam arah sebaliknya.

requestwrap-jejakwrap-paramshandlerresponsesetiap middleware menerima handler dan mengembalikan handler baru
Rantai middleware dari request menuju handler dan kembali sebagai response

wrap-content-type dan pemilihan urutan#

Middleware bawaan lain yang sering digunakan adalah wrap-content-type. Middleware ini mengisi header Content-Type pada response yang belum memilikinya, dengan menebak tipe dari ekstensi pada :uri. Berkas berakhiran .txt menjadi text/plain, sedangkan jalur tanpa ekstensi dikenali sebagai application/octet-stream secara default. Apabila handler sudah menetapkan header tersebut, middleware tidak menimpanya.

clojure Contoh: wrap-content-type menebak tipe dari ekstensi URI
(require '[ring.middleware.content-type :refer [wrap-content-type]])

(defn handler-teks [_]
  {:status 200 :headers {} :body "isi berkas etiket"})

(def app (wrap-content-type handler-teks))

(prn (app {:request-method :get :uri "/etiket.txt"}))
(prn (app {:request-method :get :uri "/etiket"}))
text Keluaran
{:status 200, :headers {"Content-Type" "text/plain"}, :body "isi berkas etiket"}
{:status 200, :headers {"Content-Type" "application/octet-stream"}, :body "isi berkas etiket"}

Urutan middleware menentukan data apa yang sudah tersedia di lapisan berikutnya. Middleware yang membaca :params wajib dipasang di luar wrap-params, karena penguraian query string baru terjadi di lapisan itu. Aturan yang sama berlaku untuk middleware bawaan lain, sehingga susunan middleware perlu dibaca dari luar ke dalam setiap kali aplikasi ditambah lapisan baru.

Kesalahan umum#

Mengembalikan nil dari handler. Kontrak Ring mengharuskan handler mengembalikan response map lengkap, sedangkan server tidak dapat mengirim nil ke browser sehingga kondisi itu menimbulkan error. Perbaikannya dengan menjadikan setiap cabang if atau cond menghasilkan response map, termasuk cabang untuk jalur yang tidak dikenal.

Mengubah isi request map secara langsung. Request map sebaiknya diperlakukan sebagai nilai yang tidak dapat diubah, dan middleware yang menambahkan informasi membangun map baru dengan assoc atau fast-assoc. Perbaikannya dengan mengembalikan map baru alih-alih mengandalkan perubahan langsung yang tidak dijamin kontraknya.

Meletakkan middleware pembaca :params di luar rantai yang menguraikan parameter. Lapisan tersebut membaca :params yang masih kosong, dan kesalahannya tidak memunculkan pesan error apa pun. Perbaikannya dengan memastikan wrap-params berada di posisi lebih luar dibanding middleware yang bergantung pada hasilnya.

Ringkasan#

  • Handler Ring adalah fungsi satu argumen yang menerima request map dan mengembalikan response map berisi :status, :headers, dan :body, sehingga dapat diuji langsung tanpa server.
  • Pustaka ring.util.response menyediakan response, status, dan content-type untuk menyusun response map secara ringkas dengan threading macro.
  • Middleware adalah fungsi handler ke handler yang ditempatkan berlapis di sekeliling handler inti, dan urutannya menentukan data yang tersedia bagi lapisan lain.
  • wrap-params menguraikan query string menjadi :params, sedangkan wrap-content-type mengisi Content-Type dari ekstensi URI apabila handler belum menetapkannya.

Referensi#

  • Ring — GitHub — Repositori resmi Ring beserta spesifikasi request map dan response map.
  • Ring Wiki — GitHub — Dokumentasi konsep handler, middleware, dan ekosistem pustaka Ring.