DrillLab
第 16 / 17 节LESSON 16 / 17约 22 分钟~22 min

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.

4 个练习4 exercisesFederation · 第 6 部分Federation · Part 6
这一页有什么On this page6
学完这节你会After this lesson you can
  • 看到 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
这在考试里考什么What the exam does with this

这门考试有一半时间花在「为什么测试还是红的」。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.

这节课要看的真实文件Real files this lesson looks at1 项 · 1 个可以展开看原文1 items · 1 can be opened
graphql-federation-practice/node-subgraph/src/所有故障都基于这个项目的真实代码Every fault is based on the real code of this project
Textsrc/ · tree源项目From source
1src/
2├── dataSources/
3│ └── orderDataSource.js
4├── resolvers/
5│ └── orderResolvers.js
6├── index.js
7└── schema.graphql
Source: graphql-federation-practice/node-subgraph/src/
§01

先分诊:GraphQL 故障的六类Triage first: six categories of GraphQL failure

类别典型信号去哪找
schema 校验Cannot query field "x" on type "Y"查询写错了,或 schema 里真没这个字段
非空违约Cannot return null for non-nullable fieldresolver 忘了 ?? [] 兜底
跨模块契约x is not a functionCannot read properties of undefined方法名 / context 键名 / 签名对不上
名字不匹配没有报错,字段静默返回 nullresolver 的键名和 schema 字段名不一致
错误语义有报错但 extensions.code 不对catch 把结构化错误重新包装了
compositionRouter 启动失败 / Unknown directiveschema 的 @link / @key 声明

第四类是 GraphQL 特有的、也是最难查的。React 里名字写错通常会有类型错误或运行时报错; GraphQL 里 resolver 就是个普通对象的键 —— 键名写错等于「这个 resolver 不存在」, 执行器于是用默认 resolver(取 parent[字段名]), 取不到就返回 null一声不响。

CategoryTypical signalWhere to look
Schema validationCannot query field "x" on type "Y"The query is wrong, or the schema really has no such field
Non-null violationCannot return null for non-nullable fieldA resolver forgot its ?? [] fallback
Cross-module contractx is not a function, Cannot read properties of undefinedA method name, a context key or a signature does not line up
Name mismatchNo error at all, the field silently returns nullThe resolver key does not match the schema field name
Error semanticsThere is an error, but extensions.code is wrongA catch block re-wrapped an already structured error
CompositionRouter fails to start / Unknown directiveThe @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.

§02

「静默返回 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.

  1. resolver 的键名和 schema 字段名不一致。schema 里是 shippingInfo, 你写成了 shippingshippingInfos大小写也算
  2. resolver 挂在了错误的类型下。shippingInfoOrder 上的字段, 写进 resolvers.Query 里就永远不会被调用。
  3. 忘了 return。async shippingInfo(parent, _, ctx) { loaders.x.load(parent.id) }—— 算了但没返回,async 函数返回 undefined

排查手法:在 resolver 第一行放一个 console.log如果那行日志根本没打印, 就是第 1 或第 2 种;打印了但结果是 null, 就是第 3 种或数据源真的没数据。

这个手法很朴素,但它能在十秒内区分 「我的 resolver 没被调用」和「我的 resolver 逻辑错了」—— 这两者的排查方向完全不同。

  1. The resolver key does not match the schema field name. The schema says shippingInfo and you wrote shipping or shippingInfos. Case counts too.
  2. The resolver is hanging off the wrong type. shippingInfo is a field on Order; put it inside resolvers.Query and it will never be called.
  3. 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 to undefined.

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.

§03

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 类型且未标 @shareableField "X.y" can only be defined in one subgraph
entity 缺 @keyType "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:

CauseWhat the error looks like
A field named in @key does not exist on the typeOn type "User", for @key(fields: "uid") — Cannot query field "uid"
A directive is used but not listed in the @link importUnknown directive "@shareable"
Two subgraphs define the same non-entity type without @shareableField "X.y" can only be defined in one subgraph
An entity is missing its @keyType "User" has no @key directive but is referenced
Wrong subgraph URL, or the service is not runningCouldn'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.

§04

