博客

🤔 GraphQL 应该因用户不同而有所不同吗?

Leonardo Losoviz
作者:Leonardo Losoviz ·

GraphQL 是一个从某个数据源获取数据的接口,GraphQL 规范定义了该接口的要求。只要满足这些要求,GraphQL 并不关心具体的实现方式。GraphQL 服务器可以用 JavaScript 的 Promise 实现,也可以使用基于 Golang 的并发架构,或者映射到 Excel 文件,等等——这些都可以是 GraphQL 规范的有效实现。

GraphQL 位于客户端和后端服务之间

服务器引擎的实现方式对 GraphQL 请求的成功执行并不重要,因为客户端与服务器之间的交互始终相同——使用定义好的语法提交 GraphQL query,并以 JSON 格式获取相应的响应。

当我说实现方式不重要时,我是从 API 用户的角度来说的。用户只是想从服务器获取数据,对返回数据是如何生成的并不感兴趣。

但对于在 API 上工作的服务端开发者来说,情况就不同了,实现的细节对他们来说确实非常重要。如果我用 PHP 编写 GraphQL API,我会尽力让 API 尽可能高效地解析,并利用 PHP 提供的能力设计尽可能优雅的架构。

PHP vs Java vs JavaScript

由此产生了一种潜在的利益冲突:一方面需要保护 API 的安全,另一方面在 API 上工作的开发者期望底层语言所支持的功能(例如执行递归代码的能力)不会被剥夺。

这种冲突在 issue #929: Allow recursive references in fragments 中变得十分明显,该 issue 主张 GraphQL 不应禁止 fragment 中的递归。

在 GraphQL 工作组过去的一次聚会上,提出该 issue 的开发者 Roman 表达了他对规范所施加限制的异议

我是一名服务端开发者,我觉得规范过多地讨论服务端执行,而应该专注于客户端希望得到什么——而不是如何实现

禁止 fragment 中递归的规则,其正当性建立在保护公开 API 安全的前提之上。毕竟,GraphQL 是 Facebook 为了向其面向公众的应用程序提供数据而创建的,用户不应能够利用 API 设计上的缺陷来使服务宕机。

GraphQL 的创始人 Lee Byron 表达了三个主要担忧:

无限递归;限制不仅仅是规范层面的问题——应该何时以及如何停止

数据验证;多次返回相同值时,在数据中如何表示。理想情况下,你希望检测到循环后立即停止,但某些服务器无法检测到这一点,可能会循环多次后才发现出了问题并停止

不支持此功能的代价是什么;这些问题是否值得?不值得;你始终可以在 query 中指定层级深度——这实际上就是我们在 GraphQL 中处理此问题时所做的脱糖版本

从各自的立场来看,Roman 和 Lee 都有道理。Lee Byron 担心公开 GraphQL API 的安全性。禁止递归 fragment 是有道理的,这样可以确保恶意行为者无法通过在 query 中执行永无止境的循环来使系统宕机,甚至消除团队「自我 DDoS」的可能性——如果他们无意中发布了一个会使系统停止的 query,这种情况就可能发生。

然而,Roman 担心的是对他自己创建 GraphQL API 能力的限制。因为 Roman 可能是其 API 的唯一消费者(即不对用户开放的私有 API),或者他的服务器可能具备检测并停止循环的能力,因此他认为 GraphQL 的这一限制是有害且不合理的。

讨论的核心不在于是否应该允许递归 fragment,而在于一个更根本的问题:GraphQL 的目标受众是谁?如果不是单一群体,单一的 API 规范能否满足所有不同利益相关者的需求?如果无法避免冲突,至少能以某种方式加以缓解吗?

让我们来探讨这些问题。

GraphQL 的目标受众是谁?

GraphQL 被不同类型的利益相关者使用,其中包括:

1. API 用户:出于各种目的从某个 GraphQL 端点获取数据的人。例如,我们都可以成为 GitHub 公开 GraphQL API 的 API 用户,以获取与我们 GitHub 仓库相关的数据。

2. 客户端开发者: 创建由某个 GraphQL 端点驱动的客户端应用程序的人。例如,使用 Gatsby 构建网站的开发者依赖 GraphQL 来获取网站内容。

3. 后端开发者: 为 GraphQL API 创建解析器的人。

此外,我们还需注意 GraphQL API 可以是公开的或私有的:

公开 API: 由于任何人都可以访问 GraphQL 端点,我们必须关注安全措施,以避免恶意行为者的攻击。

私有 API: 由于只有预期的行为者才能访问 API,因此不存在固有的安全风险,通过良好的编码实践也可以轻松避免自我 DDoS。

单一 API 规范能否满足所有利益相关者的需求?

Roman 提出的 issue 可以这样理解:"如果我的 GraphQL API 是私有的,而且我清楚地知道自己在做什么(100% 确信代码会按预期工作,不会产生执行停止的情况),那么为什么我不能在 fragment 中使用递归?"

