URI

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.parse untuk mem-parsing URL — mengembalikan objek dengan method scheme, host, port, path, query, fragment, user, password; lebih aman dan lebih ekspresif dari string manipulation manual.
  • URI.encode_www_form dan URI.decode_www_form — cara paling idiomatis untuk membangun dan mem-parsing query string; keduanya menangani encoding dengan benar.
  • URI.join untuk 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.dup jika ingin mempertahankan versi original.
  • Validasi scheme secara eksplisitURI.parse menerima banyak format; selalu cek uri.is_a?(URI::HTTP) || uri.is_a?(URI::HTTPS) untuk validasi URL web.
  • encode_www_form_component untuk query, encode_uri_component untuk path — keduanya berbeda dalam cara encoding spasi (+ vs %20) dan karakter yang di-encode.
  • URI::HTTPS.build untuk membangun dari komponen — lebih aman dari string interpolation ketika komponen berasal dari variabel.

← Sebelumnya: CSV   Berikutnya: Net::HTTP →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact