技術的なやつ

技術的なやつ

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のデータサイズを大きくしないようにするためである。しかし、階層化したほうが絶対に良い場合はそうしたほうが良い。

配列はオブジェクトとして包む

配列で包める場合も、オブジェクトで包むほうが良い。以下の様なメリットが有る。 * レスポンスデータが何を示しているものか分かりやすい * レスポンスデータをオブジェクトに統一できる * セキュリティリスクを回避できる セキュリティリスクについて、全体を配列で包むとそのJSONJavaScriptとして正しいものになり、レスポンスに不正コードを仕込まれる可能性がある。オブジェクトの場合はそれ単体では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とユーザー認証情報

リクエストでCookieAuthenticationヘッダにユーザー認証情報がセットされている場合、サーバはレスポンスでAccess-Control-Allow-Credentialsヘッダにtrueをセットする必要がある。

独自のHTTPヘッダを定義する場合

X-AppName-を接頭辞として付けることが一般的であったが、AppName-で十分であるという議論がなされている。

まとめと感想

  • レスポンスデータ構造は必要以上に複雑にせず、フラットにする。
  • レスポンスデータ内容はいくつかのデータセットを定義しておき、ユーザーが必要な情報を一度に返すような設計にする(LSUDsの場合。SSKDsの場合はその限りでない)。
  • 大きな数値データは文字列とすることに注意する。
  • ステータスコードを有効利用する。
  • 同一生成元ポリシーに対応するためにはJSONPとCORSの2通りの方法がある。JSONPは仕様の穴を突いたような方法のため、CORSの方が良いと言える。
  • 独自のHTTPヘッダを定義する場合、AppName-を接頭辞とするだけで十分だと思う。