RESTful GET,如果存在大量参数,是否有必要变通一下?

比如设计一个GET接口,用来获取满足条件的商品 GET shop/1/goods? 参数可能是五花八门的,name,create_time,description, status, owner... 通常这种情况,如果不考虑RESTful,就会设计成POST。 但是现在RESTful风格要求做成GET,如何处理会比较合适?
关注者
275
被浏览
30,632

24 个回答

先说结论,可以,使用POST来处理参数比较长的search请求并没有违反REST。

再说说@张立理 同学的答案里可以改进的地方。在张同学的理论主义玩法里:
有这样的URL:
/goods/ 商品集合

然后我们再在其下创建一个叫“查询条件”的集合:
/goods/queries 商品查询集合

我们使用POST来创建一个查询条件
POST /goods/queries

{"categories": [很长很长很长], "keyword": "很长很长很长"}

创建了之后自然会返回一个id,所以这个请求的响应大概就能这样:
HTTP/1.1 200 OK

一堆HTTP头

{"id": 1234567}

然后再拿这个id去查询,查询视为对“查询”这个集合的单个实体获取
GET /goods/queries/1234567

响应自然就是查询应该有的结果,也可以把那个POST请求的响应搞成302以减免前端再编码发下一个请求的工作量

首先要强调,status code和URI, http method一样都是REST里uniform interface的一部分,不考虑这些,不考虑怎么follow link,Restful api就只剩下CRUD了。

我们可以把查询结果建模成一个新创建的resource:
POST /goods/queries
返回的response应该是
HTTP/1.1 201 Created
Location: /goods/queries/1234567

客户端顺着Loacation去找被创建的resource才是正确的HATEOAS。在response body里返回{"id": 1234567}还可以接受,返回302在这种场景下是对redirect语义的误用。

rfc7231:section-4.3.3
If one or more resources has been created on the origin server as a result of successfully processing a POST request, the origin server SHOULD send a 201 (Created) response containing a Location header field that provides an identifier for the primary resource created and a representation that describes the status of the request while referring to the new resource(s).

而如果查询结果是已经存在的resource:
POST /goods/queries
返回的response应该是
HTTP/1.1 303 See Other
Location: /goods/queries/1234567
这么做的好处是利用已有的缓存。
rfc7231:section-4.3.3
If the result of processing a POST would be equivalent to a representation of an existing resource, an origin server MAY redirect the user agent to that resource by sending a 303 (See Other) response with the existing resource's identifier in the Location field. This has the benefits of providing the user agent a resource identifier and transferring the representation via a method more amenable to shared caching, though at the cost of an extra request if the user
agent does not already have the representation cached.

以上当然是一种比较麻烦的玩法,然而并不是说不这么玩就不RESTful了

把POST创建的resource直接在response body里将数据返回,也是非常自然和practical的。
POST /goods/queries

返回的response直接包含结果
HTTP/1.1 200 OK
other headers...

[{name: 'good1',...}, {name: 'good2',...},...]

很多人误解只有GET方法才能“读”,这里我们POST的response是对已创建资源的一个表述。这里创建的资源是一个临时资源,可以不返回id。resource是一种抽象并不必然等于entity,POST并不一定要创建一个持久化的resource。

rfc2616:section-9.5

The action performed by the POST method might not result in a resource that can be identified by a URI. In this case, either 200 (OK) or 204 (No Content) is the appropriate response status, depending on whether or not the response includes an entity that describes the result.

resource可以没有GET方法,就像resource可以没有DELETE一样自然。并且POST的相应不是不能cache,只是因为副作用的原因平时很少用。
rfc7231:section-4.3.3
Responses to POST requests are only cacheable when they include explicit freshness information.

在实际工程中(如果你不是做google),大部分情况下对search criteria的复用很低。并且HTTP的cache其实只是在client和proxy的cache,其实帮不了太多忙。更常见降低latency的方式是在查询响应里只返回基本的信息和uri reference,然后通过UI design或lazy fetching等方式沿着uri把剩下的信息拿回来。
首先承认超过GET的URL总长度的情况确实可能存在的,一个比较典型的场景就是多选id,鬼知道一个变态能选出多少个来

当然一个合理的应用不应该让这种情况出现,毕竟用手勾选到能超过URL的长度限制应该是会抽筋的,我们亲爱的产品组应该为用户着想
至于“全选”和“全选后取消几个”这种场景,其一我很怀疑后者的用户场景是否真的存在,其二可以使用全选和反选标记来给予实现,也并不是什么麻烦事儿

继续说真的超过了URL长度限制怎么办,第一反应自然是拿POST来玩,但是使用POST除去教条式的语义性和REST规范之外,一个很严重的影响是无法使用HTTP缓存
当然这个也不是什么大问题,毕竟99.99%的应用是不会精心设计HTTP缓存的(是的这句话是在喷包括自己在内的很多工程师),所以搜索这种场景从一开始就几乎是没有HTTP缓存支持

那如果我还是想要缓存,还是想要遵守REST规范使用GET怎么办呢?这里有一个理论主义的玩法
假设我们对商品进行检索,有这样的URL:

/goods/ 商品集合

然后我们再在其下创建一个叫“查询条件”的集合:

/goods/queries 商品查询集合

我们使用POST来创建一个查询条件

POST /goods/queries

{"categories": [很长很长很长], "keyword": "很长很长很长"}

创建了之后自然会返回一个id,所以这个请求的响应大概就能这样:

HTTP/1.1 200 OK
一堆HTTP头

{"id": 1234567}

然后再拿这个id去查询,查询视为对“查询”这个集合的单个实体获取

GET /goods/queries/1234567

响应自然就是查询应该有的结果,也可以把那个POST请求的响应搞成302以减免前端再编码发下一个请求的工作量

这样可以有效利用缓存,而且完全符合REST的规范,但代价挺大:
  • 需要2次HTTP请求
  • 从URL上就看不出查询条件了
  • 前后端都需要改造,后端大概并不高兴

除此之外,后端可以做一些工作,如相同的查询可以直接共享之前的id,以便更好地使用缓存

前面 Trotyl Yu 有质疑对查询建立实体是否合理,我这里很明确地说,REST就是这么玩的,一切皆资源,一切皆可以是实体,只不过也不能像 uazw 那样强行把搜索搞成POST说那是资源的创建,资源还是要有集合+对集合的操作组成的,表面文章还是得做一做

回头再来说,其实无法很好地处理这种问题,无法很顺理成章地得出一个合理的解决方案,其根本原因是大家的应用都不是在玩REST设计,只是在实现层面上“看着像REST”而已,你不是使用资源进行系统建模,不是以资源的角度来进行设计,自然遇到问题不会从资源的角度去考虑,最后和REST需要的资源第一位的观点冲突,把自己绕死
这种伪REST其实很要不得,要么你就把REST丢掉,只留下“URL好看点不错”这样的目标,要么你就玩纯粹基于资源的设计和实现