はじめまして。アニメイトラボに11月からWeb/アプリエンジニアとしてジョインしましたかんがー(@KangalMi)と申します。
新卒で入社した会社から非公開なWebAPIとiOS/Androidアプリの開発を行ってきました。
現在はアニマートのWeb側をメインに担当をしています。
今回は、僕がWebAPIを開発・運用する中で失敗したことをお伝えします。
エラーコードの上手な表現方法
自分が開発したものではエラーの表現を下のような形で常に返却していました。
"meta": { "status": 500, "message": "ユーザーの作成に失敗しました。" }
上記はmeta以下にエラー関連の情報をエンベロープで包み、statusはHTTPステータスコードに準拠したコードを、messageにはiOS/Android端末でユーザーに表示するメッセージです。こんな単純な構造でも失敗があったのです。
エラーコードはHTTPステータスコード*1を使用していました。これには問題が2つほどあります。
1.ステータスコードとエラーコードが重複しておりHTTPステータスコードとエラーコードの乖離が発生していたこと
2.ステータスコード500のとき、詳細にエラーを表現できなくなったこと
1.についてAPIではHTTPステータスは200か500か大きく2つしか返さないのにもかかわらず、APIのステータスコードは200、500を含め様々なエラーコードを返却するという状況が発生しました。つまり端末側で二重に検査してエラー状況を確認しなければいけない状況になります。 これはどう考えても効率が悪い。
2.に関して、エラーコード返す意義は端末側でユーザーのアクションに対し正しく応答をし、次にとるべきアクションを明確にすることだと思います。
ですが、エラーが500にまとめられてしまうと表現のしようがありません。
この2つの問題に対し、本来は「HTTPの仕様を最大限利用する*2」ことと「ステータスコードは独自のものを使うべき」であったと考えます。つまり、上記で使われていたstatusキーは本来HTTPレスポンスヘッダを利用し、ステータスコードはHTTPステータスコードに依存し過ぎないコードを考えるべきでした。
エラーのエンベロープは常に返すべきか
Web APIの開発を始めるとき、その構造をFoursquare API*3を参照に作成していました。
ですので当初は通信が成功であってもエラーのエンベロープは常に返していました。
加えて先述の様にHTTPの仕様を利用しない作り方をしていたために、常に同じ構造が返却される方が良いと考えていました。
が、「AFNetWorking」や「Volley」などのライブラリを使っていれば分かるのですが、HTTPステータスコードがエラーを返しているのならば成功した時とは違うブロックなりリスナーが実行されるはずです。
このことを考えれば成功時にユーザーへメッセージを表示したい意図ががなければエラー時だけで十分だと考えます。
現に公開されているTwitter、Github、Qiitaなどのサービスが提供するAPIではエラー時にしかエラーに関する情報を返却しません。
1つのAPIで返すべき範囲
Web APIのレスポンスでは基本的にサーバー側のDBテーブルに沿ってJSONの構造を決めているかと思います。
api/v1/users users: { [ { "id": 1, "name": "かんがー" }, }
などです。
前職で、アプリ全体の応答速度を上げるために1つのAPIに含める情報を増やし、APIへのアクセス回数を減らせないか?と提案されました。つまりJSONで表現するとこういうことです。
api/v1/users users: { { "id": 1, "name": "かんがー", "stars": { "gold": 10, "silver": 10, "blonze": 10 }, "articles": { [...article1...], [...articles2...] } }, }
ユーザー一覧画面において、ユーザーの情報だけではなく事前にユーザーに関連する種々の情報を1つのAPIにまとめて、詳細画面においてAPIのアクセスなしに、そしてストレスなく次画面が見られるという考えですね。
ですが、この考えだとユーザー一覧APIが重くなるという本末転倒な結果をもたらしました。(少し考えればそうなるだろうという気がしますが)
「Twitter」のツイートの詳細画面のように1つの画面に存在する情報が少ないものであるならば良いのですが、大抵のサービスではそうは行きません。
この失敗からは画面の情報量に見合った情報を与えることが必要だといえます。
ストレスなく表示する点に関しては、「ユーザーの行動をブロックしない」、「少量の情報でも表示してバックグラウンドで詳細な情報を取得する」などの工夫が必要でした。
APIの返すべき情報
加えて自分の失敗ではないですがWeb APIの調査をした時に発見した「事例」に関してお伝えします。
例えば、ここにUserのモデルがあるとします。この中にはUserに関する様々な情報が入っています。
ユーザーのID、サービスで使用されるユーザーの名前がきっと入っています。
同じユーザーの情報です。どうしてもまとめたいという気持ちが起こります。
ここにユーザーの性別を入れてみます。
次に、ユーザーのログイン時に必要な情報が必要です。これも付け加えましょう。
最後にユーザーのメールアドレス、パスワード、電話番号を追加してみます。
さて、この状態でDBから取り出したUserのモデルをユーザー一覧APIなどで返却してみましょう。
api/v1/users users: { [ { "id": 1, "name": "かんがー", "sex": 0, "email": "abcdefg@***.com", "password": "************", "tel": "xxx-xxxx-xxxx" }, ] }
便利です。何が便利かというと、サービスの全ユーザーの個人情報が取得できてしまう上に、ユーザーになりすましてサービスにログインができてしまいます。
もちろん、これを行えば不正アクセス禁止法等で罰せられることは明白です。ですが、サービスの情報をダダ流しという状態で場合によってはサービス運営自体が危うくなるレベルに発展するかもしれません。
ここで「非公開のWeb APIなのでこういった情報は見られない」、「SSLを利用しているから安心だ」という考え方は大きな間違いだと僕は思います。
これらの情報はWiresharkなどでパケットキャプチャを行えば用意に閲覧することが可能です。よって、このような重大事故を起こさないようにするには、
- DBのテーブル設計を見直す
- APIで返すべき情報は厳選する
の2点が必要であったと思われます。
さいごに
寒い季節となりました。皆様風邪など引かれませんようお気をつけ下さい。