Net::HTTP

Net::HTTP #

Ketika aplikasi Ruby perlu berkomunikasi dengan API eksternal, mengambil halaman web, atau mengirim data ke server lain, Net::HTTP adalah pilihan pertama yang tersedia tanpa instalasi gem apapun. Library ini menyediakan HTTP client yang komprehensif — GET, POST, PUT, DELETE, PATCH, semua HTTP method tersedia, dengan dukungan HTTPS, header kustom, timeout, redirect, dan upload file. Net::HTTP memang lebih verbose dibanding gem seperti Faraday atau HTTParty, tapi ia zero-dependency dan ada di setiap instalasi Ruby. Memahaminya juga membantu kamu mengerti abstraksi yang dibangun di atasnya. Artikel ini membahas pola-pola penggunaan Net::HTTP dari yang paling sederhana hingga yang cukup kompleks.

Request Paling Sederhana #

Ruby menyediakan beberapa shortcut untuk request HTTP yang paling dasar — berguna untuk skrip cepat tapi terbatas untuk penggunaan production.

require "net/http"
require "uri"

# Net::HTTP.get — paling sederhana, kembalikan body sebagai string
body = Net::HTTP.get(URI.parse("https://api.github.com/users/ruby"))
# => string JSON

# Net::HTTP.get_response — kembalikan objek response lengkap
response = Net::HTTP.get_response(URI.parse("https://httpbin.org/get"))
response.code         # => "200"
response.message      # => "OK"
response.body         # => string body
response["content-type"]  # => "application/json"

# Shortcut dengan host dan path terpisah
Net::HTTP.get("httpbin.org", "/get")
# => body string

Pola Penggunaan Standar #

Untuk penggunaan yang lebih serius, pola Net::HTTP.start memberikan kontrol penuh atas koneksi.

require "net/http"
require "uri"
require "json"

uri = URI.parse("https://api.github.com/repos/ruby/ruby")

Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
  request = Net::HTTP::Get.new(uri)
  request["Accept"] = "application/json"
  request["User-Agent"] = "MyApp/1.0"

  response = http.request(request)

  case response
  when Net::HTTPSuccess
    JSON.parse(response.body)
  when Net::HTTPRedirection
    puts "Redirect ke: #{response["location"]}"
  else
    raise "HTTP Error: #{response.code} #{response.message}"
  end
end
sequenceDiagram
    participant App as Aplikasi Ruby
    participant HTTP as Net::HTTP
    participant Server as Server

    App->>HTTP: Net::HTTP.start(host, port)
    HTTP->>Server: TCP Connection
    App->>HTTP: request = Net::HTTP::Get.new(uri)
    App->>HTTP: http.request(request)
    HTTP->>Server: HTTP GET /path
    Server-->>HTTP: HTTP Response
    HTTP-->>App: Net::HTTPResponse
    App->>App: response.code / response.body
    HTTP->>Server: Connection Close

GET Request #

require "net/http"
require "uri"
require "json"

# GET dengan query parameters
def get_pengguna(role: nil, halaman: 1)
  uri = URI.parse("https://api.example.com/v1/users")
  params = { halaman: halaman }
  params[:role] = role if role
  uri.query = URI.encode_www_form(params)

  Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    request = Net::HTTP::Get.new(uri)
    request["Authorization"] = "Bearer #{ENV["API_TOKEN"]}"
    request["Accept"] = "application/json"

    response = http.request(request)
    raise "Error: #{response.code}" unless response.is_a?(Net::HTTPSuccess)

    JSON.parse(response.body, symbolize_names: true)
  end
end

# GET dengan custom headers
def download_file(url, path_tujuan)
  uri = URI.parse(url)

  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
    request = Net::HTTP::Get.new(uri)

    http.request(request) do |response|
      raise "Download gagal: #{response.code}" unless response.is_a?(Net::HTTPSuccess)

      File.open(path_tujuan, "wb") do |file|
        response.read_body { |chunk| file.write(chunk) }
      end
    end
  end
end

download_file("https://example.com/data.csv", "/tmp/data.csv")

POST Request #

require "net/http"
require "uri"
require "json"

# POST dengan JSON body
def buat_pengguna(nama:, email:, role: "user")
  uri = URI.parse("https://api.example.com/v1/users")

  Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    request = Net::HTTP::Post.new(uri)
    request["Content-Type"] = "application/json"
    request["Authorization"] = "Bearer #{ENV["API_TOKEN"]}"
    request.body = JSON.generate({ nama: nama, email: email, role: role })

    response = http.request(request)

    case response.code.to_i
    when 201
      JSON.parse(response.body, symbolize_names: true)
    when 422
      error = JSON.parse(response.body)
      raise "Validasi gagal: #{error["message"]}"
    else
      raise "Error #{response.code}: #{response.message}"
    end
  end
