关于 customize-sdk-hooks
本指南介绍如何为 Speakeasy 生成的 SDK 添加自定义生命周期钩子,涵盖请求与响应拦截、钩子注册,以及在多种支持语言中保留再生成时的自定义文件。
主要功能
- 01初始化和请求生命周期钩子
- 02添加请求头、遥测和认证逻辑
- 03转换响应与错误
- 04再生成时保留钩子注册
- 05多种 SDK 语言的实现指引
适用场景
- 01添加自定义 User-Agent 或关联 ID 请求头
- 02使用 HMAC 认证对请求签名
- 03记录 SDK 级遥测或日志
- 04转换成功响应或 API 错误
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ speakeasy-api/ sdk-generation-skills --skill customize-sdk-hooks安装后在 Claude Code 中输入/customize-sdk-hooks确认已加载。
详细介绍
自定义 SDK 钩子
适用场景
当你需要向生成的 SDK 添加自定义代码逻辑时,使用此技能:
- 通过代码为每个 SDK 请求添加自定义标头(User-Agent、关联 ID)
- 在 SDK 层级实现遥测、日志记录或可观测性
- 添加在 SDK 代码中运行的自定义身份验证逻辑(HMAC 签名、令牌刷新)
- 在响应或错误到达调用方之前对其进行转换
- 实现自定义请求/响应中间件
- 用户提到:“SDK 钩子”“添加自定义逻辑”“用代码拦截请求”“HMAC 签名钩子”“SDK 中的遥测”
不适用于:
- 修改 OpenAPI 规范(参见
manage-openapi-overlays) - 运行时 SDK 客户端配置(参见
configure-sdk-options)
输入
- 钩子类型:要拦截的生命周期事件(初始化、请求前、成功后、错误后)
- SDK 语言:生成的 SDK 所使用的目标语言(TypeScript、Go、Python、Java、C#、Ruby、PHP 等)
- 自定义逻辑:要在钩子位置注入的行为
输出
src/hooks/中的钩子实现文件(或对应语言的目录)- 已更新的
src/hooks/registration.ts,用于注册新钩子 - SDK 重新生成后仍会保留钩子
前置条件
- 由 Speakeasy 生成的 SDK(任意受支持的语言)
- 了解 SDK 的请求/响应生命周期
- 生成的 SDK 中存在
src/hooks/目录(默认创建)
钩子类型
| 钩子 | 调用时机 | 常见用途 |
|---|---|---|
SDKInitHook | SDK 客户端初始化时 | 配置默认值、验证配置、设置基本 URL |
BeforeCreateRequestHook | 创建 HTTP 请求对象之前 | 修改输入参数、注入默认值 |
BeforeRequestHook | 发送 HTTP 请求之前 | 添加标头、记录日志、遥测、对请求签名 |
AfterSuccessHook | HTTP 响应成功之后 | 转换响应、发出警告、记录指标 |
AfterErrorHook | HTTP 错误响应之后 | 转换错误、重试逻辑、记录错误 |
钩子接口(TypeScript)
// SDKInitHook
interface SDKInitHook {
sdkInit(opts: SDKInitOptions): SDKInitOptions;
}
// BeforeCreateRequestHook
interface BeforeCreateRequestHook {
beforeCreateRequest(hookCtx: BeforeCreateRequestHookContext, input: any): any;
}
// BeforeRequestHook
interface BeforeRequestHook {
beforeRequest(
hookCtx: BeforeRequestHookContext,
request: Request
): Request;
}
// AfterSuccessHook
interface AfterSuccessHook {
afterSuccess(
hookCtx: AfterSuccessHookContext,
response: Response
): Response;
}
// AfterErrorHook
interface AfterErrorHook {
afterError(
hookCtx: AfterErrorHookContext,
response: Response | null,
error: unknown
): { response: Response | null; error: unknown };
}
目录结构
src/
hooks/
types.ts # Generated - DO NOT EDIT (hook interfaces/types)
registration.ts # Custom - YOUR registrations (preserved on regen)
custom_useragent.ts # Custom - your hook implementations
telemetry.ts # Custom - your hook implementations
重要规则:SDK 重新生成时会保留 registration.ts 和你创建的任何自定义钩子文件。types.ts 文件会重新生成,不应修改。
注册模式
所有钩子都在 src/hooks/registration.ts 中注册。此文件由生成器创建一次,此后不会被覆盖。你可以在此添加钩子:
// src/hooks/registration.ts
import { Hooks } from "./types.js";
import { CustomUserAgentHook } from "./custom_useragent.js";
import { TelemetryHook } from "./telemetry.js";
/*
* This file is only ever generated once on the first generation and then is free
* to be modified. Any hooks you wish to add should be registered in the
* initHooks function. Feel free to define them in this file or in separate files
* in the hooks folder.
*/
export function initHooks(hooks: Hooks) {
hooks.registerBeforeRequestHook(new CustomUserAgentHook());
hooks.registerAfterSuccessHook(new TelemetryHook());
}
操作步骤
向 Speakeasy 生成的 SDK 添加钩子:
- 在
src/hooks/中创建钩子实现文件 - 实现
src/hooks/types.ts中相应的接口 - 在
src/hooks/registration.ts中注册钩子 - 重新生成 SDK —— 你的钩子会保留下来
# After adding hooks, regenerate safely
speakeasy generate sdk -s openapi.yaml -o . -l typescript
# Your registration.ts and custom hook files are untouched
示例
示例 1:自定义 User-Agent 钩子
为每个发出的请求添加自定义 User-Agent 标头。
// src/hooks/custom_useragent.ts
import {
BeforeRequestHook,
BeforeRequestHookContext,
} from "./types.js";
export class CustomUserAgentHook implements BeforeRequestHook {
private userAgent: string;
constructor(appName: string, appVersion: string) {
this.userAgent = `${appName}/${appVersion}`;
}
beforeRequest(
hookCtx: BeforeRequestHookContext,
request: Request
): Request {
// Clone the request to add the custom header
const newRequest = new Request(request, {
headers: new Headers(request.headers),
});
newRequest.headers.set("User-Agent", this.userAgent);
// Optionally append the existing User-Agent
const existing = request.headers.get("User-Agent");
if (existing) {
newRequest.headers.set(
"User-Agent",
`${this.userAgent} ${existing}`
);
}
return newRequest;
}
}
注册钩子:
// src/hooks/registration.ts
import { Hooks } from "./types.js";
import { CustomUserAgentHook } from "./custom_useragent.js";
export function initHooks(hooks: Hooks) {
hooks.registerBeforeRequestHook(
new CustomUserAgentHook("my-app", "1.0.0")
);
}
示例 2:自定义安全钩子(HMAC 签名)
对于需要 HMAC 签名或无法通过 OpenAPI 规范描述的自定义身份验证的 API,可将 overlay 与 BeforeRequestHook 结合使用。
步骤 1:使用 overlay 标记安全方案,以便 Speakeasy 生成钩子点:
# overlay.yaml
overlay: 1.0.0
info:
title: Add HMAC security
actions:
- target: "$.components.securitySchemes"
update:
hmac_auth:
type: http
scheme: custom
x-speakeasy-custom-security: true
步骤 2:实现签名钩子:
// src/hooks/hmac_signing.ts
import {
BeforeRequestHook,
BeforeRequestHookContext,
} from "./types.js";
import { createHmac } from "crypto";
export class HmacSigningHook implements BeforeRequestHook {
beforeRequest(
hookCtx: BeforeRequestHookContext,
request: Request
): Request {
const timestamp = Date.now().toString();
const secret = hookCtx.securitySource?.apiSecret;
if (!secret) {
throw new Error("API secret is required for HMAC signing");
}
const signature = createHmac("sha256", secret)
.update(`${request.method}:${request.url}:${timestamp}`)
.digest("hex");
const newRequest = new Request(request, {
headers: new Headers(request.headers),
});
newRequest.headers.set("X-Timestamp", timestamp);
newRequest.headers.set("X-Signature", signature);
return newRequest;
}
}
注册钩子:
// src/hooks/registration.ts
import { Hooks } from "./types.js";
import { HmacSigningHook } from "./hmac_signing.js";
export function initHooks(hooks: Hooks) {
hooks.registerBeforeRequestHook(new HmacSigningHook());
}
最佳实践
-
让钩子专注于单一职责:每个钩子都应只处理一个关注点。分别使用不同的钩子处理用户代理、遥测和身份验证,而不是使用一个庞大的钩子。
-
读取响应正文前先克隆响应:
Response.body流只能消费一次。读取前务必先克隆:afterSuccess(hookCtx: AfterSuccessHookContext, response: Response): Response { // CORRECT: clone before reading const cloned = response.clone(); cloned.json().then((data) => console.log("Response:", data)); return response; // return the original, unconsumed } -
遥测采用即发即弃模式:不要因日志记录或指标收集等非关键操作而阻塞请求管道:
beforeRequest(hookCtx: BeforeRequestHookContext, request: Request): Request { // Fire-and-forget: do not await void fetch("https://telemetry.example.com/events", { method: "POST", body: JSON.stringify({ operation: hookCtx.operationID }), }); return request; } -
独立测试钩子:通过构造模拟的
Request/Response对象和钩子上下文,单独为钩子编写单元测试。 -
使用
hookCtx.operationID:钩子上下文提供当前操作 ID,可用于按操作执行逻辑、记录日志和收集指标。
不要做的事
- 不要编辑
types.ts:此文件会重新生成。所做的更改会丢失。 - 不要在未克隆响应的情况下消费响应正文:这会耗尽正文流,导致下游操作失败。
- 不要在钩子中执行阻塞式 I/O:在钩子中执行耗时操作(网络调用、文件 I/O)会降低 SDK 性能。对于非关键操作,请采用即发即弃模式。
- 除非你想将成功转换为失败,否则不要在
AfterSuccessHook中抛出错误:在钩子中抛出错误会扰乱正常流程。 - 未经同步,不要在钩子中存储可变的共享状态:在多线程环境中可能会并发调用钩子。
- 不要在钩子中重复实现本应放在 OpenAPI 覆盖文件中的逻辑:如果需要修改 API 规范(添加安全方案、更改参数),请使用覆盖文件,而不是钩子。
故障排除
未调用钩子
- 确认已在
src/hooks/registration.ts中注册钩子 - 确认注册的是正确的钩子类型(例如,
registerBeforeRequestHook而不是registerAfterSuccessHook) - 检查
initHooks是否已导出,并符合预期的签名
响应正文为空或已被消费
- 你在未先克隆的情况下读取了
response.body或调用了response.json() - 消费响应正文前务必先使用
response.clone(),然后返回原始响应
重新生成后钩子丢失
src/hooks/中的自定义钩子文件会保留,但前提是它们是独立文件- 首次生成后,
registration.ts不会被覆盖 types.ts会被覆盖——切勿在其中添加自定义代码
重新生成后出现 TypeScript 编译错误
types.ts中的接口可能已更新。检查钩子签名是否存在重大变更- 更新钩子实现,使其符合新的接口定义
钩子导致请求失败
- 确保返回有效的
Request或Response对象 - 检查克隆的请求是否保留了原始正文和标头
- 确认注入的标头值有效(不能为 undefined 或 null)
其他语言
虽然以上示例使用 TypeScript,但所有受支持的语言均提供 Speakeasy SDK 钩子:
- Go:钩子实现
hooks/hooks.go中的接口,并在hooks/registration.go中注册 - Python:钩子是在
src/hooks/中实现src/hooks/types.py协议的类 - Java:钩子实现
hooks/SDKHooks.java中的接口 - C#:钩子实现
Hooks/SDKHooks.cs中的接口 - Ruby:钩子使用基于 Sorbet 类型的类,并采用 Faraday 中间件模式
- PHP:钩子使用 PSR-7 请求/响应接口和 Guzzle 中间件
所有语言中的钩子类型、生命周期和注册模式都一致。请参阅 SDK 中生成的 types 文件,了解特定语言的接口定义。