如何使POST请求具有幂等性防止重复提交 - mscharhag


幂等性是一个积极的 API 特性。它有助于使 API 更具容错性,因为客户端可以在出现连接问题时安全地重试请求。
HTTP 规范将 GET、HEAD、OPTIONS、TRACE、PUT 和 DELETE 方法定义为幂等的。这些方法中的 GET、PUT 和 DELETE 是 REST API 中通常使用的方法。以幂等方式实现 GET、PUT 和 DELETE 通常不是什么大问题。
POST 和 PATCH 有点不同,它们都没有被指定为幂等的。但是,两者都可以在幂等性方面实现,从而使客户在出现问题时更容易。在这篇文章中,我们将探讨使 POST 和 PATCH 请求幂等的不同选项。
 
使用唯一的业务约束
在创建新资源(通常通过 POST 表示)时提供幂等性的最简单方法是独特的业务约束。
例如,假设我们要创建一个需要唯一电子邮件地址的用户资源:

POST /users

{
    "name": "John Doe",
   
"email": "john@doe.com"
}

如果客户端不小心发送了两次此请求,则第二个请求将返回错误,因为具有给定电子邮件地址的用户已存在。在这种情况下,通常会返回 HTTP 400(错误请求)或 HTTP 409(冲突)作为状态代码。
请注意,用于提供幂等性的约束不必是请求正文的一部分。URI 部分和关系也有助于形成唯一约束。
一个很好的例子是与父资源以一对一关系关联的资源。例如,假设我们要使用给定的订单 ID 支付订单。
付款请求可能如下所示:


POST /order/<order-id>/payment

{
    ... (payment details)
}

一个订单只能支付一次,因此/payment与其父资源/order/<order-id> 是一对一的关系。如果给定订单已经存在付款,则服务器可以拒绝任何进一步的付款尝试。
 
使用 ETag
Etag是使更新请求幂等的好方法。ETag 由服务器根据当前资源表示生成。ETag 在ETag标头值中返回。例如:
请求:
GET /users/123

响应:
HTTP/1.1 200 Ok
ETag: "a915ecb02a9136f8cfc0c2c5b2129c4b"

{
   
"name": "John Doe",
   
"email": "john@doe.com"
}

现在假设我们要使用JSON Merge Patch请求来更新用户名:

PATCH /users/123
If-Match: "a915ecb02a9136f8cfc0c2c5b2129c4b"

{
   
"name": "John Smith"
}

我们使用If-Match条件告诉服务器仅在 ETag 匹配时才执行请求。更新资源会导致服务器端更新 ETag。因此,如果请求被意外发送两次,服务器会拒绝第二次请求,因为 ETag 不再匹配。在这种情况下,通常应该返回 HTTP 412(前提条件失败)。
显然 ETags 只能在资源已经存在的情况下使用。所以这个解决方案不能用于在创建资源时确保幂等性。从好的方面来说,这是一种标准化且易于理解的方式。
 
使用单独的幂等键
另一种方法是使用单独的客户端生成的密钥来提供幂等性。通过这种方式,客户端生成一个密钥并使用自定义标头(例如Idempotency-Key)将其添加到请求中。
例如,创建新用户的请求可能如下所示:
POST /users
Idempotency-Key: 1063ef6e-267b-48fc-b874-dcf1e861a49d

{
    "name": "John Doe",
   
"email": "john@doe.com"
}

现在服务器可以保留幂等密钥Idempotency-Key并拒绝使用相同密钥的任何进一步请求。
使用这种方法有两个问题需要考虑:
  • 如何处理尚未成功完成的请求(例如通过返回 HTTP 4xx 或 5xx 状态代码)?在这些情况下,服务器是否应该保存幂等密钥?如果是这样,客户端如果想重试请求,总是需要使用新的幂等密钥。
  • 如果服务器使用已知幂等键检索请求,返回什么。

我个人倾向于仅在请求成功完成时才保存幂等键。在第二种情况下,我将返回 HTTP 409(冲突)以指示已经执行了具有给定幂等键的请求。
但是,这里的意见可能会有所不同。例如,Stripe API 使用 Idempotency-Key 标头。Stripe 在所有情况下都保存幂等键和返回的响应。如果提供的幂等键已经存在,则无需再次执行操作即可返回存储的响应。
在我看来,后者可能会使客户感到困惑。另一方面,它为客户端提供了再次检索先前执行的请求的响应的选项。
 

概括
一个简单的唯一业务密钥可用于为创建资源的操作提供幂等性。
对于非创建操作,我们可以将服务器生成的 ETag 与If-Match标头结合使用。这种方法具有标准化和广为人知的优点。
作为替代方案,我们可以使用自定义请求标头中提供的客户端生成的幂等性密钥。服务器保存那些幂等密钥并拒绝包含已使用幂等密钥的请求。这种方法可用于所有类型的请求。但是,它不是标准化的,有一些需要考虑的地方。