逃げる8回で会心の一撃

Web エンジニアのサトウリョウスケが開発とか色々書くブログです

my_api_client v0.12.0 をリリースしました🚀

my_api_client v0.12.0 に含まれる PR の内容について解説していきます。 より詳しい使い方は README.jp.md をご参照ください。

#173 Avoid sleep on testing

my_api_client では以下のように書くと、任意の例外を補足して自動的に API リクエストをリトライしてくれます。 ネットワーク系のエラーとか、 API Rate Limit に引っかかった時とかに便利なやつですね。

ActiveJob の retry_on とほぼ同じ使い方になっています。

class ExampleApiClient < MyApiClient::Base
  endpoint 'https://example.com'

  retry_on MyApiClient::ApiLimitError, wait: 1.minute, attempts: 3
  error_handling json: { '$.errors.code': 20 }, raise: MyApiClient::ApiLimitError

  # GET https://example.com/users
  def get_users
    get 'users'
  end
end

それは良いんですが、 rspec 上で wait が効いてしまっていたので、上記のコードだとリトライ3回分 wait するので、合計 3 分も待たされてしまっていました。 一応 rspecsleep を stub するとかやれば回避できますが、そもそもテストでは wait を無視して欲しいですよね。

my_api_client では be_handled_as_an_error という rspec の matcher を用意しているのですが、今回の対応で、この macher を経由してリトライが実行された場合は wait を無視するようになりました。

RSpec.describe ExampleApiClient, type: :api_client do
  let(:api_client) { described_class.new }

  # NOTE: レスポンスで `{ "errors": { "code": 20 } }` を受診した際、3 回リトライが実行された後に `MyApiClient::ApiLimitError` として例外処理される。
  it do
    expect { api_request! }
      .to be_handled_as_an_error(MyApiClient::ApiLimitError)
      .after_retry(3).times
      .when_receive(body: { errors: { code: 20 } }.to_json)
  end
end

また、 ExampleApiClientstub_api_clientstub_api_client_all を使用するとスタブ化できます。 スタブ化した状態だと任意の API レスポンスを返すか、任意の例外を発生させる、という動作になってリトライが発生しなくなるので、上記の問題はありませんでした。

stub_api_client_all(ExampleApiClient, get_users: { users: [{ id: 1 }, { id: 2 }, { id: 3 }] })

response = ExampleApiClient.new.get_users
response.users # => [{ id: 1 }, { id: 2 }, { id: 3 }]

#175 Verify arguments on error handling definition

error_handling の定義でレスポンスのステータスコードを指定することができるんですが、このオプション名が status_code なのか status なのかをよく間違える、という問題がありました。$ rails g api_client を使用するとテンプレが作成されるので、そこからエラーハンドリングの定義を行うと間違えにくいのですが、後からエラーハンドリングを追加する時とかにやらかします。作者自身もたまにやらかしてました😇

# 正解
error_handling status_code: 400..499, raise: MyApiClient::ClientError

# 間違い
error_handling status: 400..499, raise: MyApiClient::ClientError

この PR の対応で間違ったオプションを指定すると以下のような例外が発生するようになりました。

RuntimeError:
  Specified an incorrect option: `status`
  You can use options that: [:response, :status_code, :json, :with, :raise, :block]

#176 Provides a syntax sugar of retry_on on error_handling

最初の PR でも出てきた retry_on ですが、 error_handling raise: MyApiClient::ApiLimitError でも同じ例外を指定していて DRY な感じじゃなかったり、retry_onerror_handling をそれぞれ定義してるとお互いの関連が実感しづらい、という不満がありました。

class ExampleApiClient < MyApiClient::Base
  endpoint 'https://example.com'

  retry_on MyApiClient::ApiLimitError, wait: 1.minute, attempts: 3
  error_handling json: { '$.errors.code': 20 }, raise: MyApiClient::ApiLimitError

  # GET https://example.com/users
  def get_users
    get 'users'
  end
end

この PR では retry というオプションを error_handling に追加しています。これにより、以下の 2 つのコードは等価になります。

retry_on MyApiClient::ApiLimitError, wait: 1.minute, attempts: 3
error_handling json: { '$.errors.code': 20 }, raise: MyApiClient::ApiLimitError
error_handling json: { '$.errors.code': 20 }, 
                          raise: MyApiClient::ApiLimitError, 
                          retry: { wait: 1.minute, attempts: 3 }

retry_on にオプションを指定する必要がなければ retry: true と書けば OK です。

error_handling json: { '$.errors.code': 20 }, 
                          raise: MyApiClient::ApiLimitError, 
                          retry: true

ただし、 retry オプションを使用する際は以下の点に注意が必要です。

  • error_handlingraise オプションの指定が必須となります。
  • Block を使った error_handling の定義は禁止されます。