135

我有兴趣将直接 REST 接口暴露给 JSON 文档的集合(想想CouchDBPersevere)。我遇到的问题是GET如果集合很大,如何处理集合根目录上的操作。

举个例子,假设我正在公开 StackOverflow 的Questions表格,其中每一行都作为文档公开(不一定有这样的表格,只是一个相当大的“文档”集合的具体示例)。该集合将在/db/questions使用通常的 CRUD api时提供GET /db/questions/XXXPUT /db/questions/XXX,POST /db/questions正在播放中。获取整个集合的标准方法是,GET /db/questions但如果天真地将每一行作为 JSON 对象转储,您将获得相当大的下载量和服务器方面的大量工作。

解决方案当然是分页。Dojo 在其JsonRestStoreRange中解决了这个问题,方法是使用带有自定义范围单元的标头的符合 RFC2616 的巧妙扩展items。结果是 a206 Partial Content只返回请求的范围。这种方法相对于查询参数的优势在于它将查询字符串留给...查询(例如GET /db/questions/?score>200,或者某些类似的,是的,它会被编码%3E)。

这种方法完全涵盖了我想要的行为。问题是RFC 2616在 206 响应中指定了这一点(强调我的):

请求必须包含一个 Range 头字段(第14.35 节)指示所需的范围,并且可以包含一个 If-Range 头字段(第 14.27 节)以使请求有条件。

这在标头的标准用法的上下文中是有道理的,但这是一个问题,因为我希望 206 响应成为处理天真的客户/随机人探索的默认值。

我已经详细查看了 RFC 以寻找解决方案,但对我的解决方案不满意,并且对 SO 对这个问题的看法很感兴趣。

我有过的想法:

  • 200带头返回Content-Range- 我不认为这是错误的,但如果有一个更明显的指标表明响应只是部分内容,我会更喜欢。
  • Return400 Range Required - 对于所需的标头没有特殊的 400 响应代码,因此必须使用默认错误并手动读取。这也使得通过 Web 浏览器(或其他一些客户端,如 Resty)进行探索变得更加困难。
  • 使用查询参数- 标准方法,但我希望允许查询 la Persevere,这会切入查询命名空间。
  • 刚回来206- 我认为大多数客户不会惊慌失措,但我宁愿不反对 RFC 中的 MUST
  • 扩展规格!Return266 Partial Content - 行为与 206 完全相同,但响应的是不得包含Range标头的请求。我认为 266 足够高,我不应该遇到碰撞问题,这对我来说很有意义,但我不清楚这是否被视为禁忌。

我认为这是一个相当普遍的问题,我希望看到它以一种事实上的方式完成,这样我或其他人就不会重新发明轮子。

当集合很大时,通过 HTTP 公开完整集合的最佳方式是什么?

4

12 回答 12

33

我真的不同意你们中的一些人。我已经为我的 REST 服务的这个特性工作了几个星期。我最终做的事情真的很简单。我的解决方案只对 REST 人们所说的集合有意义。

客户端必须包含一个“Range”标头来指示他需要集合的哪一部分,或者当请求的集合太大而无法在单次往返中检索时,准备好处理 413 REQUESTED ENTITY TOO LARGE 错误。

服务器发送一个 206 PARTIAL CONTENT 响应,其中 Content-Range 标头指定已发送资源的哪一部分,以及一个 ETag 标头来标识集合的当前版本。我通常使用类似 Facebook 的 ETag {last_modification_timestamp}-{resource_id},并且我认为集合的 ETag 是它包含的最近修改资源的 ETag。

要请求集合的特定部分,客户端必须使用“Range”标头,并使用从先前执行的请求中获得的集合的 ETag 填充“If-Match”标头,以获取同一集合的其他部分。因此,服务器可以在发送请求的部分之前验证集合没有更改。如果存在更新版本,则返回 412 PRECONDITION FAILED 响应以邀请客户端从头开始检索集合。这是必要的,因为这可能意味着在当前请求的部分之前或之后可能已经添加或删除了一些资源。

我将 ETag/If-Match 与 Last-Modified/If-Unmodified-Since 结合使用来优化缓存。浏览器和代理可能依赖其中之一或两者来实现缓存算法。

我认为 URL 应该是干净的,除非它包含搜索/过滤查询。如果你仔细想想,搜索只不过是一个集合的局部视图。除了cars/search?q=BMW 类型的URL,我们应该看到更多的cars?manufacturer=BMW。

于 2009-06-23T09:25:14.410 回答
24

