news 2026/9/27 23:51:20

jose 中 customFetch 变量全解:用自定义 fetch 接管远程 JWKS 的获取、代理、重试与测试

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
jose 中 customFetch 变量全解:用自定义 fetch 接管远程 JWKS 的获取、代理、重试与测试
  • 网络安全
  • 认证鉴权
  • 后端

【免费下载链接】jose

JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes

项目地址:https://gitcode.com/gh_mirrors/jo/jose
点击查看免费下载

jose 提供的customFetch是一个以unique symbol形式导出的特殊选项键,将其传入 createRemoteJWKSet 的第二个参数后,可以完全接管“远程 JSON Web Key Set(JWKS)的 HTTP 获取”这一环节,从而启用高级 fetch 配置、HTTP 代理、网络错误重试、请求日志甚至测试桩(mock)等能力。读完本文,你将掌握customFetch的精确签名、四种开箱即用的实战用法(ky 重试与日志、undici 代理、undici 重试、undici 测试模拟),以及它在 jose 源码内部真实的调用链与错误处理语义。

customFetch 是什么:一个键、一种类型、一套契约

在 jose 中,customFetch并不是一个函数,而是一个unique symbol。它由 src/jwks/remote.ts 导出:

export const customFetch: unique symbol = Symbol()

之所以使用symbol而不是字符串属性名,是为了避免与RemoteJWKSetOptions上的普通选项(如timeoutDuration、headers)发生键冲突。它作为RemoteJWKSetOptions的可选计算属性存在(见 RemoteJWKSetOptions 接口):

