customize-sdk-hooks

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Customize 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
    src/hooks/
    (or language equivalent)
  • Updated
    src/hooks/registration.ts
    to register the new hook
  • 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
    src/hooks/
    directory exists in the generated SDK (created by default)
  • 一个由Speakeasy生成的SDK(支持任意受支持的语言)
  • 了解SDK的请求/响应生命周期
  • 生成的SDK中存在
    src/hooks/
    目录(默认创建)

Hook Types

钩子类型

HookWhen CalledCommon Use Cases
SDKInitHook
SDK client initializationConfigure defaults, validate config, set base URL
BeforeCreateRequestHook
Before the HTTP request object is createdModify input parameters, inject defaults
BeforeRequestHook
Before the HTTP request is sentAdd headers, logging, telemetry, sign requests
AfterSuccessHook
After a successful HTTP responseTransform response, emit warnings, log metrics
AfterErrorHook
After an HTTP error responseError transformation, retry logic, error logging
钩子调用时机常见使用场景
SDKInitHook
SDK客户端初始化时配置默认值、验证配置、设置基础URL
BeforeCreateRequestHook
HTTP请求对象创建前修改输入参数、注入默认值
BeforeRequestHook
HTTP请求发送前添加请求头、日志、遥测、请求签名
AfterSuccessHook
HTTP响应成功后转换响应、发出警告、记录指标
AfterErrorHook
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 implementations
Key rule:
registration.ts
and any custom hook files you create are preserved during SDK regeneration. The
types.ts
file is regenerated and should not be modified.
src/
  hooks/
    types.ts          # 自动生成 - 请勿编辑(钩子接口/类型)
    registration.ts   # 自定义文件 - 您的钩子注册(重新生成时保留)
    custom_useragent.ts   # 自定义文件 - 您的钩子实现
    telemetry.ts          # 自定义文件 - 您的钩子实现
核心规则
registration.ts
和您创建的任何自定义钩子文件在SDK重新生成时都会被保留。
types.ts
文件会被重新生成,请勿修改。

Registration Pattern

注册模式

All hooks are registered in
src/hooks/registration.ts
. This file is created once by the generator and never overwritten. You add your hooks here:
typescript
// 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.ts
中注册。该文件由生成器创建一次,之后不会被覆盖。您可以在此添加钩子:
typescript
// 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:
  1. Create your hook implementation file in
    src/hooks/
  2. Implement the appropriate interface from
    src/hooks/types.ts
  3. Register it in
    src/hooks/registration.ts
  4. Regenerate the SDK -- your hooks are preserved
bash
undefined
要为Speakeasy生成的SDK添加钩子:
  1. src/hooks/
    目录中创建您的钩子实现文件
  2. 实现
    src/hooks/types.ts
    中的对应接口
  3. src/hooks/registration.ts
    中注册钩子
  4. 重新生成SDK -- 您的钩子会被保留
bash
undefined

After 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和自定义钩子文件不会被修改

undefined
undefined

Examples

示例

Example 1: Custom User-Agent Hook

示例1:自定义User-Agent钩子

Add a custom
User-Agent
header to every outgoing request.
typescript
// 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-Agent
请求头。
typescript
// 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
BeforeRequestHook
.
Step 1: Use an overlay to mark the security scheme so Speakeasy generates the hook point:
yaml
undefined
对于需要HMAC签名或无法在OpenAPI规范中表达的自定义认证的API,可将覆盖与
BeforeRequestHook
结合使用。
步骤1:使用覆盖标记安全方案,以便Speakeasy生成钩子点:
yaml
undefined

overlay.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

最佳实践

  1. Keep hooks focused: Each hook should address a single concern. Use separate hooks for user-agent, telemetry, and auth rather than one monolithic hook.
  2. Clone responses before reading the body: The
    Response.body
    stream can only be consumed once. Always clone before reading:
    typescript
    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
    }
  3. 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;
    }
  4. Test hooks independently: Write unit tests for hooks in isolation by constructing mock
    Request
    /
    Response
    objects and hook contexts.
  5. Use
    hookCtx.operationID
    : The hook context provides the current operation ID, which is useful for per-operation behavior, logging, and metrics.
  1. 保持钩子聚焦:每个钩子应只处理一个关注点。为User-Agent、遥测和认证使用单独的钩子,而不是一个庞大的钩子。
  2. 读取响应体前先克隆
    Response.body
    流只能被消费一次。读取前务必克隆:
    typescript
    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
    }
  3. 遥测使用“即发即弃”模式:不要为非关键操作(如日志或指标)阻塞请求管道:
    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;
    }
  4. 独立测试钩子:通过构造模拟
    Request
    /
    Response
    对象和钩子上下文,单独为钩子编写单元测试。
  5. 使用
    hookCtx.operationID
    :钩子上下文提供当前操作ID,可用于实现按操作的行为、日志和指标。

What NOT to Do

禁止操作

  • Do NOT edit
    types.ts
    : This file is regenerated. Your changes will be lost.
  • 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
    AfterSuccessHook
    unless you intend to convert a success into a failure. Throwing in hooks disrupts the normal flow.
  • 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.,
    registerBeforeRequestHook
    vs
    registerAfterSuccessHook
    )
  • Check that
    initHooks
    is exported and follows the expected signature
  • 验证钩子是否已在
    src/hooks/registration.ts
    中注册
  • 确认您注册的是正确的钩子类型(例如
    registerBeforeRequestHook
    vs
    registerAfterSuccessHook
  • 检查
    initHooks
    是否已导出且符合预期签名

Response body is empty or already consumed

响应体为空或已被消费

  • You are reading
    response.body
    or calling
    response.json()
    without cloning first
  • Always use
    response.clone()
    before consuming the body, then return the original
  • 您在未克隆的情况下读取了
    response.body
    或调用了
    response.json()
  • 始终在消费体之前使用
    response.clone()
    ,然后返回原始响应

Hooks lost after regeneration

重新生成SDK后钩子丢失

  • Custom hook files in
    src/hooks/
    are preserved, but only if they are separate files
  • registration.ts
    is never overwritten after initial generation
  • types.ts
    IS overwritten -- never put custom code there
  • src/hooks/
    目录中的自定义钩子文件会被保留,但仅当它们是单独文件时
  • registration.ts
    在初始生成后永远不会被覆盖
  • types.ts
    会被覆盖——永远不要在其中添加自定义代码

TypeScript compilation errors after regeneration

重新生成SDK后TypeScript编译出错

  • types.ts
    may have updated interfaces. Check for breaking changes in hook signatures
  • Update your hook implementations to match the new interface definitions
  • types.ts
    可能更新了接口。检查钩子签名中的破坏性变更
  • 更新您的钩子实现以匹配新的接口定义

Hook causes request failures

钩子导致请求失败

  • Ensure you are returning a valid
    Request
    or
    Response
    object
  • 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
    hooks/hooks.go
    with registration in
    hooks/registration.go
  • Python: Hooks are classes in
    src/hooks/
    implementing protocols from
    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
types
file in your SDK for language-specific interface definitions.
虽然上述示例使用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
文件获取特定语言的接口定义。