Faraday
Ruby向けのHTTPクライアントライブラリ。複数のHTTPアダプター(Net::HTTP、Typhoeus等)に対応した統一インターフェース提供。Rackミドルウェア概念を活用したリクエスト・レスポンス処理。高い柔軟性と拡張性により、複雑なHTTP操作に対応。
GitHub概要
lostisland/faraday
Simple, but flexible HTTP client library, with support for multiple backends.
トピックス
スター履歴
ライブラリ
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