紀錄工作經驗、相關知識,解決技術相關問題。

工作雜談, 網站相關, 資訊相關

RESTful API 請求方法怎麼選?HTTP 各方法優缺點比較、冪等性與串接避坑要點

RESTful API 請求方法怎麼選?HTTP 各方法優缺點比較、冪等性與串接避坑要點 特色圖片

在規劃前後端分離架構與設計 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)。這種做法會帶來三個嚴重後果:

  1. 喪失 HTTP 快取與 CDN 加速能力:瀏覽器、代理伺服器與 CDN 預設只對 GET 進行快取,把查詢寫成 POST 會讓所有讀取流量 100% 直接打穿到後端資料庫。
  2. 喪失自動重試(Retry)的冪等保護:閘道器(API Gateway)與負載平衡器看到 GET、PUT、DELETE 斷線時可以安全重試,但看到 POST 完全不敢自動重試。
  3. 語義混亂難以維護:網址列充斥著動詞,違反 RESTful「網址只放名詞資源(如 /api/users/10)、用 HTTP 方法表達動作」的統一介面原則。

3. 當「查詢條件太長或含敏感篩選」導致 GET 裝不下時怎麼辦?

標準讀取雖然應該用 GET,但 GET 將參數放在 URL Query String 中會有兩個物理限制:

  1. 複雜報表系統可能包含數十個陣列篩選條件,導致 URL 超過瀏覽器或 Nginx 的長度上限(通常為 2KB ~ 8KB)而回傳 414 URI Too Long。
  2. 查詢條件若包含身分證字號或電話號碼,放在 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 不具備冪等性,後端直接建立了三筆一模一樣的訂單。
  • 原因與解法:
    1. 前端防呆:點擊送出瞬間立即將按鈕設為 disabled(Loading 狀態)或使用 AbortController 取消前一次未完成的請求。
    2. 後端冪等保護:進入結帳頁時由前端產生一組唯一 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 設計與串接,關鍵在於選對語義並做好邊界防禦:

  1. 依資料操作本質選方法:純查詢用 GET、新建資源用 POST、整筆覆蓋用 PUT、局部改欄位優先用 PATCH、刪除資源用 DELETE。
  2. 牢記冪等性差異:GET、PUT、DELETE 天生冪等可安全重試;POST 非冪等,重要交易務必加上前端防連點與後端 Idempotency-Key 保護。
  3. 統一封裝前端請求:透過共用函式集中處理 !response.ok 錯誤攔截與 204 No Content 空回應,即可徹底避開日常串接最常見的崩潰陷阱。

參考資料與延伸閱讀

發表迴響