[Rails] RESTfulAPIをORMするgem “her” の利用方法(1)

モバイルアプリサービス部@モバイルバックエンドグループの五十嵐です。

これから何回かに分けてherの使い方を書いていこうと思います。

概要

herはActiveRecordのように振る舞うRESTfulAPIのORMです。

今回は、herを使ってOAuth2.0のClientCredentialsGrantのアクセストークンを利用するAPIにアクセスしてみます。

環境

  • Ruby: 2.2.4p230
  • Rails: 4.2.5.1
  • Gems:
    • her (0.8.1)
    • webmock (1.22.6)
    • rspec-rails (3.4.2)

Step.1 アクセスの都度トークンを取得する

まずは、APIにアクセスする都度アクセストークンをリクエストして取得し、取得したアクセストークンをAPIのリクエストヘッダーにセットするようにします。

アクセストークンのモデルを作成する

ClientCredentialsGrantでアクセストークンを取得するAPIを実行するモデルを作成します。FaradayでOauthサーバのアクセストークン発行エンドポイントにBasic認証付きでPOSTリクエストするようにします。

# app/models/access_token.rb
require 'base64'

class AccessToken
  def self.get
    client = Faraday.new 'OauthサーバのURI' do |b|
      b.adapter Faraday.default_adapter
      b.authorization :Basic, Base64.encode64('client_id:client_secret')
      b.response :json
    end

    response = client.post 'アクセストークン発行するエンドポイント'
    response.body['access_token']
  end
end

AccessToken.get # => 'access_token'

アクセストークンを取得するfaradayのミドルウェアを作成する

APIリクエストのHTTPヘッダーに Authorization: Bearer access_token を付与するミドルウェアを作成します。 lib/ ディレクトリ内はデフォルトではautoloadされないので、autoloadパスに追加しましょう。

# lib/faraday/client_credential.rb
class ClientCredential < Faraday::Middleware
  def call(env)
    env[:request_headers]['Authorization'] = "Bearer #{AccessToken.get}"
    @app.call(env)
  end
end

アクセストークンを取得するのミドルウェアを追加する

作成したミドルウェアを Her::API にデフォルトの設定として設定することで、herを使ってAPIアクセスする度にアクセストークンを取得し、リクエストヘッダーにアクセストークンを付与することができます。

# config/initializers/her.rb
Her::API.setup url: Settings.custom.server do |c|
  # Request
  c.use ClientCredential
  ...

  # Response
  ...

  # Adapter
  ...
end

確認

ここまでできたら、リクエストログなどからリクエストヘッダーにアクセストークンがセットされていることを確認してみましょう。

Step.2 トークンの有効期限が切れたらトークンを再取得する

トークンリクエストの都度、新しいアクセストークンが発行される場合はStep.1まで問題ありませんが、されない場合はAPIのリクエスト時にアクセストークンが期限切れになる可能性があります。Step.2では、アクセストークンの有効期限が切れた場合(401エラーが返された場合)、再度アクセストークンを取得し、APIリクエストをリトライする機能をfaradayのミドルウェアで実装してみます。

リトライするミドルウェアを設定する

素晴らしいことにfaradayにはリトライするミドルウェアがデフォルトで(しかも高機能で)ありますので、そのまま利用します。

exceptionsオプションにはリトライをする条件となるExceptionのクラスを指定します。デフォルトでは401エラーのExceptionが定義されていないので、CustomRaiseError::UnauthorizedError はこのあとで作成します。

# config/initializers/her.rb
Her::API.setup url: Settings.custom.server do |c|
  # Request
  c.use Faraday::Request::Retry, max: 1, interval: 0.05,
                                 interval_randomness: 0.5, backoff_factor: 2,
                                 exceptions: [CustomRaiseError::UnauthorizedError]
  c.use ClientCredential
  ...

  # Response
  ...

  # Adapter
  ...
end

注意: faradayのミドルウェアは上から順に実行されるので、この順番は守ってください。Faraday::Request::Retry ミドルウェアはリクエスト情報を記録するため、もし ClientCredential ミドルウェアを上に書いてしまうと、トークン情報が記録され、リトライ時にトークンの再取得が行われなくなってしまいます。

カスタムエラーを起こすミドルウェアを定義する

faradayにはリクエストエラー時にエラーを発生させるミドルウェアがありますが、401エラーの実装がないため、既存の Faraday::Response::RaiseError ミドルウェアをオーバーライドして、401エラーが発生した時の挙動を追加します。また、401エラーを示す UnauthorizedError も定義します。

# lib/faraday/custom_raise_error.rb
class CustomRaiseError < Faraday::Response::RaiseError
  # オーバーライド
  def on_complete(env)
    case env[:status]
      # 以下の2行を追加
      when 401
        raise UnauthorizedError, response_values(env)
      when 404
        raise Faraday::Error::ResourceNotFound, response_values(env)
      when 407
        raise Faraday::Error::ConnectionFailed, %{407 "Proxy Authentication Required "}
      when ClientErrorStatuses
        raise Faraday::Error::ClientError, response_values(env)
    end
  end

  # 401エラーのExceptionを追加
  class UnauthorizedError < Faraday::ClientError; end
end

カスタムエラーを起こすミドルウェアを追加する

作成した CustomRaiseError ミドルウェアの設定を追加します。

# config/initializers/her.rb
Her::API.setup url: Settings.custom.server do |c|
  # Request
  c.use Faraday::Request::Retry, max: 1, interval: 0.05,
                                 interval_randomness: 0.5, backoff_factor: 2,
                                 exceptions: [CustomRaiseError::UnauthorizedError]
  c.use ClientCredential
  ...

  # Response
  c.use CustomRaiseError
  ...

  # Adapter
  ...
end

確認

リトライの挙動は目視では確認しにくいのでspecを書きました。

  • リトライ回数の設定は1なので、トークンリクエストとAPIリクエストそれぞれが合計2回のリクエストが実行されること
  • 3回目のリクエストエラーでは CustomRaiseError::UnauthorizedError エラーが発生すること
# spec/models/resource_retry_spec.rb
require 'rails_helper'
require 'webmock/rspec'

RSpec.describe Resource, type: :model do
  describe '401エラーが応答された時' do
    before do
      @access_token_stub = WebMock.stub_request(:post, 'アクセストークン発行するエンドポイント').
          to_return(status: 200, body: %Q!{ "access_token": "access_token" }!)
    end

    before do
      @resource_stub = WebMock.stub_request(:get, 'リソースのエンドポイント').to_return(status: 401)
    end

    it 'アクセストークンリクエストが2回発生すること' do
      expect{ Resource.find(1) }.to raise_exception(CustomRaiseError::UnauthorizedError)
      expect(@access_token_stub).to have_been_made.times(2)
    end

    it 'APIリクエストが2回発生すること' do
      expect{ Resource.find(1) }.to raise_exception(CustomRaiseError::UnauthorizedError)
      expect(@resource_stub).to have_been_made.times(2)
    end
  end
end

まとめ

今回紹介した内容の大部分は、herの機能というよりherが使うHTTPクライアントのfaradayの機能でしたが、faradayのミドルウェアを理解することでこの先、よりherを使いこなすことができるようになりますので、ぜひ習得しましょう。