Recursions @ xkcd

每当我们使用基于 GraphQL 的框架构建静态网站(例如 Gatsby、Next.jsRedwoodJS)时,就会出现这种情况,因为 GraphQL API 通常是私有的,我们不会无意间 DDoS 自己的应用并遭受不良后果(最多只会在开发或预发环境构建静态网站时发生崩溃)。

使用上述设置的开发者完全有理由疑惑:为什么 GraphQL 规范会禁止他们使用对他们的设置毫无不良影响的有益功能?

综上所述,通过禁止递归 fragment,GraphQL 规范强制推行了一项安全措施,该措施仅适用于 GraphQL 所有潜在用途中的一部分,而非全部,目的是确保万无一失。

GraphQL 规范能否更好地满足所有利益相关者?

如果不同利益相关者有不同的需求,GraphQL 规范如何才能满足所有人?(前提是避免分叉规范,为特定目标生成定制化版本。)

让我们探讨两个思路,第一个需要经历规范贡献流程,而第二个则不需要。

GraphQL 规范层面的 feature-toggle

一种可能的方案是让规范「建议」而非「强制」规则。在这种情况下,禁止 fragment 中递归的规则可以作为强烈建议,但该功能仍然被接受。

然而,这一方案会将递归 fragment 的默认状态从「必须」改为「可选」,从而产生两个负面后果:

  • API 默认情况下不安全(这正是 Lee Byron 想要避免的场景)
  • 由于原本被禁止的 query 将变为允许,会产生破坏性变更

因此,更好的方式是反转这一选项:fragment 中的递归默认仍然被禁止,但提供切换 feature-flag 的可能性来禁用此行为。由于该功能必须被显式禁用,只有清楚自己在做什么的管理员才会去操作。

由于该功能在特定设置下最有价值,GraphQL 服务器和框架可以决定是否、如何以及何时提供此配置。例如,Gatsby 可以在创建静态网站时通过 UI 突出显示该选项,在其他情况下则隐藏它。

总体思路是让 GraphQL 规范支持「已启用但可选的功能」,这些功能可以通过配置启用或禁用,其默认状态与规范中已有的状态相同。

禁止递归 fragment 将成为其中之一,还可能有其他类似功能,例如 Map 类型。该类型未被 Lee Byron 纳入规范,原因是

Map 类型与键/值对列表之间存在重大权衡。一个问题是对集合进行分页。值列表可以有清晰的分页规则,而 Map 通常具有无序的键值对,分页要困难得多。

另一个问题是使用方式。大多数情况下,Map 在 API 中用于对值的某个字段进行索引,在我看来这是一种 API 反模式,因为索引是存储问题和客户端缓存问题,而不是传输问题。这种反模式令我担忧。虽然 Map 在 API 中有一些好的用途,但我担心常见的用法会用于这些反模式,所以建议谨慎推进。

Lee Byron 表达了对该功能被当作反模式使用的担忧。但他也承认确实存在好的用途。因此,鉴于该 issue 获得了社区的大量支持(超过 150 个 👍),可以给开发者提供选项,让他们明确启用在其 schema 中添加 Map 类型,并自行处理相应的后果。

GraphQL 服务器层面的 feature-toggle

如果上述提案因对 GraphQL 规范风险过高而得不到支持,另一个替代方案是在 GraphQL 服务器层面实现。这样,GraphQL 服务器可以提供一个自定义功能来禁用 fragment 中的递归。

推广这一思路,GraphQL 服务器可以提供禁用规范中某些功能、启用规范中缺失功能的选项。为了不产生意外,服务器必须确保默认状态符合规范要求,而 API 管理员也必须充分了解切换功能所带来的后果。(这正是 Gato GraphQL 对其「innovative features」所采用的策略。)

总结

随着 GraphQL 日益普及,支持新能力的新框架将其纳入技术栈,新的利益相关者(以及新类型的利益相关者)也随之加入。因此,这个最初由 Facebook 创建以定义其应用程序如何从服务器获取数据的规范,需要应对越来越多的使用场景。

冲突的出现是不可避免的——某些利益相关者需要的功能,对其他利益相关者来说是适得其反甚至有害的,递归 fragment 的情况正是如此。我们能做些什么来改善这种局面,避免不满意的利益相关者对 GraphQL 感到失望?

我主张规范提供「禁用」某个功能的机会,让清楚自己在做什么的管理员能够取消某些限制以满足自身需求。不过,我本人并不认同这一解决方案,但还是将其提出来,因为这场讨论有其必要性。鉴于这一想法存在争议,更好的替代方案是由 GraphQL 服务器通过自定义功能提供此行为,且这些功能必须被显式启用。


订阅我们的新闻通讯

及时了解 Gato GraphQL 的所有更新。