mcpField
C
customize-sdk-hooks

customize-sdk-hooks

github.com/speakeasy-api开发1894 次安装37 周前更新
代码仓库

关于 customize-sdk-hooks

本指南介绍如何为 Speakeasy 生成的 SDK 添加自定义生命周期钩子,涵盖请求与响应拦截、钩子注册,以及在多种支持语言中保留再生成时的自定义文件。

主要功能

  1. 01初始化和请求生命周期钩子
  2. 02添加请求头、遥测和认证逻辑
  3. 03转换响应与错误
  4. 04再生成时保留钩子注册
  5. 05多种 SDK 语言的实现指引

适用场景

  1. 01添加自定义 User-Agent 或关联 ID 请求头
  2. 02使用 HMAC 认证对请求签名
  3. 03记录 SDK 级遥测或日志
  4. 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/ 目录(默认创建)

钩子类型

钩子调用时机常见用途
SDKInitHookSDK 客户端初始化时配置默认值、验证配置、设置基本 URL
BeforeCreateRequestHook创建 HTTP 请求对象之前修改输入参数、注入默认值
BeforeRequestHook发送 HTTP 请求之前添加标头、记录日志、遥测、对请求签名
AfterSuccessHookHTTP 响应成功之后转换响应、发出警告、记录指标
AfterErrorHookHTTP 错误响应之后转换错误、重试逻辑、记录错误
钩子接口(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 添加钩子:

  1. 在 src/hooks/ 中创建钩子实现文件
  2. 实现 src/hooks/types.ts 中相应的接口
  3. 在 src/hooks/registration.ts 中注册钩子
  4. 重新生成 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());
}

最佳实践

  1. 让钩子专注于单一职责:每个钩子都应只处理一个关注点。分别使用不同的钩子处理用户代理、遥测和身份验证,而不是使用一个庞大的钩子。

  2. 读取响应正文前先克隆响应: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
    }
    
  3. 遥测采用即发即弃模式:不要因日志记录或指标收集等非关键操作而阻塞请求管道:

    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,可用于按操作执行逻辑、记录日志和收集指标。

不要做的事

  • 不要编辑 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 文件,了解特定语言的接口定义。

评论

支持 Markdown