end

# POST dengan form data (application/x-www-form-urlencoded)
def login(username, password)
  uri = URI.parse("https://auth.example.com/login")

  Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    request = Net::HTTP::Post.new(uri)
    request.set_form_data({ username: username, password: password })
    # set_form_data otomatis set Content-Type: application/x-www-form-urlencoded

    response = http.request(request)
    raise "Login gagal" unless response.is_a?(Net::HTTPSuccess)

    JSON.parse(response.body)
  end
end

PUT, PATCH, dan DELETE #

require "net/http"
require "uri"
require "json"

# PUT — replace resource sepenuhnya
def update_pengguna(id, data)
  uri = URI.parse("https://api.example.com/v1/users/#{id}")

  Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    request = Net::HTTP::Put.new(uri)
    request["Content-Type"] = "application/json"
    request["Authorization"] = "Bearer #{ENV["API_TOKEN"]}"
    request.body = JSON.generate(data)

    http.request(request)
  end
end

# PATCH — update sebagian field
def patch_pengguna(id, fields)
  uri = URI.parse("https://api.example.com/v1/users/#{id}")

  Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    request = Net::HTTP::Patch.new(uri)
    request["Content-Type"] = "application/json"
    request["Authorization"] = "Bearer #{ENV["API_TOKEN"]}"
    request.body = JSON.generate(fields)

    response = http.request(request)
    JSON.parse(response.body, symbolize_names: true)
  end
end

# DELETE
def hapus_pengguna(id)
  uri = URI.parse("https://api.example.com/v1/users/#{id}")

  Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    request = Net::HTTP::Delete.new(uri)
    request["Authorization"] = "Bearer #{ENV["API_TOKEN"]}"

    response = http.request(request)
    response.is_a?(Net::HTTPSuccess)
  end
end

HTTPS dan Konfigurasi SSL #

require "net/http"
require "openssl"

uri = URI.parse("https://api.example.com/data")

# HTTPS standar — verifikasi SSL certificate (default dan aman)
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  # http.verify_mode default ke OpenSSL::SSL::VERIFY_PEER
  response = http.get(uri.path)
end

# Konfigurasi SSL lebih detail
Net::HTTP.start(uri.host, uri.port,
  use_ssl: true,
  verify_mode: OpenSSL::SSL::VERIFY_PEER,
  ca_file: "/etc/ssl/certs/ca-certificates.crt"
) do |http|
  response = http.get(uri.path)
end

Jangan pernah menggunakan verify_mode: OpenSSL::SSL::VERIFY_NONE di production. Ini menonaktifkan verifikasi SSL certificate dan membuat koneksi rentan terhadap serangan man-in-the-middle — siapapun bisa menyadap atau memodifikasi data yang dikirim.

# ANTI-PATTERN: mematikan verifikasi SSL
http.verify_mode = OpenSSL::SSL::VERIFY_NONE   # ✗ JANGAN di production!

# BENAR: selalu verifikasi, debug certificate jika ada masalah
http.verify_mode = OpenSSL::SSL::VERIFY_PEER   # ✓ default yang aman

Jika kamu mendapat OpenSSL::SSL::SSLError di development, penyebab umumnya adalah certificate self-signed atau expired. Update certificate store di sistemmu, jangan matikan verifikasi.


Timeout #

Tanpa timeout, request yang tidak mendapat response akan membuat aplikasimu hang selamanya. Selalu set timeout yang masuk akal.

require "net/http"

uri = URI.parse("https://api.example.com/data")

Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
  # open_timeout — waktu maksimum untuk membuka koneksi TCP
  http.open_timeout = 5    # 5 detik

  # read_timeout — waktu maksimum menunggu response setelah request dikirim
  http.read_timeout = 30   # 30 detik

  # write_timeout — waktu maksimum untuk mengirim request body (Ruby 2.6+)
  http.write_timeout = 10  # 10 detik

  begin
    response = http.get(uri.path)
  rescue Net::OpenTimeout => e
    puts "Koneksi timeout: tidak bisa terhubung ke server dalam 5 detik"
  rescue Net::ReadTimeout => e
    puts "Read timeout: server tidak merespons dalam 30 detik"
  end
end

# Atau set saat inisialisasi
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = 5
http.read_timeout = 30
http.start do |h|
  h.get(uri.path)
end

Menangani Response #

require "net/http"

# Hierarki class response Net::HTTP
# Net::HTTPResponse
#   Net::HTTPSuccess (2xx)
#     Net::HTTPOK              (200)
#     Net::HTTPCreated         (201)
#     Net::HTTPNoContent       (204)
#   Net::HTTPRedirection (3xx)
#     Net::HTTPMovedPermanently (301)
#     Net::HTTPFound           (302)
#   Net::HTTPClientError (4xx)
#     Net::HTTPBadRequest      (400)
#     Net::HTTPUnauthorized    (401)
#     Net::HTTPForbidden       (403)
#     Net::HTTPNotFound        (404)
#   Net::HTTPServerError (5xx)
#     Net::HTTPInternalServerError (500)

