RubyでHTTP通信して何か情報を取ってくるときには、だいたい Faradayを使うと楽だ。
- 公式ドキュメント https://lostisland.github.io/faraday/#/
- Faradayの使い方 59のレシピhttps://nekorails.hatenablog.com/entry/2018/09/28/152745
あたりを見れば大体の使い方はわかる。かりに、使い方が分からなくてもソースコードを読めばなんとなく動きはわかる。
まえおき
CakePHPで作られた古い社内サービスをスクレイピングするスクリプトを組んでたんだけど、何か知らないけど、ときどきUser/Passを入れ直せと、ログイン画面に飛ばされてしまってた。
- Authorizationヘッダーじゃなくて、クッキーで認証管理されてる
- 再認証のときはHTTP 401ではなく302でログイン画面に飛ばされるだけ
ということで、Faradayの標準の仕組みではどうにもならなさそう、ということで調べた。
faraday-retryは??
Faradayには、通信エラーのときにリトライするのをうまくやる仕組みがある。
ただ、このfaraday-retryは、基本的にはFaradayの例外をキャッチして、それに対してリトライをかける仕組みだ。302はFaradayの例外ではないので、標準ではリトライがかからない。
無理やり以下のようにすればリトライすることはできるが、ユーザID/パスワードで認証するロジックがあっちにもこっちも状態(初回の認証時と、再認証時で異なる場所に実装される状態)になるので、なんだか微妙だ。もっといい方法はないだろうか?
retry_options = {
retry_statuses: [302],
methods: [],
retry_if: ->(env, _exception) {
# 302でログインページに飛ばされていたらtrueを返す
},
retry_block: ->(env) {
# 再ログイン処理
},
}
自前でFaradayミドルウェアを書いてみる
Faradayは、Rackを真似たようなミドルウェアの仕組みがある。
https://lostisland.github.io/faraday/#/middleware/index?id=how-it-works
class HogeMiddleware < Faraday::Middleware
def initialize(app, options={})
@app = app
end
def call(env)
# 通信前に何かする
response = @app.call(env)
# 通信後に何かする
response
end
end
今回のようなケースでは、認証機能、再認証機能を全部おまかせするようなミドルウェアを書いてしまえばいい。
class MyAuthMiddleware < Faraday::Middleware
def initialize(app, options={})
@app = app
@options = options
fetch_token!
end
LOGIN_URL = 'https://xxx.example.com/login'.freeze
def call(env)
env.request_headers['cookies'] = "hogehoge_session=#{@token}"
response = @app.call(env)
if response.status == 302 && response.headers['location'] == LOGIN_URL
# 認証トークンを取得し直して1回だけリトライ
fetch_token!
env.request_headers['cookies'] = "hogehoge_session=#{@token}"
response = @app.call(env)
end
response
end
private
def fetch_token!
@token = LoginProcess.new(@options[:user], @options[:password]).call
end
end
Faraday::Request.register_middleware(myauth: MyAuthMiddleware)
そうすれば、スクレイピング部分からは認証ロジックをほぼ追い出すことができて、
@connection = Faraday.new do |faraday|
faraday.adapter Faraday.default_adapter
faraday.request :myauth, user: ENV['MY_USERNAME'], password: ENV['MY_PASSWORD']
...
end
こんなかんじでメインの通信をおこなうFaradayコネクションを定義すればおk。
まとめ
通信エラーのリトライくらいならfaraday-retryで十分だが、認証・再認証あたりをする場合には、むりにfaraday-retryで頑張らず、本記事で紹介したように自前でミドルウェアを書いてみるとよい。
そんなに大変ではないし、それなりに役立つと思う。