Faraday
HTTP client library for Ruby. Provides unified interface supporting multiple HTTP adapters (Net::HTTP, Typhoeus, etc.). Request/response processing utilizing Rack middleware concept. Handles complex HTTP operations through high flexibility and extensibility.
GitHub Overview
lostisland/faraday
Simple, but flexible HTTP client library, with support for multiple backends.
Topics
Star History
Library
Faraday
Overview
Faraday is "a simple, but flexible HTTP client library" developed as the most popular HTTP client library in the Ruby ecosystem. With the concept of "HTTP for Ruby with support for multiple backends," it provides HTTP request/response processing incorporating the concept of Rack middleware. Comprehensively supporting features necessary for enterprise-level web API integration such as multiple HTTP adapter support, rich middleware ecosystem, and flexible authentication systems, it has established itself as the de facto standard library for Ruby developers.
Details
Faraday 2025 edition continues to maintain its solid position as the definitive Ruby HTTP communication solution. With over 15 years of development experience, it boasts mature middleware architecture and excellent extensibility, being widely adopted in major web framework environments such as Rails, Sinatra, and Hanami. Designed with Rack-like middleware stack architecture, it enables HTTP request/response processing in a flexible and composable way. It provides rich features that meet enterprise-level HTTP communication requirements including multiple adapter support, authentication systems, retry mechanisms, logging, and caching functionality.
Key Features
- Middleware Architecture: Rack-inspired flexible and composable request processing
- Multiple Adapter Support: Integration with various HTTP libraries like Net::HTTP, Typhoeus, Excon
- Rich Authentication Options: Support for Basic, Token, OAuth2, and custom authentication
- Extensible Ecosystem: Rich selection of official and third-party middleware
- Testing Support: Testability through stub functionality and Rack::Test integration
- Abstraction Layer: Unified interface for different HTTP libraries
Pros and Cons
Pros
- Overwhelming adoption rate in Ruby ecosystem with abundant learning resources
- Flexible and composable HTTP processing architecture through middleware
- Optimization according to performance and feature requirements through multiple adapter support
- Rich community support and third-party middleware
- Excellent integration with web frameworks like Rails and Sinatra
- Comprehensive testing support features for high development efficiency
Cons
- High learning cost due to complexity of middleware stack
- Configuration tends to be complex, excessive for simple use cases
- Need to understand processing dependent on middleware order
- Difficult to trace middleware chain during debugging
- Other HTTP clients are more suitable for lightweight use cases
- Migration challenges due to middleware API changes between versions
Reference Pages
Code Examples
Installation and Basic Setup
# Add to Gemfile
gem 'faraday'
# When using specific adapters
gem 'faraday'
gem 'typhoeus' # When using Typhoeus adapter
# Install
bundle install
# Direct installation from command line
gem install faraday
# Version check
require 'faraday'
puts Faraday::VERSION # Display current version
Basic Requests (GET/POST/PUT/DELETE)
require 'faraday'
# Simplest GET request
response = Faraday.get('https://api.example.com/users')
puts response.status # 200
puts response.headers # Response headers
puts response.body # Response body
# Using connection object
conn = Faraday.new(url: 'https://api.example.com')
response = conn.get('/users')
# GET request with query parameters
response = conn.get('/users', {
page: 1,
limit: 10,
sort: 'created_at'
})
puts response.env.url # https://api.example.com/users?page=1&limit=10&sort=created_at
# POST request (sending JSON)
response = conn.post('/users') do |req|
req.headers['Content-Type'] = 'application/json'
req.body = {
name: 'John Doe',
email: '[email protected]',
age: 30
}.to_json
end
if response.status == 201
created_user = JSON.parse(response.body)
puts "User created: ID=#{created_user['id']}"
else
puts "Error: #{response.status} - #{response.body}"
end
# POST request (sending form data)
response = conn.post('/login') do |req|
req.headers['Content-Type'] = 'application/x-www-form-urlencoded'
req.body = URI.encode_www_form({
username: 'testuser',
password: 'secret123'
})
end
# PUT request (data update)
response = conn.put('/users/123') do |req|
req.headers['Content-Type'] = 'application/json'
req.headers['Authorization'] = 'Bearer your-token'
req.body = {
name: 'Jane Doe',
email: '[email protected]'
}.to_json
end
# DELETE request
response = conn.delete('/users/123') do |req|
req.headers['Authorization'] = 'Bearer your-token'
end
if response.status == 204
puts "User deleted successfully"
end
# Detailed response information inspection
puts "Status: #{response.status}"
puts "Reason: #{response.reason_phrase}"
puts "Headers: #{response.headers}"
puts "URL: #{response.env.url}"
puts "HTTP Method: #{response.env.method}"
Middleware Configuration and Connection Building
require 'faraday'
require 'faraday/net_http' # Default adapter
# Basic middleware configuration
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
# Request middleware
faraday.request :url_encoded # URL encoding for POST parameters
faraday.request :json # Automatic JSON request body conversion
# Response middleware
faraday.response :json # Automatic JSON response parsing
faraday.response :logger # Request/response log output
# Adapter (must be specified last)
faraday.adapter Faraday.default_adapter
end
# Custom headers and middleware
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
# Default header configuration
faraday.headers['User-Agent'] = 'MyApp/1.0 (Ruby Faraday)'
faraday.headers['Accept'] = 'application/json'
faraday.headers['Accept-Language'] = 'en-US,ja-JP'
# Request middleware
faraday.request :json
faraday.request :authorization, 'Bearer', 'your-token'
# Response middleware
faraday.response :json, parser_options: { symbolize_names: true }
faraday.response :raise_error # Raise errors for 4xx/5xx
# Adapter selection
faraday.adapter :net_http
end
# High-performance configuration using Typhoeus adapter
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.request :json
faraday.response :json
faraday.response :logger
# Typhoeus adapter (supports fast parallel processing)
faraday.adapter :typhoeus
# Timeout configuration
faraday.options.timeout = 10
faraday.options.open_timeout = 5
end
# Custom middleware creation example
class RequestIDMiddleware < Faraday::Middleware
def initialize(app, options = {})
super(app)
@prefix = options[:prefix] || 'req'
end
def call(env)
env.request_headers['X-Request-ID'] = "#{@prefix}-#{SecureRandom.uuid}"
@app.call(env)
end
end
# Using custom middleware
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.use RequestIDMiddleware, prefix: 'myapp'
faraday.request :json
faraday.response :json
faraday.adapter :net_http
end
# Proxy configuration
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.proxy = {
uri: 'http://proxy.example.com:8080',
user: 'proxy_user',
password: 'proxy_pass'
}
faraday.request :json
faraday.response :json
faraday.adapter :net_http
end
Authentication Configuration (Basic, Token, OAuth2)
require 'faraday'
# Basic authentication
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.request :authorization, :basic, 'username', 'password'
faraday.response :json
faraday.adapter :net_http
end
# Or, using helper method
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.basic_auth('username', 'password')
faraday.response :json
faraday.adapter :net_http
end
# Token authentication (Bearer Token)
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.request :authorization, :Bearer, 'your-jwt-token'
faraday.response :json
faraday.adapter :net_http
end
# Or, using helper method
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.token_auth('your-jwt-token')
faraday.response :json
faraday.adapter :net_http
end
# API Key authentication (custom header)
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.headers['X-API-Key'] = 'your-api-key'
faraday.response :json
faraday.adapter :net_http
end
# OAuth2 authentication (requires faraday_middleware)
# Add gem 'faraday_middleware' to Gemfile
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.request :oauth2, 'access-token', token_type: :bearer
faraday.response :json
faraday.adapter :net_http
end
# Dynamic authentication (with token refresh capability)
class DynamicAuthMiddleware < Faraday::Middleware
def initialize(app, options = {})
super(app)
@get_token = options[:token_proc]
end
def call(env)
token = @get_token.call
env.request_headers['Authorization'] = "Bearer #{token}"
@app.call(env)
end
end
# Usage example
token_provider = lambda {
# Token acquisition logic
fetch_current_access_token
}
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.use DynamicAuthMiddleware, token_proc: token_provider
faraday.response :json
faraday.adapter :net_http
end
# Custom authentication class
class APIKeyAuth < Faraday::Middleware
def initialize(app, api_key, header_name = 'X-API-Key')
super(app)
@api_key = api_key
@header_name = header_name
end
def call(env)
env.request_headers[@header_name] = @api_key
@app.call(env)
end
end
# Using custom authentication
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.use APIKeyAuth, 'your-api-key-here', 'X-Custom-API-Key'
faraday.response :json
faraday.adapter :net_http
end
Error Handling and Retry Functionality
require 'faraday'
require 'faraday/retry' # Retry middleware
# Basic error handling
def safe_request(conn, path)
begin
response = conn.get(path)
# Status code check
case response.status
when 200..299
JSON.parse(response.body)
when 401
puts "Authentication error: Please check your token"
nil
when 403
puts "Permission error: Access denied"
nil
when 404
puts "Not found: Resource does not exist"
nil
when 429
puts "Rate limit: Please wait before retrying"
nil
when 500..599
puts "Server error: #{response.status}"
nil
else
puts "Unexpected status: #{response.status}"
nil
end
rescue Faraday::Error => e
puts "Faraday error: #{e.message}"
nil
rescue JSON::ParserError => e
puts "JSON parse error: #{e.message}"
nil
rescue StandardError => e
puts "Unexpected error: #{e.message}"
nil
end
end
# Connection with retry functionality
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
# Retry middleware configuration
faraday.request :retry, {
max: 3, # Maximum retry count
interval: 1, # Basic wait time (seconds)
interval_randomness: 0.5, # Wait time randomness
backoff_factor: 2, # Backoff factor
exceptions: [ # Exceptions to retry
Faraday::Error::TimeoutError,
Faraday::Error::ConnectionFailed,
Faraday::Error::SSLError
],
methods: [:get, :post, :put, :delete], # Methods to retry
retry_statuses: [429, 500, 502, 503, 504] # Status codes to retry
}
faraday.request :json
faraday.response :json
faraday.response :raise_error # Raise exceptions for error responses
faraday.adapter :net_http
# Timeout configuration
faraday.options.timeout = 10
faraday.options.open_timeout = 5
end
# Usage example
begin
response = conn.get('/unstable-endpoint')
puts "Success: #{response.body}"
rescue Faraday::Error => e
puts "Finally failed: #{e.message}"
end
# Custom retry middleware
class CustomRetryMiddleware < Faraday::Middleware
def initialize(app, options = {})
super(app)
@max_retries = options[:max_retries] || 3
@delay = options[:delay] || 1
end
def call(env)
retries = 0
begin
@app.call(env)
rescue StandardError => e
if retries < @max_retries && retryable_error?(e)
retries += 1
sleep(@delay * retries)
retry
else
raise
end
end
end
private
def retryable_error?(error)
error.is_a?(Faraday::Error::TimeoutError) ||
error.is_a?(Faraday::Error::ConnectionFailed)
end
end
# Circuit breaker pattern
class CircuitBreakerMiddleware < Faraday::Middleware
def initialize(app, options = {})
super(app)
@failure_threshold = options[:failure_threshold] || 5
@recovery_timeout = options[:recovery_timeout] || 60
@failure_count = 0
@last_failure_time = nil
@state = :closed # :closed, :open, :half_open
end
def call(env)
case @state
when :open
if Time.now - @last_failure_time > @recovery_timeout
@state = :half_open
else
raise Faraday::Error::ConnectionFailed, "Circuit breaker is open"
end
end
begin
response = @app.call(env)
if @state == :half_open
@state = :closed
@failure_count = 0
end
response
rescue StandardError => e
@failure_count += 1
@last_failure_time = Time.now
if @failure_count >= @failure_threshold
@state = :open
end
raise
end
end
end
# Detailed error logging
class DetailedLoggerMiddleware < Faraday::Middleware
def call(env)
start_time = Time.now
puts "=== Request ==="
puts "#{env.method.upcase} #{env.url}"
puts "Headers: #{env.request_headers}"
puts "Body: #{env.body}" if env.body
begin
response = @app.call(env)
duration = Time.now - start_time
puts "=== Response ==="
puts "Status: #{response.status}"
puts "Duration: #{duration.round(3)}s"
puts "Headers: #{response.headers}"
puts "Body: #{response.body[0..500]}#{'...' if response.body.length > 500}"
response
rescue StandardError => e
duration = Time.now - start_time
puts "=== Error ==="
puts "Duration: #{duration.round(3)}s"
puts "Error: #{e.class.name} - #{e.message}"
raise
end
end
end
Concurrent Processing and Asynchronous Requests
require 'faraday'
require 'concurrent'
require 'typhoeus' # Optimal for parallel requests
# Parallel processing using Typhoeus adapter
conn = Faraday.new do |faraday|
faraday.request :json
faraday.response :json
faraday.adapter :typhoeus
end
# Parallel request execution
urls = [
'/users',
'/posts',
'/comments',
'/categories'
]
# Parallel execution with Typhoeus
responses = []
urls.each do |url|
responses << conn.get(url)
end
# Execute all responses in parallel
hydra = Typhoeus::Hydra.new
requests = urls.map do |url|
request = Typhoeus::Request.new("https://api.example.com#{url}")
hydra.queue(request)
request
end
hydra.run
# Process results
requests.each_with_index do |request, index|
if request.response.success?
puts "Success #{urls[index]}: #{request.response.code}"
else
puts "Failed #{urls[index]}: #{request.response.code}"
end
end
# Parallel processing using Ruby standard Thread
def parallel_requests(conn, paths, max_threads = 5)
results = Concurrent::Array.new
semaphore = Concurrent::Semaphore.new(max_threads)
futures = paths.map do |path|
Concurrent::Future.execute do
semaphore.acquire
begin
response = conn.get(path)
{
path: path,
status: response.status,
body: response.body,
success: response.success?
}
rescue StandardError => e
{
path: path,
error: e.message,
success: false
}
ensure
semaphore.release
end
end
end
# Wait for all Futures to complete
results = futures.map(&:value)
successful = results.count { |r| r[:success] }
puts "Success: #{successful}/#{results.length}"
results
end
# Usage example
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.request :json
faraday.response :json
faraday.adapter :net_http
faraday.options.timeout = 10
end
paths = %w[/users /posts /comments /categories /tags]
results = parallel_requests(conn, paths)
# Pagination-aware full data fetching
def fetch_all_pages(conn, base_path, params = {})
all_data = []
page = 1
per_page = params[:per_page] || 100
loop do
current_params = params.merge(page: page, per_page: per_page)
response = conn.get(base_path, current_params)
unless response.success?
puts "Error on page #{page}: #{response.status}"
break
end
data = response.body
# Process according to data format
if data.is_a?(Hash) && data['items']
items = data['items']
all_data.concat(items)
# End if no next page
break if items.empty? || !data['has_more']
elsif data.is_a?(Array)
all_data.concat(data)
break if data.empty?
else
break
end
puts "Page #{page} completed: #{data.is_a?(Array) ? data.length : data['items']&.length || 0} items"
page += 1
# Reduce API load
sleep 0.1
end
puts "Total items fetched: #{all_data.length}"
all_data
end
# Usage example
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.token_auth('your-token')
faraday.request :json
faraday.response :json
faraday.adapter :net_http
end
all_posts = fetch_all_pages(conn, '/posts', { sort: 'created_at' })
# Request queue and batch processing
class RequestQueue
def initialize(conn, max_concurrent: 5, delay: 0.1)
@conn = conn
@queue = Queue.new
@max_concurrent = max_concurrent
@delay = delay
@results = Concurrent::Array.new
end
def add_request(method, path, options = {})
@queue << { method: method, path: path, options: options }
end
def process_all
workers = @max_concurrent.times.map do
Thread.new do
while (request = @queue.pop(true) rescue nil)
process_request(request)
sleep @delay
end
end
end
workers.each(&:join)
@results.to_a
end
private
def process_request(request)
begin
response = @conn.send(
request[:method],
request[:path],
request[:options]
)
@results << {
request: request,
status: response.status,
body: response.body,
success: response.success?
}
rescue StandardError => e
@results << {
request: request,
error: e.message,
success: false
}
end
end
end
# Batch processing usage example
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
faraday.token_auth('your-token')
faraday.request :json
faraday.response :json
faraday.adapter :net_http
end
queue = RequestQueue.new(conn, max_concurrent: 3, delay: 0.2)
# Add requests to queue
(1..10).each do |i|
queue.add_request(:get, "/users/#{i}")
end
(1..5).each do |i|
queue.add_request(:post, "/posts", {
title: "Post #{i}",
content: "Content for post #{i}"
})
end
# Process all requests
results = queue.process_all
successful = results.count { |r| r[:success] }
puts "Batch processing completed: #{successful}/#{results.length} successful"
Framework Integration and Practical Examples
require 'faraday'
require 'json'
# API client for Rails integration
class APIClient
attr_reader :conn
def initialize(base_url, options = {})
@base_url = base_url
@options = options
@conn = Faraday.new(url: base_url) do |faraday|
# Basic middleware configuration
faraday.request :json
faraday.response :json, parser_options: { symbolize_names: true }
faraday.response :logger if Rails.env.development?
# Authentication configuration
if options[:token]
faraday.token_auth(options[:token])
elsif options[:api_key]
faraday.headers['X-API-Key'] = options[:api_key]
end
# Retry configuration
faraday.request :retry, max: 3, interval: 1
# Adapter
faraday.adapter :net_http
# Timeout
faraday.options.timeout = options[:timeout] || 30
faraday.options.open_timeout = options[:open_timeout] || 10
end
end
def get(path, params = {})
handle_response { @conn.get(path, params) }
end
def post(path, data = {})
handle_response { @conn.post(path, data) }
end
def put(path, data = {})
handle_response { @conn.put(path, data) }
end
def delete(path)
handle_response { @conn.delete(path) }
end
private
def handle_response
response = yield
case response.status
when 200..299
response.body
when 401
raise AuthenticationError, "Authentication required"
when 403
raise AuthorizationError, "Access denied"
when 404
raise NotFoundError, "Resource not found"
when 422
raise ValidationError, response.body
when 429
raise RateLimitError, "Rate limit reached"
when 500..599
raise ServerError, "Server error: #{response.status}"
else
raise APIError, "Unexpected response: #{response.status}"
end
rescue Faraday::Error => e
raise ConnectionError, "Connection error: #{e.message}"
end
end
# Custom exception classes
class APIError < StandardError; end
class AuthenticationError < APIError; end
class AuthorizationError < APIError; end
class NotFoundError < APIError; end
class ValidationError < APIError; end
class RateLimitError < APIError; end
class ServerError < APIError; end
class ConnectionError < APIError; end
# Usage example (Rails controller)
class UsersController < ApplicationController
before_action :setup_api_client
def index
begin
@users = @api_client.get('/users', params: {
page: params[:page] || 1,
per_page: 20
})
rescue APIError => e
flash[:error] = e.message
@users = []
end
end
def create
begin
@user = @api_client.post('/users', user_params)
redirect_to user_path(@user[:id]), notice: 'User created successfully'
rescue ValidationError => e
flash[:error] = "Validation error: #{e.message}"
render :new
rescue APIError => e
flash[:error] = e.message
render :new
end
end
private
def setup_api_client
@api_client = APIClient.new(
Rails.application.credentials.api_base_url,
token: current_user&.api_token
)
end
def user_params
params.require(:user).permit(:name, :email, :role)
end
end
# OAuth2-compatible client
class OAuth2APIClient
def initialize(client_id, client_secret, base_url)
@client_id = client_id
@client_secret = client_secret
@base_url = base_url
@access_token = nil
@token_expires_at = nil
setup_connection
end
def request(method, path, data = nil)
ensure_valid_token
case method.to_sym
when :get
@conn.get(path, data)
when :post
@conn.post(path, data)
when :put
@conn.put(path, data)
when :delete
@conn.delete(path)
end
end
private
def setup_connection
@conn = Faraday.new(url: @base_url) do |faraday|
faraday.request :json
faraday.response :json
faraday.adapter :net_http
end
end
def ensure_valid_token
if token_expired?
refresh_access_token
end
@conn.headers['Authorization'] = "Bearer #{@access_token}"
end
def token_expired?
@access_token.nil? ||
@token_expires_at.nil? ||
Time.now >= @token_expires_at
end
def refresh_access_token
auth_conn = Faraday.new do |faraday|
faraday.request :url_encoded
faraday.response :json
faraday.adapter :net_http
end
response = auth_conn.post("#{@base_url}/oauth/token", {
grant_type: 'client_credentials',
client_id: @client_id,
client_secret: @client_secret
})
if response.success?
token_data = response.body
@access_token = token_data['access_token']
expires_in = token_data['expires_in'] || 3600
@token_expires_at = Time.now + expires_in - 300 # 5-minute buffer
else
raise AuthenticationError, "Token acquisition failed: #{response.body}"
end
end
end
# File upload functionality
class FileUploadClient
def initialize(base_url, token)
@conn = Faraday.new(url: base_url) do |faraday|
faraday.token_auth(token)
faraday.request :multipart
faraday.response :json
faraday.adapter :net_http
faraday.options.timeout = 300 # 5-minute timeout
end
end
def upload_file(file_path, additional_fields = {})
file_upload = Faraday::UploadIO.new(file_path, mime_type(file_path))
payload = additional_fields.merge(file: file_upload)
response = @conn.post('/upload', payload)
if response.success?
response.body
else
raise "Upload failed: #{response.status} - #{response.body}"
end
end
def upload_multiple_files(file_paths, additional_fields = {})
files = file_paths.map do |path|
Faraday::UploadIO.new(path, mime_type(path))
end
payload = additional_fields.merge(files: files)
response = @conn.post('/upload/multiple', payload)
if response.success?
response.body
else
raise "Multi-file upload failed: #{response.status}"
end
end
private
def mime_type(file_path)
case File.extname(file_path).downcase
when '.jpg', '.jpeg'
'image/jpeg'
when '.png'
'image/png'
when '.pdf'
'application/pdf'
when '.txt'
'text/plain'
else
'application/octet-stream'
end
end
end
# WebHook reception processing (Sinatra integration example)
require 'sinatra'
require 'json'
class WebhookHandler
def initialize(webhook_secret, api_client)
@webhook_secret = webhook_secret
@api_client = api_client
end
def process(headers, body)
# Signature verification
verify_signature(headers, body)
# Webhook data analysis
webhook_data = JSON.parse(body)
# Processing according to event type
case webhook_data['event']
when 'user.created'
handle_user_created(webhook_data['data'])
when 'order.completed'
handle_order_completed(webhook_data['data'])
else
Rails.logger.info "Unsupported event: #{webhook_data['event']}"
end
rescue JSON::ParserError => e
raise "Invalid JSON: #{e.message}"
rescue SignatureError => e
raise "Signature verification failed: #{e.message}"
end
private
def verify_signature(headers, body)
expected_signature = headers['X-Webhook-Signature']
actual_signature = OpenSSL::HMAC.hexdigest(
'sha256',
@webhook_secret,
body
)
unless Rack::Utils.secure_compare(expected_signature, actual_signature)
raise SignatureError, "Invalid signature"
end
end
def handle_user_created(user_data)
# Notify internal API
@api_client.post('/internal/users/sync', user_data)
end
def handle_order_completed(order_data)
# Notify shipping system
@api_client.post('/shipping/orders', order_data)
end
end
# Sinatra route
post '/webhook' do
handler = WebhookHandler.new(
ENV['WEBHOOK_SECRET'],
APIClient.new(ENV['INTERNAL_API_URL'], token: ENV['INTERNAL_API_TOKEN'])
)
begin
handler.process(request.env, request.body.read)
status 200
{ status: 'processed' }.to_json
rescue => e
status 400
{ error: e.message }.to_json
end
end