- 后端
- API网关
- 数据库
- GraphQL
【免费下载链接】graphql-engine
Blazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.
导读
本文以 Hasura GraphQL Engine 官方设计文档 rfcs/rest-endpoints.md 为核心,完整解读 Hasura 的REST Endpoints(RESTified Endpoints)功能:它允许将一条固定的 GraphQL 查询或变更(mutation)映射为一个符合 REST 习惯的 HTTP 端点,从而同时获得 GraphQL 的开发迭代速度与 REST 二十年积累的生态工具链(缓存、CDN、OpenAPI 文档、现有客户端)。读完本文,你将掌握端点的四要素模型、URL 模板语法、HTTP 方法约束、路由匹配算法、三种变量传递方式、参数类型限制、响应码/响应体约定与元数据校验规则,并能对照仓库源码(server/src-lib/Hasura/Server/Rest.hs、server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs)理解其底层实现原理。
背景与动机:为什么要在 GraphQL 引擎上提供 REST 端点
GraphQL 的优势在于 API 设计与使用阶段可以快速迭代;而 REST 的劣势在于不够灵活,但它拥有近二十年的生产环境使用经验、成熟的优化手段与工具链。RFC 的出发点很明确:鱼与熊掌兼得——给定一条固定的 GraphQL 查询或变更,Hasura 可以为其生成一个符合习惯的 REST 端点,从而:
- 复用已有的 REST API 工具与生态;
- 利用浏览器或 CDN 层的 HTTP 缓存优化;
- 无需手写任何自定义代码即可对外暴露接口。
在实际产品中,这一特性被命名为RESTified Endpoints,官方文档位于 docs/docs/restified/overview.mdx,其定位被明确表述为:RESTified 查询不是Hasura 的主要接口(主要接口仍是原生 GraphQL),而是用于增强 Hasura、让它在偏重 REST 的环境中更容易被采纳。
端点(Endpoint)的四要素模型
RFC 规定,Hasura 元数据中包含若干定义的endpoints,每个端点由以下四部分数据组成:
| 要素 | 说明 |
|---|---|
| 名称(Name) | 作为 Hasura 元数据中的主键(除此之外不作他用) |
| URL 模板(URL template) | 端点对外暴露的路径模式 |
| HTTP 方法集合 | 该端点可接受的 HTTP 方法 |
| 查询或变更(query/mutation) | 一条不含变量的 GraphQL 操作定义 |
RFC 给出的运行示例:
- Name:
user_by_id - URL 模板:
/users/:user_id - Methods:
GET、POST - Query:
query @cached ($user_id: String!) { users(where: { id: { _eq: $user_id } }) { name email role } }从源码看,这一模型被原样实现为 server/src-lib/Hasura/RQL/Types/Endpoint.hs 中的EndpointMetadata记录:
data EndpointMetadata query = EndpointMetadata { _ceName :: EndpointName, _ceUrl :: EndpointUrl, _ceMethods :: NonEmpty EndpointMethod, _ceDefinition :: EndpointDef query, _ceComment :: Maybe Text }其中EndpointMethod枚举了五种方法:GET、POST、PUT、DELETE、PATCH(见 server/src-lib/Hasura/RQL/Types/Endpoint.hs)。而EndpointUrl仅约束为「非空文本」(server/src-lib/Hasura/RQL/Types/Endpoint.hs),实际路径语义由下一节的模板语法解析。
通过元数据 API 创建端点
在 Console 中「REST」标签页或 GraphiQL 的 REST 按钮即可创建端点;同时也可以通过元数据 API 以编程方式管理。create_rest_endpoint请求示例(见 docs/docs/api-reference/schema-metadata-api/restified-endpoints.mdx):
POST /v1/query HTTP/1.1 Content-Type: application/json X-Hasura-Role: admin { "type": "create_rest_endpoint", "args": { "name": "example-name", "url": "example", "methods": ["POST","PUT","PATCH"], "definition": { "query": { "query_name": "example_mutation", "collection_name": "test_collection" } }, "comment": "some optional comment" } }其中definition.query通过query_name与collection_name引用查询集合(Query Collection)中的既有查询;对应的删除操作是drop_rest_endpoint。在服务端,create_rest_endpoint由 server/src-lib/Hasura/RQL/DDL/Endpoint.hs 中的runCreateEndpoint处理:先检查同名端点是否已存在(存在则返回 400AlreadyExists),再通过buildSchemaCacheFor构建端点对应的 schema 缓存并写入元数据。
URL 模板:字面量(Path Literal)与路径参数(Path Parameter)
RFC 用语法形式化定义了 URL 模板的组成:一个 URL 模板是一系列path parts的序列,每个 part 要么是path literal(路径字面量),要么是path parameter(路径参数):
Part := ":", segment-nz-nc ; path parameter | segment-nz-nc ; path literal Template := *("/", Part) ; URL template其中segment-nz-nc(非零长度且不含冒号的段)定义于 RFC 3986 §1.1.1。
示例/users/:user_id由两部分组成:路径字面量users,后跟名为user_id的路径参数。注意路径参数的名字与 GraphQL 查询中唯一变量的名字恰好一致。
RFC 还明确了一个不对称约束:每个路径参数都必须对应一个已定义的查询变量,但反过来不成立——变量也可以通过 URL 的 query 部分或请求体来提供(详见「变量」一节)。
源码中路径的拆分与识别实现在 server/src-lib/Hasura/RQL/Types/Endpoint.hs:
splitPath :: (T.Text -> a) -> (T.Text -> a) -> EndpointUrl -> [a] splitPath var lit = map toPathComponent . T.split (== '/') . toTxt where toPathComponent x | ":" `T.isPrefixOf` x = var x | otherwise = lit x即以/切分路径后,以:前缀判定路径参数。而 server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs 定义了对应的组件数据类型:
data PathComponent a = PathLiteral a | PathParam deriving stock (Show, Eq, Ord, Generic)HTTP 方法约束
RFC 对方法与操作类型的配对关系给出了两条硬性约束:
- 查询(query)操作:只允许
GET和POST,使用任何其他方法都会导致校验错误; - 变更(mutation)操作:禁止
GET,使用GET会导致校验错误。
这一规则的原因在 RFC 的 Validation 一节中说明:允许POST是为了支持非原始(非标量)类型的变量——因为 URL 中只能传标量,复杂对象变量必须放到POST请求体里。GET则天然不具备请求体,故 mutation 不可用GET。
方法集合在源码中是非空的NonEmpty EndpointMethod列表(见上文的EndpointMetadata),每种方法都有对应的 JSON 字符串表示(server/src-lib/Hasura/RQL/Types/Endpoint.hs),并且在构建路由时,同一个端点会按方法逐一挂载到 trie 上(server/src-lib/Hasura/RQL/Types/Endpoint.hs)。
路由(Routing):请求如何匹配到端点
给定一组已定义的端点、HTTP 请求 URL 与方法,RFC 规定按以下算法确定正确端点:
- 按 RFC 3986 将 HTTP 请求 URL 的 path 部分拆分为若干段(segments);
- 对每个已定义的端点:
- 2.1检查请求方法是否在该端点接受的方法集合中;不在则跳到下一个端点;
- 2.2检查 URL 模板的 path parts 数量是否等于请求 URL 的段数;不等则跳到下一个端点;
- 2.3按顺序逐对检查段/part:
- 若 part 是字面量,检查段文本是否与字面量完全一致;不一致则跳到下一个端点;
- 若 part 是参数,记录「参数名 → 段文本」的映射;
- 此时每个端点要么不匹配,要么匹配并附带一组参数名到段的赋值;
- 若存在多个匹配端点:返回500 Internal Server Error(重叠端点在校验阶段就应被检测出来,见「重叠端点」一节);
- 若恰好一个端点匹配:返回该端点以及参数名到变量值的映射——对每对参数名/段,从查询定义中确定对应 GraphQL 变量的类型,并按该类型解析段文本(见「参数类型」一节);
- 若无任何端点匹配,则按情况返回错误码:
- 若没有任何端点使用同一 URL 模板(无论方法),返回404 Not Found;
- 若存在同一 URL 模板的端点但方法不同,返回405 Method Not Allowed,并在
Allow:响应头中列出支持的方法。
在运行示例中,唯一端点能匹配GET /users/abc123,但以下请求都不匹配:
GET /users(段数不符)GET /users/abc123/purchases(段数不符)PUT /users/abc123(方法不符)
源码中的实现:基于 Trie 的匹配
RFC 的路由算法在服务端被实现为一个多值路径 Trie(MultiMapPathTrie,路径组件 → 方法的 MultiMap → 端点元数据),类型定义见 server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs,端点列表通过 server/src-lib/Hasura/RQL/Types/Endpoint.hs 的buildEndpointsTrie构建。
matchPath的匹配结果是一个定义了偏序(lattice)的代数数据类型MatchResult(server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs):
data MatchResult a k v = MatchAmbiguous -- 多个端点同时匹配 | MatchFound v [a] -- 唯一匹配:返回端点和参数绑定列表 | MatchMissingKey (NonEmpty k) -- 路径匹配但方法不匹配:返回允许的方法列表 | MatchNotFound -- 路径未找到MatchAmbiguous(对应 RFC 步骤 4 的 500);MatchFound(步骤 5);MatchMissingKey(步骤 6.2 的 405,附带允许的方法列表);MatchNotFound(步骤 6.1 的 404)。
这正是 HTTP 处理入口 server/src-lib/Hasura/Server/Rest.hs 中runCustomEndpoint的分支逻辑:MatchNotFound -> throw404 "Endpoint not found"、MatchMissingKey allowedMethods -> throw405 ...、MatchAmbiguous -> throw500 "Multiple endpoints match request"。该函数在 server/src-lib/Hasura/Server/App.hs 中被装配进 HTTP 应用,端点统一挂载在/api/rest/前缀下(RestRequest注释表明reqPath是api/rest之后的剩余路径,见 server/src-lib/Hasura/Server/Rest.hs)。
仓库的 Python 集成测试也覆盖了这些场景,例如 server/tests-py/queries/endpoints/endpoint_simple_wrong_method.yaml(错误方法)、server/tests-py/queries/endpoints/endpoint_missing.yaml(404)等,可在 server/tests-py/queries/endpoints/ 目录下查阅。
变量(Variables):三种传递方式
RFC 规定 GraphQL 变量可以通过以下三种方式之一提供:
- URL 模板中的路径参数段(仅限标量类型变量);
- URL 的 query 部分(仅限标量类型变量);
- 请求体:
- 以 JSON 对象编码,
Content-Type: application/json;或 - 以键/值对编码,
Content-Type: application/x-www-form-urlencoded。
- 以 JSON 对象编码,
同名重复变量会导致 400 Bad Request。
无论变量以何种方式提供,它们都会被合并为单一的键/值集合,传给底层 GraphQL 操作,效果等同于在显式执行的 GraphQL 请求的 variables 段中提供;此后的变量检查也完全等同于用户直接发起该请求。
RFC 继续用运行示例说明:user_by_id查询把 user_id 捕获在 URL 模板中,但也可以换一个端点,改为通过 query 参数或请求体捕获。例如如下变体:
- Name:
get_user - URL 模板:
/users/get - Methods:
GET, POST - Query:
query @cached ($user_id: String!) { … }
客户端可以这样调用该端点:
curl -X GET /users/get?user_id=abc123curl -X POST /users/get?user_id=abc123curl -X POST /users/get \ -d '{ "user_id": "abc123" }' \ -H 'Content-Type: application/json'curl -X POST /users/get \ -d 'user_id=abc123' \ -H 'Content-Type: application/x-www-form-urlencoded'源码中的变量解析流程
runCustomEndpoint中变量处理的完整链路([server/src-lib/Hasura/Server/Rest.hs](https://link.gitcode.com/i/80e2d23ec95b8322837deac36f6828b2#L50-L85, L138-L166))可以拆解为三步:
- 提取路径参数名:
parseVariableNames从端点 URL 中取出所有以:开头的段名(server/src-lib/Hasura/Server/Rest.hs); - 对齐期望与已提供变量:
alignVars将查询定义中的变量定义列表与「URL 参数 + 请求体参数 + 路径参数」合并后的集合做 join,得到These(四态)组合——期望且提供、仅期望、仅提供(server/src-lib/Hasura/Server/Rest.hs); - 逐变量解析:
resolveVar处理四种情况(server/src-lib/Hasura/Server/Rest.hs):- 期望但缺失 → 赋
Nothing,交给查询执行层做 null 默认值处理; - 未期望但出现 → 报错「Unexpected variable」;
- 来自请求体 → 直接透传,无需解析;
- 来自 URL(路径或 query)→ 按变量类型解析(详见下节)。
- 期望但缺失 → 赋
解析完成后,构造一个GQLReq(mkPassthroughRequest,server/src-lib/Hasura/Server/Rest.hs),把原始查询字符串与合并后的变量一起转发给底层 GraphQL 执行层(GH.runGQ,相当于内部走一遍/v1/graphql)。这正是 RFC 所说「合并后传给底层 GraphQL 操作」的实现。
参数类型(Parameter Types):URL 变量仅限标量
RFC 的关键限制:通过 URL(路径或 query 部分)提供的变量仅限于原始(标量)类型,即必须是以下五种 GraphQL 原始类型之一:String、ID、Int、Float、Boolean。
解析规则:这些类型的值会从解码后的 URL 文本中按RFC 7159(JSON)字面量的编码方式解析——String与ID按 JSON 字符串字面量解码,Boolean按 JSON 布尔字面量解码,Int与Float按 JSON 数字字面量解码。
Nullable 类型与 List(列表)类型的解析当前不支持,RFC 注明可能在未来版本中添加(对应文末「Future Work」)。
从源码看,resolveVar对 URL 提供的值先尝试 JSON 解码,命中布尔/数字且与变量类型匹配时按 JSON 字面量取值,否则按字符串字面量处理;若变量类型是TypeList(列表类型)则直接报错「List variables are not currently supported in URL or Query parameters」;若是可空类型且值为空,则解析为null(server/src-lib/Hasura/Server/Rest.hs)。这也印证了 RFC 的标量限制与 JSON 字面量解析语义。
响应头(Response Headers):通过 Cache-Control 支持缓存
RFC 规定:服务端通过提供Cache-Control响应头来支持客户端缓存。要启用该行为,查询中应包含@cached指令(即运行示例中的query @cached (...))。返回的Cache-Control头会带有一个max-age参数,表示服务端对返回数据缓存(如 Redis 缓存)的剩余生存时间。
这一设计让浏览器或 CDN 可以直接利用 HTTP 缓存语义。仓库测试 server/tests-py/queries/endpoints/endpoint_simple_cached.yaml 正是针对带@cached指令端点场景的用例。官方文档也建议:与其把是否使用缓存交给客户端决定(可能被遗漏),不如通过@cached指令在服务端查询上固化缓存行为(见 docs/docs/restified/restified-config.mdx 的「Simplifying caching」一节)。
响应码(Response Codes)与响应体(Response Body)
RFC 规定生成的端点对用户错误与配置错误返回标准 HTTP 错误码,包括(但不限于):
| 状态码 | 含义 | 触发场景 |
|---|---|---|
| 400 Bad Request | 请求格式错误 | 意外或错误的查询变量(具体见错误消息) |
| 404 Not Found | 端点不存在 | URL 模板无任何匹配 |
| 405 Method Not Acceptable | 端点存在但方法不符 | 同一 URL 模板下存在其他方法的端点(RFC 正文中此处写作 405 Method Not Allowed) |
| 409 Conflict | 请求包含不一致数据 | 例如重叠的端点定义 |
| 500 Internal Server Error | 服务端内部状态错误 | 如多个端点同时匹配 |
响应体约定:
- 操作成功时,响应体包含等价 GraphQL 响应的
data键的内容(注意:不包一层{"data": ...},直接返回 data 本身——官方 quickstart 中的示例响应即为裸对象,见 docs/docs/restified/create.mdx); - 操作失败时,响应体包含所发生错误的 JSON 表示,并伴随语义明确的 HTTP 状态码。
校验(Validation)
重叠端点(Overlapping Endpoints)
两个端点定义为重叠,当且仅当存在一个合法 HTTP 请求能够同时匹配这两个端点(依据上文 Routing 一节的匹配规则)。
在元数据校验阶段,任何包含重叠端点的元数据都应被拒绝,返回409 Conflict错误码,并附上重叠端点的名称。这也解释了路由算法中「多个端点匹配时返回 500」为何只是运行时兜底——正常配置下重叠在入库前已被拦截。
源码层面,Trie 的lookupPath允许PathParam匹配任意段(server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs),因此像/users/:id与/users/me这类模板就可能重叠,需要靠校验去重。runCustomEndpoint中的注释也指出:/:a/b与/a/:b这类端点在请求/a/b时是「运行时才判定无效」的歧义场景(server/src-lib/Hasura/Server/Rest.hs)。仓库中的 server/tests-py/queries/endpoints/endpoint_conflicting.yaml 即为重叠端点的测试用例。
操作类型(Operation Type)
校验基于底层 GraphQL 操作的类型进行:
- query:HTTP 方法应为
GET或POST(允许POST以支持非原始变量类型); - mutation:HTTP 方法不得为
GET; - subscription:不允许(RFC 注明未来可能改变)。
对应地,server/tests-py/queries/endpoints/endpoint_subscription.yaml 验证了订阅操作的拒绝行为。
变量名与类型(Variable Names and Types)
- URL 模板中提及的每个变量,都必须以原始类型GraphQL 变量的名字出现在底层操作中(即:模板参数必须对应操作里声明的变量,且类型必须为标量)。
未来工作(Future Work)
RFC 在文末列出了若干可能的演进方向,说明该规范是刻意收敛边界、留有扩展空间的设计:
- Subscription 支持:未来可能借助server-sent events这类标准,在 HTTP 之上编码单向事件流,并在浏览器兼容性上优雅降级;
- 嵌套错误支持:目前任何顶层错误都会转换为 HTTP 错误码,但嵌套错误的处理行为尚未定义;未来也可能通过底层操作上的 GraphQL 指令,选择性地把嵌套错误转成 HTTP 错误码;
- Nullable 与 List 类型支持:URL 变量的原始类型限制自然地扩展到 list 与 nullable 类型;
- 生成 Swagger 文档与元数据:利用实体与字段上存储的注释生成 Swagger/OpenAPI 文档与元数据。
其中「Swagger/OpenAPI」方向已部分落地:Console 的 REST 端点页可一键Export OpenAPI Spec,下载覆盖全部 RESTified 端点的 OpenAPI 3.0 规范 JSON(见 docs/docs/restified/export-oas.mdx),服务端对应实现为 server/src-lib/Hasura/Server/OpenAPI.hs。这正呼应了 RFC 开篇「利用现有 REST 工具链与自动化集成」的初衷。
小结
RFC rfcs/rest-endpoints.md 为 Hasura 的 RESTified Endpoints 提供了完整而克制的规范:端点由名称、URL 模板、方法集合与固定 GraphQL 操作四要素构成;URL 模板区分字面量与参数;路由按「方法 → 段数 → 逐段匹配」的算法裁决并区分 404/405/500;变量支持路径参数、URL query 与请求体三种来源且 URL 仅限标量;@cached指令驱动Cache-Control缓存;元数据校验拒绝重叠端点、非法方法组合与越界变量。对照 server/src-lib/Hasura/Server/Rest.hs、server/src-lib/Hasura/RQL/Types/Endpoint/Trie.hs 与 server/tests-py/queries/endpoints/ 的测试用例,可以确认规范中的每一项语义都有对应的工程实现与验证。对于想在偏 REST 的环境中渐进式采纳 Hasura、或希望以 OpenAPI 打通自动化集成的团队,这一特性提供了从 GraphQL 到 REST 的低成本桥梁。
- 后端
- API网关
- 数据库
- GraphQL
【免费下载链接】graphql-engine
Blazing fast, instant realtime GraphQL APIs on all your data with fine grained access control, also trigger webhooks on database events.
相关推荐
Headlamp 源码解析:lib/k8s/endpoints 中的 Endpoints 类如何封装 Kubernetes Endpoints 资源
Headlamp 源码解析:lib/k8s/endpoints 中的 Endpoints 类如何封装 Kubernetes Endpoints 资源 本文以 H
云原生开发工具Hasura GraphQL Engine 的 Apollo Federation v1 支持:从 RFC 设计到源码实现
Hasura GraphQL Engine 的 Apollo Federation v1 支持:从 RFC 设计到源码实现 导读 本文以 rfcs/apollo
后端API网关数据库GraphQLShields Badge URL 设计规范:从路由约定到源码实现
Shields Badge URL 设计规范:从路由约定到源码实现 导读 :本文以 doc/badge urls.md https://link.gitcode
开发工具后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考