HTTP Methods: Idempotent và Safe
1. Safe Methods
Một method được coi là "safe" (an toàn) nếu nó không gây ra sự thay đổi trạng thái server. Hay nói các khác, các method này được dùng để read-only (chỉ đọc). HTTP RFC xác định các method không làm thay đổi resource như GET, HEAD, OPTIONS và TRACE là safe method. Còn những method làm thay đổi resource như POST, PUT, PATCH, DELETE không phải là safe method.
Trong thực tế, thường không thể thực hiện safe method mà không làm thay đổi trạng thái của server, vì một GET request có thể được lưu vào file log của server hay làm mới bộ nhớ cache, đây được gọi side effect của method. Điểm khác biệt quan trọng ở đây là client không yêu cầu các side effect, do đó cũng không chịu trách nhiệm về chúng.
2. Idempotent methods
Idempotency là một khái niệm trong khoa học máy tính và toán học mô tả một thao tác hoặc hàm mà khi được áp dụng nhiều lần sẽ có tác dụng và kết quả như lần đầu (nghe khá giống pure function đúng không, nhưng idempotency thì có thể có side effect còn pure function thì không). Idempotency là một tính chất rất quan trọng để thiết kế một hệ thống api có khả năng chịu lỗi.
Ví dụ, có một lỗi ở phía frontend khiến cho một endpoint bị gọi liên tiếp nhiều lần, thì phía backend phải đảm bảo dù endpoint này có bị gọi nhiều lần, nhưng với cùng một input thì nó sẽ không cho ra kết quả khác nhau. Để làm được điều này thì api cần phải đảm bảo tính idempotency. Vậy cần phải biết được một method có phải idempotent method hay không, nếu không thì cần làm gì để đảm bảo idempotency cho method đấy.
Một method được coi là "idempotent" nếu method đó được gọi nhiều lần thì cũng chỉ cho ra một kết quả. Các method sau đây là idempotent: GET, HEAD, OPTIONS, TRACE, PUT và DELETE. Còn POST và PATCH sẽ không phải là idempotent method.
Cần lưu ý là POST và PATCH không phải là idempotent method không có nghĩa là nó không thể idempotency, mà nó có nghĩa là nó CÓ THỂ idempotency HOẶC KHÔNG idempotency TÙY vào cách người thực hiện. Đoạn này hơi rối một chút, thì giải thích đơn giản hơn là idempotent method là lúc nào cũng idempotency, còn không phải idempotent method thì có thể idempotency hoặc không idempotency tùy thuộc vào người sử dụng.
Đối với các method read-only như GET, HEAD, OPTIONS, TRACE thì khá dễ hiểu vì không làm thay đổi resource nên có request bao nhiêu lần cũng vậy.
PUT và DELETE được coi là idempotent vì với cùng một input sẽ cho ra cùng một kết quả cho dù có thực hiện request bao nhiêu lần.
DELETE Method
Method DELETE dùng để xóa một resource:
DELETE /projects/123
Lần đầu thực hiện request, server sẽ xóa project có id là 123, kết quả là project 123 bị xóa, những lần sau thực hiện lại request, thì do project 123 đã bị xóa rồi nên sẽ không làm thay đổi kết quả.
PUT Method
Method PUT dùng để update toàn bộ hoặc tạo mới một resource (nếu chưa có):
PUT /users/123
Content-Type: application/json
{"name": "John", "age": "30"}
Lần đầu thực hiện request, server sẽ update toàn data của user 123 thành {"name": "John", "age": "30"}
, sau đó, thực hiện lại request trên bao nhiêu lần thì kết quả của user 123 cũng là {"name": "John", "age": "30"}
.
POST Method
Method POST được sử dụng để tạo mới resource:
POST /users/
Content-Type: application/json
{"name": "Max"}
Mỗi lần thực hiện request trên, do server tự tạo id nên một document dạng {"_id": "example_id", "name": "Max"}
sẽ được tạo ra, điều này dẫn đến hiện tượng duplicate dữ liệu. Trong thực tế, sẽ có những trường hợp hiện tượng trên là mục đích của người tạo request, nhưng chúng ta sẽ không bàn đến trường hợp đấy ở đây mà mình chỉ đang muốn lấy ví dụ khi nào thì POST không idempotency.
PATCH Method
Method PATCH được dùng để update một phần resource (khác với PUT là update toàn bộ):
PATCH /users/123
Content-Type: application/json
{"age": "30"}
Đây chắc hẳn sẽ là dạng ví dụ mà nhiều bạn sẽ nghĩ đến khi nhắc đến method PATCH và sẽ thắc mắc tại sao PATCH lại không phải idempotent method vì cho dù thực hiện bao nhiêu lần thì user 123 cũng chỉ thay đổi "age" thành "30". Đúng là như vậy, trong trường hợp này thì PATCH có tính chất idempotency.
Vậy trong trường hợp nào thì PATCH không idempotency? Đến đây chúng ta sẽ cần làm quen với một khái niệm là JSON Patch.
3. JSON Patch
JSON Patch (RFC 6902) là định dạng để mô tả những thay đổi với một JSON document. Nó cho phép sửa đổi một phần JSON document thông qua một chuỗi các thao tác.
Ví dụ về JSON Patch:
[
{ "op": "replace", "path": "/name", "value": "Bob" },
{ "op": "add", "path": "/country", "value": "Fantasyland" },
{ "op": "remove", "path": "/age" }
]
Qua ví dụ trên thì có thể thấy bản thân JSON Patch là một mảng JSON, trong đó mỗi element là một object chứa thông tin thao tác. Các fields chính trong object là:
"op": hành động được thực hiện. Các hành động thường gặp là:
- "add": Thêm một giá trị vào một vị trí được chỉ định trong tài liệu.
- "remove": Xóa giá trị tại một vị trí được chỉ định.
- "replace": Thay thế giá trị tại một vị trí được chỉ định bằng một giá trị mới.
- "move": Di chuyển một giá trị từ vị trí này sang vị trí khác.
- "copy": Sao chép một giá trị từ vị trí này sang vị trí khác.
- "test": Kiểm tra xem một giá trị tại một vị trí đã chỉ định có bằng một giá trị nhất định hay không.
"path": Một chuỗi chứa JSON Pointer xác định vị trí trong tài liệu nơi thao tác sẽ được áp dụng.
"value": Giá trị được sử dụng trong hành động. Trường này là bắt buộc đối với các hành động "add", "replace" và "test".
"from": Một chuỗi chứa JSON Pointer xác định vị trí cho các hành động "move" và "copy".
Ví dụ cụ thể về cách sử dụng JSON Patch
Ta có một JSON document ban đầu:
{
"name": "Alice",
"age": 30,
"city": "Wonderland"
}
Và một JSON Patch:
[
{ "op": "replace", "path": "/name", "value": "Bob" },
{ "op": "add", "path": "/country", "value": "Fantasyland" },
{ "op": "remove", "path": "/age" }
]
Sau khi áp dụng JSON Patch cho JSON document:
{ "op": "replace", "path": "/name", "value": "Bob" }
: Thay thế value ở field "name" thành "Bob".{ "op": "add", "path": "/country", "value": "Fantasyland" }
: Thêm field "country" có value "Fantasyland" vào.{ "op": "remove", "path": "/age" }
: Xóa field "age" đi.
Kết quả sau khi thực hiện:
{
"name": "Bob",
"city": "Wonderland",
"country": "Fantasyland"
}
Tại sao cần dùng JSON Patch?
Tại sao cần phải dùng JSON Patch để làm những việc bên trên? Tại sao không chỉ đơn giản là thay thế JSON document ban đầu bằng cái mới, hay chỉnh sửa JSON document bằng javascript...
Đúng, chúng ta hoàn toàn có thể làm vậy, nhưng đó là đối với những JSON document nhỏ. Còn với những JSON document lớn (có vài chục, thậm chí hàng trăm field) thì việc thay thế toàn bộ hay chỉnh sửa bằng js sẽ rất mất thời gian và tốn tài nguyên.
Lợi ích của JSON Patch
Tiết kiệm băng thông, tăng tốc độ thực hiện request: Thay vì phải gửi toàn bộ JSON document về server, client chỉ cần gửi JSON Patch với nội dung cần thay đổi về, giúp tiết kiệm băng thông và giảm thời gian thực hiện request.
Giảm tải máy chủ: Thay vì phải xử lý toàn bộ JSON document thì server chỉ cần phải xử lý những phần thay đổi, giúp tiết kiệm tài nguyên hơn.
Giảm thiểu lỗi: Việc xử lý toàn bộ JSON document cũng rất dễ xảy ra nhầm lẫn dẫn đến lỗi, việc sử dụng JSON Patch giúp ta kiểm soát, hạn chế lỗi tốt hơn do chỉ làm việc với từng phần cần chỉnh sửa.
Tăng khả năng tương tác: Do được tiêu chuẩn hóa ở đặc tả RFC 6902 nên JSON Patch có thể hoạt động trên nhiều ngôn ngữ lập trình khác nhau, giúp có thể giao tiếp giữa 2 hệ thống không đồng nhất.
Nhược điểm của JSON Patch
Chỉ mang lại hiệu quả rõ rệt trên những JSON document lớn: Chúng ta thường làm việc với những JSON document nhỏ nên thường sẽ không cần sử dụng đến.
Tăng độ phức tạp: Khi sử dụng JSON Patch, cả phía client và server đều sẽ phải thêm một lớp để handle. Đồng thời, cũng phải xây dựng thêm một cơ chế xử lý lỗi, chẳng hạn như khi hành động hoặc path bị sai...
Khó thực hiện và maintain: Đối với những JSON document có nhiều object lồng nhau thì việc sử dụng và maintain JSON Patch cũng rất khó khăn.
Non-Idempotency: Nhược điểm chính mà mình muốn nói đến trong bài này. Khi sử dụng PATCH kết hợp với JSON Patch có thể khiến method PATCH không idempotency.
Ví dụ về Non-Idempotency của PATCH với JSON Patch
JSON document ban đầu:
{
"_id": 123,
"name": "Max",
"age": "30",
"hobbies": []
}
Request:
PATCH /user/123
Content-Type: application/json-patch+json
[
{ "op": "add", "path": "/hobbies/-", "value": "reading" }
]
Mỗi khi request này được gọi, user 123 sẽ có thêm một hobby "reading" vào cuối mảng hobbies. Sau 3 lần gọi, user 123 lúc này sẽ trở thành:
{
"_id": 123,
"name": "Max",
"age": "30",
"hobbies": ["reading", "reading", "reading"]
}
Đến đây, chúng ta đã biết vì sao PATCH không phải là idempotent method.
4. Tổng kết
HTTP Method | Safe | Idempotent |
---|---|---|
GET | ✓ | ✓ |
HEAD | ✓ | ✓ |
OPTIONS | ✓ | ✓ |
TRACE | ✓ | ✓ |
PUT | ✗ | ✓ |
DELETE | ✗ | ✓ |
POST | ✗ | ✗ |
PATCH | ✗ | ✗ |
Qua những thông tin trên, ta có các kết luận sau:
Safe method là những methods không làm thay đổi trạng thái của server (GET, HEAD, TRACE, OPTIONS).
Idempotent method là những methods cho dù có được gọi nhiều lần (với cùng một input) thì cũng chỉ cho ra cùng một kết quả (GET, HEAD, TRACE, OPTIONS, PUT, DELETE).
JSON Patch được sử dụng để chỉnh sửa JSON document lớn (có hàng chục, hàng trăm fields). Và sử dụng JSON Patch có thể làm method PATCH không idempotency.