Net::HTTP & HTTP Clients
Master Ruby's HTTP capabilities with Net::HTTP, modern HTTP client gems, and best practices for consuming REST APIs and web services.
HTTP in Ruby Overview
Ruby provides multiple options for HTTP communication, from the built-in Net::HTTP library to modern gems like HTTParty, Faraday, and RestClient.
Built-in Options
- Net::HTTP: Standard library, full-featured but verbose
- URI: URL parsing and basic HTTP operations
- Open-URI: Simple interface for reading URLs
Popular HTTP Gems
- HTTParty: Simple and intuitive API
- Faraday: Flexible middleware architecture
- RestClient: Simple REST client
- Typhoeus: High-performance with libcurl
Net::HTTP Fundamentals
Basic GET Request
require 'net/http'
require 'uri'
require 'json'
# Simple GET request
uri = URI('https://api.github.com/users/octocat')
response = Net::HTTP.get_response(uri)
puts response.code # "200"
puts response.message # "OK"
puts response.body # JSON response
# Parse JSON response
if response.code == '200'
user_data = JSON.parse(response.body)
puts "User: #{user_data['name']}"
puts "Followers: #{user_data['followers']}"
end
More Control with Net::HTTP.start
uri = URI('https://api.github.com/users/octocat')
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
request = Net::HTTP::Get.new(uri)
request['User-Agent'] = 'MyApp/1.0'
request['Accept'] = 'application/json'
response = http.request(request)
case response
when Net::HTTPSuccess
puts "Success: #{response.body}"
when Net::HTTPRedirection
puts "Redirected to: #{response['location']}"
else
puts "Error: #{response.code} #{response.message}"
end
end
HTTP Methods with Net::HTTP
POST Request with JSON
uri = URI('https://jsonplaceholder.typicode.com/posts')
# Prepare data
post_data = {
title: 'My New Post',
body: 'This is the post content',
userId: 1
}
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request['Accept'] = 'application/json'
request.body = post_data.to_json
response = http.request(request)
if response.code == '201'
created_post = JSON.parse(response.body)
puts "Created post with ID: #{created_post['id']}"
else
puts "Failed to create post: #{response.code}"
end
end
PUT Request for Updates
uri = URI('https://jsonplaceholder.typicode.com/posts/1')
update_data = {
id: 1,
title: 'Updated Post Title',
body: 'Updated post content',
userId: 1
}
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
request = Net::HTTP::Put.new(uri)
request['Content-Type'] = 'application/json'
request.body = update_data.to_json
response = http.request(request)
puts "Update response: #{response.code}"
end
DELETE Request
uri = URI('https://jsonplaceholder.typicode.com/posts/1')
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
request = Net::HTTP::Delete.new(uri)
response = http.request(request)
case response.code
when '200', '204'
puts "Successfully deleted"
when '404'
puts "Resource not found"
else
puts "Failed to delete: #{response.code}"
end
end
Modern HTTP Client Gems
HTTParty - Simple and Elegant
# Gemfile
gem 'httparty'
# Usage
require 'httparty'
class GitHubAPI
include HTTParty
base_uri 'https://api.github.com'
def self.user(username)
get("/users/#{username}")
end
def self.user_repos(username)
get("/users/#{username}/repos")
end
end
# Using the API
user = GitHubAPI.user('octocat')
if user.success?
puts "User: #{user['name']}"
puts "Followers: #{user['followers']}"
end
# With custom headers and options
response = HTTParty.get(
'https://api.github.com/user',
headers: {
'Authorization' => 'token YOUR_TOKEN',
'User-Agent' => 'MyApp/1.0'
},
timeout: 10
)
Faraday - Flexible Middleware
# Gemfile
gem 'faraday'
gem 'faraday-retry'
require 'faraday'
require 'faraday/retry'
# Basic usage
conn = Faraday.new(url: 'https://api.github.com') do |f|
f.request :json # encode req bodies as JSON
f.response :json # decode response bodies as JSON
f.response :retry, max: 3, interval: 0.5
f.response :logger # log requests and responses
f.adapter Faraday.default_adapter
end
response = conn.get('/users/octocat')
puts response.body['name']
# Advanced configuration
client = Faraday.new do |f|
f.request :authorization, 'Bearer', 'your_token'
f.request :json
f.response :json
f.response :raise_error # raise exception for 4xx/5xx
f.adapter :typhoeus # use Typhoeus adapter for performance
end
# POST with Faraday
response = client.post('/posts') do |req|
req.body = { title: 'New Post', content: 'Post content' }
end
RestClient - Simple REST Operations
# Gemfile
gem 'rest-client'
require 'rest-client'
require 'json'
# Simple GET
response = RestClient.get('https://api.github.com/users/octocat')
user = JSON.parse(response.body)
# POST with JSON
RestClient.post(
'https://api.example.com/posts',
{ title: 'New Post', content: 'Content' }.to_json,
{ content_type: :json, accept: :json }
)
# With authentication
RestClient.get(
'https://api.github.com/user',
{ Authorization: 'token YOUR_TOKEN' }
)
# Error handling
begin
response = RestClient.get('https://api.example.com/resource')
rescue RestClient::ExceptionWithResponse => e
case e.http_code
when 404
puts "Resource not found"
when 401
puts "Unauthorized"
else
puts "HTTP Error: #{e.http_code}"
end
end
Authentication Methods
Basic Authentication
# Net::HTTP Basic Auth
uri = URI('https://api.example.com/protected')
req = Net::HTTP::Get.new(uri)
req.basic_auth('username', 'password')
# HTTParty Basic Auth
HTTParty.get(
'https://api.example.com/protected',
basic_auth: { username: 'user', password: 'pass' }
)
# Faraday Basic Auth
conn = Faraday.new do |f|
f.request :authorization, :basic, 'username', 'password'
end
Bearer Token Authentication
# Net::HTTP Bearer Token
req['Authorization'] = "Bearer #{access_token}"
# HTTParty Bearer Token
HTTParty.get(
'https://api.example.com/user',
headers: { 'Authorization' => "Bearer #{access_token}" }
)
# Faraday Bearer Token
conn = Faraday.new do |f|
f.request :authorization, 'Bearer', access_token
end
API Key Authentication
# Query parameter API key
HTTParty.get(
'https://api.example.com/data',
query: { api_key: 'your_api_key' }
)
# Header-based API key
HTTParty.get(
'https://api.example.com/data',
headers: { 'X-API-Key' => 'your_api_key' }
)
Error Handling & Resilience
Comprehensive Error Handling
require 'net/http'
require 'timeout'
def make_request(url, retries: 3)
uri = URI(url)
retries.times do |attempt|
begin
response = Timeout::timeout(10) do
Net::HTTP.get_response(uri)
end
case response
when Net::HTTPSuccess
return JSON.parse(response.body)
when Net::HTTPRedirection
location = response['location']
return make_request(location, retries: retries - attempt - 1)
when Net::HTTPClientError
if response.code == '429' # Rate limited
sleep(2 ** attempt) # Exponential backoff
next
else
raise "Client error: #{response.code} #{response.message}"
end
when Net::HTTPServerError
if attempt < retries - 1
sleep(2 ** attempt)
next
else
raise "Server error: #{response.code} #{response.message}"
end
end
rescue Timeout::Error
puts "Request timed out, attempt #{attempt + 1}"
raise if attempt == retries - 1
rescue SocketError => e
puts "Network error: #{e.message}"
raise if attempt == retries - 1
rescue JSON::ParserError => e
puts "Invalid JSON response: #{e.message}"
return nil
end
end
end
# Usage
begin
data = make_request('https://api.example.com/data')
puts "Received data: #{data}"
rescue => e
puts "Failed to fetch data: #{e.message}"
end
Rate Limiting & Backoff
class APIClient
include HTTParty
base_uri 'https://api.example.com'
def self.with_rate_limiting(path, options = {})
max_retries = options.delete(:max_retries) || 3
max_retries.times do |attempt|
response = get(path, options)
case response.code
when 200..299
return response
when 429
# Check rate limit headers
retry_after = response.headers['retry-after']&.to_i || (2 ** attempt)
puts "Rate limited. Waiting #{retry_after} seconds..."
sleep(retry_after)
when 500..599
# Server error - exponential backoff
if attempt < max_retries - 1
wait_time = 2 ** attempt
puts "Server error. Retrying in #{wait_time} seconds..."
sleep(wait_time)
else
raise "Server error after #{max_retries} attempts"
end
else
raise "HTTP Error: #{response.code}"
end
end
end
end
# Usage
response = APIClient.with_rate_limiting('/users', max_retries: 5)
Testing HTTP Clients
WebMock for HTTP Testing
# Gemfile
gem 'webmock', group: :test
# spec/spec_helper.rb
require 'webmock/rspec'
WebMock.disable_net_connect!(allow_localhost: true)
# Testing with RSpec
RSpec.describe GitHubAPI do
describe '.user' do
it 'fetches user information' do
user_data = {
login: 'octocat',
name: 'The Octocat',
followers: 1000
}
stub_request(:get, 'https://api.github.com/users/octocat')
.with(headers: { 'Accept' => '*/*' })
.to_return(
status: 200,
body: user_data.to_json,
headers: { 'Content-Type' => 'application/json' }
)
user = GitHubAPI.user('octocat')
expect(user['name']).to eq('The Octocat')
expect(user['followers']).to eq(1000)
end
it 'handles API errors gracefully' do
stub_request(:get, 'https://api.github.com/users/nonexistent')
.to_return(status: 404, body: '{"message": "Not Found"}')
expect {
GitHubAPI.user('nonexistent')
}.to raise_error(HTTParty::ResponseError)
end
end
end
VCR for Recording Real Requests
# Gemfile
gem 'vcr', group: :test
# spec/spec_helper.rb
require 'vcr'
VCR.configure do |config|
config.cassette_library_dir = 'spec/cassettes'
config.hook_into :webmock
config.configure_rspec_metadata!
# Filter sensitive data
config.filter_sensitive_data('') { ENV['API_TOKEN'] }
end
# Test with VCR
RSpec.describe GitHubAPI do
it 'fetches real user data', :vcr do
user = GitHubAPI.user('octocat')
expect(user['login']).to eq('octocat')
end
# Use custom cassette
it 'handles rate limiting' do
VCR.use_cassette('github_rate_limit') do
response = GitHubAPI.user('octocat')
expect(response).to be_success
end
end
end
Performance & Best Practices
Connection Reuse
- Keep-Alive: Use persistent connections for multiple requests
- Connection Pooling: Share connections across threads
- HTTP/2: Leverage multiplexing when available
- SSL Session Reuse: Avoid handshake overhead
Timeout Configuration
# Net::HTTP timeouts
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.open_timeout = 5 # Connection timeout
http.read_timeout = 30 # Read timeout
http.write_timeout = 10 # Write timeout (Ruby 2.6+)
# HTTParty timeouts
HTTParty.get(url, timeout: 30)
# Faraday timeouts
conn = Faraday.new do |f|
f.options.timeout = 30 # read timeout
f.options.open_timeout = 5 # connection timeout
end
Parallel Requests
require 'concurrent-ruby'
# Parallel requests with Concurrent Ruby
urls = %w[
https://api.github.com/users/rails
https://api.github.com/users/ruby
https://api.github.com/users/sinatra
]
futures = urls.map do |url|
Concurrent::Future.execute do
HTTParty.get(url)
end
end
# Wait for all requests to complete
results = futures.map(&:value)
# Using Typhoeus for parallel requests
# gem 'typhoeus'
require 'typhoeus'
hydra = Typhoeus::Hydra.new
requests = urls.map do |url|
Typhoeus::Request.new(url)
end
requests.each { |req| hydra.queue(req) }
hydra.run
responses = requests.map(&:response)
Security Best Practices
- HTTPS Only: Always use SSL/TLS for sensitive data
- Certificate Verification: Don't disable SSL verification
- Token Security: Store API keys securely, use environment variables
- Request Validation: Validate and sanitize all input data
- Rate Limiting: Respect API rate limits and implement backoff