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.

HTTP ClientRubyMiddlewareAdapterAuthentication

GitHub Overview

lostisland/faraday

Simple, but flexible HTTP client library, with support for multiple backends.

Stars5,867
Watchers90
Forks998
Created:December 10, 2009
Language:Ruby
License:MIT License

Topics

None

Star History

lostisland/faraday Star History
Data as of: 10/22/2025, 09:54 AM

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