URI #
URL adalah salah satu struktur data yang paling sering dimanipulasi di aplikasi web — tapi manipulasi URL dengan string biasa penuh dengan jebakan: bagaimana menggabungkan base URL dengan path tanpa double slash, bagaimana mengekstrak query parameter dari URL yang kompleks, bagaimana memastikan karakter spesial di-encode dengan benar. Modul URI di Ruby standard library menyediakan representasi URL sebagai objek dengan method, bukan sekadar string yang dimanipulasi secara manual. Dengan URI, kamu bisa mem-parsing URL ke dalam komponen-komponennya, memodifikasi satu komponen tanpa merusak yang lain, membangun URL secara programatik, dan menangani encoding dengan benar. Artikel ini membahas seluruh API URI — dari parsing dasar hingga pembangunan URL yang kompleks.
Struktur URI #
Sebelum masuk ke kode, penting memahami anatomi sebuah URI lengkap dan bagaimana Ruby memetakannya ke method.
https://alice:[email protected]:8443/v1/users?role=admin&aktif=true#hasil
│ │ │ │ │ │ │ │
scheme user password host port path query fragment
require "uri"
url = URI.parse("https://alice:[email protected]:8443/v1/users?role=admin&aktif=true#hasil")
url.scheme # => "https"
url.user # => "alice"
url.password # => "secret"
url.host # => "api.example.com"
url.port # => 8443
url.path # => "/v1/users"
url.query # => "role=admin&aktif=true"
url.fragment # => "hasil"
# Representasi lengkap
url.to_s
# => "https://alice:[email protected]:8443/v1/users?role=admin&aktif=true#hasil"
# userinfo — user:password sebagai satu string
url.userinfo # => "alice:secret"
# host dengan port
url.host_with_port # NoMethodError — tidak ada method ini
"#{url.host}:#{url.port}" # => "api.example.com:8443"
# origin — scheme + host + port
"#{url.scheme}://#{url.host}:#{url.port}" # => "https://api.example.com:8443"
flowchart TD
A["URI.parse(url_string)"] --> B{Tipe URI}
B --> C[URI::HTTPS]
B --> D[URI::HTTP]
B --> E[URI::FTP]
B --> F[URI::MailTo]
B --> G[URI::Generic]
C --> H[scheme / host / port\npath / query / fragment\nuser / password]
D --> H
E --> I[scheme / host / port\npath / userinfo]
F --> J[to / headers]Parsing URI #
URI.parse #
URI.parse adalah entry point utama — ia menganalisis string URL dan mengembalikan objek URI dari subclass yang sesuai.
require "uri"
# URI::HTTPS untuk HTTPS
https_uri = URI.parse("https://example.com/path")
https_uri.class # => URI::HTTPS
# URI::HTTP untuk HTTP
http_uri = URI.parse("http://example.com/path")
http_uri.class # => URI::HTTP
# URI::FTP
ftp_uri = URI.parse("ftp://files.example.com/data.zip")
ftp_uri.class # => URI::FTP
# URI::MailTo
mailto = URI.parse("mailto:[email protected]")
mailto.class # => URI::MailTo
mailto.to # => "[email protected]"
# URI dengan path saja (relatif)
relative = URI.parse("/api/v1/users")
relative.class # => URI::Generic
relative.path # => "/api/v1/users"
# URI tidak valid
begin
URI.parse("bukan uri yang valid !!!!")
rescue URI::InvalidURIError => e
puts "URI tidak valid: #{e.message}"
end
Komponen Default #
Setiap scheme HTTP/HTTPS memiliki port default yang otomatis digunakan ketika port tidak disebutkan.
require "uri"
# HTTP — port default 80
http = URI.parse("http://example.com/path")
http.port # => 80 (default, bukan dari string)
http.default_port # => 80
# HTTPS — port default 443
https = URI.parse("https://example.com/path")
https.port # => 443
https.default_port # => 443
# Port eksplisit berbeda dari default
custom = URI.parse("https://example.com:8443/path")
custom.port # => 8443
custom.default_port # => 443
# Cek apakah menggunakan port non-default
def non_default_port?(uri)
uri.port != uri.default_port
end
non_default_port?(https) # => false
non_default_port?(custom) # => true
Memodifikasi Komponen URI #
Objek URI bisa dimodifikasi — kamu bisa mengubah satu komponen tanpa menyentuh yang lain.
require "uri"
base = URI.parse("https://api.example.com/v1/users")
# Ubah path
base.path = "/v2/pengguna"
base.to_s # => "https://api.example.com/v2/pengguna"
# Ubah host
base.host = "api.staging.example.com"
base.to_s # => "https://api.staging.example.com/v2/pengguna"
# Tambah query
base.query = "halaman=1&per_halaman=20"
base.to_s # => "https://api.staging.example.com/v2/pengguna?halaman=1&per_halaman=20"
# Ubah scheme
base.scheme = "http"
base.to_s # => "http://api.staging.example.com/v2/pengguna?halaman=1&per_halaman=20"
# Dup sebelum modifikasi — jika tidak ingin mengubah original
original = URI.parse("https://api.example.com/v1/users")
staging = original.dup
staging.host = "api.staging.example.com"
original.to_s # => "https://api.example.com/v1/users"
staging.to_s # => "https://api.staging.example.com/v1/users"
Membangun URI Secara Programatik #
Selain mem-parsing string, kamu bisa membangun URI dari komponen-komponennya.
require "uri"
# URI::HTTP.build — dari Hash komponen
uri = URI::HTTP.build(
host: "api.example.com",
path: "/v1/users",
query: "role=admin"
)
uri.to_s # => "http://api.example.com/v1/users?role=admin"
# URI::HTTPS.build
uri = URI::HTTPS.build(
host: "api.example.com",
port: 8443,
path: "/v1/produk",
query: "aktif=true&kategori=elektronik"
)
uri.to_s # => "https://api.example.com:8443/v1/produk?aktif=true&kategori=elektronik"
# Build dari array [scheme, userinfo, host, port, registry, path, opaque, query, fragment]
uri = URI::Generic.build(
scheme: "https",
host: "example.com",
path: "/path",
fragment: "section-1"
)
uri.to_s # => "https://example.com/path#section-1"
Membangun Query String #
Query string adalah bagian URI yang paling sering dibangun secara dinamis. URI tidak menyediakan method khusus untuk ini, tapi polanya sudah well-established.
require "uri"
# Membangun query string dari Hash
def hash_ke_query(params)
params.map { |k, v| "#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(v.to_s)}" }.join("&")
end
# Cara yang lebih idiomatis: URI.encode_www_form
params = { role: "admin", aktif: true, halaman: 1 }
URI.encode_www_form(params)
# => "role=admin&aktif=true&halaman=1"
# Menambahkan query ke URI yang sudah ada
def tambah_query(uri_string, params)
uri = URI.parse(uri_string)
existing = URI.decode_www_form(uri.query || "").to_h
merged = existing.merge(params.transform_keys(&:to_s))
uri.query = URI.encode_www_form(merged)
uri.to_s
end
tambah_query("https://api.example.com/users?role=admin", { halaman: 2, per_halaman: 20 })
# => "https://api.example.com/users?role=admin&halaman=2&per_halaman=20"
# Parsing query string kembali ke Hash
def query_ke_hash(uri_string)
uri = URI.parse(uri_string)
return {} if uri.query.nil? || uri.query.empty?
URI.decode_www_form(uri.query).to_h
end
query_ke_hash("https://api.example.com/users?role=admin&aktif=true&halaman=1")
# => {"role"=>"admin", "aktif"=>"true", "halaman"=>"1"}
Encoding dan Decoding #
URI encoding (percent encoding) adalah proses mengubah karakter yang tidak aman dalam URL menjadi representasi %XX. Ini penting untuk karakter seperti spasi, &, =, #, dan karakter non-ASCII.
require "uri"
# encode_www_form_component — encode satu nilai (spasi jadi +)
URI.encode_www_form_component("hello world") # => "hello+world"
URI.encode_www_form_component("Alice & Bob") # => "Alice+%26+Bob"
URI.encode_www_form_component("harga=50.000") # => "harga%3D50.000"
URI.encode_www_form_component("Jakarta Selatan") # => "Jakarta+Selatan"
# decode_www_form_component — decode kembali
URI.decode_www_form_component("hello+world") # => "hello world"
URI.decode_www_form_component("Alice+%26+Bob") # => "Alice & Bob"
# encode_www_form — encode Hash/Array sebagai query string (spasi jadi +)
URI.encode_www_form([["nama", "Alice Bob"], ["kota", "Jakarta Selatan"]])
# => "nama=Alice+Bob&kota=Jakarta+Selatan"
URI.encode_www_form({ nama: "Alice Bob", filter: "a&b" })
# => "nama=Alice+Bob&filter=a%26b"
# decode_www_form — parse query string ke Array of pairs
URI.decode_www_form("nama=Alice+Bob&kota=Jakarta+Selatan")
# => [["nama", "Alice Bob"], ["kota", "Jakarta Selatan"]]
URI.decode_www_form("nama=Alice+Bob&kota=Jakarta+Selatan").to_h
# => {"nama"=>"Alice Bob", "kota"=>"Jakarta Selatan"}
# encode_uri_component — encode path segment (spasi jadi %20, bukan +)
URI.encode_uri_component("hello world") # => "hello%20world" (Ruby 3.2+)
# Untuk versi Ruby lama, gunakan CGI.escape atau ERB::Util.url_encode
require "cgi"
CGI.escape("hello world") # => "hello+world"
CGI.unescape("hello+world") # => "hello world"
CGI.escapeURIComponent("hello world") # => "hello%20world" (Ruby 3.2+)
# Kapan pakai encode_www_form_component vs encode_uri_component?
# encode_www_form_component: untuk nilai dalam query string (? ... )
# encode_uri_component: untuk segmen path (/path/segment)
base = "https://example.com"
path_param = "produk/elektronik & komputer"
query_param = "kata kunci & filter"
# Path: spasi jadi %20
encoded_path = URI.encode_uri_component(path_param) # Ruby 3.2+
# => "produk%2Felektronik%20%26%20komputer"
# Query: spasi jadi +
encoded_query = URI.encode_www_form_component(query_param)
# => "kata+kunci+%26+filter"
url = "#{base}/#{encoded_path}?q=#{encoded_query}"
# => "https://example.com/produk%2Felektronik%20%26%20komputer?q=kata+kunci+%26+filter"
Validasi URI #
require "uri"
# URI.parse melempar exception untuk URI yang sangat tidak valid
# Tapi beberapa string yang "aneh" tetap diterima
def valid_uri?(string)
uri = URI.parse(string)
uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
rescue URI::InvalidURIError
false
end
valid_uri?("https://example.com") # => true
valid_uri?("http://example.com/path") # => true
valid_uri?("ftp://files.example.com") # => false (bukan HTTP/HTTPS)
valid_uri?("bukan uri") # => false
valid_uri?("") # => false
# Validasi lebih ketat
def valid_http_url?(string)
return false if string.nil? || string.strip.empty?
uri = URI.parse(string.strip)
return false unless uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)
return false if uri.host.nil? || uri.host.empty?
true
rescue URI::InvalidURIError
false
end
valid_http_url?("https://example.com") # => true
valid_http_url?("https://example.com/path?q=1") # => true
valid_http_url?("javascript:alert(1)") # => false
valid_http_url?(nil) # => false
valid_http_url?("") # => false
URI.join — Menggabungkan URI #
URI.join menggabungkan base URI dengan relative URI mengikuti aturan RFC 3986 — cara yang benar untuk membangun URL dari base dan path.
require "uri"
base = "https://api.example.com/v1/"
# URI.join mengikuti aturan RFC 3986
URI.join(base, "users").to_s
# => "https://api.example.com/v1/users"
URI.join(base, "users/123").to_s
# => "https://api.example.com/v1/users/123"
# Hati-hati: path tanpa leading slash menggantikan segmen terakhir
URI.join("https://api.example.com/v1/users", "products").to_s
# => "https://api.example.com/v1/products" (bukan /v1/users/products!)
# Dengan leading slash: path absolut dari root
URI.join(base, "/v2/users").to_s
# => "https://api.example.com/v2/users" (override seluruh path)
# Chain join
URI.join("https://api.example.com/", "v1/", "users/", "123").to_s
# => "https://api.example.com/v1/users/123"
# ANTI-PATTERN: string concatenation yang rentan double-slash
base_url = "https://api.example.com/v1"
endpoint = "/users"
"#{base_url}#{endpoint}" # => "https://api.example.com/v1/users" (kebetulan benar)
"#{base_url}/#{endpoint}" # => "https://api.example.com/v1//users" (double slash!)
# BENAR: gunakan URI.join
URI.join("https://api.example.com/v1/", "users").to_s
# => "https://api.example.com/v1/users"
Pola Penggunaan Nyata #
HTTP Client Helper #
require "uri"
require "net/http"
class ApiClient
def initialize(base_url)
@base_uri = URI.parse(base_url)
end
def get(path, params = {})
uri = build_uri(path, params)
Net::HTTP.get_response(uri)
end
private
def build_uri(path, params = {})
uri = @base_uri.dup
uri.path = File.join(uri.path, path)
uri.query = URI.encode_www_form(params) unless params.empty?
uri
end
end
client = ApiClient.new("https://api.example.com/v1")
response = client.get("/users", { role: "admin", halaman: 1 })
URL Builder yang Aman #
require "uri"
class UrlBuilder
def initialize(base)
@uri = URI.parse(base)
@params = URI.decode_www_form(@uri.query || "").to_h
end
def path(new_path)
@uri.path = new_path
self
end
def param(key, value)
@params[key.to_s] = value.to_s
self
end
def params(hash)
hash.each { |k, v| param(k, v) }
self
end
def fragment(value)
@uri.fragment = value
self
end
def build
@uri.query = @params.empty? ? nil : URI.encode_www_form(@params)
@uri.to_s
end
def to_s
build
end
end
url = UrlBuilder.new("https://api.example.com")
.path("/v2/produk")
.param(:kategori, "elektronik")
.param(:harga_min, 100_000)
.param(:harga_maks, 5_000_000)
.fragment("daftar")
.build
# => "https://api.example.com/v2/produk?kategori=elektronik&harga_min=100000&harga_maks=5000000#daftar"
Sanitasi dan Normalisasi URL #
require "uri"
def normalisasi_url(input)
# Tambah scheme jika tidak ada
input = "https://#{input}" unless input.match?(%r{\A[a-z][a-z0-9+\-.]*://}i)
uri = URI.parse(input)
# Pastikan scheme lowercase
uri.scheme = uri.scheme.downcase
# Hapus trailing slash dari path (kecuali root)
uri.path = uri.path.chomp("/") if uri.path != "/"
# Hapus fragment (untuk canonical URL)
uri.fragment = nil
# Hapus query kosong
uri.query = nil if uri.query&.empty?
uri.to_s
rescue URI::InvalidURIError
nil
end
normalisasi_url("HTTPS://Example.COM/path/")
# => "https://example.com/path"
normalisasi_url("example.com/path")
# => "https://example.com/path"
normalisasi_url("https://example.com/path#section")
# => "https://example.com/path"
Ringkasan #
URI.parseuntuk mem-parsing URL — mengembalikan objek dengan methodscheme,host,port,path,query,fragment,user,password; lebih aman dan lebih ekspresif dari string manipulation manual.URI.encode_www_formdanURI.decode_www_form— cara paling idiomatis untuk membangun dan mem-parsing query string; keduanya menangani encoding dengan benar.URI.joinuntuk menggabungkan URL — mengikuti aturan RFC 3986; hindari string concatenation yang rentan double slash atau override path yang tidak diinginkan.- Dup sebelum modifikasi — objek URI bisa dimodifikasi; gunakan
uri.dupjika ingin mempertahankan versi original.- Validasi scheme secara eksplisit —
URI.parsemenerima banyak format; selalu cekuri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS)untuk validasi URL web.encode_www_form_componentuntuk query,encode_uri_componentuntuk path — keduanya berbeda dalam cara encoding spasi (+ vs %20) dan karakter yang di-encode.URI::HTTPS.builduntuk membangun dari komponen — lebih aman dari string interpolation ketika komponen berasal dari variabel.