iT邦幫忙

2022 iThome 鐵人賽

DAY 24
0
Modern Web

這些那些你可能不知道我不知道的Web技術細節系列 第 24

你可能不知道的HTTP Header--If-Match和該怎麼設計Web API

前言

問個 假如我今天有個表單在前端要送,然後我想要先檢查這個表單有沒有被其他人更新

這是在一次和朋友的討論中有人提出來的問題。

恩...這是很典型資料競爭的問題,但並不是多個執行緒存取一份資料,而是多個瀏覽器客戶端(Browser Client)存取同份伺服器資源。

通常,我們在討論RESTful - 一種當下很常見的Web API設計方式的時候,只會提到建立(Create / POST)、讀取(Read / GET)、更新(Update / PUT)、補充(PATCH)和刪除(DELETE),也就是一般的CRUD。甚少會在深度討論一些細節,就如開頭提到的問題。並且通常在瀏覽網站,也很少去注意各個網站的API設計方式。

那麼這個問題要怎麼解決呢?

不管怎麼說,伺服器上的資料正確性最終需要由伺服器處理保護。
可以簡單一些在資料上添加版本(Version)或最後修改時間(Last Update time)的欄位。當收到更新或刪除訊息,該欄位應該保持一致。不過這就會出現一個很奇妙的現象:

> GET /data1


< 200 OK
< Content-Type: application/json
< 
< {"name": "Bob", "age": 18, "version": 1}

當我們有一個資料data1其版本為1,如果我們需要更新的話,下面的訊息代表什麼意思?

> PUT /data1
> Content-Type: application/json
> 
> {"name": "Alice", "age": 18, "version": 1}


< 204 No Content

這是一個更新,版本是不是應該跳到2?但是按照前述這個版本應該要與記錄一致才會被修改,但是這樣似乎又與PUT的意圖衝突?

或許有一些設計是檢查更新版本-1是不是與當前記錄版本相同作為檢查判斷條件。但是想想,其實版本號的決定,似乎不該由Client傳進的資料決定,而應該由後端資料管理行為決定。

那麼來介紹另一種設計方式,其實HTTP一般性規範都幫你準備好了,只是在我經驗觀察上,似乎並沒有那麼易用(也可能是因為了解的人真的不多),並不經常看到這樣的設計。這可能會包含一些你可能不知道的HTTP狀態碼(HTTP Status Code)和HTTP Headers。

透過HTTP Header添加預檢條件

像是這樣只是檢查而不是更新的資料,HTTP規範裡其實定義了一些可選的Header:

  • If-Match
  • If-None-Match
  • If-Modified-Since
  • If-Unmodified-Since
  • If-Range
  • ETag

繼續以上個例子來說,版號version可能並不是客戶端需要在意的資料,可以將移至Header的ETag:

> GET /data1


< 200 OK
< Content-Type: application/json
< ETag: 1
< 
< {"name": "Bob", "age": 18}

生成ETag常用的方法包括對資源內容使用抗碰撞散列函數,使用最近修改的時間戳的哈希值,甚至只是一個版本號。^1

而當要更新資料的時候,帶著相同的ETag或是更明確的使用If-Match

> PUT /data1
> Content-Type: application/json
> ETag: 1
> If-Match: 1
> 
> {"name": "Alice", "age": 18}


< 204 No Content

當下次下詢問資料ETag也就會變更:

> GET /data1


< 200 OK
< Content-Type: application/json
< ETag: 2
< 
< {"name": "Alice", "age": 18}

或者也可以使用Last-ModifiedIf-Unmodified-Since

> GET /data1


< 200 OK
< Content-Type: application/json
< Last-Modified: Sat, 08 Oct 2022 04:24:55 GMT
< 
< {"name": "Bob", "age": 18}

而伺服器是檢查If-Unmodified-Since條件進行處理:

> PUT /data1
> Content-Type: application/json
> If-Unmodified-Since: Sat, 08 Oct 2022 04:24:55 GMT
> 
> {"name": "Alice", "age": 18}


