跳至正文

🚦 HTTP 狀態碼

📖 定義

HTTP 狀態碼是一個三位數字,用於告訴客戶端伺服器如何處理其請求。根據第一位數字,它分為五個組別。

🎯 用比喻來理解

餐廳訂單系統

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": "wang@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": "wang@example.com"
}

回應:
HTTP/1.1 201 Created
Location: /api/users/123
Content-Type: application/json

{
"id": 123,
"name": "王小明",
"email": "wang@example.com"
}
fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: '王小明',
email: 'wang@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": "找不到使用者"
}

問題:
├─ 妨礙 HTTP 快取
├─ 難以追蹤錯誤
├─ 客戶端處理複雜
└─ 違反 HTTP 標準

✅ 好的做法:

// 使用適當的狀態碼
HTTP/1.1 404 Not Found
{
"error": "Not Found",
"message": "找不到使用者"
}

優點:
├─ 遵守 HTTP 語義
├─ 自動化快取、記錄
├─ 簡化客戶端處理
└─ 便於除錯

🎓 實作練習

1. 處理所有狀態碼

async function handleResponse(response) {
// 2xx
if (response.ok) {
if (response.status === 204) {
return null; // 無內容
}
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 等標頭。