Pipeline permintaan

Endpoint, router, dan plug dalam pipeline permintaan.

Daftar isi

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.

browserEndpointplug umumRoutercocokkan jalurPipelineplug berurutanControllerGET /api/catatan:accepts, plug kustom,fetch_session, dan seterusnya
Alur permintaan dari browser ke controller

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:

elixir Potongan endpoint.ex hasil generator Phoenix 1.8
# 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.Router

Baris 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:

elixir Potongan 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
end

Pipeline :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.

elixir gerbang.exs
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.

text Keluaran
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 scope dengan pipe_through, dan lebih dari satu pipeline dapat diberikan sebagai daftar.
  • Plug modul kustom cukup menyediakan init/1 dan call/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#