一个脚本把该验的全验一遍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.

JavaScriptverify-schema.mjs已跑通Verified
1// verify-schema.mjs —— 放在 node-subgraph/ 下:node verify-schema.mjs
2import { buildSubgraphSchema } from '@apollo/subgraph';
3import { graphql } from 'graphql';
4import gql from 'graphql-tag';
5import { readFileSync } from 'fs';
6import { resolvers, createShippingInfoLoader, createOrderLoader } from './src/resolvers/orderResolvers.js';
7import { OrderDataSource, InventoryDataSource, ShippingDataSource } from './src/dataSources/orderDataSource.js';
8
9const typeDefs = gql(readFileSync('./src/schema.graphql', 'utf-8'));
10const schema = buildSubgraphSchema([{ typeDefs, resolvers }]);
11
12function ctx() {
13 const orderDataSource = new OrderDataSource();
14 const inventoryDataSource = new InventoryDataSource();
15 const shippingDataSource = new ShippingDataSource();
16 return {
17 dataSources: { orderDataSource, inventoryDataSource, shippingDataSource },
18 loaders: {
19 shippingInfoLoader: createShippingInfoLoader(shippingDataSource),
20 orderLoader: createOrderLoader(orderDataSource),
21 },
22 correlationId: 'verify-1',
23 };
24}
25
26const run = (source, variableValues) =>
27 graphql({ schema, source, contextValue: ctx(), variableValues });
28
29const log = console.log;
30console.log = () => {}; // 静音 resolver 日志,只看结果
31
32const sdl = await run('{ _service { sdl } }');
33log('SDL emitted:', !!sdl.data?._service?.sdl, '| errors:', sdl.errors?.length ?? 0);
34log('SDL has @key:', /@key\(fields:\s*"id"\)/.test(sdl.data._service.sdl));
35
36const q1 = await run('{ orders(userId:"123") { id status totalAmount shippingInfo { status trackingNumber } } }');
37log('Query.orders + shippingInfo:', JSON.stringify(q1.data), '| errors:', JSON.stringify(q1.errors ?? []));
38
39const q2 = await run('{ order(id:"order-457") { id userId status } }');
40log('Query.order:', JSON.stringify(q2.data));
41
42const q3 = await run('{ order(id:"order-999") { id } }');
43log('Query.order not found code:', q3.errors?.map(e => e.extensions.code));
44
45const q4 = await run(
46 'query($r:[_Any!]!){ _entities(representations:$r) { ... on User { id orders { id status } } } }',
47 { r: [{ __typename: 'User', id: '123' }] }
48);
49log('_entities User.orders:', JSON.stringify(q4.data), '| errors:', JSON.stringify(q4.errors ?? []));
50
51const q5 = await run('mutation { createOrder(userId:"789", items:[{productId:"prod-789", quantity:2}]) { id totalAmount items { productId quantity price } } }');
52log('Mutation.createOrder:', JSON.stringify(q5.data), '| errors:', JSON.stringify(q5.errors ?? []));
53
54const q6 = await run('mutation { createOrder(userId:"789", items:[]) { id } }');
55log('createOrder empty items code:', q6.errors?.map(e => e.extensions.code));
1// verify-schema.mjs — put it in node-subgraph/, run: node verify-schema.mjs
2import { buildSubgraphSchema } from '@apollo/subgraph';
3import { graphql } from 'graphql';
4import gql from 'graphql-tag';
5import { readFileSync } from 'fs';
6import { resolvers, createShippingInfoLoader, createOrderLoader } from './src/resolvers/orderResolvers.js';
7import { OrderDataSource, InventoryDataSource, ShippingDataSource } from './src/dataSources/orderDataSource.js';
8
9const typeDefs = gql(readFileSync('./src/schema.graphql', 'utf-8'));
10const schema = buildSubgraphSchema([{ typeDefs, resolvers }]);
11
12function ctx() {
13 const orderDataSource = new OrderDataSource();
14 const inventoryDataSource = new InventoryDataSource();
15 const shippingDataSource = new ShippingDataSource();
16 return {
17 dataSources: { orderDataSource, inventoryDataSource, shippingDataSource },
18 loaders: {
19 shippingInfoLoader: createShippingInfoLoader(shippingDataSource),
20 orderLoader: createOrderLoader(orderDataSource),
21 },
22 correlationId: 'verify-1',
23 };
24}
25
26const run = (source, variableValues) =>
27 graphql({ schema, source, contextValue: ctx(), variableValues });
28
29const log = console.log;
30console.log = () => {}; // silence the resolver logs, show only results
31
32const sdl = await run('{ _service { sdl } }');
33log('SDL emitted:', !!sdl.data?._service?.sdl, '| errors:', sdl.errors?.length ?? 0);
34log('SDL has @key:', /@key\(fields:\s*"id"\)/.test(sdl.data._service.sdl));
35
36const q1 = await run('{ orders(userId:"123") { id status totalAmount shippingInfo { status trackingNumber } } }');
37log('Query.orders + shippingInfo:', JSON.stringify(q1.data), '| errors:', JSON.stringify(q1.errors ?? []));
38
39const q2 = await run('{ order(id:"order-457") { id userId status } }');
40log('Query.order:', JSON.stringify(q2.data));
41
42const q3 = await run('{ order(id:"order-999") { id } }');
43log('Query.order not found code:', q3.errors?.map(e => e.extensions.code));
44
45const q4 = await run(
46 'query($r:[_Any!]!){ _entities(representations:$r) { ... on User { id orders { id status } } } }',
47 { r: [{ __typename: 'User', id: '123' }] }
48);
49log('_entities User.orders:', JSON.stringify(q4.data), '| errors:', JSON.stringify(q4.errors ?? []));
50
51const q5 = await run('mutation { createOrder(userId:"789", items:[{productId:"prod-789", quantity:2}]) { id totalAmount items { productId quantity price } } }');
52log('Mutation.createOrder:', JSON.stringify(q5.data), '| errors:', JSON.stringify(q5.errors ?? []));
53
54const q6 = await run('mutation { createOrder(userId:"789", items:[]) { id } }');
55log('createOrder empty items code:', q6.errors?.map(e => e.extensions.code));
Text审计时的真实输出The real output from the audit已跑通Verified
1SDL emitted: true | errors: 0
2SDL has @key: true
3Query.orders + shippingInfo: {"orders":[
4 {"id":"order-456","status":"SHIPPED","totalAmount":299.99,
5 "shippingInfo":{"status":"IN_TRANSIT","trackingNumber":"TRACK123456"}},
6 {"id":"order-457","status":"DELIVERED","totalAmount":89.99,
7 "shippingInfo":{"status":"DELIVERED","trackingNumber":"TRACK123457"}}]} | errors: []
8Query.order: {"order":{"id":"order-457","userId":"123","status":"DELIVERED"}}
9Query.order not found code: [ 'ORDER_NOT_FOUND' ]
10_entities User.orders: {"_entities":[{"id":"123","orders":[
11 {"id":"order-456","status":"SHIPPED"},{"id":"order-457","status":"DELIVERED"}]}]} | errors: []
12Mutation.createOrder: {"createOrder":{"id":"order-1785737900978","totalAmount":299.98,
13 "items":[{"productId":"prod-789","quantity":2,"price":149.99}]}} | errors: []
14createOrder empty items code: [ 'INVALID_INPUT' ]
练习Practice

