当资源已经存在时,POST请求的HTTP响应代码是什么?

1309

我正在构建一个服务器,允许客户端存储对象。这些对象在客户端完全构造,包括对象ID在内,而这些ID对于对象的整个生命周期都是固定的。

我已经定义了API,使得客户端可以使用PUT创建或修改对象:

PUT /objects/{id} HTTP/1.1
...

{json representation of the object}

{id}是对象的ID,因此它是请求URI的一部分。

现在,我还考虑允许客户端使用POST创建该对象:

POST /objects/ HTTP/1.1
...

{json representation of the object, including ID}

由于POST是“追加”操作,如果对象已经存在,我不确定该怎么办。 我应该将请求视为修改请求还是返回某些错误代码(哪个)?


7
截至2016年6月,如果电子邮件已存在,FB在注册时明确设置为200。 - Green
14
使用已存在的名称创建资源(团队/仓库),Github API 返回422错误。 - Ken
2
这取决于您是否认为对象的存在是错误。如果您处理附加操作,那么200或204是最合适的响应代码。 - Suncat2000
2
总的说来,这是一个409冲突和422无法处理实体之间的抉择 - 我认为这里的答案权重指向409,从人类的角度来看更容易理解。 - danday74
@Green 这将防止账户枚举攻击。 - Cameron Martin
1
我用409来表示正常情况,422则表示不良表单。 - Anthony O
18个回答

1604

我认为409冲突是最合适的状态码,但在实际应用中很少见到:

由于与资源当前状态存在冲突,请求无法完成。该状态码仅在预期用户可能能够解决冲突并重新提交请求的情况下使用。响应正文应包含足够的信息,以便用户识别冲突的源头。最理想的情况是响应实体包含足够的信息供用户或用户代理程序修复问题;然而,这可能是不可能的,也不是必需的。

PUT请求最有可能导致冲突。例如,如果正在使用版本控制,并且被PUT的实体包括与先前(第三方)请求所做的更改相冲突的更改,则服务器可能使用409响应来指示无法完成请求。在这种情况下,响应实体可能会以响应Content-Type定义的格式列出两个版本之间的差异列表。


28
为什么不返回400 Bad Request状态码呢?对我来说,这似乎是一种验证错误(您提供了带有非法ID的错误有效负载)。 - manuel aldana
440
"由于语法格式错误,服务器无法理解该请求"。服务器完全理解请求,但由于冲突无法执行。请求和语法没有问题,只是数据有问题。如果返回400状态码,我会立刻认为整个机制都有缺陷,而不仅仅是数据有问题。 - Wrikken
93
@Wrikken那个观点已经不正确了。根据RFC 7231,HTTP 400的含义已经被改变,意思是“由于某些被认为是客户端错误的原因(例如,请求语法不正确、请求消息格式无效或请求路由具有欺骗性),服务器无法或者不会处理该请求”。我不是说在这种情况下400是正确的用法,但是根据新的400定义,它可能是正确的。 - javajavajavajavajava
35
@javajavajavajavajava:在我的看法中,重复数据并不是客户端错误,但当然这取决于不同的观点。 - Wrikken
68
我会返回 HTTP 409 状态码,并在响应头中添加 Location,指向已经存在或发生冲突的资源。 - Gili
显示剩余12条评论

170

这一切都与上下文有关,以及谁负责处理请求的重复(服务器、客户端或两者都有)


如果服务器仅指出重复,请查看4xx:

  • 400 Bad Request - 当服务器不处理请求是因为明显是客户端的问题时
  • 409 Conflict - 如果服务器不处理请求,但原因不是客户端的问题
  • ...

对于隐含的重复处理,请查看2XX:

  • 200 OK
  • 201 Created
  • ...

如果服务器期望返回某些内容,请查看3XX:

  • 302 Found
  • 303 See Other
  • ...

当服务器能够指向现有资源时,它意味着重定向。


如果以上还不够,准备一些错误信息放在响应体中始终是一个好习惯。


