逆ジオAPIのリプレイスから得たHTTPクライアントの設計ノウハウ

アプリG長岡大樹
はじめまして! アプリGサーバーサイドエンジニアの長岡です。最近の趣味は特撮鑑賞です!JapanTaxi Advent Calendar 2018の20日目を担当させていただきます!😎

この記事は、 「JapanTaxi Advent Calendar 2018」の20日目です。

はじめに

JapanTaxiアプリでは 注文から乗車まで、様々なサービスとのAPI接続を行い、エンドユーザーの皆様へステキな乗車体験を提供しています。

今回は、緯度経度から住所情報を取得する逆ジオコーディングAPI(以下「逆ジオ」)のリプレイス業務から得たHTTPクライアントの設計ノウハウをご紹介します!

経緯

これまでJapanTaxiのサーバーサイドでは、位置情報系の外部APIとしてGoogle Maps APIを利用していました。
しかし、今年の6月からGoogle Maps Platformとしてサービスが大幅刷新、同時に料金プランも改定されました。

一部APIについては、コスト削減や精度の観点からほかサービスへのリプレイスを検討することとなり、他社サービスとの連携を行うことになりました。

そこで、新たな逆ジオAPIの実装プロジェクトが立ち上がったのです!

RubyライブラリのFaradayを使用

JapanTaxiアプリのサーバーサイドでは、Ruby on Railsのフレームワークを採用しています。
今回は外部API接続用のHTTPクライアントのgemであるFaradayを使用しました。

開発当初では、以下のような実装を進めていました。

faraday = Faraday.new(url) do |faraday|
  faraday.request :retry,
    response        :json
    max:            2,
    interval:       0.001,
    exceptions:     [Faraday::Error::ConnectionFailed]
  faraday.options.timeout = 4
  faraday.adapter   Faraday.default_adapter
end

response = faraday.get

if response.success?
  # レスポンス整形処理など
else
  # エラーハンドリング
end

パラメータは連携先の仕様書に従って別のメソッドで定義、アダプターはデフォルトのnet/httpを指定しました。
実装は比較的スムーズに進み、無事リリースすることができましたが、しばらくの運用期間を経て、とある問題が発生しました…

Faraday::ParsingErrorが多発

リクエスト数の瞬間的な増加によって、連携先のAPIで502エラーが返却されているのが分かりました。

最初はAPIが返却するステータスコード、予期せぬエラーが発生した場合のエラーハンドリングも行っていましたが、このようなエラーが確認されるのは何故なんだろう…

調査した結果、この問題の原因はレスポンスボディがJSON形式ではなく、NginxのHTML形式のエラーレスポンスが返却されており、FaradayのresponseオプションにJSONを定義していたことによってパースに失敗し、Faraday::ParsingErrorがraiseされていた事が発覚しました。

今回はresponseオプションを削除し、エラーハンドリング後API通信が成功した場合にJSONデータをパースするように修正しました。

この時点での変更で以下のような実装になりました。

faraday = Faraday.new(url) do |faraday|
  faraday.request :retry,
    max:            2,
    interval:       0.001,
    exceptions:     [Faraday::Error::ConnectionFailed]
  faraday.options.timeout = 4
  faraday.adapter   Faraday.default_adapter
end

response = faraday.get

if response.success?
  JSON.parse(response.body)
  # レスポンス整形処理など
else
  # エラーハンドリング
end

日本語のエラーメッセージが文字化けてしまう

今度はエラーログにpostされた内容が文字化けする問題が発生😱
レスポンスヘッダからcharsetを確認してみるもUTF-8になっており、ますます謎が深まってしまいました。

原因はFaradayなどのIssueを確認すると、Faradayのバグではなく、どちらかというとnet/httpの仕様の問題でした…

net/httpはヘッダのcharsetの値を確認し、途中まで指定されたcharsetで変換を行いますが、レスポンスボディの変換まではしないとのこと。そのため、JSON.parseしない場合、レスポンスボディのcharsetはデフォルトのバイナリ(ASCII-8BIT)で返却されていることが判明しました。

結論として、force_encodingメソッドを使用するのが良いということになりました。force_encodingメソッドを使用することによって、レシーバのエンコード情報を引数で指定した文字コードに変更できます。今回はUTF-8を指定し、日本語に変換できました。

faraday = Faraday.new(url) do |faraday|
  faraday.request :retry,
    max:            2,
    interval:       0.001,
    exceptions:     [Faraday::Error::ConnectionFailed]
  faraday.options.timeout = 4
  faraday.adapter   Faraday.default_adapter
end

response = faraday.get

if response.success?
  JSON.parse(response.body)
  # レスポンス整形処理など
else
  error_message = response.body.force_encoding('UTF-8')
  # エラーハンドリング
end

最終的に、以上のような実装になりました!

ちなみに、JSON.loadメソッドを使用するべきかも検討しました。JSON.parseは引数のソースがJSONの文字列しか受け付けないのに対して、 JSON.loadはto_str, to_io, readメソッドを持つオブジェクトに対してパース処理を行ってしまうため、parseを引き続き使用しました。

学び

FaradayはRubyのHTTPクライアントライブラリとして簡易的に扱える点などから、様々な技術記事やAPI仕様書で見かけますが、本番で運用するにあたって様々な考慮を入れることが必要だと痛感しました…

様々な試行錯誤を重ねながら、サーバーサイドの力で「移動で人を幸せに」できるように、日々精進していきます!

JapanTaxiでは、ITの力で「移動で人を幸せに。」を実現するための一連のサービス開発に取り組んでいます。全部署にてメンバーも積極的に募集しているので、興味のある方はWantedlyもぜひご覧ください!

JapanTaxiに興味を持ったら、まずはお話しませんか?