メインコンテンツにスキップ

🚦 HTTPステータスコード

📖 定義

HTTPステータスコードは、サーバーがクライアントのリクエストをどのように処理したかを示す3桁の数字です。最初の桁の数字によって5つのグループに分類されます。

🎯 比喩で理解する

レストランの注文システム

2xx (成功) = 「注文された料理が出来ました!」
├─ 200: 料理完成、すぐにお召し上がりください
├─ 201: 新しいメニューの登録完了
└─ 204: 料理を片付けました (テーブルクリーニング完了)

3xx (リダイレクション) = 「別の場所にご案内します」
├─ 301: 店舗が永久的に移転しました
├─ 302: 一時的に別の場所で提供しています
└─ 304: すでに召し上がった料理なので、新しく出しません (キャッシュ)

4xx (クライアントエラー) = 「お客様のミスです」
├─ 400: 注文を理解できません
├─ 401: 会員のみ注文可能です
├─ 403: このメニューは注文できません
├─ 404: そのようなメニューはありません
└─ 429: 注文が多すぎます

5xx (サーバーエラー) = 「こちらの不手際です」
├─ 500: キッチンに問題が発生しました
├─ 502: キッチンと接続できません
├─ 503: 現在非常に忙しいため、注文を受け付けられません
└─ 504: キッチンの応答が遅すぎます

💡 ステータスコードグループ

┌─────┬─────────────┬────────────────────┐
│ コード│ 分類 │ 意味 │
├─────┼─────────────┼────────────────────┤
│ 1xx │ 情報 │ リクエスト処理中 │
│ 2xx │ 成功 │ リクエスト成功 │
│ 3xx │ リダイレクション │ 追加アクション必要 │
│ 4xx │ クライアント │ クライアントエラー │
│ 5xx │ サーバーエラー │ サーバーエラー │
└─────┴─────────────┴────────────────────┘

✅ 2xx - 成功

200 OK

最も一般的な成功レスポンス

リクエスト:
GET /api/users/123 HTTP/1.1

レスポンス:
HTTP/1.1 200 OK
Content-Type: application/json

{
"id": 123,
"name": "田中太郎",
"email": "tanaka@example.com"
}
fetch('https://api.example.com/users/123')
.then(response => {
if (response.status === 200) {
return response.json();
}
})
.then(data => console.log(data));

使用例:

  • GET リクエスト成功
  • PUT、PATCHリクエスト成功
  • データを含むすべての成功レスポンス

201 Created

新しいリソース作成成功

リクエスト:
POST /api/users HTTP/1.1
Content-Type: application/json

{
"name": "田中太郎",
"email": "tanaka@example.com"
}

レスポンス:
HTTP/1.1 201 Created
Location: /api/users/123
Content-Type: application/json

{
"id": 123,
"name": "田中太郎",
"email": "tanaka@example.com"
}
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: '田中太郎',
email: 'tanaka@example.com'
})
})
.then(response => {
if (response.status === 201) {
console.log('ユーザー作成成功!');
// Locationヘッダーから新しいリソースのURLを確認
console.log(response.headers.get('Location'));
return response.json();
}
});

204 No Content

成功したが、返すコンテンツがない

リクエスト:
DELETE /api/users/123 HTTP/1.1

レスポンス:
HTTP/1.1 204 No Content
fetch('https://api.example.com/users/123', {
method: 'DELETE'
})
.then(response => {
if (response.status === 204) {
console.log('削除成功!');
// レスポンスボディなし
}
});

使用例:

  • DELETE リクエスト成功
  • PUT、PATCH後に返すデータがない場合
  • 作業成功したがクライアントに渡す内容がない場合

その他の2xxコード

202 Accepted
├─ リクエスト受付、処理は非同期で進行
└─ 例: 大容量ファイル処理、メール送信

206 Partial Content
├─ 一部のコンテンツのみ返却
└─ 例: 動画ストリーミング、大容量ファイルダウンロード

🔀 3xx - リダイレクション

301 Moved Permanently

リソースが永久的に移動

リクエスト:
GET /old-page HTTP/1.1

レスポンス:
HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-page
// ブラウザが自動的にリダイレクト
fetch('https://api.example.com/old-endpoint')
.then(response => {
if (response.status === 301) {
console.log('永久移動:', response.headers.get('Location'));
}
});

使用例:

  • サイトアドレス変更
  • APIエンドポイント永久変更
  • SEO: 検索エンジンが新しいURLを保存

302 Found (一時リダイレクト)

リソースが一時的に移動

レスポンス:
HTTP/1.1 302 Found
Location: https://example.com/temp-page

使用例:

  • 一時的なページへ移動
  • ログインページへリダイレクト
  • A/Bテスティング