动手做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.

L3Debug LabDebug Lab故障 1 · resolver 写了,字段还是 nullFault 1 · the resolver is written, the field is still null

你确信写了 shippingInfo 的实现, 测试也不报错,但查询返回的 shippingInfonull。控制台里连你加的 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.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 $ node verify-schema.mjs Query.orders + shippingInfo: {"orders":[ {"id":"order-456","status":"SHIPPED","shippingInfo":null}, {"id":"order-457","status":"DELIVERED","shippingInfo":null}]} errors: [] # 你在 resolver 第一行加的 console.log('>>> shippingInfo called') # 一次都没打印。# No error at all. $ node verify-schema.mjs Query.orders + shippingInfo: {"orders":[ {"id":"order-456","status":"SHIPPED","shippingInfo":null}, {"id":"order-457","status":"DELIVERED","shippingInfo":null}]} errors: [] # The console.log('>>> shippingInfo called') you added as the resolver's first line # never printed, not once.
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1export const resolvers = {
2 Order: {
3 async shipping(parent, _, { loaders }) { // ← 名字
4 console.log('>>> shippingInfo called');
5 return loaders.shippingInfoLoader.load(parent.id);
6 }
7 },
8 ...
9};
10
11// 参考 schema.graphql:
12// type Order {
13// ...
14// shippingInfo: ShippingInfo
15// }
1export const resolvers = {
2 Order: {
3 async shipping(parent, _, { loaders }) { // ← the name
4 console.log('>>> shippingInfo called');
5 return loaders.shippingInfoLoader.load(parent.id);
6 }
7 },
8 ...
9};
10
11// For reference, schema.graphql says:
12// type Order {
13// ...
14// shippingInfo: ShippingInfo
15// }
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
L2Debug LabDebug Lab故障 2 · Cannot return null for non-nullable fieldFault 2 · Cannot return null for non-nullable field