interface RemoteJWKSetOptions { // ... [customFetch]?: FetchImplementation }

与customFetch配套的FetchImplementation类型(见 FetchImplementation 类型别名)精确定义了你需要实现的函数形态:

type FetchImplementation = ( /** 要发起请求的 URL */ url: string, /** 本应直接传给全局 fetch 的 options 参数 */ options: { headers: Headers // HTTP 请求头 method: 'GET' // 请求方法,固定为 GET redirect: 'manual' // 重定向策略,固定为 manual signal: AbortSignal // 用于超时/取消 }, ) => Promise<Response>

也就是说,你提供的函数接收(url, options)两个参数,返回一个Promise<Response>——这与原生fetch的调用约定完全一致,因此任何符合该形态的 fetch 兼容实现(如 ky、undici.fetch、Node 全局 fetch)都可以无缝替换。它由 jose 主入口统一导出(见 src/index.ts),你可以直接import { customFetch, createRemoteJWKSet } from 'jose'使用;createRemoteJWKSet本身也同时支持从子路径'jose/jwks/remote'导入。

为什么需要 customFetch?

createRemoteJWKSet默认使用全局fetch发起 JWKS 请求。但在真实生产环境中,全局fetch往往不足以满足需求:

  • 企业网络要求流量经过指定的 HTTP/HTTPS 代理;
  • 弱网或间歇性故障环境下需要自动重试网络错误;
  • 需要记录每次出站请求与响应以便排查问题或做审计;
  • 单元测试中不希望真正发起网络请求,而希望用 mock 拦截响应。

customFetch正是为这些场景预留的“后门”:把 fetch 的决策权完全交给你,而 jose 只负责组装好url与options并消费返回的Response。

实战一:用 ky 实现自动重试与请求/响应日志

ky 是 sindresorhus 开发的轻量级 fetch 封装,内置指数退避重试(默认重试 2 次)与丰富的事件钩子。将customFetch指向 ky 后,你既能获得重试能力,又能借助hooks记录出站请求、重试事件和响应:

import ky from 'ky' import { createRemoteJWKSet, customFetch } from 'jose' // 前置条件(按你的实际场景赋值) let url!: URL let logRequest!: (request: Request) => void let logResponse!: (request: Request, response: Response) => void let logRetry!: (request: Request, error: Error, retryCount: number) => void const JWKS = createRemoteJWKSet(url, { [customFetch]: (...args) => ky(args[0], { ...args[1], // 透传 jose 组装的 headers/method/redirect/signal hooks: { beforeRequest: [ (request) => { logRequest(request) }, ], beforeRetry: [ ({ request, error, retryCount }) => { logRetry(request, error, retryCount) }, ], afterResponse: [ (request, _, response) => { logResponse(request, response) }, ], }, }), })

关键点在于...args[1]:jose 会把headers、method: 'GET'、redirect: 'manual'、signal完整传入,你只需原样展开给 ky,即可保住 jose 内置的超时中止与请求头行为。beforeRequest、beforeRetry、afterResponse三个钩子分别覆盖“发请求前”“重试前”“响应后”三个时机,足以支撑完整的可观测性需求。

实战二:用 undici 的 EnvHttpProxyAgent 自动探测并使用 HTTP 代理

在 Node.js 服务端环境中,许多部署要求所有外呼流量经由代理。undici 的EnvHttpProxyAgent会读取HTTP_PROXY/HTTPS_PROXY/NO_PROXY等环境变量并自动为请求选择代理,配合customFetch即可让 JWKS 请求也走代理:

import * as undici from 'undici' import { createRemoteJWKSet, customFetch } from 'jose' let url!: URL // 自动从环境变量读取代理配置(参见 undici 的 EnvHttpProxyAgent 文档) let envHttpProxyAgent = new undici.EnvHttpProxyAgent() // @ts-ignore const JWKS = createRemoteJWKSet(url, { [customFetch]: (...args) => { // @ts-ignore return undici.fetch(args[0], { ...args[1], dispatcher: envHttpProxyAgent }) }, })

dispatcher是 undici fetch 的扩展选项,用于指定底层 Agent 实例;...args[1]依然透传 jose 的标准选项。这段代码让 JWKS 获取自动适配企业代理环境,无需手写任何代理解析逻辑。

实战三:用 undici 的 RetryAgent 自动重试网络错误

默认全局 fetch 在遇到ECONNRESET、ECONNREFUSED这类瞬时网络错误时会直接失败,导致 JWT 校验中断。undici 的RetryAgent可以针对特定错误码自动重试:

import * as undici from 'undici' import { createRemoteJWKSet, customFetch } from 'jose' let url!: URL let retryAgent = new undici.RetryAgent(new undici.Agent(), { statusCodes: [], // 只重试网络错误,不重试 HTTP 状态码 errorCodes: [ 'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN', 'ENETUNREACH', 'EHOSTDOWN', 'UND_ERR_SOCKET', ], }) // @ts-ignore const JWKS = createRemoteJWKSet(url, { [customFetch]: (...args) => { // @ts-ignore return undici.fetch(args[0], { ...args[1], dispatcher: retryAgent }) }, })

这里将statusCodes置为空数组、仅针对errorCodes重试,是一个经过刻意收敛的配置:HTTP 层错误(如 404、5xx)仍交由 jose 处理并直接失败,只有底层网络错误才触发重试,避免对远端不稳定服务做无意义的过度重试。值得注意的是,jose 源码中默认是不做任何网络错误重试的,自定义RetryAgent正好弥补了这一环(详见下文源码分析中的测试佐证)。

实战四:用 undici 的 MockAgent 在测试中模拟 JWKS 响应

在单元测试中,你既不希望依赖真实网络,也不想为 mock 重写整个解析链路。undici 的MockAgent配合customFetch可以拦截请求并返回预设的 JWKS 数据,且能严格禁止真实网络连接:

import * as undici from 'undici' import { createRemoteJWKSet, customFetch } from 'jose' let url!: URL let mockAgent = new undici.MockAgent() mockAgent.disableNetConnect() // 禁止任何真实网络请求 // @ts-ignore const JWKS = createRemoteJWKSet(url, { [customFetch]: (...args) => { // @ts-ignore return undici.fetch(args[0], { ...args[1], dispatcher: mockAgent }) }, }) // 随后按需对 mockAgent 注册拦截规则: // mockAgent.get('https://as.example.com').intercept({ path: '/jwks' }).reply(200, jwks)

jose 自己的测试套件正是这么做的:在 test/jwks/remote.test.ts 中,测试代码创建MockAgent、disableNetConnect(),然后把包装后的undiciFetch通过[customFetch]传入createRemoteJWKSet(见 test/jwks/remote.test.ts),从而在完全离线的环境下验证jwtVerify的完整流程。

类型注意事项:为什么要写 @ts-expect-error

官方文档对此有一个明确的已知坑(Known caveat):当你把customFetch的入参透传给 ky、undici.fetch 这类 fetch 兼容模块时,极大概率会遇到类型不匹配——因为这些模块的签名几乎不可能与标准fetch完全一致(例如 undici 的 options 比标准多出dispatcher,ky 的 options 又包含hooks等扩展字段)。文档建议在透传处使用@ts-expect-error明确豁免:

Expect Type-related issues when passing the inputs through to fetch-like modules, they hardly ever get their typings inline with actual fetch, you should@ts-expect-errorthem.

这也是上面所有示例中反复出现// @ts-ignore或// @ts-expect-error注释的原因。在真实项目中,更推荐把这些兼容函数抽成一个带宽松类型签名的工具函数,把豁免注释集中在一处,而不是散落在业务代码里。

源码级原理:customFetch 在内部如何被消费

理解了用法之后,再看 jose 内部如何使用customFetch,能帮你更准确地预判行为。在 src/jwks/remote.ts 中,fetchJwks是唯一调用你自定义实现的入口:

async function fetchJwks(url, headers, signal, fetchImpl = fetch) { const response = await fetchImpl(url, { method: 'GET', signal, redirect: 'manual', headers, }).catch((err) => { if (err.name === 'TimeoutError') { throw new JWKSTimeout() } throw err }) if (response.status !== 200) { throw new JOSEError('Expected 200 OK from the JSON Web Key Set HTTP response') } try { return await response.json() } catch { throw new JOSEError('Failed to parse the JSON Web Key Set HTTP response as JSON') } }

由这段代码可以提炼出几个与customFetch直接相关的契约:

  • 参数形态固定:jose 始终以method: 'GET'、redirect: 'manual'、signal(来自AbortSignal.timeout(timeoutDuration))和组装好的headers调用你的实现,返回的Response会被response.json()消费;
  • HTTP 状态码语义:只要响应状态不是200,无论你的自定义实现做了什么(重试、代理、mock),最终都会抛出JOSEError(Expected 200 OK from the JSON Web Key Set HTTP response)。因此自定义实现不应在“非 200”上自行放行;
  • 超时映射:如果请求因AbortSignal.timeout中止而抛出名为TimeoutError的错误,会被映射为JWKSTimeout(错误码ERR_JWKS_TIMEOUT,见 src/util/errors.ts);
  • JSON 解析失败:即使你的实现正常返回响应,若response.json()失败,也会抛出JOSEError(Failed to parse the JSON Web Key Set HTTP response as JSON)。

另外,src/jwks/remote.ts 显示默认会注入两个请求头:User-Agent: jose/v6.2.10(浏览器环境为了规避不必要的 CORS 预检会省略)以及accept: application/json, application/jwk-set+json。自定义实现透传...args[1]时这些头会被保留;如果你完全丢弃传入的 options,则需要自行补充。

在调用策略层面,createRemoteJWKSet采用“惰性 + 冷却窗口”模型(src/jwks/remote.ts):只有缓存过期或没有键匹配时才触发一次请求,且在cooldownDuration(默认 30 秒)内成功获取后不再重复请求,以防止滥用;reload()方法则可绕过冷却窗口强制刷新。多个并发触发会被合并到同一个pendingFetch(src/jwks/remote.ts),其中对 Cloudflare Workers 这类无状态运行时有专门的pendingFetch重置处理(src/jwks/remote.ts)——这意味着即使你注入自定义 fetch,这部分并发合并与冷却语义依然生效。

与其它选项的协同

customFetch不是孤立存在的选项,它通常与 RemoteJWKSetOptions 中的其余配置配合使用:

选项默认值说明
timeoutDuration5000 ms请求超时,超时后中止并抛JWKSTimeout,必须是非负整数
cooldownDuration30000 ms成功获取后在此窗口内不再触发新请求,防止滥用
cacheMaxAge600000 ms(10 分钟)缓存 JWKS 的最大有效期,超过后重新获取
headers见上附加到请求的额外 HTTP 头
[jwksCache]—云函数等无法常驻内存缓存场景下的可写缓存对象(见 jwksCache 变量)
[customFetch]全局fetch自定义 fetch 实现(本文主题)

例如你可以在使用customFetch的同时收紧timeoutDuration、放宽cooldownDuration,并用headers携带认证信息——这些都由 jose 组装后原样传给你的实现。

测试佐证:customFetch 在 jose 测试套件中的实际覆盖

jose 的官方测试 test/jwks/remote.test.ts 全面覆盖了customFetch相关行为,可作为你实现自测的参考基线:

  • 正常校验流:通过[customFetch]注入包装后的 undici fetch,配合MockAgent拦截https://as.example.com/jwks,验证jwtVerify能成功校验签发的 JWT(test/jwks/remote.test.ts);
  • 网络错误传播:测试直接命中真实网络场景,验证ENOTFOUND、ECONNREFUSED会被原样向上抛出并携带err.cause.code(test/jwks/remote.test.ts)——这正是“用 RetryAgent 重试这类错误码”能生效的前提;
  • 超时映射:将timeoutDuration: 500与[customFetch]组合,验证超时最终抛出errors.JWKSTimeout(test/jwks/remote.test.ts);
  • 响应控制:通过可控的 fetch 实现(先挂起再逐一 resolve)验证并发reload()的去重与新旧序列取舍(test/jwks/remote.test.ts)。

这些测试从侧面印证了一个结论:customFetch只是替换“发请求”这一步,其上游的缓存、冷却、超时、并发合并与错误映射语义全部由 jose 自身保证,不会因为换了 fetch 实现而丢失。

小结

customFetch是 jose 远程 JWKS 模块留出的一个高价值扩展点:以unique symbol作为选项键、以FetchImplementation类型定义契约,让你的 JWKS 获取从“全局 fetch 一步到位”升级为可代理、可重试、可观测、可 mock 的生产级网络栈。将其与 createRemoteJWKSet 的冷却窗口、缓存有效期、超时等内置策略组合使用,即可在保持 jose 安全语义不变的前提下,把远程 JWKS 的获取行为完全掌控在自己手中。更完整的模块视图可参考 jwks/remote 文档 及其中的 RemoteJWKSet 接口。

  • 网络安全
  • 认证鉴权
  • 后端

【免费下载链接】jose

JWA, JWS, JWE, JWT, JWK, JWKS for Node.js, Browser, Cloudflare Workers, Deno, Bun, and other Web-interoperable runtimes

项目地址:https://gitcode.com/gh_mirrors/jo/jose
点击查看免费下载

相关推荐

上一篇:GHelper终极指南:华硕笔记本性能优化神器,轻量级替代Armoury Crate
下一篇:AWS CLI 实践指南:使用 `aws codecommit update-comment` 更新 CodeCommit 提交评论

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 23:42:54

二手车价格预测数据挖掘大作业:从源码到实验报告的完整解析

简介&#xff1a;这是一份面向计算机相关专业学生的数据挖掘课程大作业资源&#xff0c;以二手车价格预测为实战主题&#xff0c;适合作为课程设计、期末大作业或自学练习的完整案例。项目经导师指导并评审通过&#xff0c;得分98分&#xff0c;内容覆盖数据预处理、特征工程、…

作者头像 李华
网站建设 2026/9/27 23:39:41

基于PyTorch的深度强化学习复现:DDPG、SAC、TD3统一框架与避坑指南

简介&#xff1a;这是一份基于PyTorch的深度强化学习算法研究与对比实践资源&#xff0c;聚焦DDPG、SAC、TD3三种主流连续控制算法&#xff0c;完整实现了网络构建、经验回放、训练与评估流程。资源面向具备一定深度学习基础、希望深入理解连续动作空间DRL算法的研究人员、学生…

作者头像 李华