HTTP Method for REST API
HTTP 1.1 Method 相关RFC
January 1997
RFC 2068 Hypertext Transfer Protocol -- HTTP/1.1 (Obsoleted)
June 1999
RFC 2616 Hypertext Transfer Protocol -- HTTP/1.1 (Obsoleted)
March 2010
RFC 5789 PATCH Method for HTTP
June 2014
RFC 7230 Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing
RFC 7231 Hypertext Transfer Protocol (HTTP/1.1): Semantics and Content
HTTP Method 概览
HTTP Method 是体现 REST API 语义最重要的组成部分之一。例如最常见的CREATE/READ/UPDATE/DELETE (即CRUD 操作)映射到HTTP Method POST/GET/PUT/DELETE(虽然常见但并不严谨,详见下文)。
RFC 7231 定义了最常见的 8 种HTTP Method,加上RFC 5789 定义的PATCH,共9 种常见Method。
GET
GET 方法通常用于获得URI 所指定的资源。GET 的标准语义通过一系列Headers 定义了诸如条件检查,返回局部结果,以及缓存等功能。这些Headers 常被用于返回静态资源,并且精确支持其语义。但是在处理动态资源的时候,还有一种方法也很受欢迎,那就是URL Parameters,即问号(?)后面的参数。两者相比各有各的优劣:
Headers:并非URL 的一部分,易于结构化,对于程序化处理比较友好,并且能够支持较大的Payload,也无需做URL Encode。但是通常需要程序化处理,且浏览器地址栏不可见。
URL Parameters:URL 的一部分,浏览器地址栏可见,可单字符串收藏。但是长度受限比Headers 严重,且通常需要URL Encode。
比如,同样是过滤器,放置在Headers 和URL Parameters 里都有各自的好处。具体选择哪一种方案取决于项目API 的整体风格,以及该资源的属性。
POST
POST 通常用来被创建资源,该资源可以是持久化的,或者发起一个处理请求。后者很好的覆盖了大多数非“创建” 类的动作。
例如,我需要一杯咖啡这个请求,在有些语境下我们可以直接创建一个咖啡的资源,例如我不关心咖啡的来源,可以从星巴克买;有些情况下则需要从另一个资源来进行转换,比如从咖啡豆冲泡成咖啡。为了体现这种非创建型动作(冲泡),我们可以发起一个冲泡请求,请求里包含所有需要的资源,例如咖啡豆,咖啡机,冲泡参数等等。
在这个例子里,我们最终得到了一杯咖啡资源,但是这个请求事实上可以是任何动作,可以不创建资源,可以修改资源,删除资源等等。这也导致POST 经常被滥用,任何动作都可以用POST 来解决。使用POST 本身没什么不妥,问题在于对请求的适当抽象和归类,可以使用不代表可以滥用。
另外一点,POST 请求通常是非幂等的。这意味着同样的请求POST 一次或者多次会导致不同的系统状态。因此,发起POST 请求时应仔细检查是否需要保证 “有且仅有一次”或者 “最多一次” 的执行序。
PUT
PUT 通常被用来修改资源,但是当资源不存在时也可以创建资源。和POST 不一样的是,POST 通常是创建一个某种类型的资源,而PUT 是创建、修改指定的某一个资源。这也是为什么POST 请求通常不带资源标志符(ID),而PUT 请求一般都通过标志符指定特定资源。
另外一点就是,PUT 通常认为是覆盖式的操作,是幂等的。这意味着API 设计者应该保证对同一个资源的PUT 操作无论执行多少次都产生同一个系统状态。
PATCH
PATCH 的语义和PUT 类似,用于修改资源,但是只修改差集指定的部分。API 的定义者应该明确该PATCH 的格式以及如何应用于指定的资源。但是,和PUT 不一样的是,PATCH 不是幂等的,服务器决定了如何处理PATCH 请求中对资源的修改。从这点来说,PATCH 的行为更接近于POST 一个请求,两者都可以是对某个资源进行修改,而修改的内容则不一定像PUT 那样直接,也可能是间接的,过程化的。
在大多数情况下,PATCH 能做的事情,PUT都能做,实在不行还有POST,因此很多API 实现都不会再多此一举支持PATCH 操作。这样的实践遵守了API 的正交性原则,对于基础级别的API,也推荐这样实现。而在面向应用的API 中(例如BFF - Backend For Frontend ),则不受这样的规则限制。相反,那些API 会更多的向应用需求倾斜。相比PUT,PATCH 有时候能够提供更佳的语义以及更小的数据交换量。
DELETE
DELETE 的语义是删除指定的资源。一个常见的争论是,当删除一个不存在的资源的时候,是应该返回错误,还是应该返回成功。同样的争论也发生很多其他地方。例如在Java - HashMap 里,反复删除同一个Key 是不会报错的。但是在操作系统中,如果尝试对同一个资源地址进行删除操作,操作系统通常会抱怨该资源已不存在。
在RFC 7231 中,DELETE 操作是幂等的,这意味着按照该语义,重复DELETE 同一个资源应产生同样的系统状态。但是这并不意味着API 返回也必须一样。API 的行为常常受到API 使用者的用户体验影响,如果用户需要精确的反馈,报错会很有用;但是如果用户只在乎最终状态,并且希望简化资源的回收和状态管理,那么成功的静默返回也是一个不错的注意。
HEAD
HEAD 用于仅返资源回头部信息,通常包含资源的大小等属性,客户端可以以此决定后续GET 操作的具体实现,例如进度追踪,或者仅获取资源的部分结果等。RFC 7231 中,要求HEAD、GET 同一个资源必须返回一致的头部信息。在实际实现中,则常考虑HEAD 的实现比GET 要轻量。因为HEAD 不需要传输具体的资源内容,应考虑仅读取资源元数据,避免读取整个资源等重量级操作。
OPTIONS
OPTIONS 用于测试服务器处理资源的可选项,常见的例如针对某资源允许的Method,或者允许的跨域请求等。这里不多展开。
TRACE,CONNECT
在REST API 的环境里,TRACE,CONNECT 语义不常用,略过。
最后值得一提的是,从最早的RFC 2068 到最新的RFC 7230、7231,HTTP Method 都可以被定义为任意 “token”[1][2]。因此,在REST API 定义中,我们是可以自定义Method 的。这样做的理由常常是,他们比常见的这9 种Method 具有更加丰富,以及可能更加精确的语义。一个常见的梗是RFC 2324:Hyper Text Coffee Pot Control Protocol (HTCPCP/1.0),1998年的一个愚人节项目,也可以说是程序员的幽默 - 私以为用来展示HTTP 的可扩展性其实是挺好的例子。
但是,不这么做的理由看上去也很充分:
和项目上下文有关的扩展HTTP Method 并非广为人知,意味着各类框架的支持会非常有限,结果就是常常很难,甚至无法做到。比如时至这篇文章的发表日期,Spring 框架的HttpMethod 就只有除了CONNECT 之外的8 个,不支持扩展Method。
可扩展意味着灵活,但是控制不好也会变成桎梏,导致API 语义混乱,不易于理解。所以需要组织级别的强有力的统一管理,确保扩展Method 的规范化,正交化,去重等,而不是随心所欲的定义。
因此在大多数项目中,我看到的结果常常是为了管理、集成的方便而向传统的Method 妥协。更何况传统的Method 已经深入人心,几乎可以涵盖90% 以上的资源操作语义,我们也就不用费尽心思再去发明些什么。也许有一天物联网真的发达了,会出现支持类似RFC 2324 那种协议的茶壶吧;)。
[1] "token" 定义参见:RFC 7230/Section 3.2.6 Field Value Components
[2] 参见已注册的HTTP Method:IANA Hypertext Transfer Protocol (HTTP) Method Registry