customize-sdk-hooks
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCustomize SDK Hooks
自定义SDK钩子
When to Use
使用场景
Use this skill when you need to add custom code logic to the generated SDK:
- Add custom headers (User-Agent, correlation IDs) to every SDK request via code
- Implement telemetry, logging, or observability at the SDK level
- Add custom authentication logic (HMAC signatures, token refresh) that runs in SDK code
- Transform responses or errors before they reach the caller
- Implement custom request/response middleware
- User says: "SDK hooks", "add custom logic", "intercept requests with code", "HMAC signing hook", "telemetry in SDK"
NOT for:
- OpenAPI spec modifications (see )
manage-openapi-overlays - Runtime SDK client config (see )
configure-sdk-options
当您需要为生成的SDK添加自定义代码逻辑时,使用本技能:
- 通过代码为每个SDK请求添加自定义请求头(User-Agent、关联ID)
- 在SDK层面实现遥测、日志或可观测性功能
- 添加在SDK代码中运行的自定义认证逻辑(HMAC签名、令牌刷新)
- 在响应或错误到达调用方之前对其进行转换
- 实现自定义请求/响应中间件
- 用户提及:“SDK hooks”、“添加自定义逻辑”、“用代码拦截请求”、“HMAC签名钩子”、“SDK中的遥测”
不适用于:
- OpenAPI规范修改(请参考)
manage-openapi-overlays - 运行时SDK客户端配置(请参考)
configure-sdk-options
Inputs
输入参数
- Hook type: Which lifecycle event to intercept (init, before request, after success, after error)
- SDK language: The target language of the generated SDK (TypeScript, Go, Python, Java, C#, Ruby, PHP, etc.)
- Custom logic: The behavior to inject at the hook point
- 钩子类型:要拦截的生命周期事件(初始化、请求前、成功后、错误后)
- SDK语言:生成的SDK目标语言(TypeScript、Go、Python、Java、C#、Ruby、PHP等)
- 自定义逻辑:要注入到钩子点的行为
Outputs
输出结果
- Hook implementation file(s) in (or language equivalent)
src/hooks/ - Updated to register the new hook
src/hooks/registration.ts - The hook is preserved across SDK regenerations
- 目录下的钩子实现文件(或对应语言的等效目录)
src/hooks/ - 更新以注册新钩子
src/hooks/registration.ts - 钩子在SDK重新生成后会被保留
Prerequisites
前置条件
- A Speakeasy-generated SDK (any supported language)
- Understanding of the SDK's request/response lifecycle
- The directory exists in the generated SDK (created by default)
src/hooks/
- 一个由Speakeasy生成的SDK(支持任意受支持的语言)
- 了解SDK的请求/响应生命周期
- 生成的SDK中存在目录(默认创建)
src/hooks/
Hook Types
钩子类型
| Hook | When Called | Common Use Cases |
|---|---|---|
| SDK client initialization | Configure defaults, validate config, set base URL |
| Before the HTTP request object is created | Modify input parameters, inject defaults |
| Before the HTTP request is sent | Add headers, logging, telemetry, sign requests |
| After a successful HTTP response | Transform response, emit warnings, log metrics |
| After an HTTP error response | Error transformation, retry logic, error logging |
| 钩子 | 调用时机 | 常见使用场景 |
|---|---|---|
| SDK客户端初始化时 | 配置默认值、验证配置、设置基础URL |
| HTTP请求对象创建前 | 修改输入参数、注入默认值 |
| HTTP请求发送前 | 添加请求头、日志、遥测、请求签名 |
| HTTP响应成功后 | 转换响应、发出警告、记录指标 |
| HTTP响应错误后 | 错误转换、重试逻辑、错误日志 |
Hook Interfaces (TypeScript)
TypeScript钩子接口
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 };
}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 };
}Directory Structure
目录结构
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 implementationsKey rule: and any custom hook files you create are preserved
during SDK regeneration. The file is regenerated and should not be modified.
registration.tstypes.tssrc/
hooks/
types.ts # 自动生成 - 请勿编辑(钩子接口/类型)
registration.ts # 自定义文件 - 您的钩子注册(重新生成时保留)
custom_useragent.ts # 自定义文件 - 您的钩子实现
telemetry.ts # 自定义文件 - 您的钩子实现核心规则:和您创建的任何自定义钩子文件在SDK重新生成时都会被保留。文件会被重新生成,请勿修改。
registration.tstypes.tsRegistration Pattern
注册模式
All hooks are registered in . This file is created once
by the generator and never overwritten. You add your hooks here:
src/hooks/registration.tstypescript
// 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());
}所有钩子都在中注册。该文件由生成器创建一次,之后不会被覆盖。您可以在此添加钩子:
src/hooks/registration.tstypescript
// 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());
}Command
命令
To add a hook to a Speakeasy-generated SDK:
- Create your hook implementation file in
src/hooks/ - Implement the appropriate interface from
src/hooks/types.ts - Register it in
src/hooks/registration.ts - Regenerate the SDK -- your hooks are preserved
bash
undefined要为Speakeasy生成的SDK添加钩子:
- 在目录中创建您的钩子实现文件
src/hooks/ - 实现中的对应接口
src/hooks/types.ts - 在中注册钩子
src/hooks/registration.ts - 重新生成SDK -- 您的钩子会被保留
bash
undefinedAfter adding hooks, regenerate safely
添加钩子后,安全地重新生成SDK
speakeasy generate sdk -s openapi.yaml -o . -l typescript
speakeasy generate sdk -s openapi.yaml -o . -l typescript
Your registration.ts and custom hook files are untouched
您的registration.ts和自定义钩子文件不会被修改
undefinedundefinedExamples
示例
Example 1: Custom User-Agent Hook
示例1:自定义User-Agent钩子
Add a custom header to every outgoing request.
User-Agenttypescript
// 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;
}
}Register it:
typescript
// 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")
);
}为每个 outgoing 请求添加自定义请求头。
User-Agenttypescript
// 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;
}
}注册钩子:
typescript
// 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")
);
}Example 2: Custom Security Hook (HMAC Signing)
示例2:自定义安全钩子(HMAC签名)
For APIs requiring HMAC signatures or custom authentication that cannot be
expressed in the OpenAPI spec, combine an overlay with a .
BeforeRequestHookStep 1: Use an overlay to mark the security scheme so Speakeasy generates
the hook point:
yaml
undefined对于需要HMAC签名或无法在OpenAPI规范中表达的自定义认证的API,可将覆盖与结合使用。
BeforeRequestHook步骤1:使用覆盖标记安全方案,以便Speakeasy生成钩子点:
yaml
undefinedoverlay.yaml
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
**Step 2**: Implement the signing hook:
```typescript
// 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;
}
}Register it:
typescript
// src/hooks/registration.ts
import { Hooks } from "./types.js";
import { HmacSigningHook } from "./hmac_signing.js";
export function initHooks(hooks: Hooks) {
hooks.registerBeforeRequestHook(new HmacSigningHook());
}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**:实现签名钩子:
```typescript
// 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;
}
}注册钩子:
typescript
// src/hooks/registration.ts
import { Hooks } from "./types.js";
import { HmacSigningHook } from "./hmac_signing.js";
export function initHooks(hooks: Hooks) {
hooks.registerBeforeRequestHook(new HmacSigningHook());
}Best Practices
最佳实践
-
Keep hooks focused: Each hook should address a single concern. Use separate hooks for user-agent, telemetry, and auth rather than one monolithic hook.
-
Clone responses before reading the body: Thestream can only be consumed once. Always clone before reading:
Response.bodytypescriptafterSuccess(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 } -
Fire-and-forget for telemetry: Do not block the request pipeline for non-critical operations like logging or metrics:typescript
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; } -
Test hooks independently: Write unit tests for hooks in isolation by constructing mock/
Requestobjects and hook contexts.Response -
Use: The hook context provides the current operation ID, which is useful for per-operation behavior, logging, and metrics.
hookCtx.operationID
-
保持钩子聚焦:每个钩子应只处理一个关注点。为User-Agent、遥测和认证使用单独的钩子,而不是一个庞大的钩子。
-
读取响应体前先克隆:流只能被消费一次。读取前务必克隆:
Response.bodytypescriptafterSuccess(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 } -
遥测使用“即发即弃”模式:不要为非关键操作(如日志或指标)阻塞请求管道:typescript
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 -
使用:钩子上下文提供当前操作ID,可用于实现按操作的行为、日志和指标。
hookCtx.operationID
What NOT to Do
禁止操作
- Do NOT edit : This file is regenerated. Your changes will be lost.
types.ts - Do NOT consume the response body without cloning: This causes downstream failures because the body stream is exhausted.
- Do NOT perform blocking I/O in hooks: Long-running operations (network calls, file I/O) in hooks will degrade SDK performance. Use fire-and-forget patterns for non-critical work.
- Do NOT throw errors in unless you intend to convert a success into a failure. Throwing in hooks disrupts the normal flow.
AfterSuccessHook - Do NOT store mutable shared state in hooks without synchronization. Hooks may be called concurrently in multi-threaded environments.
- Do NOT duplicate logic that belongs in an OpenAPI overlay. If you need to modify the API spec (add security schemes, change parameters), use an overlay instead of a hook.
- 请勿修改:该文件会被重新生成,您的更改会丢失。
types.ts - 请勿在未克隆的情况下消费响应体:这会导致下游失败,因为体流已被耗尽。
- 请勿在钩子中执行阻塞I/O:钩子中的长时间运行操作(网络调用、文件I/O)会降低SDK性能。对于非关键工作,使用“即发即弃”模式。
- 请勿在中抛出错误,除非您打算将成功转换为失败。在钩子中抛出错误会中断正常流程。
AfterSuccessHook - 请勿在钩子中存储可变共享状态,除非有同步机制。在多线程环境中,钩子可能会被并发调用。
- 请勿复制属于OpenAPI覆盖的逻辑。如果您需要修改API规范(添加安全方案、更改参数),请使用覆盖而非钩子。
Troubleshooting
故障排除
Hook is not being called
钩子未被调用
- Verify the hook is registered in
src/hooks/registration.ts - Confirm you are registering for the correct hook type (e.g., vs
registerBeforeRequestHook)registerAfterSuccessHook - Check that is exported and follows the expected signature
initHooks
- 验证钩子是否已在中注册
src/hooks/registration.ts - 确认您注册的是正确的钩子类型(例如vs
registerBeforeRequestHook)registerAfterSuccessHook - 检查是否已导出且符合预期签名
initHooks
Response body is empty or already consumed
响应体为空或已被消费
- You are reading or calling
response.bodywithout cloning firstresponse.json() - Always use before consuming the body, then return the original
response.clone()
- 您在未克隆的情况下读取了或调用了
response.bodyresponse.json() - 始终在消费体之前使用,然后返回原始响应
response.clone()
Hooks lost after regeneration
重新生成SDK后钩子丢失
- Custom hook files in are preserved, but only if they are separate files
src/hooks/ - is never overwritten after initial generation
registration.ts - IS overwritten -- never put custom code there
types.ts
- 目录中的自定义钩子文件会被保留,但仅当它们是单独文件时
src/hooks/ - 在初始生成后永远不会被覆盖
registration.ts - 会被覆盖——永远不要在其中添加自定义代码
types.ts
TypeScript compilation errors after regeneration
重新生成SDK后TypeScript编译出错
- may have updated interfaces. Check for breaking changes in hook signatures
types.ts - Update your hook implementations to match the new interface definitions
- 可能更新了接口。检查钩子签名中的破坏性变更
types.ts - 更新您的钩子实现以匹配新的接口定义
Hook causes request failures
钩子导致请求失败
- Ensure you are returning a valid or
RequestobjectResponse - Check that cloned requests preserve the original body and headers
- Verify any injected headers have valid values (no undefined or null)
- 确保您返回的是有效的或
Request对象Response - 检查克隆的请求是否保留了原始体和请求头
- 验证任何注入的请求头是否具有有效值(无undefined或null)
Other Languages
其他语言
While the examples above are in TypeScript, Speakeasy SDK hooks are available
across all supported languages:
- Go: Hooks implement interfaces in with registration in
hooks/hooks.gohooks/registration.go - Python: Hooks are classes in implementing protocols from
src/hooks/src/hooks/types.py - Java: Hooks implement interfaces from
hooks/SDKHooks.java - C#: Hooks implement interfaces from
Hooks/SDKHooks.cs - Ruby: Hooks use Sorbet-typed classes with Faraday middleware patterns
- PHP: Hooks use PSR-7 request/response interfaces with Guzzle middleware
The hook types, lifecycle, and registration pattern are consistent across all
languages. Refer to the generated file in your SDK for language-specific
interface definitions.
types虽然上述示例使用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