def request_dengan_error_handling(uri_string)
  uri = URI.parse(uri_string)

  Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
                  open_timeout: 5, read_timeout: 30) do |http|
    response = http.get(uri.request_uri)

    case response
    when Net::HTTPSuccess
      response.body
    when Net::HTTPUnauthorized
      raise "Unauthorized: cek API token"
    when Net::HTTPForbidden
      raise "Forbidden: tidak punya akses"
    when Net::HTTPNotFound
      nil  # resource tidak ditemukan, kembalikan nil
    when Net::HTTPTooManyRequests
      retry_after = response["retry-after"]&.to_i || 60
      raise "Rate limited. Coba lagi dalam #{retry_after} detik."
    when Net::HTTPServerError
      raise "Server error #{response.code}: #{response.message}"
    when Net::HTTPRedirection
      # Ikuti redirect secara manual
      location = response["location"]
      request_dengan_error_handling(location)
    else
      raise "Unexpected response: #{response.code} #{response.message}"
    end
  end
rescue Net::OpenTimeout
  raise "Connection timeout"
rescue Net::ReadTimeout
  raise "Read timeout"
rescue SocketError => e
  raise "Network error: #{e.message}"
end

Upload File (Multipart Form) #

require "net/http"
require "mime/types"  # gem tambahan untuk MIME type detection