我的直觉是 HTTP 范围扩展不是为您的用例设计的,因此您不应该尝试。部分响应意味着206,并且206必须仅在客户要求时才发送。

您可能需要考虑一种不同的方法,例如在 Atom 中的一种使用(其中设计表示可能是部分的,并返回一个 status200和可能的分页链接)。请参阅RFC 4287RFC 5005

于 2009-05-29T06:54:28.207 回答
8

您仍然可以返回Accept-RangesContent-Ranges使用200响应代码。这两个响应标头为您提供了足够的信息来推断206响应代码明确提供的相同信息。

我会使用Range分页,并让它简单地返回200一个普通的GET

这感觉 100% RESTful并且不会让浏览变得更加困难。

编辑:我写了一篇关于这个的博客文章:http: //otac0n.com/blog/2012/11/21/range-header-i-choose-you.html

于 2012-11-14T06:09:35.547 回答
5

如果有超过一页的回复,并且您不想一次提供整个集合,这是否意味着有多种选择?

在对 的请求中/db/questions,返回指定如何访问每个页面的标头以及带有 URL 列表的 JSON 对象或 HTML 页面300 Multiple ChoicesLink

Link: <>; rel="http://paged.collection.example/relation/paged"
Link: <>; rel="http://paged.collection.example/relation/paged"
...

每页结果都有一个Link标题(空字符串表示当前 URL,每个页面的 URL 相同,只是使用不同的范围访问),并且根据即将发布的Link规范将关系定义为自定义关系. 这种关系会解释你的习惯266,或者你的违反206。这些标头是您的机器可读版本,因为无论如何您的所有示例都需要一个理解客户端。

(如果您坚持使用“范围”路线,我相信您自己的2xx返回代码,正如您所描述的,将是这里的最佳行为。您应该为您的应用程序执行此操作,并且此类 ["HTTP 状态代码是可扩展的。 "],你有充分的理由。)

300 Multiple Choices说您还应该为主体提供一种供用户代理选择的方式。如果您的客户理解,它应该使用Link标题。如果是用户手动浏览,可能是一个带有指向特殊“分页”根资源的链接的 HTML 页面,该根资源可以处理基于 URL 呈现该特定页面? /humanpage/1/db/questions或类似的可怕的东西?


对 Richard Levasseur 帖子的评论让我想起了另一个选项:Accept标题(第 14.1 节)。回到 oEmbed 规范出来的时候,我想知道为什么它没有完全使用 HTTP 完成,并写了一个使用它们的替代方案。

保留初始天真的 HTTP 的 、标头和 HTML 页面300 Multiple Choices,而不是使用范围,让您的新分页关系定义标头的使用。您的后续 HTTP 请求可能如下所示:LinkGETAccept

GET /db/questions HTTP/1.1
Host: paged.collection.example
Accept: application/json;PagingSpec=1.0;page=1

Accept头允许您定义可接受的内容类型(您的 JSON 返回),以及该类型的可扩展参数(您的页码)。从我的 oEmbed 文章中翻阅我的笔记(此处无法链接到它,我将在我的个人资料中列出),您可以非常明确并在此处提供规范/关系版本,以防您需要重新定义page参数的含义在将来。

于 2009-06-04T06:00:20.200 回答
4

编辑:

在考虑了更多之后,我倾向于同意 Range 标头不适合分页。逻辑是,Range 标头用于服务器的响应,而不是应用程序。如果您提供 100 兆字节的结果,但服务器(或客户端)一次只能处理 1 兆字节,那么这就是 Range 标头的用途。

我也认为资源的子集是它自己的资源(类似于关系代数。),因此它应该在 URL 中表示。

所以基本上,我放弃了关于使用标题的原始答案(如下)。


我认为您或多或少地回答了您自己的问题-使用内容范围返回 200 或 206 并可选择使用查询参数。我会嗅探用户代理和内容类型,并根据这些检查查询参数。否则,需要范围标头。

您基本上有相互冲突的目标 - 让人们使用他们的浏览器进行探索(这不容易允许自定义标头),或者强迫人们使用可以设置标头的特殊客户端(不允许他们探索)。

你可以根据请求为他们提供特殊的客户端——如果它看起来像一个普通的浏览器,发送一个小的 ajax 应用程序来呈现页面并设置必要的标题。

当然,还有关于 URL 是否应该包含此类事情的所有必要状态的争论。使用标题指定范围可能被某些人认为是“不安的”。

顺便说一句,如果服务器可以使用“Can-Specify: Header1, header2” 标题进行响应,并且 Web 浏览器将呈现 UI 以便用户可以根据需要填写值,那就太好了。

