Debug Lab · Federation 十种典型故障Debug Lab · ten common Federation failures
从「resolver 写了但返回 null」到「composition 失败」,每一种都给真实报错。From a resolver that runs but returns null, to a composition failure. Every case comes with the real error text.
这一页有什么On this page6
- 看到 GraphQL 报错能先归类,再决定去哪个文件找Sort a GraphQL error into a category first, then decide which file to open
- 认出「不报错但返回 null」这一类最难查的故障Recognize the hardest failure type: nothing is reported, but the field comes back null
- 掌握 composition 失败的排查顺序Know the order in which to check a composition failure
- 把错误信息和根因建立稳定的对应关系Build a reliable mapping from each error message to its root cause
这门考试有一半时间花在「为什么测试还是红的」。GraphQL 的报错比 React 更隐蔽 —— 很多错误表现为「静默返回 null」而不是抛异常。Half of the time in this exam goes to one question: why is the test still failing? GraphQL errors are harder to spot than React errors. Many of them show up as a null value with no message, not as a thrown exception.
graphql-federation-practice/node-subgraph/src/所有故障都基于这个项目的真实代码Every fault is based on the real code of this project
graphql-federation-practice/node-subgraph/src/先分诊:GraphQL 故障的六类Triage first: six categories of GraphQL failure
| 类别 | 典型信号 | 去哪找 |
|---|---|---|
| schema 校验 | Cannot query field "x" on type "Y" | 查询写错了,或 schema 里真没这个字段 |
| 非空违约 | Cannot return null for non-nullable field | resolver 忘了 ?? [] 兜底 |
| 跨模块契约 | x is not a function、Cannot read properties of undefined | 方法名 / context 键名 / 签名对不上 |
| 名字不匹配 | 没有报错,字段静默返回 null | resolver 的键名和 schema 字段名不一致 |
| 错误语义 | 有报错但 extensions.code 不对 | catch 把结构化错误重新包装了 |
| composition | Router 启动失败 / Unknown directive | schema 的 @link / @key 声明 |
第四类是 GraphQL 特有的、也是最难查的。React 里名字写错通常会有类型错误或运行时报错; GraphQL 里 resolver 就是个普通对象的键 —— 键名写错等于「这个 resolver 不存在」, 执行器于是用默认 resolver(取 parent[字段名]), 取不到就返回 null。一声不响。
| Category | Typical signal | Where to look |
|---|---|---|
| Schema validation | Cannot query field "x" on type "Y" | The query is wrong, or the schema really has no such field |
| Non-null violation | Cannot return null for non-nullable field | A resolver forgot its ?? [] fallback |
| Cross-module contract | x is not a function, Cannot read properties of undefined | A method name, a context key or a signature does not line up |
| Name mismatch | No error at all, the field silently returns null | The resolver key does not match the schema field name |
| Error semantics | There is an error, but extensions.code is wrong | A catch block re-wrapped an already structured error |
| Composition | Router fails to start / Unknown directive | The @link / @key declarations in the schema |
The fourth category is peculiar to GraphQL, and the hardest to track down. In React a misspelled name usually gives you a type error or a runtime throw. In GraphQL a resolver is just a key on a plain object — misspell the key and the resolver simply does not exist, so the executor falls back to the default resolver (read parent[fieldName]) and returns null when there is nothing there. Not a peep.
「静默返回 null」的三种成因Three causes of a silent null
看到某个字段是 null 而你确信写了 resolver,按这三条查。When a field comes back null and you are sure you wrote the resolver, check these three things.
- resolver 的键名和 schema 字段名不一致。schema 里是
shippingInfo, 你写成了shipping或shippingInfos。大小写也算。 - resolver 挂在了错误的类型下。
shippingInfo是Order上的字段, 写进resolvers.Query里就永远不会被调用。 - 忘了 return。
async shippingInfo(parent, _, ctx) { loaders.x.load(parent.id) }—— 算了但没返回,async 函数返回undefined。
排查手法:在 resolver 第一行放一个 console.log。如果那行日志根本没打印, 就是第 1 或第 2 种;打印了但结果是 null, 就是第 3 种或数据源真的没数据。
这个手法很朴素,但它能在十秒内区分 「我的 resolver 没被调用」和「我的 resolver 逻辑错了」—— 这两者的排查方向完全不同。
- The resolver key does not match the schema field name. The schema says
shippingInfoand you wroteshippingorshippingInfos. Case counts too. - The resolver is hanging off the wrong type.
shippingInfois a field onOrder; put it insideresolvers.Queryand it will never be called. - You forgot the return.
async shippingInfo(parent, _, ctx) { loaders.x.load(parent.id) }— the work happens but nothing comes back, so the async function resolves toundefined.
How to find out which: drop a console.log on the first line of the resolver. If that line never prints, it is case 1 or 2. If it prints but the result is null, it is case 3 — or the data source genuinely has no data.
It is a crude trick, but it tells you within ten seconds whether your resolver was never called or your resolver logic is wrong — and those two send you looking in completely different places.
composition 失败怎么排How to debug a composition failure
本仓库没有 Router,但这类问题值得知道 —— 而且 _service 能测出一半。This repository has no Router, but the failure is still worth knowing. A test on _service already catches half of these cases.
Router 启动时会向每个 subgraph 查{ _service { sdl } }, 然后把所有 SDL 组合成 supergraph。 这一步失败的常见原因:
| 原因 | 报错长什么样 |
|---|---|
@key 指定的字段在类型里不存在 | On type "User", for @key(fields: "uid") — Cannot query field "uid" |
用了 directive 但 @link 的 import 里没列 | Unknown directive "@shareable" |
两个 subgraph 定义了同名非 entity 类型且未标 @shareable | Field "X.y" can only be defined in one subgraph |
entity 缺 @key | Type "User" has no @key directive but is referenced |
| subgraph URL 写错 / 服务没起 | Couldn't load service definitions for ... |
本地能测出一半:前四类里有三类会在 buildSubgraphSchema这一步就炸(服务起不来),或者让{ _service { sdl } } 报错。 所以「服务能起来 + SDL 查得出来」 已经排除了大部分 composition 问题。
真跨 subgraph 的冲突(第三类)本地测不出来 —— 需要两个 subgraph 才能复现。这类问题在本次 assessment 里不会遇到, 因为只有一个 subgraph。
At startup the Router asks every subgraph for { _service { sdl } } and stitches all the SDL into a supergraph. Common reasons that step fails:
| Cause | What the error looks like |
|---|---|
A field named in @key does not exist on the type | On type "User", for @key(fields: "uid") — Cannot query field "uid" |
A directive is used but not listed in the @link import | Unknown directive "@shareable" |
Two subgraphs define the same non-entity type without @shareable | Field "X.y" can only be defined in one subgraph |
An entity is missing its @key | Type "User" has no @key directive but is referenced |
| Wrong subgraph URL, or the service is not running | Couldn't load service definitions for ... |
You can catch half of these locally: three of the first four fail right at buildSubgraphSchema (the service will not start), or make { _service { sdl } } throw. So “the service starts and the SDL comes out” has already ruled out most composition problems.
A genuine cross-subgraph conflict (the third row) cannot be reproduced locally — you need two subgraphs for that. You will not hit it in this assessment, because there is only one subgraph.
一个脚本把该验的全验一遍One script that checks every item at once
做完 Task 1 之后,跑这个比反复 npm test 有用。After you finish Task 1, running this tells you more than running npm test again and again.
npm test 只覆盖了单元层面。 下面这个脚本(审计时实际用的)在进程内把 federation 的关键路径全走一遍, 不需要起服务器、不占端口:
期望输出(参考解法下审计实测):
八行输出对应八件事:SDL 出得来、@key 在里面、 普通查询 + 字段 resolver 正常、按 id 查正常、 找不到时错误码正确、entity 解析正常、 mutation 的价格补全正常、校验错误码正确。全对了,Task 1 就真的做完了。
npm test only covers the unit level. The script below (the one actually used during the audit) walks every important federation path in-process — no server to start, no port to occupy:
Expected output (measured against the reference solution during the audit):
Eight lines of output cover: the SDL comes out, @key is in it, a plain query plus a field resolver work, lookup by id works, the error code on a miss is right, entity resolution works, the mutation fills in prices, and the validation error code is right. All green means Task 1 is genuinely finished.
动手做Get your hands on it
填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.
你确信写了 shippingInfo 的实现, 测试也不报错,但查询返回的 shippingInfo 是null。控制台里连你加的 log 都没打印。
You are sure you wrote an implementation for shippingInfo, and no test reports anything, but the query returns shippingInfo as null. Not even the log line you added prints on the console.
查一个没有订单的用户,整个 data 变成了null,而且 errors 里有一条很长的消息。
You query a user who has no orders, the whole data turns into null, and errors carries one very long message.
查两个订单的物流,返回的数据对上了错的订单。 没有任何报错。这是 DataLoader 最阴险的一类误用。
You query the shipping info for two orders and the data comes back attached to the wrong order. Nothing reports an error. This is the hardest kind of DataLoader misuse to notice.
Java 那边。mvn test 全过, 但客户端传小写的 shipped 时服务返回 500。
This one is on the Java side. mvn test passes everything, but the service returns 500 when the client sends the lowercase shipped.
换一道题也能用Works on other problems too
考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.
- GraphQL 故障六类:schema 校验 / 非空违约 / 跨模块契约 / 名字不匹配 / 错误语义 / composition。Six categories of GraphQL failure: schema validation, non-null violation, cross-module contract, name mismatch, error semantics, composition.
- 「名字不匹配」是 GraphQL 特有的静默故障 —— resolver 键名错了就等于不存在。A name mismatch is the silent failure that is specific to GraphQL: a wrong resolver key means the resolver does not exist at all.
- 排查静默 null 的第一步:在 resolver 第一行 log,看它有没有被调用。First step for a silent null: log on the first line of the resolver and check whether it is called.
- DataLoader 的 batch 函数永远不要 filter —— 长度和顺序都是硬契约。Never filter inside a DataLoader batch function. Both the length and the order are a strict contract.
- 「服务能起来 + _service 查得出 SDL」已经排除了大部分 composition 问题。If the service starts and _service returns the SDL, most composition problems are already ruled out.