Ruby Logo

Net::HTTP & HTTP Clients

Master HTTP requests and REST API consumption using Net::HTTP and popular Ruby HTTP client libraries like HTTParty, Faraday, and RestClient.

Home Ruby Net::HTTP & HTTP Clients

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

Quick Navigation

Related Topics

Video Tutorial

Watch and learn net::http & http clients

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!