< 204 No Content

The response 204 means that the request was processed successfully.^2^3

如果在檢驗時間後已經被變動過了,則拒絕修改:

< 412 Precondition Failed

When a client makes a conditional request to a resource, the request succeeds only if the resource has not been updated since the client last accessed that resource. This also helps in preventing corruption of the resource since some updates to a resource can only be performed starting from a certain base point. ^2^4

類似的在Long Polling有使用到相關的Header,不同的是需要變動的資料才取回來,所以是使用If-Modified-Since。其實Pooling同樣可以做類似設計,當資料未被變動,不是直接回傳資料,而是回傳304資料未修改,這樣做可以減少網路傳輸:

< 304 Not Modified

該怎麼設計Web API

說穿了伺服器與客戶端的溝通,只要雙方可以理解,有一致概念就可以運作。這一節也只是提供一些基於預檢條件可以作為參考的設計而已。

PUT代表更新整筆資料;PATCH代表更新部分資料

The HTTP PATCH request method applies partial modifications to a resource.(MDN)

Unlike PUT Request, PATCH does partial update e.g. Fields that need to be updated by the client, only that field is updated without modifying the other field.^5

繼續以上面例子只是更改名字,所以同樣可以使用PATCH

> PATCH /data1
> Content-Type: application/json
> If-Unmodified-Since: Sat, 08 Oct 2022 04:24:55 GMT
> 
> {"name": "Alice"}


< 204 No Content



> GET /data1

< 200 OK
< Content-Type: application/json
< Last-Modified: Sat, 08 Oct 2022 04:25:55 GMT
< 
< {"name": "Alice", "age": 18}

同樣的資料內容代表刪除其他未出現的欄位:

> PUT /data1
> Content-Type: application/json
> If-Unmodified-Since: Sat, 08 Oct 2022 04:24:55 GMT
> 
> {"name": "Alice"}


< 204 No Content



> GET /data1

< 200 OK
< Content-Type: application/json
< Last-Modified: Sat, 08 Oct 2022 04:25:55 GMT
< 
< {"name": "Alice"}

PATCH同樣可以指定值為null表示刪除特定欄位:

> PATCH /data1
> Content-Type: application/json
> If-Unmodified-Since: Sat, 08 Oct 2022 04:24:55 GMT
> 
> {"age": null}


< 204 No Content



> GET /data1

< 200 OK
< Content-Type: application/json
< Last-Modified: Sat, 08 Oct 2022 04:25:55 GMT
< 
< {"name": "Bob"}

要求預檢,如果未給預檢條件則回傳428狀態碼

當缺少預檢條件,就可能需要計算內容的雜湊值,檢查與原資料是否相同。還有更簡單的方式是直接回傳狀態碼428,表是需要預檢條件。

> PUT /data1
> Content-Type: application/json
> 
> {"name": "Alice", "age": 18}


< 428 Precondition Required

預檢失敗回傳412狀態碼

如果預檢失敗,就需要拒絕處理請求,並回傳狀態碼412表示處理失敗。

> PUT /data1
> Content-Type: application/json
> ETag: 8
> If-Match: 8
> 
> {"name": "Alice", "age": 18}


< 412 Precondition Failed

使用206狀態碼回傳部分內容

其實Pooling同樣可以做類似設計,當資料未被變動,不是直接回傳資料,而是回傳304資料未修改,這樣做可以減少網路傳輸。

除了 304 ,還可以只回傳差異內容減少網路傳輸。

< 206 Partial Content
< 
< {"name": "Alice"}

不過差異比對的方式通常有蠻多需要考量的,這感覺並不實用哈哈。

參考資料

本文同時發表於我的隨筆


上一篇
你可能不知道的JS自動型別轉換
下一篇
你可能不知道的跨站腳本攻擊(Cross-Site Scripting,XSS)
系列文
這些那些你可能不知道我不知道的Web技術細節33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言