4
请求并非复制资源,而是向其附加数据。在我看来,你的回答是最好的。 - Suncat2000
11
所有4xx错误都是客户端的“错”。所有5xx错误都是服务器的“错”。(而提交重复数据是客户端需要修复,而不是服务器。) - Paul Draper
@Paul Draper:当资源已经存在时,5xx没有任何用处。这里4xx、2xx、3xx的顺序不是巧合。大多数情况下会是4xx,但在许多情况下其他状态码也是可以接受的,特别是当客户端完全不知道如何处理重复内容或者根本不重要的情况下。 - Sławomir Lenart

154
根据RFC 7231303 See Other可能会被用于如果处理POST的结果等同于现有资源的表示

5
我认为这可能是被接受的答案。虽然“MAY”表示完全可选的项目,但它是官方RFC 7231文档建议的唯一响应代码。 - Nando
19
这是最符合REST原则的回答。 - Seth
10
我认为上下文很重要。例如:返回303意味着需要重定向到找到的资源。这在服务器之间的调用中可能有意义,但如果你正在进行用户注册流程,这就毫无意义了。 - Sinaesthetic
64
抱歉,我要给这个点赞数打个反对。HTTP 300 状态码是用来进行重定向的,而重定向到另一个可能具有不同属性的对象会非常令人误解。 - Michael Scheper
9
不用道歉。但是,如果表示与现有资源等效,它如何具有不同的属性?即使它有所不同,重定向会如何具有误导性?原帖作者说:我不确定如果对象已经存在应该怎么办。实际上,它是“相同”的对象。为什么重定向会具有误导性?你谈论另一个对象,而在原帖作者的看法中,这个对象显然不是另一个对象。 - Nullius
显示剩余16条评论

118

个人而言,我会选择使用WebDAV扩展 422 Unprocessable Entity

根据RFC 4918

422 Unprocessable Entity状态码表示服务器理解请求实体的内容类型(因此,415 Unsupported Media Type状态码不合适),并且请求实体的语法正确(所以,400 Bad Request状态码不合适),但无法处理其中包含的指令。


26
这是一个有趣的想法,它促使我最终阅读了WebDAV RFC。然而,我认为422的意思是请求和包含的实体在语法上是正确的,但在语义上并没有意义。 - vmj
6
格式不正确的JSON是一个语法上不正确的实体,所以422状态码让我感到奇怪... - awendt
9
我不会选择这个选项。从答案中提供的相同URL引用中可以得知:“例如,如果一个XML请求正文包含格式良好(即语法正确),但语义错误的XML指令,则可能会发生此错误条件。” 这就是“unprocessable entity”实际的含义,与您发送具有有效语法和语义的完全有效请求实体,但唯一的问题是它与现有实体冲突的情况不同。事实上,如果请求实体的语义无效,根本不应该存在类似的现有实体。 - Tamer Shlash
1
补充Tamer的评论,如果第二个请求先到达,则它将成功,这在语义上是不正确的情况下是不可能的。因此,在这里不适用于正确的语义。 - Harish
4
@Tamer 为什么这样做?命令“请创建对象xy”在语法上是正确的。仅当可以创建对象xy时,它才在语义上是正确的。如果对象xy已经存在,则不能再创建它,因此这是一个语义错误。 - Hagen von Eitzen
显示剩余3条评论