于 2009-06-03T03:12:24.703 回答
3

您可能会考虑使用类似 Atom Feed Protocol 的模型,因为它有一个健全的 HTTP 集合模型以及如何操作它们(其中疯狂意味着 WebDAV)。

有定义集合模型和 REST 操作的Atom 发布协议,此外,您可以使用RFC 5005 - Feed Paging and Archiving对大集合进行分页。

从 Atom XML 切换到 JSON 内容应该不会影响这个想法。

于 2009-06-03T02:10:40.787 回答
3

我认为这里真正的问题是规范中没有任何内容告诉我们在面对 413 - Requested Entity Too Large 时如何进行自动重定向。

我最近也在为同样的问题苦苦挣扎,我在RESTful Web Services书中寻找灵感。由于标题要求,我个人认为 206 不合适。我的想法也将我带到了 300,但我认为对于不同的 mime 类型来说更多,所以我在附录 B,第 377 页中查找了 Richardson 和 Ruby 就这个主题所说的话。他们建议服务器只选择首选表示并将其发送回 200,基本上忽略了它应该是 300 的概念。

这也与我们从 atom 获得的下一个资源的链接的概念相吻合。我实施的解决方案是将“下一个”和“上一个”键添加到我发回的 json 映射中并完成它。

后来我开始想也许要做的事情是发送一个 307 - 临时重定向到一个类似于 /db/questions/1,25 的链接 - 将原始 URI 作为规范资源名称,但它会让你适当命名的从属资源。这是我希望从 413 中看到的行为,但 307 似乎是一个很好的折衷方案。不过,实际上还没有在代码中尝试过。更好的是重定向重定向到包含最近提出的问题的实际 ID 的 URL。例如,如果每个问题都有一个整数 ID,并且系统中有 100 个问题,并且您想显示最近的 10 个问题,那么对 /db/questions 的请求应该是 307 到 /db/questions/100,91

这是一个很好的问题,谢谢你的提问。你向我证实,我并没有因为花了几天时间思考这件事而发疯。

于 2009-06-04T21:11:24.727 回答
2

随着rfc723x的发布,未注册的范围单位确实违反了规范中的明确建议。考虑rfc7233(弃用 rfc2616):

新的范围单位应该在 IANA 注册”(连同对HTTP Range Unit Registry的引用)。

于 2014-06-27T08:17:26.710 回答
2

范围标头的一大问题是许多公司代理将它们过滤掉。我建议改用查询参数。

于 2014-08-15T13:17:35.293 回答
1

您可以检测Range标头,如果存在则模仿 Dojo,如果不存在则模仿 Atom。在我看来,这巧妙地划分了用例。如果您从应用程序响应 REST 查询,您希望它使用Range标题进行格式化。如果您正在响应一个随意的浏览器,那么如果您返回分页链接,它将让该工具提供一种探索集合的简单方法。

于 2009-06-04T05:18:31.323 回答
0

在我看来,最好的方法是将范围作为查询参数。例如,GET /db/questions/?date>mindate&date<maxdate。在没有查询参数的情况下对 /db/questions/ 进行 GET,返回 303 和 Location: /db/questions/?query-parameters-to-retrieve-the-default-page。然后提供一个不同的 URL,由使用您的 API 的任何人获取有关集合的统计信息(例如,如果她/他想要整个集合,则使用什么查询参数);

于 2009-06-05T19:43:46.527 回答
0

虽然可以为此目的使用 Range 标头,但我认为这不是目的。它似乎是为处理不稳定的连接以及限制数据而设计的(因此,如果缺少某些内容或大小太大而无法处理,客户端可以请求部分请求)。您正在将分页侵入可能在通信层用于其他目的的东西。处理分页的“正确”方法是使用您返回的类型。与其返回问题对象,不如返回一个新类型。

所以如果问题是这样的:

<questions> <question index=1></question> <question index=2></question> ... </questions>

新类型可能是这样的:

<questionPage> <startIndex>50</startIndex> <returnedCount>10</returnedCount> <totalCount>1203</totalCount> <questions> <question index=50></question> <question index=51></question> .. </questions> <questionPage>

当然,您可以控制您的媒体类型,因此您可以使您的“页面”成为适合您需要的格式。如果你做的是通用的,你可以在客户端上有一个解析器来处理所有类型的相同分页。我认为这更符合 HTTP 规范的精神,而不是为了别的东西而捏造 Range 参数。

于 2014-10-03T17:15:28.127 回答