查一个没有订单的用户,整个 data 变成了null,而且 errors 里有一条很长的消息。

You query a user who has no orders, the whole data turns into null, and errors carries one very long message.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ node verify-schema.mjs Query.orders: {"orders":null} errors: [{ "message": "Cannot return null for non-nullable field Query.orders.", "path": ["orders"], "extensions": { "code": "INTERNAL_SERVER_ERROR" } }] # 更严重的情况:如果查询是嵌套的,整个 data 会变成 null$ node verify-schema.mjs Query.orders: {"orders":null} errors: [{ "message": "Cannot return null for non-nullable field Query.orders.", "path": ["orders"], "extensions": { "code": "INTERNAL_SERVER_ERROR" } }] # Worse case: if the query is nested, the whole data object turns into null
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1async orders(_, { userId }, { dataSources, correlationId }) {
2 const orders = await dataSources.orderDataSource.getOrdersByUserId(userId);
3 return orders; // 数据源可能返回 undefined
4}
5
6// 参考 schema.graphql:
7// type Query {
8// orders(userId: ID!): [Order!]! ← 双重非空
9// }
1async orders(_, { userId }, { dataSources, correlationId }) {
2 const orders = await dataSources.orderDataSource.getOrdersByUserId(userId);
3 return orders; // the data source may return undefined
4}
5
6// For reference, schema.graphql says:
7// type Query {
8// orders(userId: ID!): [Order!]! ← non-null twice
9// }
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
L3Debug LabDebug Lab故障 3 · A 拿到了 B 的数据Fault 3 · A receives B's data

