ステータスコードの選び方
このレッスンでは、操作の種類と結果の状況から、返すべき HTTP ステータスコードを決められるようになります。
まず 3 つの階層で考える
ステータスコードは 3 桁の数字ですが、意味を決めているのは先頭の 1 桁です。
| 先頭 | 意味 | 誰の話か |
|---|---|---|
| 2xx | 成功 | 依頼どおりに処理できた |
| 4xx | クライアント側の問題 | リクエストの内容がおかしい |
| 5xx | サーバー側の問題 | リクエストは正しいがこちらが失敗した |
この分類を間違えないだけで、API の使い勝手はかなり良くなります。クライアントが直せる問題なのか、直せない問題なのかを数字で伝えるのが本質です。
よく使う 9 つ
| コード | 名前 | 使いどころ |
|---|---|---|
| 200 | OK | 取得・更新が成功した |
| 201 | Created | 新しいリソースを作成した |
| 204 | No Content | 成功したが返す本文が無い。削除の後など |
| 400 | Bad Request | 入力の形式や内容がおかしい |
| 401 | Unauthorized | 認証されていない。誰なのか分からない |
| 403 | Forbidden | 認証済みだが権限が足りない |
| 404 | Not Found | 対象が存在しない |
| 409 | Conflict | 現在の状態と矛盾する。重複登録など |
| 500 | Internal Server Error | サーバー側の想定外の失敗 |
混同しやすい組み合わせ
200 と 201 — 作成が成功したときは 201 を返します。200 でも動きますが、201 には「新しいリソースができた」という追加の情報があります。あわせて Location ヘッダで場所を伝えるのが定石です。
200 と 204 — 削除が成功したときに返すものがないなら 204 が適切です。空の JSON を本文にして 200 を返すより意図がはっきりします。
401 と 403 — この 2 つは「誰か分からない」と「誰かは分かるが許されていない」の違いです。トークンが無い、または期限切れなら 401、ログイン済みの一般利用者が管理者専用の操作を叩いたなら 403 です。401 を返すということは、認証をやり直せば通るかもしれないという意味になります。
404 と 403 — 存在するリソースへの権限が無いとき、403 ではなくあえて 404 を返す設計もあります。403 だと「そこに何かはある」ことが漏れてしまうためです。どちらが正しいという話ではなく、情報を隠したいかどうかで選びます。
400 と 409 — 形式がおかしいのは 400 です。形式は正しいが今の状態と衝突するのが 409 です。メールアドレスの形が壊れているなら 400、形は正しいが既に登録済みなら 409 になります。
500 を返してはいけない場面 — 入力が悪くて落ちたのに 500 を返してしまうと、クライアントは自分に直せるものだと気づけません。想定できる失敗はすべて 4xx として明示し、500 は本当に想定外のものだけに残します。
Spring での書き方
Java
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserResponse create(@Valid @RequestBody CreateUserRequest req) { ... }@ResponseStatus で固定のコードを宣言できます。状況によってコードを変えたい場合は、次のレッスンで扱う ResponseEntity を使います。
演習で確かめます
Spring では @ResponseStatus や ResponseEntity が「決めたコードを実際に返す」部分を引き受けてくれますが、どのコードを返すかを決めるのは常に自分の仕事です。ここではその判断そのものを関数にします。操作の種類と結果の状況を受け取り、返すべきステータスコードを決めてください。
要件
- outcome が ok 以外のときは action を見ずに、invalid は 400、unauthenticated は 401、forbidden は 403、missing は 404、conflict は 409、error は 500 を返すこと
- outcome が ok のときだけ action を見て、create は 201、delete は 204、それ以外は 200 を返すこと
- 未知の outcome は 500 を返すこと
入出力例
decideStatus("create", "ok") → "201"
decideStatus("delete", "ok") → "204"
decideStatus("get", "ok") → "200"
decideStatus("update", "ok") → "200"
decideStatus("create", "invalid") → "400"
decideStatus("get", "unauthenticated") → "401"
decideStatus("delete", "forbidden") → "403"
decideStatus("get", "missing") → "404"
decideStatus("create", "conflict") → "409"
decideStatus("get", "error") → "500"
decideStatus("get", "unknown") → "500"