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.
# 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.listenRantai 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.
# tidak-dijalankan
HTTP::Server.new([
HTTP::ErrorHandler.new,
HTTP::LogHandler.new,
]) do |context|
context.response.content_type = "text/plain"
context.response.print "ok"
endMiddleware 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.
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.listenDua 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.
# kompilasi program server menjadi biner
crystal build -o server.bin server.crJalankan 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.
# 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 %1listen 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
listenyang tidak pernah dipanggil, ataulistenyang dibungkusspawntanpa penjaga sehingga fiber utama selesai dan program berhenti. Panggilserver.listensecara 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 padabind_tcpmaupun pada perintah curl. - Permintaan dengan parameter yang hilang menjawab status 500. Metode
["..."]tanpa tanda tanya melemparKeyErrorapabila kunci tidak ditemukan, danHTTP::ErrorHandlermengubah exception tersebut menjadi respons 500. Gunakan["..."]?untuk kunci yang boleh tidak ada. - Respons sampai ke klien tetapi ditafsirkan salah. Header
Content-Typetidak ditetapkan sehingga klien harus menebak tipe isi respons. Tetapkancontext.response.content_typesebelum menulis tubuh respons.
Ringkasan#
HTTP::Server.newmenerima rantai handler opsional dan satu blok penangan terakhir, dan objek context-nya memisahkancontext.requestdaricontext.response.- Routing manual disusun dengan
caseatascontext.request.path, sedangkan parameter query dibaca daricontext.request.query_paramsdengan akses["..."]?yang aman terhadapnil. - Middleware meng-include
HTTP::Handlerdan meneruskan pekerjaan dengancall_next, sedangkan pustaka standar menyediakanHTTP::ErrorHandlerdanHTTP::LogHandler. bind_tcpmengikat alamat dan port, lalulistenmenjalankan event loop secara blocking sampai program dihentikan.
Referensi#
- API Crystal 1.22.1 — Crystal
— Dokumentasi kelas
HTTP::Server,HTTP::Request, danHTTP::Server::Contextyang menjadi dasar topik ini. - Panduan bahasa Crystal 1.22 — Crystal — Dokumentasi konsep bahasa yang digunakan pada program di topik ini, termasuk blok, kelas, dan fiber.