查两个订单的物流,返回的数据对上了错的订单。 没有任何报错。这是 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.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 # 查询:{ orders(userId:"123") { id shippingInfo { trackingNumber } } } # # 期望: # order-456 -> TRACK123456 # order-457 -> TRACK123457 # # 实际: # order-456 -> TRACK123457 ← 串了! # order-457 -> null # 日志:[DataLoader] Batching 2 shipping info requests ← 合并是生效的# No error at all. # Query: { orders(userId:"123") { id shippingInfo { trackingNumber } } } # # Expected: # order-456 -> TRACK123456 # order-457 -> TRACK123457 # # Actual: # order-456 -> TRACK123457 ← got the other one's number! # order-457 -> null # Log: [DataLoader] Batching 2 shipping info requests ← the batching does work
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1function createShippingInfoLoader(shippingDataSource) {
2 return new DataLoader(async orderIds => {
3 console.log(`[DataLoader] Batching ${orderIds.length} shipping info requests`);
4
5 const all = await Promise.all(
6 orderIds.map(id => shippingDataSource.getShippingInfo(id))
7 );
8
9 // 「过滤掉没有物流信息的」—— 看起来很合理
10 return all.filter(info => info !== null);
11 });
12}
1function createShippingInfoLoader(shippingDataSource) {
2 return new DataLoader(async orderIds => {
3 console.log(`[DataLoader] Batching ${orderIds.length} shipping info requests`);
4
5 const all = await Promise.all(
6 orderIds.map(id => shippingDataSource.getShippingInfo(id))
7 );
8
9 // "drop the ones with no shipping info" — this looks reasonable
10 return all.filter(info => info !== null);
11 });
12}
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
L2Debug LabDebug Lab故障 4 · PATCH 传了小写状态,返回 500Fault 4 · PATCH sends a lowercase status and gets a 500

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.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ curl -i -X PATCH localhost:8080/api/orders/1/status \ -H 'Content-Type: application/json' -d '{"status":"shipped"}' HTTP/1.1 500 {"timestamp":"...","status":500,"error":"Internal Server Error"} # 服务端日志: java.lang.IllegalArgumentException: No enum constant com.techflow.orders.model.OrderStatus.shipped at java.base/java.lang.Enum.valueOf(Enum.java:293) at com.techflow.orders.model.OrderStatus.valueOf(OrderStatus.java:3) at c.t.orders.controller.OrderController.updateOrderStatus(OrderController.java:71) # mvn test:Tests run: 5, Failures: 0 ← 测试全过$ curl -i -X PATCH localhost:8080/api/orders/1/status \ -H 'Content-Type: application/json' -d '{"status":"shipped"}' HTTP/1.1 500 {"timestamp":"...","status":500,"error":"Internal Server Error"} # Server log: java.lang.IllegalArgumentException: No enum constant com.techflow.orders.model.OrderStatus.shipped at java.base/java.lang.Enum.valueOf(Enum.java:293) at com.techflow.orders.model.OrderStatus.valueOf(OrderStatus.java:3) at c.t.orders.controller.OrderController.updateOrderStatus(OrderController.java:71) # mvn test: Tests run: 5, Failures: 0 ← every test passes
JavaOrderController.java示意Illustrative
1@PatchMapping("/api/orders/{id}/status")
2public ResponseEntity<Order> updateOrderStatus(
3 @PathVariable Long id,
4 @RequestBody Map<String, String> statusUpdate) {
5 OrderStatus status = OrderStatus.valueOf(statusUpdate.get("status"));
6 return ResponseEntity.ok(orderService.updateOrderStatus(id, status));
7}
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
迁移Transfer

换一道题也能用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.

字段静默返回 nullA field returns null and nothing is reported
在 resolver 第一行 log,确认它有没有被调用Log on the first line of the resolver to see whether it runs at all
Cannot return null for non-nullable fieldCannot return null for non-nullable field
?? [] 兜底,别改 schemaAdd a ?? [] fallback; do not change the schema
数据串了但不报错Values land on the wrong records, with no error
查 DataLoader batch 函数有没有 filter 或改顺序Check whether the DataLoader batch function filters or reorders the results
xxx is not a functionxxx is not a function
核对方法名与 context 键名Compare the method name and the context key name
Unknown directiveUnknown directive
@link 的 import 列表里漏了它It is missing from the import list of @link
客户端输入问题返回 500Bad client input returns 500
在最靠近的地方转成 400,别加 catch-allTurn it into a 400 at the closest point to the cause; do not add a catch-all handler
这节的要点What to take away
  1. GraphQL 故障六类:schema 校验 / 非空违约 / 跨模块契约 / 名字不匹配 / 错误语义 / composition。Six categories of GraphQL failure: schema validation, non-null violation, cross-module contract, name mismatch, error semantics, composition.
  2. 「名字不匹配」是 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.
  3. 排查静默 null 的第一步:在 resolver 第一行 log,看它有没有被调用。First step for a silent null: log on the first line of the resolver and check whether it is called.
  4. DataLoader 的 batch 函数永远不要 filter —— 长度和顺序都是硬契约。Never filter inside a DataLoader batch function. Both the length and the order are a strict contract.
  5. 「服务能起来 + _service 查得出 SDL」已经排除了大部分 composition 问题。If the service starts and _service returns the SDL, most composition problems are already ruled out.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises4 个,就在这一页上面 —— 别攒着最后一起做4 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lesson从零重写:空目录到 10 个测试全过Rewrite it: from an empty directory to all 10 tests passing
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 两道书面题:延迟传播与生产配置The two written questions: how delay spreads, and production configuration