41
阅读了这篇以及其他几篇长达数年的关于状态码使用的讨论后,我得出的主要结论是必须仔细阅读规范,关注使用的术语、它们的定义、关系和周围的上下文。而经常发生的情况是,正如不同答案所示,规范的某些部分被剥离出其上下文并孤立地解释,基于感觉和假设。
简短的总结是,在“添加新资源”操作失败的情况下,HTTP 409是报告此错误最合适的状态码,如果具有相同标识符的资源已存在。以下是根据权威来源(RFC 7231)仅基于规范中的内容解释原因的说明。
那么为什么在OP的问题描述中,409 Conflict是最合适的状态码呢?
RFC 7231将409 Conflict状态码描述如下:
“409(Conflict)状态码表示由于与目标资源的当前状态发生冲突,无法完成请求。”
关键组件在于目标资源及其状态目标资源 RFC 7231如下定义资源:
“HTTP请求的目标称为“资源”。 HTTP不限制资源的性质; 它只是定义可能用于与资源交互的接口。 每个资源都由统一资源标识符(URI)标识,如[RFC7230]第2.7节所述。”
因此,当使用HTTP接口时,我们始终通过将HTTP方法应用于它们来操作由URI标识的资源。
当我们的意图是基于OP的示例添加新资源时,我们可以:
- 使用具有资源/objects/{id}PUT; - 使用具有资源/objectsPOST/objects/{id}不感兴趣,因为使用PUT方法时不可能发生冲突:
“PUT方法请求使用请求消息有效负载中包含的表示定义的状态创建或替换目标资源的状态。”
如果具有相同标识符的资源已存在,则将其替换为PUT
因此,我们将重点放在/objects资源和POST上。
RFC 7231关于POST的说法:
POST方法请求目标资源根据其自身特定语义处理请求中包含的表示形式。例如,POST用于以下功能(等等):3)创建尚未由源服务器识别的新资源;4)将数据附加到资源的现有表示形式。与OP理解的POST方法相反,将数据附加到资源的现有表示形式只是可能的POST“功能”之一。此外,OP在提供的示例中实际执行的操作不是直接将数据附加到/objects表示形式,而是创建一个新的独立资源/objects/{id},该资源随后成为/objects表示形式的一部分。但这并不重要。重要的是资源表示的概念,这使我们想起了资源状态。RFC7231解释道:“考虑到资源可以是任何东西,并且HTTP提供的统一接口类似于通过消息通信与另一侧的某个独立参与者观察和对其进行操作的窗口,因此需要一种抽象来表示(“代替”)该事物的当前或所需状态在我们的通信中。该抽象称为表示[REST]。”为了HTTP的目的,"表示"是旨在以协议可以轻松通信的格式反映给定资源的过去、当前或所需状态的信息,由表示元数据和潜在的无限制流表示数据组成。现在我们已经掌握了理解409冲突状态代码所需的两个部分。409(冲突)状态代码表示由于与目标资源的当前状态存在冲突,无法完成请求。
  1. 我们向 /objects 发送 POST 请求 => 我们的目标资源是 /objects
  2. OP 没有描述 /objects 资源,但示例看起来像是一个常见的情况,其中 /objects 是一个资源集合,包含所有单独的 "object" 资源。也就是说,/objects 资源的 状态 包括了所有现有的 /object/{id} 资源的信息。
  3. /objects 资源处理 POST 请求时,它需要 a) 根据请求负载中传递的数据创建一个新的 /object/{id} 资源;b) 通过添加有关新创建资源的数据来修改自己的状态。
  4. 当要创建的资源具有重复标识符时,即已经存在具有相同 /object/{id} URI 的资源时,/objects 资源将无法处理 POST 请求,因为其状态已经包含了重复的 /object/{id} URI。

这正是 409 Conflict 状态码描述中提到的与目标资源当前状态的冲突。


非常好的解释!你已经说服我反对我的答案,这个答案反对错误403。 - Grant Gryczan
1
但是,您的答案也应该更新,因为RFC 9110现在已经使RFC 7231过时了。 - Grant Gryczan
好的观点,Grant!我会仔细看看,看看有哪些变化。 - wombatonfire

38

编辑:我不再完全同意这个观点,现在建议使用错误代码409,但我会保留我的答案,因为我的原始答案是人们赞同的。请参见底部以了解改变我的想法的原因。只有在您同意我的原始答案时才点赞:

我会选择422 Unprocessable Entity,用于在请求无法被处理但问题不在语法或身份验证方面时使用,请参见RFC 9110

