Faraday

Ruby向けのHTTPクライアントライブラリ。複数のHTTPアダプター(Net::HTTP、Typhoeus等)に対応した統一インターフェース提供。Rackミドルウェア概念を活用したリクエスト・レスポンス処理。高い柔軟性と拡張性により、複雑なHTTP操作に対応。

HTTPクライアントRubyミドルウェアアダプター認証

GitHub概要

lostisland/faraday

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

スター5,867
ウォッチ90
フォーク998
作成日:2009年12月10日
言語:Ruby
ライセンス:MIT License

トピックス

なし

スター履歴

lostisland/faraday Star History
データ取得日時: 2025/10/22 09:54

ライブラリ

Faraday

概要

Faradayは「シンプルで柔軟なHTTPクライアントライブラリ」として開発された、Ruby エコシステムで最も人気のあるHTTPクライアントライブラリです。「複数のバックエンドをサポートするHTTP for Ruby」をコンセプトに、Rack ミドルウェアの概念を取り入れたHTTP リクエスト/レスポンス処理を提供。複数のHTTPアダプター対応、豊富なミドルウェアエコシステム、柔軟な認証システムなど、エンタープライズ級Web API統合に必要な機能を包括的にサポートし、Ruby開発者にとって事実上の標準ライブラリとして地位を確立しています。

詳細

Faraday 2025年版はRuby HTTP通信の決定版として確固たる地位を維持し続けています。15年以上の開発実績により成熟したミドルウェアアーキテクチャと優れた拡張性を誇り、Rails、Sinatra、Hanami等の主要Webフレームワーク環境で広く採用されています。Rack ライクなミドルウェアスタック設計により、HTTPリクエスト/レスポンス処理を柔軟かつ組み立て可能な方法で実装可能。複数アダプター対応、認証システム、リトライ機構、ロギング、キャッシュ機能など企業レベルのHTTP通信要件を満たす豊富な機能を提供します。

主な特徴

  • ミドルウェアアーキテクチャ: Rack風の柔軟で組み立て可能なリクエスト処理
  • 複数アダプター対応: Net::HTTP、Typhoeus、Excon等多様なHTTPライブラリ統合
  • 豊富な認証オプション: Basic、Token、OAuth2、カスタム認証対応
  • 拡張可能なエコシステム: 公式・サードパーティ製ミドルウェアの豊富な選択肢
  • テスト支援機能: スタブ機能とRack::Test統合によるテスタビリティ
  • 抽象化レイヤー: 異なるHTTPライブラリを統一インターフェースで利用

メリット・デメリット

メリット

  • Rubyエコシステムでの圧倒的な普及率と豊富な学習リソース
  • ミドルウェアによる柔軟で組み立て可能なHTTP処理アーキテクチャ
  • 複数アダプター対応による性能・機能要件に応じた最適化
  • 豊富なコミュニティサポートとサードパーティミドルウェア
  • Rails、Sinatra等のWebフレームワークとの優れた統合性
  • 包括的なテスト支援機能による高い開発効率

デメリット

  • ミドルウェアスタックの複雑さによる学習コストの高さ
  • 設定が複雑になりがちで、シンプルな用途には過剰
  • ミドルウェアの順序に依存する処理の理解が必要
  • デバッグ時にミドルウェアチェーンの追跡が困難
  • 軽量な用途では他のHTTPクライアントの方が適切
  • バージョン間でのミドルウェアAPIの変更による移行課題

参考ページ

書き方の例

インストールと基本セットアップ

# Gemfileに追加
gem 'faraday'

# 特定のアダプターを使用する場合
gem 'faraday'
gem 'typhoeus'  # Typhoeusアダプターを使用する場合

# インストール
bundle install

# コマンドラインから直接インストール
gem install faraday

# バージョン確認
require 'faraday'
puts Faraday::VERSION  # 現在のバージョンを表示

基本的なリクエスト(GET/POST/PUT/DELETE)

require 'faraday'

# 最もシンプルなGETリクエスト
response = Faraday.get('https://api.example.com/users')
puts response.status     # 200
puts response.headers    # レスポンスヘッダー
puts response.body       # レスポンスボディ

# コネクションオブジェクトを使った方法
conn = Faraday.new(url: 'https://api.example.com')
response = conn.get('/users')

# クエリパラメータ付きGETリクエスト
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リクエスト(JSON送信)
response = conn.post('/users') do |req|
  req.headers['Content-Type'] = 'application/json'
  req.body = {
    name: '田中太郎',
    email: '[email protected]',
    age: 30
  }.to_json
end

if response.status == 201
  created_user = JSON.parse(response.body)
  puts "ユーザー作成完了: ID=#{created_user['id']}"
else
  puts "エラー: #{response.status} - #{response.body}"
end