304 Not Modified

キャッシュされたデータを使用可能

リクエスト:
GET /api/users/123 HTTP/1.1
If-None-Match: "abc123"

レスポンス:
HTTP/1.1 304 Not Modified
ETag: "abc123"
// ブラウザが自動的にキャッシュを使用
fetch('https://api.example.com/users/123')
.then(response => {
if (response.status === 304) {
console.log('キャッシュ使用中');
}
});

利点:

  • ネットワーク帯域幅の節約
  • レスポンス速度の向上
  • サーバー負荷の軽減

その他の3xxコード

303 See Other
├─ 別のURLでGETで照会
└─ 例: POST後に結果ページへ移動

307 Temporary Redirect
├─ 302と類似だが、メソッドを維持
└─ 例: POST → POSTへリダイレクト

308 Permanent Redirect
├─ 301と類似だが、メソッドを維持
└─ 例: POST → POSTへ永久リダイレクト

❌ 4xx - クライアントエラー

400 Bad Request

不正なリクエスト形式

リクエスト:
POST /api/users HTTP/1.1
Content-Type: application/json

{
"name": "", // 空の名前
"email": "invalid-email" // 不正なメールフォーマット
}

レスポンス:
HTTP/1.1 400 Bad Request
Content-Type: application/json

{
"error": "Validation Error",
"message": "Invalid request data",
"details": [
{
"field": "name",
"message": "Name is required"
},
{
"field": "email",
"message": "Invalid email format"
}
]
}
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: '',
email: 'invalid-email'
})
})
.then(response => {
if (response.status === 400) {
return response.json();
}
})
.then(error => {
console.error('妥当性検証失敗:', error);
});

401 Unauthorized

認証が必要

リクエスト:
GET /api/profile HTTP/1.1

レスポンス:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="example"
Content-Type: application/json

{
"error": "Unauthorized",
"message": "Authentication required"
}
fetch('https://api.example.com/profile', {
headers: {
'Authorization': 'Bearer YOUR_TOKEN_HERE'
}
})
.then(response => {
if (response.status === 401) {
console.error('認証が必要 - ログインページへ移動');
window.location.href = '/login';
}
});

使用例:

  • ログインしていないユーザー
  • 期限切れのトークン
  • 不正な認証情報

403 Forbidden

権限がない

リクエスト:
DELETE /api/users/999 HTTP/1.1
Authorization: Bearer user_token

レスポンス:
HTTP/1.1 403 Forbidden
Content-Type: application/json

{
"error": "Forbidden",
"message": "You don't have permission to delete this user"
}
fetch('https://api.example.com/admin/users/999', {
method: 'DELETE',
headers: {
'Authorization': 'Bearer USER_TOKEN'
}
})
.then(response => {
if (response.status === 403) {
console.error('権限がありません');
alert('管理者のみ削除できます');
}
});

401 vs 403:

401 Unauthorized (認証失敗)
├─ 「あなたが誰であるかわかりません」
├─ ログインしていない
├─ トークンがない/期限切れ
└─ 解決: ログインが必要

403 Forbidden (認可失敗)
├─ 「あなたが誰であるかは知っていますが、権限がありません」
├─ ログインは完了
├─ 権限不足
└─ 解決: 管理者に権限を要求

404 Not Found

リソースが見つからない

リクエスト:
GET /api/users/999999 HTTP/1.1

レスポンス:
HTTP/1.1 404 Not Found
Content-Type: application/json

{
"error": "Not Found",
"message": "User with ID 999999 not found"
}
fetch('https://api.example.com/users/999999')
.then(response => {
if (response.status === 404) {
console.error('ユーザーが見つかりません');
// 404ページ表示
}
return response.json();
});

使用例:

  • 存在しないページ
  • 削除されたリソース
  • 不正なURL

その他の4xxコード

405 Method Not Allowed
├─ 許可されていないHTTPメソッド
└─ 例: GETのみ可能なのにPOST要求

409 Conflict
├─ リクエストが現在のサーバー状態と衝突
└─ 例: すでに存在するメールアドレスで登録を試みる

422 Unprocessable Entity
├─ 文法は正しいが意味的に処理不可
└─ 例: 日付フォーマットは正しいが未来の日付

429 Too Many Requests
├─ リクエスト回数制限超過
└─ 例: API呼び出し制限 (レートリミット)

429 Too Many Requests の処理

async function fetchWithRetry(url, options = {}, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch(url, options);

if (response.status === 429) {
const retryAfter = response.headers.get('Retry-After') || 60;
console.log(`リクエストが多すぎます。${retryAfter}秒後に再試行...`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}

return response;
}

throw new Error('最大再試行回数を超えました');
}