422(无法处理的内容)状态码表示服务器理解请求内容的内容类型(因此,415(不支持的媒体类型)状态码不合适),并且请求内容的语法是正确的,但是它无法处理包含的指令。例如,如果XML请求内容包含形式良好(即语法上正确),但语义上错误的XML指令,则可以发送此状态代码。

作为反对其他答案的论据,使用任何非4xx错误代码都将意味着这不是客户端错误,而显然是。没有符合规范的理由使用非4xx错误代码来表示客户端错误。

看起来409 Conflict是最常见的答案,但是根据规范,这意味着资源已经存在,您请求的操作与其当前状态不兼容。如果您发送一个POST请求,例如使用已经被占用的用户名,它实际上并没有与目标资源冲突,因为目标资源(您正在尝试创建的资源)甚至还不存在。这主要是版本控制的错误,当存储的资源版本与客户端在其请求中假定的资源版本之间存在冲突时。

值得一提的是,当您尝试创建一个已经存在的仓库名称时,GitHub也会使用422作为状态码。

编辑:HTTP现在通过RFC 9110将错误422标准化了!它不再只是WebDAV的一部分。

我改变主意的原因:

在阅读新的RFC 9110时,我意识到我以前对错误409的解释是错误的。

409(冲突)状态码表示由于目标资源的当前状态与请求存在冲突,无法完成请求。在用户可能解决冲突并重新提交请求的情况下使用此代码。服务器生成包含足够信息的内容,以便用户能够识别冲突的来源。
这并不限制错误的意图只针对版本控制,正如我最初认为的那样。它将409错误泛化为任何类型的冲突。我仍然认为422错误适用于此处,但是因为409错误比422错误更具体(而且,“冲突”比“无法处理的实体”更有效地向人类传达了错误的语义),所以我建议使用409错误。

关于错误代码409的充分论据,请参见this answer。简而言之,有冲突的“目标资源”实际上是您要发布的资源集合,而不是您正在创建的资源,这是符合规范的解释(该答案详细阐述了原因)。如果您同意该答案,请为其点赞,而非本答案!

(注:我改变主意时我的投票数为31,因此现在这个答案有多少更多/更少的投票应该有助于衡量多少人仍然同意/不同意我的旧答案,尽管我已经进行了编辑。)


3
422是WebDAV规范,因此我不建议将其用于REST API。 - rwenz3l
4
为什么不呢?它相当传统,明显符合目的,并传达了预期的信息。 - Grant Gryczan
@GrantGryczan 因为它是为其他用途而设计的。有些客户端会相应地行事,正确地实现WebDAV规范。由于这个事实,我曾经无意中锁定了生产Web客户端,并且很难弄清楚到底发生了什么。不要将WebDAV代码用于除WebDAV之外的任何事情。 - Sinaesthetic
仅为上述内容添加更多背景,102是服务器表示“我没有挂起,我仍在处理,请不要超时”的方式。客户端实现通过绕过或延长其正常超时设置来补充。当服务器从未完成调用且客户端从未关闭连接时,我遇到了这个问题,导致客户端永远挂起。原因是我们使用WebDav代码来通信消息,但没有完全按照设计实现,但客户端库确实如此。这就是为什么它们被注释为WebDav而不仅仅是HTTP的一部分的原因。 - Sinaesthetic
@Sinaesthetic 嘿,看那个!422错误现在已经成为RFC 9110的官方(IETF)标准了。 - Grant Gryczan
显示剩余10条评论

33

也许我有些晚了,但在尝试创建REST API时,我遇到了这个语义问题。

稍微解释一下Wrikken的答案,我认为你可以根据情况使用409 Conflict403 Forbidden - 简而言之,当用户无法采取任何措施来解决冲突并完成请求(例如他们无法发送DELETE请求显式删除资源)时,请使用403错误,否则如果可能有所作为,则使用409。

10.4.4 403 Forbidden