# POSTリクエスト(フォームデータ送信)
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リクエスト(データ更新)
response = conn.put('/users/123') do |req|
  req.headers['Content-Type'] = 'application/json'
  req.headers['Authorization'] = 'Bearer your-token'
  req.body = {
    name: '田中次郎',
    email: '[email protected]'
  }.to_json
end

# DELETEリクエスト
response = conn.delete('/users/123') do |req|
  req.headers['Authorization'] = 'Bearer your-token'
end

if response.status == 204
  puts "ユーザー削除完了"
end

# レスポンス詳細情報の確認
puts "ステータス: #{response.status}"
puts "リーズン: #{response.reason_phrase}"
puts "ヘッダー: #{response.headers}"
puts "URL: #{response.env.url}"
puts "HTTPメソッド: #{response.env.method}"

ミドルウェア設定とコネクション構築

require 'faraday'
require 'faraday/net_http'  # デフォルトアダプター

# 基本的なミドルウェア設定
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
  # リクエストミドルウェア
  faraday.request :url_encoded  # POST パラメータの URL エンコーディング
  faraday.request :json         # JSON リクエストボディの自動変換
  
  # レスポンスミドルウェア
  faraday.response :json        # JSON レスポンスの自動パース
  faraday.response :logger      # リクエスト/レスポンスログ出力
  
  # アダプター(最後に指定する必要がある)
  faraday.adapter Faraday.default_adapter
end

# カスタムヘッダーとミドルウェア
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
  # デフォルトヘッダー設定
  faraday.headers['User-Agent'] = 'MyApp/1.0 (Ruby Faraday)'
  faraday.headers['Accept'] = 'application/json'
  faraday.headers['Accept-Language'] = 'ja-JP,en-US'
  
  # リクエストミドルウェア
  faraday.request :json
  faraday.request :authorization, 'Bearer', 'your-token'
  
  # レスポンスミドルウェア
  faraday.response :json, parser_options: { symbolize_names: true }
  faraday.response :raise_error  # 4xx/5xx でエラーを発生
  
  # アダプター選択
  faraday.adapter :net_http
end

# Typhoeusアダプターを使用した高性能設定
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
  faraday.request :json
  faraday.response :json
  faraday.response :logger
  
  # Typhoeusアダプター(高速な並列処理対応)
  faraday.adapter :typhoeus
  
  # タイムアウト設定
  faraday.options.timeout = 10
  faraday.options.open_timeout = 5
end

# カスタムミドルウェア作成例
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

# カスタムミドルウェアの使用
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

# プロキシ設定
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

認証設定(Basic、Token、OAuth2)

require 'faraday'

# Basic認証
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
  faraday.request :authorization, :basic, 'username', 'password'
  faraday.response :json
  faraday.adapter :net_http
end

# または、ヘルパーメソッドを使用
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
  faraday.basic_auth('username', 'password')
  faraday.response :json
  faraday.adapter :net_http
end

# Token認証(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

# または、ヘルパーメソッドを使用
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認証(カスタムヘッダー)
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認証(faraday_middlewareが必要)
# gem 'faraday_middleware' を 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

# 動的認証(トークン更新機能付き)
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

# 使用例
token_provider = lambda { 
  # トークン取得ロジック
  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

# カスタム認証クラス
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

# カスタム認証の使用
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

エラーハンドリングとリトライ機能

require 'faraday'
require 'faraday/retry'  # リトライミドルウェア

# 基本的なエラーハンドリング
def safe_request(conn, path)
  begin
    response = conn.get(path)
    
    # ステータスコードチェック
    case response.status
    when 200..299
      JSON.parse(response.body)
    when 401
      puts "認証エラー: トークンを確認してください"
      nil
    when 403
      puts "権限エラー: アクセス権限がありません"
      nil
    when 404
      puts "見つかりません: リソースが存在しません"
      nil
    when 429
      puts "レート制限: しばらく待ってから再試行してください"
      nil
    when 500..599
      puts "サーバーエラー: #{response.status}"
      nil
    else
      puts "予期しないステータス: #{response.status}"
      nil
    end
    
  rescue Faraday::Error => e
    puts "Faraday エラー: #{e.message}"
    nil
  rescue JSON::ParserError => e
    puts "JSON パースエラー: #{e.message}"
    nil
  rescue StandardError => e
    puts "予期しないエラー: #{e.message}"
    nil
  end
end

# リトライ機能付きコネクション
conn = Faraday.new(url: 'https://api.example.com') do |faraday|
  # リトライミドルウェア設定
  faraday.request :retry, {
    max: 3,                                     # 最大リトライ回数
    interval: 1,                                # 基本待機時間(秒)
    interval_randomness: 0.5,                   # 待機時間のランダム性
    backoff_factor: 2,                          # バックオフ係数
    exceptions: [                               # リトライ対象の例外
      Faraday::Error::TimeoutError,
      Faraday::Error::ConnectionFailed,
      Faraday::Error::SSLError
    ],
    methods: [:get, :post, :put, :delete],      # リトライ対象メソッド
    retry_statuses: [429, 500, 502, 503, 504]  # リトライ対象ステータス
  }
  
  faraday.request :json
  faraday.response :json
  faraday.response :raise_error  # エラーレスポンスで例外発生
  faraday.adapter :net_http
  
  # タイムアウト設定
  faraday.options.timeout = 10
  faraday.options.open_timeout = 5
end

# 使用例
begin
  response = conn.get('/unstable-endpoint')
  puts "成功: #{response.body}"
rescue Faraday::Error => e
  puts "最終的に失敗: #{e.message}"
end

# カスタムリトライミドルウェア
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

# サーキットブレーカーパターン
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

# エラー詳細ログ出力
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

並行処理と非同期リクエスト

require 'faraday'
require 'concurrent'
require 'typhoeus'  # 並列リクエストに最適

# Typhoeusアダプターを使った並列処理
conn = Faraday.new do |faraday|
  faraday.request :json
  faraday.response :json
  faraday.adapter :typhoeus
end

# 並列リクエスト実行
urls = [
  '/users',
  '/posts', 
  '/comments',
  '/categories'
]

# Typhoeusでの並列実行
responses = []
urls.each do |url|
  responses << conn.get(url)
end

# すべてのレスポンスを並列実行
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

# 結果処理
requests.each_with_index do |request, index|
  if request.response.success?
    puts "成功 #{urls[index]}: #{request.response.code}"
  else
    puts "失敗 #{urls[index]}: #{request.response.code}"
  end
end

# Ruby標準の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
  
  # すべての Future の完了を待機
  results = futures.map(&:value)
  
  successful = results.count { |r| r[:success] }
  puts "成功: #{successful}/#{results.length}"
  
  results
end

# 使用例
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)

