Graffle 请求头(Headers)配置实战:合并策略、空字符串取消与 anyware 校验
Graffle 请求头(Headers)配置实战:合并策略、空字符串取消与 anyware 校验
导读
本文以 Graffle 官方示例 transport-http_headers_raw__headers.ts 为骨架,深入讲解如何在 Graffle 客户端中通过 transport.headers 与 transport.raw.headers 配置 HTTP 请求头,理解多次 .transport() 调用之间请求头的合并规则、用空字符串“取消”已设置请求头的机制,以及如何借助 anyware 在请求发出前审查最终请求头。读完本文,你将能精确控制 Graffle 发出的每一个 GraphQL HTTP 请求的 Header 集合,并避免常见的覆盖顺序误区。
示例总览:一个包含全部核心机制的请求头配置
官方示例在同一个客户端上通过多次 .transport() 调用完成了请求头的设置、覆盖与取消,并在发送前用 anyware 打印出最终请求头:
import { Graffle } from 'graffle'
const graffle = Graffle
.create()
.transport({
url: `http://localhost:3000/graphql`,
headers: {
authorization: `Bearer MY_TOKEN`,
'x-something-to-unset': `true`,
},
raw: {
headers: {
'x-from-raw': `true`,
},
},
})
.transport({
headers: { 'x-something-to-unset': `` },
})
.anyware(({ exchange }) => {
if (exchange.input.transportType !== `http`) return exchange()
console.log(exchange.input.request.headers)
return exchange()
})
await graffle.gql('{ pokemons { name } }').$send()
对应示例测试快照(transport-http_headers_raw__headers.snap)记录了实际发出的请求头:
Headers {
accept: 'application/graphql-response+json; charset=utf-8, application/json; charset=utf-8',
'content-type': 'application/json',
'x-from-raw': 'true',
authorization: 'Bearer MY_TOKEN'
}
对比配置输入与输出可以发现三条关键事实,也是本示例要传达的三个核心机制:
authorization与x-from-raw被保留——分别来自transport.headers与transport.raw.headers,说明两类配置会合并进最终请求;x-something-to-unset从结果中消失了——它先在第一次.transport()中被设为true,又在第二次.transport()中被赋空字符串'',从而被“取消”;accept与content-type由 Graffle 自动注入——这是 GraphQL Over HTTP 规范要求的默认请求头,用户无需(也不应)手动设置。
下面逐项拆解这些机制及其底层实现。
请求头的三个来源:默认头、transport.headers 与 transport.raw.headers
在 Graffle 的 TransportHttp 扩展(源码见 src/extensions/TransportHttp/TransportHttp.ts)中,每个 HTTP 请求的请求头由三个层次叠加而成,按合并顺序依次为:
| 来源 | 配置入口 | 说明 |
|---|---|---|
| 默认请求头 | 无(内置) | 按请求方法自动注入 accept、content-type 等规范头 |
transport.headers |
构造器 / with / 扩展栈中的 headers 字段 |
用户声明式配置的请求头 |
transport.raw.headers |
transport.raw 下的 headers 字段 |
直接透传给 fetch 的底层配置,优先级最高 |
在源码的 run 阶段(TransportHttp.ts),请求头的构建顺序清晰地体现了这一层次:
const baseRequestInit = Http.Req.mergeInit(
Http.Req.mergeInit(
{
headers: requestMethod === `get` ? getRequestHeadersRec : postRequestHeadersRec,
},
input.transport.raw,
),
{
...(input.transport.headers !== undefined && { headers: input.transport.headers }),
},
)
即:先以内置的 getRequestHeadersRec / postRequestHeadersRec(GET 与 POST 各自的规范请求头集合)为基底,再合并 raw 中的内容,最后合并 transport.headers。示例输出中的 accept: 'application/graphql-response+json; charset=utf-8, application/json; charset=utf-8' 与 content-type: 'application/json' 正是由默认层注入,而 x-from-raw、authorization 则分别来自 raw 与 headers 两层。
空字符串:取消先前已设置的请求头
transport.headers 的类型是 HeadersInit(见 TransportHttp.ts),因此你可以给任何头赋空字符串 ''。Graffle 的约定是:
若某个请求头被赋予空字符串,则会在合并时删除该请求头(若此前已被设置)。
这正是示例中 x-something-to-unset 的遭遇:第一次 .transport() 将它设为 true,第二次 .transport() 又将它设为 '',最终发出的请求中不再包含它。该行为在 src/extensions/TransportHttp/documentation.md 的“Configuration”一节中有明确说明("If a header is given an empty string value, then it deletes that header value if previously set"),是 Graffle 官方设计用于“清除默认或先前配置”的手段,典型场景包括:覆盖扩展栈中下层扩展注入的请求头、取消某环境预设的认证头等。
多次 .transport() 调用:请求头是合并而非替换
注意示例连续调用了两次 .transport()。Graffle 的配置系统在这里遵循合并(merge)语义,而不是整个覆盖:第二次调用只影响它声明的字段,其余字段(url、raw.headers 中的 x-from-raw 等)均被保留。
这一行为在配置解析器中有直接对应实现。TransportHttp 的 inputResolver 使用 Http.Headers.mergeInitWithStrategyMerge(current.headers, input.headers) 将当前已累积的请求头与本次输入按“合并策略”叠加(TransportHttp.ts),每次 .transport() 调用都会在此累积的基础上继续合并,从而形成“后调用在前调用之上增量修改”的效果。空字符串删除语义正是在这种增量合并中生效的。
用 anyware 审查最终请求头
anyware 是 Graffle 的请求管道拦截机制。示例中的拦截器先通过 exchange.input.transportType !== 'http' 过滤出 HTTP 传输的请求,再读取 exchange.input.request.headers 并打印。此时 request.headers 已经过上述全部合并,呈现给用户的正是即将随 fetch 发出的最终请求头。
利用这一点,你可以实现请求头审计、调试、以及按需注入动态请求头(例如在拦截器中读取当前用户 token 后重新赋值)。这也是官方文档建议的“动态请求头”扩展点之一——对应示例 transport-http_extension_headers__dynamicHeaders。
配置优先级总结与注意事项
结合 src/extensions/TransportHttp/documentation.md 与源码,请求头的整体优先级规则可归纳为:
- 扩展栈优先:后注册的扩展配置覆盖先注册的(配置可通过扩展注入);
with配置优先于构造器配置:.with({ transport: {...} })中的请求头覆盖.create().transport({...})中的同名请求头;- 同一层次内
raw优先:transport.raw.headers的优先级高于直接写在transport.headers中的配置; - 所有请求头最终按上述合并顺序叠加,同名冲突时由后合并者胜出,空字符串则执行删除。
使用 raw 时需格外谨慎:transport.raw 是直接透传 fetch 的 RequestInit,没有任何护栏(zero guard rails)。例如在 raw 中设置 method: 'PATCH' 会覆盖 methodMode 决定的 HTTP 方法,从而产生非法的 GraphQL Over HTTP 请求。请求头场景下同理——如果你在 raw.headers 中写入了与默认头冲突的值,将以你的值为准,但这也意味着你可能破坏规范要求的 accept / content-type 语义,请务必明确自己在做什么。
延伸阅读
- 扩展完整文档:src/extensions/TransportHttp/documentation.md,覆盖
url(含相对路径)、methodMode(post/getReads)、raw等全部配置项; - 本文示例源码:examples/10_transport-http/transport-http_headers_raw__headers.ts,以及测试快照 transport-http_headers_raw__headers.snap;
- 请求头合并与请求构建的底层实现:src/extensions/TransportHttp/TransportHttp.ts;
- 更多请求头相关的官方示例,可浏览 examples/10_transport-http 目录下的
transport-http_extension_headers__dynamicHeaders.ts、transport-http_headers_raw__headers.ts等文件。