Setiap permintaan HTTP yang diterima sebuah aplikasi Phoenix melewati rangkaian
tahap yang tetap sebelum sampai pada controller. Topik ini membahas urutan tahap
tersebut, yaitu endpoint, router, pipeline, dan plug, beserta cara
mengamati alurnya melalui demo mandiri. Setelah membaca, struktur berkas
router.ex dan endpoint.ex pada proyek hasil mix phx.new dapat
dibaca dengan penuh pemahaman, dan plug kustom pertama dapat ditulis.
Alur permintaan dari browser ke controller#
Permintaan yang datang dari browser masuk melalui endpoint, yaitu modul yang
berperan sebagai pintu depan aplikasi dan menampung tumpukan plug yang berlaku
untuk semua permintaan. Endpoint menjalankan pekerjaan yang tidak bergantung pada jalur
yang diminta, misalnya memecah tubuh permintaan, mengelola
session, dan mencatat log. Di proyek Phoenix, pintu depan ini berada di
berkas lib/nama_proyek_web/endpoint.ex.
Setelah melewati plug endpoint, permintaan dioper ke router, yaitu modul yang mencocokkan metode HTTP dan jalur dengan rute yang terdaftar. Router pada Phoenix 1.8 juga memutuskan pipeline mana yang dilewati permintaan tersebut, kemudian meneruskan permintaan ke controller yang ditetapkan oleh rute tersebut. Diagram berikut merangkum alur tersebut.
Kotak penting dalam diagram tersebut adalah pipeline, yaitu deretan plug yang
berjalan berurutan untuk sekelompok rute. Struktur Plug.Conn dibawa dari satu
plug ke plug berikutnya, sehingga setiap plug dapat membaca maupun mengubah
keadaan permintaan, misalnya menambahkan header atau menyimpan nilai untuk
digunakan controller. Pipeline inilah tulang punggung pemrosesan permintaan pada
Phoenix, dan mekanismenya ditelusuri pada bagian berikutnya.
Plug sebagai satuan pemrosesan#
Plug adalah konvensi sederhana di atas Plug.Conn, yaitu sebuah modul dengan
fungsi init/1 dan call/2, atau sebuah fungsi yang menerima conn dan satu
argumen pilihan, kemudian mengembalikan conn yang sudah diubah. Semua tahap
yang disebutkan di atas adalah plug, termasuk router itu sendiri. Konvensi ini membuat
pemrosesan permintaan dapat disusun seperti rangkaian fungsi: keluaran satu plug
menjadi masukan plug berikutnya.
Urutan penyusunan plug menentukan keadaan conn yang diterima tahap berikutnya.
Plug yang berjalan lebih dulu dapat menolak permintaan, sehingga plug di
belakangnya tidak pernah dieksekusi, atau menyiapkan data yang dibutuhkan tahap
selanjutnya. Pemisahan init/1 dan call/2 juga memiliki makna praktis. Fungsi
init/1 dijalankan sekali pada waktu kompilasi dan hasilnya disimpan, sehingga
perhitungan yang berat sebaiknya diletakkan di sana. Fungsi call/2
dijalankan untuk setiap permintaan dan hanya menangani pekerjaan yang
diperhitungkan per permintaan.
Potongan berikut diambil dari endpoint.ex hasil generator Phoenix 1.8
dan memperlihatkan deretan plug yang berjalan sebelum router terlibat:
# tidak-dijalankan (fragmen konfigurasi, bukan skrip utuh)
plug Plug.RequestId
plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint]
plug Plug.Parsers,
parsers: [:urlencoded, :multipart, :json],
pass: ["*/*"],
json_decoder: Phoenix.json_library()
plug Plug.MethodOverride
plug Plug.Head
plug Plug.Session, @session_options
plug NamaProyekWeb.RouterBaris terakhir menegaskan posisi router: router adalah plug terakhir pada endpoint. Setelah itu, kendali berpindah ke router beserta pipeline yang melekat pada rute permintaan.
Router dan pipeline pada proyek nyata#
Router proyek Phoenix mendeklarasikan pipeline dengan makro pipeline dan
melekatkan pipeline pada kelompok rute dengan makro pipe_through. Potongan
berikut diambil dari router.ex hasil generator Phoenix 1.8:
# tidak-dijalankan (fragmen proyek, bukan skrip utuh)
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {NamaProyekWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", NamaProyekWeb do
pipe_through :browser
get "/", PageController, :home
endPipeline :browser menyiapkan segala sesuatu yang dibutuhkan halaman HTML,
antara lain negosiasi format, session, flash, dan perlindungan CSRF.
Sebaliknya, pipeline :api hanya memuat :accepts dengan daftar ["json"].
Dengan pembagian ini, rute yang melewati :api tidak melalui pemrosesan session
dan CSRF yang tidak diperlukan. Makro pipe_through menetapkan pipeline per
scope, dan lebih dari satu pipeline dapat diberikan sekaligus dalam bentuk
daftar, misalnya pipe_through [:api, :pemeriksa].
Makro scope mengelompokkan rute berdasarkan jalur awal dan modul controller.
Kelompok yang berbagi jalur awal dan pipeline yang sama ditulis dalam satu
scope, sehingga kebijakan keamanan terbaca dari strukturnya, bukan dari
perbandingan rute satu per satu. Seluruh daftar rute beserta pipeline-nya
dapat diinspeksi dengan perintah mix phx.routes dari dalam proyek, yang
mencetak tabel rute hasil ekspansi makro-makro tersebut.
Pipeline juga dapat berisi plug berupa modul kustom. Plug semacam ini menyediakan dua
fungsi, yaitu init/1 yang dijalankan sekali saat kompilasi dan call/2 yang
dijalankan untuk setiap permintaan. Bentuk ini digunakan pada demo berikutnya.
Demo mandiri: dua pipeline dan plug kustom#
Skrip berikut membangun router lengkap tanpa proyek Mix, dengan dua
pipeline dan satu plug modul kustom bernama PlugPemeriksa. Plug tersebut
menambahkan header x-diperiksa dan nilai assign :diperiksa pada setiap
permintaan yang melewatinya, sehingga jejaknya dapat diamati pada keluaran.
Mix.install([{:phoenix, "~> 1.8"}, {:jason, "~> 1.4"}])
Logger.configure(level: :warning)
# plug modul kustom: menandai setiap permintaan yang melewatinya
defmodule PlugPemeriksa do
import Plug.Conn
def init(pilihan), do: pilihan
def call(conn, _pilihan) do
conn
|> put_resp_header("x-diperiksa", "ya")
|> assign(:diperiksa, true)
end
end
defmodule CatatanController do
use Phoenix.Controller, formats: [:json]
def index(conn, _params) do
json(conn, %{catatan: [], diperiksa: conn.assigns.diperiksa})
end
end
defmodule GerbangRouter do
use Phoenix.Router
pipeline :api do
plug :accepts, ["json"]
end
pipeline :pemeriksa do
plug PlugPemeriksa
end
scope "/api" do
pipe_through [:api, :pemeriksa]
get "/catatan", CatatanController, :index
end
end
conn =
Phoenix.ConnTest.build_conn(:get, "/api/catatan", nil)
|> GerbangRouter.call(GerbangRouter.init([]))
IO.inspect(conn.status, label: "status")
IO.inspect(Enum.filter(conn.resp_headers, fn {k, _v} -> String.starts_with?(k, "x-") end), label: "header")
IO.inspect(conn.assigns.diperiksa, label: "assign")
IO.inspect(Jason.decode!(conn.resp_body), label: "isi")Jalankan skripnya dengan elixir gerbang.exs. Keluarannya menunjukkan bahwa
permintaan melewati kedua pipeline: status 200, header x-diperiksa sudah
terpasang, nilai assign tersedia di controller, dan tubuh respons berformat
JSON.
status: 200
header: [{"x-diperiksa", "ya"}]
assign: true
isi: %{"catatan" => [], "diperiksa" => true}Fungsi Phoenix.ConnTest.build_conn/3 membangun conn buatan tanpa server
yang berjalan, lalu Router.call/2 menjalankan seluruh rangkaian di atas
conn tersebut. Pola ini berbeda dari proyek nyata, yang permintaannya datang
dari jaringan dan diproses oleh endpoint yang sesungguhnya. Prinsipnya sama:
pipeline berjalan berurutan, dan controller hanya menerima permintaan yang
lolos.
Kesalahan umum#
Kesalahan pertama adalah melupakan pipe_through. Rute yang dideklarasikan
di dalam scope tanpa pipe_through tetap berjalan, tetapi tidak melalui
pipeline apa pun, sehingga conn sampai ke controller tanpa negosiasi format
maupun perlindungan lain. Akibatnya, permintaan dengan format yang salah tidak
ditolak sejak dini. Perbaikannya, pastikan setiap scope menyebut pipeline
yang memang diperlukan.
Kesalahan kedua adalah menganggap plug :fetch_session dapat dipasang bebas.
Plug tersebut membaca session store, dan session store disiapkan oleh
Plug.Session pada endpoint. Pada skrip mandiri yang tidak memuat endpoint,
pemanggilannya melempar error. Perbaikannya, pindahkan percobaan ke proyek
Phoenix yang utuh, atau gunakan plug yang tidak bergantung endpoint seperti
contoh PlugPemeriksa.
Kesalahan ketiga menyangkut bentuk plug fungsi. Sebuah plug fungsi hanya
menerima dua argumen, yaitu conn dan satu pilihan, sehingga penulisan seperti
plug :put_resp_header, "nama", "nilai" di dalam pipeline gagal dikompilasi.
Bungkus pemanggilan fungsi semacam itu ke dalam plug modul kustom, dan biarkan
call/2 memanggil fungsi tersebut dengan argumen lengkap.
Ringkasan#
- Permintaan melewati endpoint, lalu router, lalu pipeline, dan berakhir di
controller. Setiap tahapnya adalah plug yang mengubah
Plug.Conn. - Pipeline adalah deretan plug yang dilekatkan pada
scopedenganpipe_through, dan lebih dari satu pipeline dapat diberikan sebagai daftar. - Plug modul kustom cukup menyediakan
init/1dancall/2, sehingga pemrosesan permintaan dapat diperluas tanpa mengubah framework. - Router adalah plug terakhir pada endpoint, dan semua plug lain pada endpoint berjalan sebelum pencocokan rute.
Referensi#
- Routing — Phoenix Framework — Panduan router, pipeline, dan scope pada Phoenix.
- Phoenix.Endpoint — Phoenix Framework — Dokumentasi endpoint dan tumpukan plug-nya.
- Plug.Conn — Plug
— Struktur
Plug.Connyang dibawa di sepanjang pipeline.