HTTP::Server

Membangun server HTTP dengan routing handler dan middleware di pustaka standar.

Daftar isi

Modul http/server pada pustaka standar Crystal menyediakan kelas HTTP::Server untuk menerima dan menjawab permintaan (request) HTTP tanpa dependensi tambahan. Topik ini membangun satu program server utuh dengan routing manual, pembacaan parameter query, serta susunan middleware, lalu mengompilasinya menjadi biner mandiri yang berjalan pada alamat 127.0.0.1 dengan port 8951. Konsep handler, yaitu komponen yang memproses satu permintaan, diperkenalkan lebih dahulu karena seluruh bagian server mulai dari routing hingga middleware dibangun dari handler yang sama.

Struktur dasar HTTP::Server#

HTTP::Server.new menerima satu blok yang dipanggil untuk setiap permintaan yang masuk. Blok tersebut menerima satu objek context bertipe HTTP::Server::Context yang menggabungkan dua sisi komunikasi, yaitu context.request yang berisi data dari klien dan context.response yang digunakan menyusun jawaban. Tubuh respons ditulis dengan metode print, sedangkan header Content-Type ditetapkan lebih dahulu melalui context.response.content_type agar klien menafsirkan isi respons (response) dengan benar.

Server yang sudah dibentuk belum menerima koneksi apa pun sebelum dua metode berikutnya dipanggil. Metode bind_tcp mengikat server pada alamat dan port tertentu, misalnya server.bind_tcp("127.0.0.1", 8080), dan mengembalikan alamat efektif sebagai Socket::IPAddress. Metode listen kemudian menjalankan event loop yang bersifat blocking, sehingga program terus melayani permintaan sampai dihentikan. Blok yang diterima HTTP::Server.new pada bentuk paling sederhana ini sebenarnya adalah handler terakhir, sehingga bentuk tersebut dapat dibaca sebagai rantai handler dengan satu anggota.

crystal Server HTTP paling sederhana
# tidak-dijalankan
require "http/server"

server = HTTP::Server.new do |context|
  context.response.content_type = "text/plain"
  context.response.print "Halo dari Crystal"
end

server.bind_tcp("127.0.0.1", 8080)
server.listen

Rantai handler dan middleware#

Selain satu blok tunggal, HTTP::Server.new juga menerima daftar handler yang dijalankan berurutan sebelum blok penangan terakhir. Setiap handler adalah objek yang meng-include modul HTTP::Handler dan mendefinisikan metode call(context). Handler pertama dalam daftar dipanggil lebih dahulu, dan setiap handler memutuskan apakah pekerjaan diteruskan dengan call_next(context) atau diselesaikan sendiri. Setelah call_next kembali, handler masih boleh membaca dan mengubah respons sebelum penanganan berakhir. Pola inilah yang disebut middleware, yaitu lapisan handler yang melingkupi penanganan permintaan.

Pustaka standar menyertakan dua middleware yang siap pakai. HTTP::ErrorHandler menangkap exception yang tidak tertangani di handler mana pun dan mengubahnya menjadi respons dengan status 500, sehingga klien tidak hanya menerima koneksi yang terputus. HTTP::LogHandler mencatat satu baris INFO untuk setiap permintaan yang memuat alamat asal, metode, resource, status, durasi, dan stempel waktu dari backend Log. Kedua middleware ini digunakan pada program utuh di bagian berikutnya.

crystal Contoh: middleware bawaan dalam rantai handler
# tidak-dijalankan
HTTP::Server.new([
  HTTP::ErrorHandler.new,
  HTTP::LogHandler.new,
]) do |context|
  context.response.content_type = "text/plain"
  context.response.print "ok"
end

Middleware juga dapat ditulis sendiri. Handler kustom didefinisikan sebagai kelas yang meng-include HTTP::Handler, lalu dimasukkan ke daftar handler bersama middleware bawaan. Program pada topik ini menggunakan handler bernama Catat yang menghitung setiap permintaan dan menambahkan teks urutan permintaan di akhir tubuh respons, sehingga efek middleware terlihat langsung pada respons yang diterima curl. Penghitungnya berupa variabel kelas @@jumlah yang bertambah satu setiap metode call dipanggil.

Server utuh dengan routing manual#

Program berikut menggabungkan seluruh unsur di atas. Pustaka standar tidak menyediakan router, sehingga pemetaan dari path ke penangan disusun manual dengan percabangan case atas context.request.path. Parameter query dibaca dari context.request.query_params yang bertipe HTTP::Params. Akses ["..."]? mengembalikan nil apabila kunci tidak ada, sehingga nilai bawaan dapat diberi dengan ||, dan konversi to_i mengubah string angka menjadi bilangan untuk perhitungan. Path yang tidak cocok dengan label mana pun jatuh ke cabang else yang menetapkan status 404.

Simpan kode berikut sebagai server.cr.

crystal server.cr
require "http/server"