# ページネーション対応の全データ取得
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 "ページ #{page} でエラー: #{response.status}"
      break
    end
    
    data = response.body
    
    # データの形式に応じて処理
    if data.is_a?(Hash) && data['items']
      items = data['items']
      all_data.concat(items)
      
      # 次のページがない場合は終了
      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} 取得完了: #{data.is_a?(Array) ? data.length : data['items']&.length || 0}件"
    page += 1
    
    # API負荷軽減
    sleep 0.1
  end
  
  puts "総取得件数: #{all_data.length}件"
  all_data
end

# 使用例
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' })

# リクエストキューとバッチ処理
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

# バッチ処理の使用例
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)

# リクエストをキューに追加
(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

# すべてのリクエストを処理
results = queue.process_all
successful = results.count { |r| r[:success] }
puts "バッチ処理完了: #{successful}/#{results.length} 成功"

フレームワーク統合と実用例

require 'faraday'
require 'json'

# Rails統合用のAPIクライアント
class APIClient
  attr_reader :conn

  def initialize(base_url, options = {})
    @base_url = base_url
    @options = options
    
    @conn = Faraday.new(url: base_url) do |faraday|
      # 基本ミドルウェア設定
      faraday.request :json
      faraday.response :json, parser_options: { symbolize_names: true }
      faraday.response :logger if Rails.env.development?
      
      # 認証設定
      if options[:token]
        faraday.token_auth(options[:token])
      elsif options[:api_key]
        faraday.headers['X-API-Key'] = options[:api_key]
      end
      
      # リトライ設定
      faraday.request :retry, max: 3, interval: 1
      
      # アダプター
      faraday.adapter :net_http
      
      # タイムアウト
      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, "認証が必要です"
    when 403
      raise AuthorizationError, "アクセスが拒否されました"
    when 404
      raise NotFoundError, "リソースが見つかりません"
    when 422
      raise ValidationError, response.body
    when 429
      raise RateLimitError, "レート制限に達しました"
    when 500..599
      raise ServerError, "サーバーエラー: #{response.status}"
    else
      raise APIError, "予期しないレスポンス: #{response.status}"
    end
    
  rescue Faraday::Error => e
    raise ConnectionError, "接続エラー: #{e.message}"
  end
end

# カスタム例外クラス
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

# 使用例(Railsコントローラー)
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: 'ユーザーが作成されました'
    rescue ValidationError => e
      flash[: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対応クライアント
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分の余裕
    else
      raise AuthenticationError, "トークン取得失敗: #{response.body}"
    end
  end
end

# ファイルアップロード機能
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分のタイムアウト
    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 "アップロード失敗: #{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 "マルチファイルアップロード失敗: #{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受信処理(Sinatra統合例)
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)
    # 署名検証
    verify_signature(headers, body)
    
    # Webhookデータ解析
    webhook_data = JSON.parse(body)
    
    # イベントタイプに応じた処理
    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 "未対応イベント: #{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)
    # 内部APIに通知
    @api_client.post('/internal/users/sync', user_data)
  end

  def handle_order_completed(order_data)
    # 配送システムに通知
    @api_client.post('/shipping/orders', order_data)
  end
end

# Sinatraルート
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