8.2 Web API: The Good Parts 3章~4章
第3章 レスポンスデータの設計
JSONP
JSONをブラウザに渡す際、JavaScriptで以下のようにpaddingしたものをJSONPという。
callback({"id":123, "name":"Jack"})
JSONPが考えだされた背景には、同一生成元ポリシーによりXHTTPRequestは同じ生成元へのアクセスしか行うことが出来ないがscript要素はその制限の対象外であるということがある。つまり、抜け道としての手段である。そのため、必要な場合のみサポートするという姿勢を取ったほうが良い。
JSONPをサポートする際は、クエリパラメータとしてコールバック関数の名前を指定させるほうが良い。
以下に理由を説明する。まず、グローバル変数にJSONを渡す仕様は、グローバルを汚染してしまう。次に、コールバック関数名を固定する方法は、関数名の衝突が考えられ、柔軟性がない。よって、コールバック関数の名前をユーザーが指定できるような設計にすべきである。この時のパラメータ名にはcallback
が多用される。
データの内部構造
ユーザーが必要とするであろう情報を返す
たとえば、SNSの友達一覧を取得するAPIでユーザーIDの配列だけが返されても、ユーザーはそのIDを利用して別のAPIを呼ぶ必要があり、不便な上にオーバーヘッドが増加する。よって、IDの他に名前等の情報も返すべきである。
レスポンス内容をユーザーが選べるようにする
上記方針でレスポンス内容を設計すると、ユーザーに返すデータが膨大になる。そこで、いくつかのデータセットを選択肢として与えて、どのデータセットを渡すかユーザーに選択させる設計を行ったほうが良い。この時のパラメータ名にはfields
が多用される。
エンベロープは不必要
すべてのAPIを同じ構造でくるむことをエンベロープという。たとえば、すべてのAPIを以下のような構造にくるむと、一見良い設計に思える。
{ "header": { "status": "success", "errorCode": 0 }, "response": { ... } }
しかし、APIはそもそもHTTPを介しているので、HTTPというエンベロープが存在する。そのため、実際に利用される以外のメタデータに関しては、HTTPを利用する方が理に適っている。
データはなるべくフラットに
深い階層にデータを置くべきではない。これは、JSONのデータサイズを大きくしないようにするためである。しかし、階層化したほうが絶対に良い場合はそうしたほうが良い。
配列はオブジェクトとして包む
配列で包める場合も、オブジェクトで包むほうが良い。以下の様なメリットが有る。 * レスポンスデータが何を示しているものか分かりやすい * レスポンスデータをオブジェクトに統一できる * セキュリティリスクを回避できる セキュリティリスクについて、全体を配列で包むとそのJSONはJavaScriptとして正しいものになり、レスポンスに不正コードを仕込まれる可能性がある。オブジェクトの場合はそれ単体ではJavaScriptとして正しいコードではないため、ブラウザからアクセスしても不正コードは実行されない。
日付のフォーマット
HTTPヘッダで用いられているRFC3339を利用するのが良い。2015-10-12T11:30:22+09:00
のような形式である。
大きな数字
大きな数字は、数値データではなく文字列として返した方が良い。ユーザーのプログラムでのオーバーフローを防ぐためである。
エラーの伝え方
エラーが発生した場合は、おおよその意味をHTTPのステータスコードで返し、HTTPヘッダまたはJSON内にエラーの詳細を記述すれば良い。 また、エラー発生の際にHTMLが返ってしまうことは避け、メンテナンス時にもきちんとJSONを返す(ステータスコードは503)。
第4章 HTTPの仕様を最大限利用する
なぜHTTPの仕様を利用するのか?
前章で述べたように、メタデータをユーザーに渡すためのエンベロープとして使えるため。
ステータスコードの利用
大まかに、200番台:成功、300番台:追加で処理が必要、400番台:クライアントのリクエストに起因するエラー、500番台:サーバエラーである。 以下、APIに必要そうなやつだけ。
200:OK
PUT
PATCH
メソッドで正常にデータ更新が出来た時。
201:Created
POST
メソッドで、正常にデータが新規登録された時。
202:Accepted
処理は受理されたが、まだ完了していない時に返すステータスコード。
204:No Content
レスポンスが空の時に利用。DELETE
メソッドでデータの削除を行った時。
401:Unauthorized
「認証がなされていない(アクセスユーザーが特定できない)」。アクセスにユーザー情報が必要なAPIで、トークン無しでアクセスしてきた場合。
403:Authorization
「アクセス権限がない(ユーザーは特定できたが、そのユーザーに操作権限がない)」。許可されたユーザー以外のトークンでアクセスしてきた場合。
404:Not Found
存在しないユーザー情報にアクセスしようとした場合など。ただし、エンドポイントそのものが存在しないのか、データが存在しないのかが判別できないため、詳しい情報を付加するのが親切。
405:Method Not Allowed
エンドポイントは存在しているが、そのアクセスメソッドは許可されていない。
406:Not Acceptable
クライアントが指定してきたデータ形式にAPIが対応していない(たとえば、JSONしか対応していないのにXMLを指定してきた時)。
409:Conflict
リソースの競合。たとえば、ユニークキーを指定して新規登録するようなAPIで、重複キーを送ってきた場合。
410:Gone
かつて存在したが今は存在しないデータにアクセスしてきた場合。ただし、410を実装するなら、過去のデータを全て残しておく必要があり、セキュリティ上問題がある可能性もある(かつて登録されたメールアドレスなどが分かってしまう可能性がある)。
429:Too Many Requests
アクセス回数が許容範囲の限界を超えた。
503:Service Unavailable
サーバが一時的に利用できない。メンテナンス時など。
キャッシュについて
期限切れモデル
Cache-Control
レスポンスヘッダまたはExpires
レスポンスヘッダを利用して、データの有効期限を指定する。プロキシサーバで蓄えられたデータが再利用されるため、処理が高速になる。
検証モデル
Last-Modified
レスポンスヘッダとETag
(エンティティタグ)レスポンスヘッダを利用して、オリジンサーバとプロキシサーバの持つデータが一致しているか確認する。ETag
には、レスポンスデータのハッシュ値などが入る。この方式では、期限切れモデルと違いオリジンサーバとの通信が発生するため、レスポンスデータが膨大な場合には効果があるが、それ以外の場合はあまり意味を成さない。
キャッシュさせたくない場合
Cache-Control
ヘッダにno-cache
を入れる。
メディアタイプの指定
JSONの場合、Content-Type
ヘッダにapplication/json
を入れる。指定しなかった場合、XSSの影響を受ける可能性がある。
{"data":"<script>alert('xss');</script>"}
クロスオリジンリソース共有
同一生成元ポリシーによりXHTTPRequest
で異なるドメインに対してアクセスは出来ない。しかし、CORSを行うことによって特定のアクセス元からのアクセスを許可することが可能である。
リクエストのOrigin
ヘッダにアクセス元を指定する。
レスポンスのAccess-Control-Allow-Origin
ヘッダに、CORSを許可するアクセス元を指定する。ワイルドカードの利用が可能。
CORSとユーザー認証情報
リクエストでCookie
やAuthentication
ヘッダにユーザー認証情報がセットされている場合、サーバはレスポンスでAccess-Control-Allow-Credentials
ヘッダにtrue
をセットする必要がある。
独自のHTTPヘッダを定義する場合
X-AppName-
を接頭辞として付けることが一般的であったが、AppName-
で十分であるという議論がなされている。