# Upload file dengan multipart/form-data
def upload_file(url, file_path, field_name: "file", tambahan_params: {})
  uri = URI.parse(url)
  file = File.open(file_path, "rb")

  boundary = "----RubyMultipart#{SecureRandom.hex(8)}"

  # Bangun body multipart secara manual
  body_parts = []

  # Field tambahan
  tambahan_params.each do |key, value|
    body_parts << "--#{boundary}\r\n"
    body_parts << "Content-Disposition: form-data; name=\"#{key}\"\r\n\r\n"
    body_parts << "#{value}\r\n"
  end

  # File field
  filename = File.basename(file_path)
  body_parts << "--#{boundary}\r\n"
  body_parts << "Content-Disposition: form-data; name=\"#{field_name}\"; filename=\"#{filename}\"\r\n"
  body_parts << "Content-Type: application/octet-stream\r\n\r\n"
  body_parts << file.read
  body_parts << "\r\n--#{boundary}--\r\n"

  body = body_parts.join

  Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    request = Net::HTTP::Post.new(uri)
    request["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
    request["Content-Length"] = body.bytesize.to_s
    request.body = body

    http.request(request)
  end
ensure
  file&.close
end

Reuse Koneksi dan Connection Pooling #

Membuka koneksi TCP baru untuk setiap request itu mahal. Gunakan Net::HTTP.start dengan blok atau persistent connection untuk request berturut-turut ke host yang sama.

require "net/http"

# ANTI-PATTERN: buka koneksi baru setiap request
def ambil_banyak_pengguna_lambat(ids)
  ids.map do |id|
    response = Net::HTTP.get_response(URI.parse("https://api.example.com/users/#{id}"))
    JSON.parse(response.body)
    # Membuka dan menutup koneksi TCP untuk setiap ID!
  end
end

# BENAR: reuse koneksi untuk semua request
def ambil_banyak_pengguna(ids)
  uri = URI.parse("https://api.example.com")

  Net::HTTP.start(uri.host, uri.port, use_ssl: true,
                  open_timeout: 5, read_timeout: 10) do |http|
    ids.map do |id|
      request = Net::HTTP::Get.new("/users/#{id}")
      request["Authorization"] = "Bearer #{ENV["API_TOKEN"]}"
      response = http.request(request)
      JSON.parse(response.body, symbolize_names: true)
    end
  end
  # Koneksi TCP dibuka sekali, digunakan untuk semua request, ditutup sekali
end

Wrapper Class untuk API Client #

Untuk penggunaan production, membungkus Net::HTTP dalam kelas sendiri jauh lebih maintainable.

require "net/http"
require "uri"
require "json"

class HttpClient
  class Error < StandardError
    attr_reader :code, :body
    def initialize(msg, code: nil, body: nil)
      super(msg)
      @code = code
      @body = body
    end
  end

  class NotFound < Error; end
  class Unauthorized < Error; end
  class ServerError < Error; end
  class Timeout < Error; end

  DEFAULT_TIMEOUT = { open: 5, read: 30 }.freeze
  DEFAULT_HEADERS = {
    "Accept" => "application/json",
    "Content-Type" => "application/json"
  }.freeze

  def initialize(base_url, headers: {}, timeout: {})
    @base_uri = URI.parse(base_url)
    @headers = DEFAULT_HEADERS.merge(headers)
    @timeout = DEFAULT_TIMEOUT.merge(timeout)
  end

  def get(path, params: {})
    uri = build_uri(path, params)
    execute(Net::HTTP::Get.new(uri))
  end

  def post(path, body: {})
    uri = build_uri(path)
    request = Net::HTTP::Post.new(uri)
    request.body = JSON.generate(body)
    execute(request)
  end

  def put(path, body: {})
    uri = build_uri(path)
    request = Net::HTTP::Put.new(uri)
    request.body = JSON.generate(body)
    execute(request)
  end

  def patch(path, body: {})
    uri = build_uri(path)
    request = Net::HTTP::Patch.new(uri)
    request.body = JSON.generate(body)
    execute(request)
  end

  def delete(path)
    uri = build_uri(path)
    execute(Net::HTTP::Delete.new(uri))
  end

  private

  def build_uri(path, params = {})
    uri = @base_uri.dup
    uri.path = File.join(uri.path.chomp("/"), path)
    uri.query = URI.encode_www_form(params) unless params.empty?
    uri
  end

  def execute(request)
    @headers.each { |k, v| request[k] = v }

    http = Net::HTTP.new(@base_uri.host, @base_uri.port)
    http.use_ssl = @base_uri.scheme == "https"
    http.open_timeout = @timeout[:open]
    http.read_timeout = @timeout[:read]

    response = http.request(request)
    handle_response(response)
  rescue Net::OpenTimeout, Net::ReadTimeout => e
    raise Timeout, "Request timeout: #{e.message}"
  rescue SocketError => e
    raise Error, "Network error: #{e.message}"
  end

  def handle_response(response)
    case response
    when Net::HTTPSuccess
      return nil if response.body.nil? || response.body.empty?
      JSON.parse(response.body, symbolize_names: true)
    when Net::HTTPUnauthorized
      raise Unauthorized.new("Unauthorized", code: 401, body: response.body)
    when Net::HTTPNotFound
      raise NotFound.new("Not found", code: 404, body: response.body)
    when Net::HTTPServerError
      raise ServerError.new("Server error #{response.code}", code: response.code.to_i, body: response.body)
    else
      raise Error.new("HTTP #{response.code}: #{response.message}", code: response.code.to_i, body: response.body)
    end
  end
end

# Penggunaan
client = HttpClient.new("https://api.example.com/v1",
  headers: { "Authorization" => "Bearer #{ENV["API_TOKEN"]}" },
  timeout: { open: 3, read: 15 }
)

begin
  pengguna = client.get("/users", params: { role: "admin" })
  profil_baru = client.post("/users", body: { nama: "Alice", email: "[email protected]" })
  client.patch("/users/1", body: { aktif: false })
  client.delete("/users/99")
rescue HttpClient::Unauthorized
  puts "Token expired, perlu refresh"
rescue HttpClient::NotFound
  puts "Resource tidak ditemukan"
rescue HttpClient::Timeout
  puts "Request timeout, coba lagi nanti"
rescue HttpClient::Error => e
  puts "HTTP Error #{e.code}: #{e.message}"
end

Ringkasan #

  • Net::HTTP.start dengan blok — pola standar yang memberikan kontrol penuh atas koneksi dan otomatis menutup koneksi setelah blok selesai.
  • use_ssl: uri.scheme == "https" — cara idiomatis mengaktifkan HTTPS; deteksi otomatis dari scheme URI.
  • Selalu set timeoutopen_timeout untuk batas waktu koneksi TCP, read_timeout untuk batas waktu menunggu response; tanpa ini aplikasimu bisa hang selamanya.
  • Reuse koneksi untuk request berturut-turut — buka satu Net::HTTP.start dan lakukan banyak request di dalamnya; jauh lebih efisien dari membuka koneksi baru setiap kali.
  • Jangan matikan verifikasi SSLVERIFY_NONE hanya boleh untuk debugging lokal dengan certificate self-signed, tidak pernah di production.
  • Tangani response berdasarkan class — gunakan when Net::HTTPSuccess, when Net::HTTPNotFound dan seterusnya, bukan mengecek response.code == "200" sebagai string.
  • Bungkus dalam kelas sendiri untuk production — wrapper class membuat code lebih bersih, menambahkan error handling terpusat, dan memudahkan penggantian ke library HTTP lain di kemudian hari.
  • response.read_body dengan blok untuk download besar — streaming response body ke disk tanpa memuat seluruh file ke memori.

← Sebelumnya: URI   Berikutnya: Tempfile →

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