🔥 5xx - サーバーエラー

500 Internal Server Error

サーバー内部エラー

リクエスト:
GET /api/users HTTP/1.1

レスポンス:
HTTP/1.1 500 Internal Server Error
Content-Type: application/json

{
"error": "Internal Server Error",
"message": "An unexpected error occurred"
}
fetch('https://api.example.com/users')
.then(response => {
if (response.status === 500) {
console.error('サーバーエラー発生');
alert('一時的なエラーです。しばらく後に再試行してください。');
}
});

原因:

  • コードバグ
  • 未処理の例外
  • データベースエラー
  • サーバー設定の問題

502 Bad Gateway

ゲートウェイエラー

レスポンス:
HTTP/1.1 502 Bad Gateway
Content-Type: text/html

<html>
<body>
<h1>502 Bad Gateway</h1>
<p>The server received an invalid response from the upstream server</p>
</body>
</html>

原因:

  • プロキシサーバーとバックエンドサーバー間の通信失敗
  • バックエンドサーバーダウン
  • ネットワーク問題

503 Service Unavailable

サービス利用不可

レスポンス:
HTTP/1.1 503 Service Unavailable
Retry-After: 3600
Content-Type: application/json

{
"error": "Service Unavailable",
"message": "Server is under maintenance",
"retryAfter": 3600
}
fetch('https://api.example.com/users')
.then(response => {
if (response.status === 503) {
const retryAfter = response.headers.get('Retry-After');
console.log(`サービスメンテナンス中。${retryAfter}秒後に再試行`);
}
});

原因:

  • サーバーメンテナンス
  • 過負荷
  • 一時的な中断

504 Gateway Timeout

ゲートウェイタイムアウト

レスポンス:
HTTP/1.1 504 Gateway Timeout
Content-Type: application/json

{
"error": "Gateway Timeout",
"message": "The server did not respond in time"
}

原因:

  • バックエンドサーバーの応答遅延
  • ネットワーク遅延
  • データベースクエリタイムアウト

📊 ステータスコードチートシート

┌─────┬────────────────────┬─────────────────────────┐
│ コード│ 名前 │ いつ使用? │
├─────┼────────────────────┼─────────────────────────┤
│ 200 │ OK │ 成功 (一般) │
│ 201 │ Created │ 作成成功 │
│ 204 │ No Content │ 成功 (レスポンスなし) │
├─────┼────────────────────┼─────────────────────────┤
│ 301 │ Moved Permanently │ 永久移動 │
│ 302 │ Found │ 一時移動 │
│ 304 │ Not Modified │ キャッシュ使用 │
├─────┼────────────────────┼─────────────────────────┤
│ 400 │ Bad Request │ 不正なリクエスト │
│ 401 │ Unauthorized │ 認証必要 │
│ 403 │ Forbidden │ 権限なし │
│ 404 │ Not Found │ リソースなし │
│ 429 │ Too Many Requests │ リクエスト制限超過 │
├─────┼────────────────────┼─────────────────────────┤
│ 500 │ Internal Error │ サーバーエラー │
│ 502 │ Bad Gateway │ ゲートウェイエラー │
│ 503 │ Service Unavailable│ サービス不可 │
│ 504 │ Gateway Timeout │ ゲートウェイタイムアウト │
└─────┴────────────────────┴─────────────────────────┘

🛠️ 実践的なエラー処理

基本的なエラー処理

async function fetchData(url) {
try {
const response = await fetch(url);

// ステータスコード別処理
switch (response.status) {
case 200:
return await response.json();

case 401:
console.error('認証が必要');
window.location.href = '/login';
break;

case 403:
console.error('権限なし');
alert('アクセス権限がありません');
break;

case 404:
console.error('リソースなし');
return null;

case 500:
case 502:
case 503:
case 504:
console.error('サーバーエラー');
alert('一時的なエラーです。しばらく後に再試行してください');
break;

default:
console.error('不明なエラー:', response.status);
}
} catch (error) {
console.error('ネットワークエラー:', error);
}
}

再試行ロジック

async function fetchWithRetry(url, options = {}, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url, options);

// 成功または修正不可能なクライアントエラー
if (response.ok || response.status < 500) {
return response;
}