# Middleware untuk menambahkan catatan urutan permintaan pada respons
class Catat
  include HTTP::Handler
  @@jumlah = 0

  def call(context)
    @@jumlah += 1
    call_next(context)
    context.response.print " (permintaan ke-#{@@jumlah})\n"
  end
end

server = HTTP::Server.new([
  HTTP::ErrorHandler.new,
  Catat.new,
]) do |context|
  context.response.content_type = "text/plain"

  case context.request.path
  when "/"
    context.response.print "Halo dari HTTP::Server Crystal"
  when "/salam"
    nama = context.request.query_params["nama"]? || "dunia"
    context.response.print "Halo, #{nama}!"
  when "/hitung"
    a = context.request.query_params["a"]?.try(&.to_i) || 0
    b = context.request.query_params["b"]?.try(&.to_i) || 0
    context.response.print "#{a} + #{b} = #{a + b}"
  else
    context.response.status_code = 404
    context.response.print "404 Not Found"
  end
end

address = server.bind_tcp("127.0.0.1", 8951)
puts "listen di #{address}"
server.listen

Dua hal pada program tersebut perlu diperhatikan. Handler Catat menambah angka penghitung sebelum call_next dan menambahkan teks (permintaan ke-N) pada tubuh respons sesudahnya, sehingga setiap respons yang keluar memuat urutan permintaannya. Baris puts "listen di #{address}" dijalankan setelah bind_tcp dan sebelum listen, sehingga pesan kesiapan muncul sebelum server sibuk melayani permintaan.

Kompilasi dan pengujian#

Kompilasi dilakukan dengan crystal build. Perintah berikut menghasilkan satu biner bernama server.bin tanpa keluaran apa pun ketika berhasil.

bash
# kompilasi program server menjadi biner
crystal build -o server.bin server.cr

Jalankan biner tersebut di latar, tunggu dua detik hingga server siap, lalu uji empat endpoint dengan curl. Perintah kill %1 di akhir menghentikan server sehingga port 8951 tidak terus terpakai.

bash
# jalankan server di latar, uji empat endpoint, lalu hentikan
# build ulang bila binernya belum ada, misalnya setelah cache compile bersih
[ -f server.bin ] || crystal build -o server.bin server.cr

./server.bin &
sleep 2
curl -s http://127.0.0.1:8951/
curl -s "http://127.0.0.1:8951/salam?nama=Citra"
curl -s "http://127.0.0.1:8951/hitung?a=12&b=30"
curl -s http://127.0.0.1:8951/halaman-tak-ada
kill %1
text Keluaran
listen di 127.0.0.1:8951
Halo dari HTTP::Server Crystal (permintaan ke-1)
Halo, Citra! (permintaan ke-2)
12 + 30 = 42 (permintaan ke-3)
404 Not Found (permintaan ke-4)

Baris pertama berasal dari puts pada program server, sedangkan empat baris berikutnya adalah tubuh respons yang diterima curl. Parameter query nama=Citra dan a=12&b=30 dipetakan melalui query_params, dan path yang tidak terdaftar jatuh ke cabang else dengan teks 404 Not Found. Status 404 yang sebenarnya tidak terlihat pada keluaran karena curl hanya menampilkan tubuh responsnya. Akhiran (permintaan ke-N) pada keempat respons membuktikan middleware Catat bekerja pada setiap penanganan.

Kesalahan umum#

  • Server berakhir segera setelah dijalankan. Penyebabnya adalah listen yang tidak pernah dipanggil, atau listen yang dibungkus spawn tanpa penjaga sehingga fiber utama selesai dan program berhenti. Panggil server.listen secara blocking pada baris terakhir program.
  • Program berhenti dengan error saat bind_tcp. Alamat dan port yang diminta sudah digunakan proses lain, misalnya server sebelumnya yang belum dihentikan. Hentikan proses lama atau gunakan port lain pada bind_tcp maupun pada perintah curl.
  • Permintaan dengan parameter yang hilang menjawab status 500. Metode ["..."] tanpa tanda tanya melempar KeyError apabila kunci tidak ditemukan, dan HTTP::ErrorHandler mengubah exception tersebut menjadi respons 500. Gunakan ["..."]? untuk kunci yang boleh tidak ada.
  • Respons sampai ke klien tetapi ditafsirkan salah. Header Content-Type tidak ditetapkan sehingga klien harus menebak tipe isi respons. Tetapkan context.response.content_type sebelum menulis tubuh respons.

Ringkasan#

  • HTTP::Server.new menerima rantai handler opsional dan satu blok penangan terakhir, dan objek context-nya memisahkan context.request dari context.response.
  • Routing manual disusun dengan case atas context.request.path, sedangkan parameter query dibaca dari context.request.query_params dengan akses ["..."]? yang aman terhadap nil.
  • Middleware meng-include HTTP::Handler dan meneruskan pekerjaan dengan call_next, sedangkan pustaka standar menyediakan HTTP::ErrorHandler dan HTTP::LogHandler.
  • bind_tcp mengikat alamat dan port, lalu listen menjalankan event loop secara blocking sampai program dihentikan.

Referensi#