服务器理解请求,但拒绝执行它。授权不会有帮助,请求不应重复。如果请求方法不是HEAD,并且服务器希望公开说明未能满足请求的原因,则应在实体中描述拒绝的原因。如果服务器不希望向客户端提供此信息,则可以使用状态代码404(未找到)代替。

现在,当有人说“403”时,会想到权限或身份验证问题,但规范表明这基本上是服务器告诉客户端它不会执行请求,不要再问了,并且这是为什么客户端不应该这样做。
至于PUT vs. POST... 当用户无法或不应该为资源创建标识符时,应使用POST来创建资源的新实例。当已知资源的标识时,应使用PUT。

9.6 PUT

...

POST和PUT请求的根本区别在于Request-URI的不同含义。POST请求中的URI标识将处理所包含实体的资源,该资源可能是数据接受进程、其他协议的网关或接受注释的单独实体。相比之下,PUT请求中的URI标识了请求中所包含的实体,用户代理知道要使用哪个URI,服务器不得尝试将请求应用于其他资源。如果服务器希望将请求应用于不同的URI,则必须发送301(永久移动)响应;然后用户代理可以自行决定是否重定向请求。


15
我认为“403 Forbidden”表示即使用户已经“验证”了,但他没有“授权”执行请求的操作。我不会将其用于验证错误。例如:未登录,我尝试删除某些内容。服务器向我发送“401未经授权”(名称有误,应为“401未验证”)。我登录后再试一次。这时,服务器检查我的权限,发现我不允许,返回“403 Forbidden”。另请参见此问题 - Stijn de Witt
嗯...没错。这里的想法是直接告诉用户,在OP的用例中,他们的授权使资源不可变 - 它已经存在,您没有权限对其进行任何操作以解决冲突,请不要尝试再次创建该资源。 - p0lar_bear
3
根据规范所述,如果使用正确的话,一个POST请求不会返回错误409(冲突错误)。规范指出在与“目标资源”发生冲突时才应该返回此错误码。因为目标资源尚未被提交,所以它不可能发生冲突,因此回复409 Conflict毫无意义。 - Grant Gryczan
1
我不会推断一个 409 错误不能被 POST 返回,事实上,我会推断相反的是因为 "冲突最有可能发生在对 PUT 请求的响应中。" 似乎表明其他请求方法也可以使用这个代码。 另外,"响应正文 应该 包含足够的信息,让用户识别冲突的来源。理想情况下,响应实体将包括足够的信息,让用户或用户代理程序修复问题;然而,这可能是不可能的,并且是 不必要的。" (http://www.webdav.org/specs/rfc2616.html#status.409) - JWAspin
我不再同意我之前的评论了。我改变主意的原因可以在我的回答底部看到解释。 - Grant Gryczan

19

在您的情况下,您可以使用409 Conflict

如果您想要查看下面列表中的其他HTTPs状态码

1××信息性状态码

100 Continue
101 Switching Protocols
102 Processing

2×× 成功

200 OK
201 Created
202 Accepted
203 Non-authoritative Information
204 No Content
205 Reset Content
206 Partial Content
207 Multi-Status
208 Already Reported
226 IM Used

3×× 重定向

300 Multiple Choices
301 Moved Permanently
302 Found
303 See Other
304 Not Modified
305 Use Proxy
307 Temporary Redirect
308 Permanent Redirect

4×× 客户端错误

400 Bad Request
401 Unauthorized
402 Payment Required
403 Forbidden
404 Not Found
405 Method Not Allowed
406 Not Acceptable
407 Proxy Authentication Required
408 Request Timeout
409 Conflict
410 Gone
411 Length Required
412 Precondition Failed
413 Payload Too Large
414 Request-URI Too Long
415 Unsupported Media Type
416 Requested Range Not Satisfiable
417 Expectation Failed
418 I’m a teapot
421 Misdirected Request
422 Unprocessable Entity
423 Locked
424 Failed Dependency
426 Upgrade Required
428 Precondition Required
429 Too Many Requests
431 Request Header Fields Too Large
444 Connection Closed Without Response
451 Unavailable For Legal Reasons
499 Client Closed Request

5×× 服务器错误

500 Internal Server Error
501 Not Implemented
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeout
505 HTTP Version Not Supported
506 Variant Also Negotiates
507 Insufficient Storage
508 Loop Detected
510 Not Extended
511 Network Authentication Required
599 Network Connect Timeout Error

12

我认为你不应该这样做。

正如您所知,POST用于修改集合,并用于创建新项。因此,如果您发送ID(我认为这不是一个好主意),则应该修改集合,即修改该项,但这会令人困惑。

最佳实践是使用它来添加一个没有ID的项目。

如果您想捕获唯一约束(而不是ID),则可以像PUT请求中那样响应409。但不要使用ID。


一个具有join table关系的对象怎么办?比如我们有账户、产品和账户产品作为数据库表。我想要将一个产品添加到一个账户中,所以我希望将其发布到 /account/{id}/product 并带上product_id。如果只允许一个账户-产品关系,我应该返回什么? - partkyle
2
忘掉数据库表吧。假设一个产品只能与一个账户相关联...那么这就是一对多的关系。所以,使用{'account':account_id}进行POST /product/{id}。如果将最大基数设置为“1”(一对一关系)...为什么它们被分开成REST对象?基数错误只会是400错误。保持简单。我希望我理解了你的问题。 - Alfonso Tienda
1
有些实体在它们上面有独特的限制,不仅仅是ID。比如一个账户,如果用户没有提供用户名,就无法创建账户。而添加一个没有用户名的账户显然是不可能的。 - rocketspacer
当实体具有唯一约束条件时,服务器必须响应 409 CONFLICT。我认为这不是问题的重点。当您 PUT 一个项目并且该项目已经存在 (id),您可以修改它,但如果您复制唯一约束,则会出现 409。 - Alfonso Tienda
对于关系 - 添加新经理 POST /managers 添加或替换 PUT /managers/{id} 将员工添加到经理 POST /managers/{id}/direct-reports 添加或替换员工 PUT /managers/{id}/direct-reports/{id} 等。其余部分(例如约束)是验证。1:1关系和部分更新可以以相同的方式处理,或作为单个属性进行PATCH。如果需要多个步骤,则使用多个步骤,例如 POST /users,然后是 POST /managers/{id}/direct-reports/{user-id-returned-by-previous-call} - Sinaesthetic
显示剩余3条评论

8
"302 Found" 对我来说听起来很合理。而RFC 2616表示,它可以用于除GET和HEAD之外的其他请求(这肯定包括POST)。
但根据RFC的规定,它仍然会让访问者前往此URL以获取此“Found”资源。为了直接转到实际的“Found” URL,应该使用“303 See Other”,这很有道理,但需要再次调用GET来获取其后续的URL。好处是,这个GET是可缓存的。
我认为我会使用“303 See Other”。我不知道是否可以回复正文中找到的“thing”,但我想这样做可以节省一次与服务器的往返。 更新:重新阅读RFC后,我仍然认为一个不存在的“4XX+303 Found”代码应该是正确的。然而,“409 Conflict”是最好的现有答案代码(由@Wrikken指出),可能包括一个指向现有资源的位置标头。

116
3xx 状态码用于重定向。 - Aviram Netanel
1
所请求的资源暂时驻留在不同的URI下。 - Cory
2
在我看来,“307临时重定向”才是真正的临时重定向。“302”是含糊不清的,但“FOUND!!”才是真正想要的信息。在HTTP语义上,最好的明确妥协是“303查看其他”。我会选择“303查看其他”。 - alanjds
是谁犯了错?服务器还是客户端? - alanjds
2
@DavidVartanian 感谢讨论。已更新答案至 409。即使客户不知道某些事情是不可能的,要求不可能的东西也是错误的。 - alanjds
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接