在規劃前後端分離架構與設計 RESTful API 時,開發者最常面臨的抉擇不是「怎麼寫出請求語法」,而是「什麼場景該選哪種 HTTP 請求方法?」例如修改會員資料時該用 PUT 還是 PATCH?為什麼把所有 API 都偷懶寫成 POST 會導致快取失效與重複扣款災難?
本文為 HTTP 請求方法系列的進階延伸篇,聚焦於各方法的優缺點權衡、安全性與冪等性(Idempotency)規格對照、易混淆場景選型決策,以及生產環境通用的 Fetch 封裝與排錯。若您想先了解 HTTP 超文本傳輸協定的基礎定義與各方法的入門語法,建議先前往第一篇 淺談 HTTP 請求方法 (HTTP Request Methods):GET、POST、PUT、DELETE 基礎概念與 Fetch 入門 觀看。
一、一張表看懂 HTTP 請求方法:優缺點與技術特性對照
在實務專案中挑選 HTTP 方法時,必須同時評估該方法的「優缺點限制」與底層協定定義的「冪等性(Idempotency)與快取特性」。以下透過兩張對照表完整拆解:
1. 七大 HTTP 請求方法優缺點與適用場景總表
| HTTP 方法 | 核心優點 | 使用限制與缺點 | RESTful API 典型適用場景 |
|---|---|---|---|
| GET | 簡單高效;支援瀏覽器/CDN 快取與加入書籤 | 參數暴露於 URL 且受長度限制;嚴禁傳送密碼等敏感資料 | 取得文章列表、查詢商品詳情、關鍵字分頁檢索 |
| POST | 靈活度最高;Body 可傳輸大量 JSON 或二進位檔案 | 不具備冪等性與預設快取;連點重送會重複建立資源 | 會員註冊、登入驗證、建立新訂單、上傳圖片或檔案 |
| PUT | 語義明確且具備冪等性;重試多次結果一致 | 需傳送完整資源欄位,若漏傳欄位可能將舊資料覆蓋清空 | 整筆覆蓋更新使用者設定檔、指定固定識別碼建立或替換資源 |
| PATCH | 節省頻寬;僅需傳送有變更的單一或少數欄位 | 實作複雜度較高,需注意併發局部更新時的欄位衝突 | 單獨修改使用者 Email、切換訂單狀態開關、變更文章標題 |
| DELETE | 語義直觀且具備冪等性,明確表達移除資源意圖 | 屬於高風險不可逆操作,需嚴格控管權限與二次確認 | 刪除指定文章、註銷帳號、從購物車移除特定商品 |
| HEAD | 極省頻寬;僅回傳標頭,不消耗主體傳輸流量 | 無回應主體(Body),僅能讀取狀態碼與 Header 資訊 | 下載大型檔案前預先檢查 Content-Length 大小或檔案是否存在 |
| OPTIONS | 自動探測伺服器支援的方法與跨來源(CORS)權限 | 僅供通訊預檢與除錯,不執行實際業務資料處理 | 瀏覽器跨域非簡單請求前的 CORS Preflight 預檢請求 |
2. 架構設計與面試必考:安全性(Safe)、冪等性(Idempotent)與快取對照
在 RFC 9110(HTTP Semantics)規範中,有兩個決定 API 容錯與重試機制的關鍵指標:
- 安全性(Safe / 唯讀性):指該請求「不會改變伺服器上的任何資源狀態」(純讀取)。
- 冪等性(Idempotent):指「同一個請求連續發送 1 次與連續發送 10 次,對伺服器資源產生的最終狀態完全相同」。這決定了當網路發生 Timeout 時,前端或 API Gateway 能不能自動重試(Retry)該請求。
| HTTP 方法 | 安全性 (Safe 唯讀) | 冪等性 (Idempotent) | 網路斷線自動重試與快取特性 |
|---|---|---|---|
| GET / HEAD / OPTIONS | 是(純讀取) | 是 | 可安全自動重試;GET 與 HEAD 支援快取 |
| PUT | 否(會修改資源) | 是(覆蓋為相同狀態) | 可安全自動重試(重複覆蓋結果不變);預設不快取 |
| DELETE | 否(會刪除資源) | 是(目標維持已刪除) | 可安全自動重試(刪 N 次最終皆不存在);不快取 |
| PATCH | 否(會修改資源) | 否(視實作而定) | 需視後端是否為絕對值更新而定;不快取 |
| POST | 否(會新增資源) | 否(每次皆新建一筆) | 不可直接重試(重送會造成重複建單/扣款);預設不快取 |
二、RESTful API 設計最常搞混的 3 組選型決策
1. 更新資料該用 PUT 還是 PATCH?(全量覆蓋 vs 局部更新)
這是前後端訂定 API 規格時最常爭論的問題。假設資料庫裡有一筆編號 10 的會員資料:
{ "id": 10, "name": "Sean", "email": "[email protected]", "role": "admin" }
現在前端只想把 email 改成 "[email protected]":
- 若使用
PUT /api/users/10(全量替換):
前端必須把整包完整欄位都傳過去。如果前端只傳了{ "email": "[email protected]" },遵循嚴格PUT語義的後端會把沒傳的name與role視為要清空,直接覆蓋成null! - 若使用
PATCH /api/users/10(局部更新):
前端只需要傳送有變動的欄位{ "email": "[email protected]" },後端只會更新email,原有的name與role保持不變。 - 實務選型建議:表單「整頁儲存」可用
PUT;若是列表上的「狀態開關切換、單一欄位快速編輯」,請一律設計為PATCH,既省頻寬又能避免誤清空其他欄位。
2. 為什麼「所有 API 一律無腦用 POST」是危險的反模式?
有些趕進度的專案會把查詢、修改、刪除全部寫成 POST(例如 POST /api/getUser、POST /api/updateUser、POST /api/deleteUser)。這種做法會帶來三個嚴重後果:
- 喪失 HTTP 快取與 CDN 加速能力:瀏覽器、代理伺服器與 CDN 預設只對
GET進行快取,把查詢寫成POST會讓所有讀取流量 100% 直接打穿到後端資料庫。 - 喪失自動重試(Retry)的冪等保護:閘道器(API Gateway)與負載平衡器看到
GET、PUT、DELETE斷線時可以安全重試,但看到POST完全不敢自動重試。 - 語義混亂難以維護:網址列充斥著動詞,違反 RESTful「網址只放名詞資源(如
/api/users/10)、用 HTTP 方法表達動作」的統一介面原則。
3. 當「查詢條件太長或含敏感篩選」導致 GET 裝不下時怎麼辦?
標準讀取雖然應該用 GET,但 GET 將參數放在 URL Query String 中會有兩個物理限制:
- 複雜報表系統可能包含數十個陣列篩選條件,導致 URL 超過瀏覽器或 Nginx 的長度上限(通常為
2KB ~ 8KB)而回傳414 URI Too Long。 - 查詢條件若包含身分證字號或電話號碼,放在 URL 會被明文記錄在伺服器 Access Log 與瀏覽器歷史紀錄中。
- 實務折衷解法:針對這類「超大條件或敏感條件查詢」,業界標準做法是設計一個語義明確的子資源端點
POST /api/orders/search或POST /api/orders/query,將複雜篩選 JSON 放在 Request Body 中傳送,兼顧安全性與長度需求。
三、告別重複樣板:生產環境通用的 Fetch API 請求封裝
在真實專案中,我們不會在每個頁面重複手寫幾十次零散的 fetch(),而是會封裝一個統一處理錯誤狀態碼、204 No Content 空回應、請求逾時(Timeout)與防重複提交標頭的共用函式:
/**
* 生產環境通用 RESTful API 請求封裝函式
* @param {string} endpoint - API 路徑 (例如 '/users/10')
* @param {object} options - 設定項 { method, body, headers, timeoutMs, idempotencyKey }
*/
async function apiRequest(endpoint, {
method = 'GET',
body = null,
headers = {},
timeoutMs = 8000,
idempotencyKey = ''
} = {}) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
const requestHeaders = {
'Accept': 'application/json',
...headers
};
// 若有傳入主體資料且非 FormData,自動設定 JSON 標頭
if (body !== null && !(body instanceof FormData)) {
requestHeaders['Content-Type'] = 'application/json';
}
// 針對非冪等的 POST 重要交易(如建單/扣款),支援注入 Idempotency-Key 防重複執行
if (idempotencyKey) {
requestHeaders['Idempotency-Key'] = idempotencyKey;
}
try {
const response = await fetch(`https://api.example.com${endpoint}`, {
method: method.toUpperCase(),
headers: requestHeaders,
body: body !== null ? (body instanceof FormData ? body : JSON.stringify(body)) : undefined,
signal: controller.signal
});
// 關鍵防呆 1:Fetch 遇到 4xx / 5xx 不會自動 throw,必須手動檢查 response.ok
if (!response.ok) {
const errorPayload = await response.json().catch(() => ({}));
throw new Error(errorPayload.message || `HTTP 請求失敗 (狀態碼: ${response.status})`);
}
// 關鍵防呆 2:DELETE 或 PUT 常回傳 204 No Content(無內容主體),直接呼叫 .json() 會崩潰
if (response.status === 204 || response.headers.get('content-length') === '0') {
return null;
}
return await response.json();
} finally {
clearTimeout(timer);
}
}
// ==================== 實際呼叫示範 ====================
// 1. 讀取列表 (GET)
// const articles = await apiRequest('/articles?page=1');
// 2. 建立訂單並附上冪等鍵防手抖連點 (POST)
// const newOrder = await apiRequest('/orders', {
// method: 'POST',
// body: { productId: 99, quantity: 1 },
// idempotencyKey: 'order-checkout-20250106-uuid01'
// });
// 3. 局部更新單一欄位 (PATCH)
// await apiRequest('/users/10', { method: 'PATCH', body: { email: '[email protected]' } });
// 4. 刪除資源並安全處理 204 空回應 (DELETE)
// await apiRequest('/users/10', { method: 'DELETE' });
四、前後端串接 4 大實務踩坑與排錯要點
1. 發送 PUT / PATCH / DELETE 時觸發 CORS OPTIONS 預檢失敗
- 踩坑現象:前端用
GET跟POST測 API 都正常,一換成PUT、PATCH、DELETE或加上Authorization: Bearer <token>標頭,瀏覽器 Console 立刻報 CORS 錯誤,且 F12 Network 出現一筆紅色的OPTIONS請求。 - 原因與解法:根據瀏覽器同源政策,
PUT、PATCH、DELETE與Content-Type: application/json屬於「非簡單請求」,瀏覽器在送出真實請求前,會強制先發送OPTIONS預檢請求。請通知後端工程師在 CORS Middleware 中明確放行:Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS以及Access-Control-Allow-Headers: Content-Type, Authorization, Idempotency-Key,並確保OPTIONS請求直接回傳204或200(不要被後端登入驗證中介層擋下回傳401)。
2. DELETE 或 PUT 回傳 204 No Content 時呼叫 response.json() 報錯
- 踩坑現象:後端明明已經成功刪除資料並遵循標準回傳
204 No Content,前端卻進入catch印出SyntaxError: Unexpected end of JSON input。 - 原因與解法:
204 No Content代表「執行成功且回應沒有任何 Body 字串」,此時對空字串執行response.json()必然拋出解析錯誤。請如上方封裝範本所示,先判斷if (response.status === 204) return null;。
3. 網路卡頓連點按鈕導致 POST 重複建單或重複扣款
- 踩坑現象:使用者在手機網路不穩時連按三次「送出訂單」,因為
POST不具備冪等性,後端直接建立了三筆一模一樣的訂單。 - 原因與解法:
- 前端防呆:點擊送出瞬間立即將按鈕設為
disabled(Loading 狀態)或使用AbortController取消前一次未完成的請求。 - 後端冪等保護:進入結帳頁時由前端產生一組唯一 UUID 放在 Header
Idempotency-Key隨POST送出;後端於 Redis 檢查該 Key 若已處理過,就直接回傳第一次的結果而不重複寫入資料庫。
- 前端防呆:點擊送出瞬間立即將按鈕設為
4. 誤以為 fetch() 收到 400 / 404 / 500 會自動進入 catch
- 踩坑現象:伺服器回傳
404 Not Found或500 Internal Server Error,前端卻沒有跳進catch (err),反而繼續往下執行渲染導致畫面崩潰。 - 原因與解法:原生
fetch()只有在「完全斷網、DNS 解析失敗或被 CORS 阻擋」時才會 reject Promise;只要伺服器有回應任何 HTTP 狀態碼(包含 4xx 與 5xx),Promise 都會視為 fulfilled。務必手動檢查if (!response.ok)並拋出例外。
五、常見問題(Q&A)
Q1:為什麼有些舊系統或企業內部專案不用 PUT 與 DELETE,只用 GET 和 POST?
早期部分老舊防火牆、代理伺服器或傳統 HTML <form> 表單僅支援 GET 與 POST(詳見 HTML Form GET 與 POST 差異比較與常見使用場景解析),會直接攔截 PUT、PATCH 與 DELETE 封包。若在受限網路環境中想維持 RESTful 架構,常見做法是發送 POST 並搭配自訂標頭 X-HTTP-Method-Override: PUT,由後端框架轉譯為對應方法。
Q2:為什麼規格書說 PUT 一定具備冪等性,但 PATCH 卻「不一定具備冪等性」?
因為 PUT 是「將整筆資源直接替換為指定狀態」,執行 N 次結果都一樣;但 PATCH 允許傳送「增量操作指令」(例如傳送 { "op": "increment", "field": "loginCount", "value": 1 } 要求將登入次數 +1)。如果 PATCH 實作為「每次呼叫加 1」,連續呼叫 10 次就會加 10,此時就不具備冪等性;唯有當 PATCH 實作為「將特定欄位設為固定值」時才具備冪等性。
Q3:批次刪除多筆資料時,DELETE 請求可以帶 Request Body 嗎?
RFC 9110 規範雖然沒有明文禁止 DELETE 攜帶 Body,但許多雲端 WAF 防火牆、CDN(如部分 Cloudflare 預設規則)與代理伺服器會直接丟棄或拒絕帶有 Body 的 DELETE 請求。若需批次刪除少量 ID,建議使用 Query String(如 DELETE /api/items?ids=1,2,3);若 ID 數量龐大,則建議設計為 POST /api/items/batch-delete 並在 Body 傳入陣列最為穩妥。
總結
從基礎語法邁向成熟的 RESTful API 設計與串接,關鍵在於選對語義並做好邊界防禦:
- 依資料操作本質選方法:純查詢用
GET、新建資源用POST、整筆覆蓋用PUT、局部改欄位優先用PATCH、刪除資源用DELETE。 - 牢記冪等性差異:
GET、PUT、DELETE天生冪等可安全重試;POST非冪等,重要交易務必加上前端防連點與後端Idempotency-Key保護。 - 統一封裝前端請求:透過共用函式集中處理
!response.ok錯誤攔截與204 No Content空回應,即可徹底避開日常串接最常見的崩潰陷阱。
發表迴響