// サーバーエラー - 再試行
if (i < retries - 1) {
console.log(`サーバーエラー。${i + 1}回目の再試行...`);
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
} catch (error) {
if (i === retries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}

グローバルエラーハンドラー

// axios interceptor
axios.interceptors.response.use(
response => response,
error => {
const status = error.response?.status;

switch (status) {
case 401:
// トークン更新またはログインページへ
refreshToken().catch(() => {
window.location.href = '/login';
});
break;

case 403:
alert('アクセス権限がありません');
break;

case 404:
console.error('リソースが見つかりません');
break;

case 429:
alert('リクエストが多すぎます。しばらく後に再試行してください');
break;

case 500:
case 502:
case 503:
case 504:
alert('サーバーエラーが発生しました');
break;

default:
console.error('不明なエラー:', status);
}

return Promise.reject(error);
}
);

🤔 よくある質問

Q1. 200と201の違いは?

答:

200 OK
├─ 一般的な成功
├─ GET、PUT、PATCHで主に使用
└─ 例: データ取得成功

201 Created
├─ 新しいリソース作成成功
├─ POSTで主に使用
├─ Locationヘッダーに新しいリソースのURLを含む
└─ 例: 会員登録成功

実践例:
GET /api/users/123 → 200 OK
POST /api/users → 201 Created
PUT /api/users/123 → 200 OK

Q2. 401と403はいつ使用するか?

答:

401 Unauthorized (認証問題)
├─ ログインしていない
├─ トークンがない
├─ トークン期限切れ
└─ 解決: ログインが必要

403 Forbidden (権限問題)
├─ ログインは完了
├─ 一般ユーザーが管理者機能にアクセス
├─ アカウント停止/ブロック
└─ 解決: 権限の取得が必要

フロー:
1. トークンがあるか? → なし → 401
2. トークンは有効か? → 期限切れ → 401
3. 権限はあるか? → なし → 403
4. すべてOK → 200

Q3. 404と410の違いは?

答:

404 Not Found
├─ リソースが見つからない
├─ 一時的かもしれない、永久的かもしれない
└─ 例: 不正なURL、存在しないID

410 Gone
├─ リソースが永久的に削除された
├─ 意図的に削除された
├─ 二度と使用できない
└─ 例: 期限切れのプロモーション、削除されたアカウント

実務では主に404を使用:
- 404がより汎用的
- 410は「永久に削除」を明示的に示す必要がある場合のみ

Q4. 500と503の違いは?

答:

500 Internal Server Error
├─ 予期せぬサーバーエラー
├─ コードバグ、例外処理失敗
├─ 再試行しても継続して発生
└─ 開発者が修正する必要あり

503 Service Unavailable
├─ 一時的にサービス利用不可
├─ サーバーメンテナンス、過負荷
├─ 再試行すると成功する可能性あり
├─ Retry-Afterヘッダーを含む可能性
└─ まもなく正常化予定

いつ使用するか?
500: コードバグ発生時
503: 意図的な中断、過負荷時

Q5. ステータスコードを誤用するとどうなるか?

答:

❌ 悪い例:

// すべてのエラーを200で返す
HTTP/1.1 200 OK
{
"success": false,
"error": "User not found"
}

問題点:
├─ HTTPキャッシングを妨害
├─ エラー追跡が困難
├─ クライアント処理が複雑
└─ HTTP標準違反

✅ 良い例:

// 適切なステータスコードを使用
HTTP/1.1 404 Not Found
{
"error": "Not Found",
"message": "User not found"
}

利点:
├─ HTTP意味論の遵守
├─ キャッシング、ロギングの自動化
├─ クライアント処理が簡単
└─ デバッグが容易

🎓 実習してみましょう

1. すべてのステータスコードを処理

async function handleResponse(response) {
// 2xx
if (response.ok) {
if (response.status === 204) {
return null; // No Content
}
return await response.json();
}

// 3xx
if (response.status >= 300 && response.status < 400) {
const location = response.headers.get('Location');
console.log('リダイレクト:', location);
return null;
}

// 4xx
if (response.status >= 400 && response.status < 500) {
const error = await response.json();
throw new Error(error.message || 'クライアントエラー');
}

// 5xx
if (response.status >= 500) {
throw new Error('サーバーエラー');
}
}

2. ステータスコードのテスト

// httpstat.usを使用したテスト
async function testStatusCodes() {
const codes = [200, 201, 204, 400, 401, 403, 404, 500, 503];

for (const code of codes) {
try {
const response = await fetch(`https://httpstat.us/${code}`);
console.log(`${code}: ${response.statusText}`);
} catch (error) {
console.error(`${code}: エラー`, error);
}
}
}

🔗 関連文書

🎬 おわりに

HTTPステータスコードはクライアントとサーバー間のコミュニケーションを明確にします。適切なステータスコードを使用することで、デバッグが容易になり、エラー処理が簡単になり、HTTPの標準に準拠した堅牢なAPIを作成できます!

次のステップ: HTTPヘッダーを読んで、Content-Type、Authorizationなどのヘッダーを学びましょう。