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 CloseGET 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_NONEdi 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 amanJika kamu mendapat
OpenSSL::SSL::SSLErrordi 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.startdengan 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 timeout —
open_timeoutuntuk batas waktu koneksi TCP,read_timeoutuntuk batas waktu menunggu response; tanpa ini aplikasimu bisa hang selamanya.- Reuse koneksi untuk request berturut-turut — buka satu
Net::HTTP.startdan lakukan banyak request di dalamnya; jauh lebih efisien dari membuka koneksi baru setiap kali.- Jangan matikan verifikasi SSL —
VERIFY_NONEhanya boleh untuk debugging lokal dengan certificate self-signed, tidak pernah di production.- Tangani response berdasarkan class — gunakan
when Net::HTTPSuccess,when Net::HTTPNotFounddan seterusnya, bukan mengecekresponse.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_bodydengan blok untuk download besar — streaming response body ke disk tanpa memuat seluruh file ke memori.