大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
规范与传输
这一课你将能做到
- 构造一个完全合规的 GraphQL over HTTP 请求,说清每个 header 与 body 字段的作用
- 根据返回的 HTTP 状态码 +
Content-Type判断失败发生在哪个阶段 - 讲清 graphql-response+json 媒体类型、GET/POST 分工与增量交付的传输格式
为什么要学
「GraphQL 永远返回 200」是流传最广的半真半假的说法。老实现确实如此,但 GraphQL over HTTP 规范(2023 年起成为官方规范的一部分) 重新规定了状态码语义:解析错误 400、校验错误 400、未认证 401、执行错误仍可 200。不理解这一层,你的网关、重试逻辑、监控告警都会基于错误假设搭建。
人话:GraphQL 只管「文档怎么写、结果怎么算」,HTTP 规范管「文档怎么寄、信封上盖什么章」。
先建立图像
请求信封的完整结构:
POST /graphql HTTP/1.1
Host: api.example.com
Content-Type: application/json ← body 的格式
Accept: application/graphql-response+json ← 期望的响应格式(现代)
Authorization: Bearer eyJhbGciOi... ← 鉴权(协议无关)
{
"query": "query Q($slug: String!) { lesson(slug: $slug) { title } }",
"operationName": "Q",
"variables": { "slug": "gqlrm-spec" },
"extensions": { "persistedQuery": { "version": 1, "sha256Hash": "..." } }
}
状态码决策树:
请求到达
├─ body 不是合法 JSON? → 400
├─ query 缺失 / 语法错误? → 400
├─ 校验失败(字段不存在等)? → 400(现代规范)
├─ 未认证 / 无权限整个操作? → 401 / 403
├─ GET 发了 mutation? → 405
└─ 执行期错误(resolver 抛错) → 200 + { data(部分), errors }
跟着做
步骤 1:用 curl 走一遍现代规范请求
curl -i http://localhost:8080/graphql \
-H "Content-Type: application/json" \
-H "Accept: application/graphql-response+json" \
-d '{
"query": "query Ping { lesson(slug: \"gqlrm-spec\") { title } }",
"operationName": "Ping"
}'
观察响应头:Content-Type: application/graphql-response+json; charset=utf-8。这就是 IANA 注册的官方媒体类型,区别于老的 application/json。
步骤 2:GET 请求做纯查询(可缓存场景)
规范允许 GET 承载无副作用的 query(不允许 mutation):
curl -i "http://localhost:8080/graphql?query=%7B%20lesson(slug%3A%5C%22gqlrm-spec%5C%22)%20%7B%20title%20%7D%20%7D&operationName=X"
query/variables/extensions 都放 URL 参数(URL 编码)。意义:GET 响应理论上可被 CDN/浏览器按 HTTP 缓存语义缓存——这是 GraphQL 蹭 HTTP 缓存的唯一通道。
步骤 3:分别制造三类失败,看状态码
# ① 语法错误 → 400
curl -s -o /dev/null -w "%{http_code}\n" -X POST localhost:8080/graphql \
-H "Content-Type: application/json" -d '{"query":"{ lesson("}'
# ② 校验错误 → 400(现代实现)
curl -s -o /dev/null -w "%{http_code}\n" -X POST localhost:8080/graphql \
-H "Content-Type: application/json" -d '{"query":"{ notExistField }"}'
# ③ 执行错误 → 200 + errors
curl -s -X POST localhost:8080/graphql \
-H "Content-Type: application/json" -d '{"query":"{ lesson(slug:\"no-such\") { title } }"}'
# → HTTP 200, {"data":{"lesson":null}} 或带 errors,视 Schema 可空性
步骤 4:持久化查询(Persisted Query)走 extensions
生产环境只放行预注册查询(APQ,Automatic Persisted Queries):
// 第一次:只发 hash,服务端没存过
{ "extensions": { "persistedQuery": { "version": 1, "sha256Hash": "abc..." } } }
// ← 200 { "errors": [{ "message": "PersistedQueryNotFound" }] }
// 第二次:hash + 全文一起发,服务端缓存映射
{ "query": "query Q {...}",
"extensions": { "persistedQuery": { "version": 1, "sha256Hash": "abc..." } } }
// 以后:只发 hash 即可,省带宽 + 天然白名单
步骤 5:增量交付的传输格式
@defer/@stream 的响应是 multipart/mixed:
HTTP/1.1 200 OK
Content-Type: multipart/mixed; boundary="graphql"
--graphql
Content-Type: application/json
{"data":{"lesson":{"title":"...","content":"..."}},"hasNext":true}
--graphql
Content-Type: application/json
{"incremental":[{"data":{"comments":[...]},"path":["lesson"]}],"hasNext":false}
--graphql--
客户端按分块流式消费,path 指明这块数据该合并到哪个位置。
讲清楚要点
- body 四字段:
query(必填)、operationName(多操作时必填)、variables、extensions(协议扩展点,持久化查询/追踪都挂这)。 application/graphql-response+json是正统:新实现用它;老客户端发Accept: application/json时服务端可降级,但语义上执行错误也该带正确媒体类型。- 状态码分两类:请求本身有问题(解析/校验/方法错误)→ 4xx;文档合法但执行出错 → 200 + errors。网关告警应只抓 4xx/5xx,别对 200+errors 误报。
- GET 仅限 query 且无副作用:mutation 走 GET 必须 405;GET 的价值是 HTTP 缓存兼容性。
- errors 数组的法定成员:
message必有,locations/path辅助定位,extensions放机器可读的错误码(如"code": "UNAUTHENTICATED")——客户端分支逻辑应依赖 extensions.code,别解析 message 文案。 - 规范三层:GraphQL 核心规范(语言/类型/执行)+ GraphQL over HTTP(传输)+ 各协议子规范(graphql-ws 等),出问题先定位在哪层。
你可能会懵的地方
- 那「GraphQL 永远 200」过时了吗? 对。2023 年 GraphQL over HTTP 并入官方规范后,解析/校验错误必须 400。但执行期错误仍是 200 + errors,因为「部分成功」无法用单一状态码表达。
- 为什么错误码要放 extensions 而不是 message?
message 给人看(会改文案、会国际化),code 给机器分支(稳定契约)。客户端
if (code === 'UNAUTHENTICATED') 跳登录。 - OPTIONS 预检会有问题吗? 跨域时浏览器先发 OPTIONS,服务端要正常回 CORS 头。内网 BFF 同域部署可绕开。
- 文件上传规范吗?
核心规范不管上传。社区事实是
graphql-multipart-request-spec(multipart/form-data,files + map 字段),不少库内置支持。 - batching(一次发多个操作数组)规范吗?
不在官方规范。部分服务端支持
[{query...},{query...}]数组请求,属实现扩展,别当通用能力依赖。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 客户端只对非 200 报错 | 执行错误(200+errors)静默失败 | 同时检查 HTTP 状态与 body.errors |
| 用 message 文案做分支 | 文案一改逻辑崩 | 用 errors[].extensions.code |
| 全站 GET 调 mutation | 语义错乱 + 缓存污染风险 | mutation 一律 POST |
| 监控按「非 200 即告警」 | 400 校验失败刷屏 | 区分客户端错误与服务端错误,4xx 采样 |
| Accept 头乱写 | 服务端内容协商失败 406 | 标准 application/graphql-response+json |
| 重试所有 4xx | 400 重试一万次还是 400 | 仅网络错误/5xx 可重试,4xx 直接失败 |
卡点急救
- 返回 406 Not Acceptable:Accept 头服务端不支持,改
application/graphql-response+json, application/json。 - 本地 200、过网关 400:网关对 body 大小/URL 长度有限制,GET 长查询容易撞 URL 上限——改用 POST。
- 客户端拿不到错误详情,只有 "Internal Server Error":生产模式屏蔽了原始 message(安全正确),真正定位靠 extensions.code + 服务端日志的 requestId。
面试常问
- GraphQL 的 HTTP 状态码语义? 按 GraphQL over HTTP 规范:请求格式/解析/校验错误 400,方法不允许 405,内容协商失败 406,认证授权 401/403;文档合法但执行期错误 200 + errors(支持部分成功)。老实现一律 200 是历史遗留。
- errors 数组的结构与使用约定? message(必有,人读)、locations(行列)、path(字段路径)、extensions(机器可读码如 UNAUTHENTICATED)。客户端分支依赖 extensions.code,message 仅展示。
- GET vs POST 在 GraphQL 中的分工? POST 是默认,承载所有操作类型;GET 仅允许 query(无副作用),价值在于可接入 HTTP 缓存体系。mutation 经 GET 必须拒绝(405)。
自测三问
- 校验失败和执行失败的状态码分别是什么?为什么这样设计?
errors[].extensions.code和message的分工?- 持久化查询(APQ)的两阶段握手是怎么走的?
小结一句话
GraphQL 核心规范定义语言与执行,over HTTP 规范定义信封与盖章:body 四字段、graphql-response+json 媒体类型、4xx 拦坏请求、200+errors 承载部分成功、extensions 承载机器可读错误码。
延伸
- GraphQL over HTTP 规范
- GraphQL 核心规范
- 下一课:客户端——Apollo、urql、Relay 的缓存模型与选型。