Graffle 请求头(Headers)配置实战:合并策略、空字符串取消与 anyware 校验

原创2026-10-09 17:13:141,723 阅读
文章标签:后端

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'
}

对比配置输入与输出可以发现三条关键事实,也是本示例要传达的三个核心机制:

  1. authorization 与 x-from-raw 被保留——分别来自 transport.headers 与 transport.raw.headers,说明两类配置会合并进最终请求;
  2. x-something-to-unset 从结果中消失了——它先在第一次 .transport() 中被设为 true,又在第二次 .transport() 中被赋空字符串 '',从而被“取消”;
  3. 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 与源码,请求头的整体优先级规则可归纳为:

  1. 扩展栈优先:后注册的扩展配置覆盖先注册的(配置可通过扩展注入);
  2. with 配置优先于构造器配置:.with({ transport: {...} }) 中的请求头覆盖 .create().transport({...}) 中的同名请求头;
  3. 同一层次内 raw 优先:transport.raw.headers 的优先级高于直接写在 transport.headers 中的配置;
  4. 所有请求头最终按上述合并顺序叠加,同名冲突时由后合并者胜出,空字符串则执行删除。

使用 raw 时需格外谨慎:transport.raw 是直接透传 fetch 的 RequestInit,没有任何护栏(zero guard rails)。例如在 raw 中设置 method: 'PATCH' 会覆盖 methodMode 决定的 HTTP 方法,从而产生非法的 GraphQL Over HTTP 请求。请求头场景下同理——如果你在 raw.headers 中写入了与默认头冲突的值,将以你的值为准,但这也意味着你可能破坏规范要求的 accept / content-type 语义,请务必明确自己在做什么。

延伸阅读

登录后查看全文
graffle