npm package: @lark-apaas/http-client
Description: HTTP client with Axios-style interceptors, built on native fetch API
Author: Lark APaaS
Latest version: 0.1.3

# @lark-apaas/http-client

技术栈无关的 HTTP 客户端，基于原生 fetch API，拦截器模式遵循 Axios 标准。

## 特性

- ✅ **零依赖**: 基于 Node.js 原生 fetch API (Node 18+)
- ✅ **技术栈无关**: 可在任何 Node.js 环境使用（NestJS, Express, Koa, 纯 Node）
- ✅ **Fetch 风格 API**: 返回标准 `Response` 对象，原生支持流式响应
- ✅ **Axios 风格拦截器**: 拦截器模式与 Axios 完全一致，AI 模型和开发者都熟悉
- ✅ **流式响应支持**: 完美支持 SSE、OpenAI Streaming API、大文件下载等场景
- ✅ **完整类型支持**: 100% TypeScript 编写
- ✅ **请求超时**: 支持配置请求超时时间
- ✅ **请求取消**: 支持 AbortController，可与超时结合使用
- ✅ **Query Params**: 自动处理查询参数
- ✅ **baseURL 支持**: 支持配置基础 URL
- ✅ **平台 JWT 鉴权**: 可选启用 AK/SK → JWT 方案，自动注入 Authorization 与 x-api-key
- ✅ **文件 Token 支持**: 支持从文件读取外部签发的 JWT token，多路径优先级链 + 自动 fallback AK/SK
- ✅ **JWT 缓存**: 自动缓存 token，在过期前自动刷新，支持多租户和多用户
- ✅ **安全特性**: 敏感信息脱敏、协议白名单、响应体大小限制

## 安装

```bash
npm install @lark-apaas/http-client
```

或使用 yarn:

```bash
yarn add @lark-apaas/http-client
```

## 环境要求

- Node.js >= 18.0.0 (需要原生 fetch 支持)

## 快速开始

### 基础使用

```typescript
import { HttpClient } from '@lark-apaas/http-client';

const client = new HttpClient({
  baseURL: 'https://api.example.com',
  timeout: 5000,
});

// GET 请求
const response = await client.get('/users');
const data = await response.json();
console.log(data);

// POST 请求
const newUserResponse = await client.post('/users', {
  name: 'John Doe',
  email: 'john@example.com',
});
const newUser = await newUserResponse.json();
console.log(newUser);
```

### 响应格式

所有请求都返回标准的 `Response` 对象（原生 fetch API）：

```typescript
const response = await client.get('/users');

// 响应属性
console.log(response.status);      // HTTP 状态码 (例如: 200)
console.log(response.ok);          // 布尔值，状态码 200-299 为 true
console.log(response.statusText);  // 状态文本 (例如: "OK")
console.log(response.headers);     // Headers 对象

// 解析响应体
const jsonData = await response.json();        // 解析 JSON
const textData = await response.text();        // 解析文本
const blobData = await response.blob();        // 解析 Blob
const arrayBuffer = await response.arrayBuffer(); // 解析 ArrayBuffer
```

### 流式响应支持

`Response` 对象原生支持流式读取，适用于 SSE、实时 API、大文件下载等场景：

```typescript
// OpenAI Streaming API 示例
const response = await client.post('/v1/chat/completions', {
  model: 'gpt-4',
  messages: [{ role: 'user', content: 'Hello!' }],
  stream: true,
});

const reader = response.body!.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  const chunk = decoder.decode(value);
  console.log(chunk); // 实时输出每个数据块
}
```

```typescript
// Server-Sent Events (SSE) 示例
const response = await client.get('/events');
const reader = response.body!.getReader();
const decoder = new TextDecoder();

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  const text = decoder.decode(value);
  const events = text.split('\n\n');

  for (const event of events) {
    if (event.startsWith('data: ')) {
      const data = JSON.parse(event.slice(6));
      console.log('Event:', data);
    }
  }
}
```

```typescript
// 大文件下载进度示例
const response = await client.get('/large-file.zip');
const contentLength = parseInt(response.headers.get('content-length') || '0');
const reader = response.body!.getReader();

let receivedLength = 0;
const chunks: Uint8Array[] = [];

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  chunks.push(value);
  receivedLength += value.length;

  const progress = (receivedLength / contentLength) * 100;
  console.log(`Downloaded: ${progress.toFixed(2)}%`);
}

const blob = new Blob(chunks);
console.log('Download complete!', blob.size);
```

## 请求取消

`http-client` 支持通过 `AbortController` 来取消请求，这与原生的 `fetch` API 和新版 `axios` 保持一致。

你可以在请求配置中传入一个 `AbortSignal`。当该 `signal` 被触发时，请求会被中断。

```typescript
import { HttpClient } from '@lark-apaas/http-client';

const client = new HttpClient();
const controller = new AbortController();

async function fetchWithCancellation() {
  try {
    const promise = client.get('https://api.example.com/long-running-task', {
      signal: controller.signal,
    });

    // 2秒后取消请求
    setTimeout(() => controller.abort(), 2000);

    const response = await promise;
    const data = await response.json();
    console.log('Response:', data);
  } catch (error) {
    // HttpError 的 message 会是 'Request aborted'
    console.error('Error:', error.message);
  }
}

fetchWithCancellation();
```

该机制可以与 `timeout` 配置结合使用，任何一个先触发（超时或外部取消）都会中断请求。

## 拦截器

拦截器模式完全遵循 Axios 标准，API 与 Axios 一致。

### 请求拦截器

请求拦截器接收并返回完整的 `RequestConfig` 对象：

```typescript
// 添加请求拦截器
const requestInterceptorId = client.interceptors.request.use(
  (config) => {
    // 在请求发送前修改配置
    config.headers = config.headers || {};
    config.headers['Authorization'] = `Bearer ${getToken()}`;
    config.headers['X-Request-ID'] = generateRequestId();
    return config;
  },
  (error) => {
    // 处理请求错误
    console.error('Request error:', error);
    return Promise.reject(error);
  }
);

// 移除拦截器
client.interceptors.request.eject(requestInterceptorId);
```

### 响应拦截器

响应拦截器接收并返回标准的 `Response` 对象：

```typescript
// 添加响应拦截器
client.interceptors.response.use(
  (response) => {
    // 处理响应
    console.log(`Response status: ${response.status}`);

    // 可以直接返回 Response，让业务代码自行解析
    return response;
  },
  async (error) => {
    // 处理响应错误
    if (error.response) {
      // 请求已发出，服务器返回错误状态码
      const status = error.response.status;

      if (status === 401) {
        // 处理未授权
        redirectToLogin();
      } else if (status === 500) {
        // 处理服务器错误
        const errorData = await error.response.json();
        console.error('Server error:', errorData);
      }
    } else {
      // 网络错误或请求超时
      console.error('Network error:', error.message);
    }

    return Promise.reject(error);
  }
);
```

## 完整示例

### 在 NestJS 中使用

```typescript
import { Injectable } from '@nestjs/common';
import { HttpClient } from '@lark-apaas/http-client';

@Injectable()
export class ApiService {
  private client: HttpClient;

  constructor() {
    this.client = new HttpClient({
      baseURL: process.env.API_BASE_URL,
      timeout: 10000,
    });

    // 配置请求拦截器
    this.client.interceptors.request.use(
      (config) => {
        config.headers = config.headers || {};
        config.headers['X-Service-Name'] = 'my-service';
        return config;
      }
    );

    // 配置响应拦截器
    this.client.interceptors.response.use(
      (response) => response,
      async (error) => {
        if (error.response?.status === 401) {
          // 刷新 token 并重试
          await this.refreshToken();
          return this.client.request(error.config);
        }
        return Promise.reject(error);
      }
    );
  }

  async getUsers() {
    const response = await this.client.get('/users');
    return response.json();
  }

  async createUser(userData: any) {
    const response = await this.client.post('/users', userData);
    return response.json();
  }
}
```

### 在纯 Node.js 中使用

```typescript
import { HttpClient } from '@lark-apaas/http-client';

const client = new HttpClient({
  baseURL: 'https://jsonplaceholder.typicode.com',
  timeout: 5000,
  headers: {
    'User-Agent': 'MyApp/1.0',
  },
});

// 添加日志拦截器
client.interceptors.request.use((config) => {
  console.log(`[Request] ${config.method} ${config.url}`);
  return config;
});

client.interceptors.response.use(
  (response) => {
    console.log(`[Response] ${response.status} ${response.statusText}`);
    return response;
  },
  (error) => {
    console.error(`[Error] ${error.message}`);
    return Promise.reject(error);
  }
);

async function main() {
  try {
    // GET 请求
    const usersResponse = await client.get('/users', {
      params: { page: 1, limit: 10 }
    });
    const users = await usersResponse.json();
    console.log('Users:', users);

    // POST 请求
    const newPostResponse = await client.post('/posts', {
      title: 'Hello World',
      body: 'This is a test post',
      userId: 1,
    });
    const newPost = await newPostResponse.json();
    console.log('New post:', newPost);
  } catch (error) {
    console.error('Request failed:', error);
  }
}

main();
```

## 平台 JWT 鉴权

在平台内部署的服务可以开启内置 JWT 鉴权能力，SDK 会：

- 读取 `apaas_ak` / `apaas_sk`（名称可通过配置覆盖）
- 使用 HS256 生成短期 JWT（默认 30 分钟有效）
- 自动在请求头中注入 `Authorization: Bearer <token>` 与 `x-api-key`
- **自动缓存 token**，在过期前 5 分钟自动刷新（可配置）

### 启用方式

```typescript
const client = new HttpClient({
  platform: {
    enabled: true,
    domainEnv: 'FORCE_AUTHN_INNERAPI_DOMAIN', // 默认即可省略
    refreshBeforeMs: 5 * 60 * 1000, // 可选：过期前多久刷新（默认 5 分钟）
    defaultClaims: {
      tenant_id: 1001,
      app_id: 'demo-app',
      app_env: 'prod',
    },
  },
});

// baseURL 将默认为 process.env.FORCE_AUTHN_INNERAPI_DOMAIN
await client.get('/secure-resource');
```

也可以在单次请求中覆写 `platformAuth`，用于注入当前用户等动态信息：

```typescript
await client.post('/secure-resource', body, {
  platformAuth: {
    customClaims: {
      user_id: currentUserId,
    },
  },
});
```

> ⚠️ 默认情况下该能力关闭；若 AK/SK 缺失或 JWT 生成失败，会直接抛错阻断请求。
> 注意：平台默认/请求级 `customClaims` 禁止设置 `access_key`，该字段始终由 SDK 根据 AK 自动填充。

### 文件 Token Provider

在某些场景下（如 openclaw），JWT token 由外部进程签发并写入本地文件。通过配置 `tokenProvider` 可让 SDK 自动从文件读取 token，无需 AK/SK：

```typescript
// 最简配置：使用内置默认路径
const client = new HttpClient({
  platform: {
    enabled: true,
    tokenProvider: { type: 'file' },
  },
});

// 自定义路径
const client = new HttpClient({
  platform: {
    enabled: true,
    tokenProvider: {
      type: 'file',
      paths: ['/custom/path/a', '/custom/path/b'],
    },
  },
});
```

**工作机制：**

- `paths` 可选，不传时使用内置默认路径
- 按 `paths` 数组顺序依次尝试，命中第一个有效 token 即返回
- `Authorization` 和 `x-api-key` 均从文件中的 JWT payload 解析，无需额外配置
- 基于 token 的 `exp` 字段做内存缓存，在过期前 `refreshBeforeMs` 窗口内重新读取文件
- 所有路径均无有效 token 时，自动 fallback 到 AK/SK 自签逻辑
- 消费方无需感知 token 来源，行为完全透明

### 性能优化：Token 缓存

平台插件会自动缓存生成的 JWT token，避免每次请求都重新生成：

- **缓存策略**：基于 `claims` 的组合（支持多租户和多用户场景）
- **自动刷新**：在 token 过期前 5 分钟自动刷新（可通过 `refreshBeforeMs` 配置）
- **多租户支持**：不同 `tenant_id` 或 `user_id` 会生成不同的 token 并分别缓存

```typescript
const client = new HttpClient({
  platform: {
    enabled: true,
    refreshBeforeMs: 3 * 60 * 1000, // 过期前 3 分钟刷新
  },
});
```

## 安全特性

### 严格模式

启用严格模式后，会自动开启所有安全检查：

```typescript
const client = new HttpClient({
  security: {
    strictMode: true, // 启用严格模式
  },
});
```

严格模式包含：
- ✅ **URL 协议限制**：仅允许 `http:` 和 `https:`，防止 SSRF 攻击
- ✅ **响应体大小限制**：默认 50MB，防止内存耗尽攻击
- ✅ **敏感信息自动脱敏**：错误对象中的敏感 headers 会被替换为 `[REDACTED]`

### 自定义安全配置

您也可以单独配置各项安全特性：

```typescript
const client = new HttpClient({
  security: {
    allowedProtocols: ['https:'], // 仅允许 HTTPS
    maxResponseSize: 10 * 1024 * 1024, // 限制 10MB
  },
});
```

### 敏感信息保护

`HttpError` 对象会自动清理敏感的 headers，保护您的凭证安全：

```typescript
try {
  await client.get('/api', {
    headers: {
      Authorization: 'Bearer secret-token-12345',
      'x-api-key': 'my-secret-api-key',
    },
  });
} catch (error) {
  console.log(error.config.headers.Authorization); // '[REDACTED]'
  console.log(error.config.headers['x-api-key']); // '[REDACTED]'
  // 其他非敏感 headers 保持不变
}
```

以下 headers 会被自动脱敏：
- `Authorization`
- `x-api-key`
- `Cookie`
- `x-secret`

## API 文档

### HttpClient

#### 构造函数

```typescript
new HttpClient(config?: HttpClientConfig)
```

#### 请求方法

所有请求方法都返回 `Promise<Response>`（标准 fetch Response 对象）：

```typescript
// GET 请求
client.get(url: string, config?: RequestConfig): Promise<Response>

// POST 请求
client.post(url: string, data?: any, config?: RequestConfig): Promise<Response>

// PUT 请求
client.put(url: string, data?: any, config?: RequestConfig): Promise<Response>

// DELETE 请求
client.delete(url: string, config?: RequestConfig): Promise<Response>

// PATCH 请求
client.patch(url: string, data?: any, config?: RequestConfig): Promise<Response>

// 通用请求
client.request(config: RequestConfig): Promise<Response>
```

#### 拦截器

```typescript
// 请求拦截器
client.interceptors.request.use(
  onFulfilled?: (config: RequestConfig) => RequestConfig | Promise<RequestConfig>,
  onRejected?: (error: any) => any
): number

// 响应拦截器
client.interceptors.response.use(
  onFulfilled?: (response: Response) => Response | Promise<Response>,
  onRejected?: (error: any) => any
): number

// 移除拦截器
client.interceptors.request.eject(id: number): void
client.interceptors.response.eject(id: number): void
```

### RequestConfig

```typescript
interface RequestConfig extends RequestInit {
  url?: string;
  params?: Record<string, any>;  // Query 参数
  timeout?: number;               // 超时时间（毫秒）
  baseURL?: string;               // 基础 URL
  platformAuth?: {
    customClaims?: {
      tenant_id?: number;
      user_id?: string;
      app_id?: string;
      app_env?: string;
      sandbox_id?: string;
    };
  };
}

interface SecurityConfig {
  /**
   * 允许的 URL 协议白名单
   * 默认: null (不限制)
   * 设置为 ['http:', 'https:'] 可限制只允许 HTTP/HTTPS
   */
  allowedProtocols?: string[] | null;

  /**
   * 最大响应体大小（字节）
   * 默认: 0 (不限制)
   */
  maxResponseSize?: number;

  /**
   * 是否启用严格模式（启用所有安全检查）
   * 默认: false
   * 启用后会自动设置：
   * - allowedProtocols = ['http:', 'https:']
   * - maxResponseSize = 50MB
   */
  strictMode?: boolean;
}

interface HttpClientConfig extends RequestConfig {
  platform?: {
    enabled?: boolean;            // 开启后自动注册平台插件
    baseURL?: string;             // 直接指定平台域名
    domainEnv?: string;           // 域名环境变量（默认 FORCE_AUTHN_INNERAPI_DOMAIN）
    accessKeyEnv?: string;        // AK 环境变量名称（默认 FORCE_AUTHN_ACCESS_KEY）
    secretKeyEnv?: string;        // SK 环境变量名称（默认 FORCE_AUTHN_ACCESS_SECRET）
    accessKey?: string;           // 直接传入 AK（多用于本地测试）
    secretKey?: string;           // 直接传入 SK（多用于本地测试）
    expireTimeMs?: number;        // JWT 过期时间，默认 30 分钟
    refreshBeforeMs?: number;     // Token 刷新时机，默认过期前 5 分钟
    defaultClaims?: {             // 默认 Claim，可被每次请求覆盖
      tenant_id?: number;
      user_id?: string;
      app_id?: string;
      app_env?: string;
      sandbox_id?: string;
    };
  };
  security?: SecurityConfig;      // 安全配置
}
```

### HttpError

```typescript
class HttpError extends Error {
  isHttpError: boolean;                // 始终为 true，用于类型判断
  response: Response | undefined;      // 响应对象（如果有）
  config: SafeRequestConfig;           // 已清理敏感信息的请求配置
}
```

**注意**：为了安全考虑，`error.config` 中的敏感 headers（如 `Authorization`, `x-api-key`, `Cookie`）会被自动替换为 `[REDACTED]`。

### SafeRequestConfig

已清理敏感信息的请求配置，用于 `HttpError.config`：

```typescript
interface SafeRequestConfig extends Omit<RequestConfig, 'headers'> {
  headers?: Record<string, string>; // 敏感 headers 已替换为 '[REDACTED]'
}
```

### JWT 辅助函数

本包导出了 JWT 生成和验证的辅助函数，可用于手动处理 JWT token：

```typescript
import {
  generateJWTToken,
  parseJWTTokenWithVerify,
  DEFAULT_JWT_EXPIRE_TIME_MS,    // 30 * 60 * 1000 (30分钟)
  DEFAULT_CLOCK_TOLERANCE_SEC,   // 60 (秒)
} from '@lark-apaas/http-client';

// 生成 JWT token
const token = generateJWTToken(
  {
    access_key: 'your-access-key',
    tenant_id: 1001,
    user_id: 'user-123',
    app_id: 'app-456',
  },
  {
    accessKey: 'your-access-key',
    secretKey: 'your-secret-key',
    expireTimeMs: 30 * 60 * 1000, // 30分钟
  }
);

// 验证并解析 JWT token
const claims = parseJWTTokenWithVerify(
  token,
  {
    accessKey: 'your-access-key',
    secretKey: 'your-secret-key',
    expireTimeMs: 30 * 60 * 1000,
  },
  {
    clockTolerance: 60, // 时钟容差（秒）
    skipExpiration: false, // 是否跳过过期验证
  }
);
```

### JWT 类型定义

```typescript
interface CustomClaims {
  tenant_id?: number;
  user_id?: string;
  app_id?: string;
  app_env?: string;
  sandbox_id?: string;
  access_key: string;  // 必填
}

interface JWTClaims extends CustomClaims {
  iss: string;   // 签发者
  iat: number;   // 签发时间
  nbf: number;   // 生效时间
  exp: number;   // 过期时间
  jti: string;   // JWT ID
  [key: string]: any;
}

interface JWTConfig {
  accessKey: string;
  secretKey: string;
  expireTimeMs: number;
}

interface JWTVerifyOptions {
  clockTolerance?: number;  // 时钟容差（秒），默认 60
  skipExpiration?: boolean; // 跳过过期验证，默认 false
}
```

### 平台插件函数

以下函数用于手动注册平台插件（通常通过 `HttpClientConfig.platform` 自动注册）：

```typescript
import {
  registerPlatformPlugin,
  resolvePlatformBaseURL,
} from '@lark-apaas/http-client';

// 解析平台 baseURL（从环境变量或配置）
const baseURL = resolvePlatformBaseURL({
  enabled: true,
  baseURL: 'https://api.example.com', // 优先使用
  domainEnv: 'FORCE_AUTHN_INNERAPI_DOMAIN', // 其次从环境变量读取
});

// 手动注册平台插件到 HttpClient 拦截器
const client = new HttpClient();
const tokenManager = registerPlatformPlugin(client.interceptors, {
  enabled: true,
  accessKey: 'your-ak',
  secretKey: 'your-sk',
});
```

### 平台插件类型定义

```typescript
interface PlatformPluginOptions {
  enabled?: boolean;            // 是否启用
  baseURL?: string;             // 直接指定平台域名
  domainEnv?: string;           // 域名环境变量名
  accessKeyEnv?: string;        // AK 环境变量名
  secretKeyEnv?: string;        // SK 环境变量名
  accessKey?: string;           // 直接传入 AK
  secretKey?: string;           // 直接传入 SK
  expireTimeMs?: number;        // JWT 过期时间
  refreshBeforeMs?: number;     // Token 刷新时机
  defaultClaims?: PlatformClaims;
  tokenProvider?: TokenProviderConfig; // 文件 Token Provider
}

interface TokenProviderConfig {
  type: 'file';                 // Provider 类型
  paths?: string[];             // 文件路径优先级链（可选，不传使用内置默认路径）
}

interface PlatformRequestAuthConfig {
  customClaims?: PlatformClaims;
}

// PlatformClaims 不包含 access_key（由 SDK 自动填充）
type PlatformClaims = Omit<CustomClaims, 'access_key'>;
```

## 设计理念

### 为什么采用 Fetch 风格的响应格式？

1. **原生支持流式响应**: `Response.body` 是 `ReadableStream`，完美支持 SSE、实时 API、大文件下载
2. **Web 标准**: 遵循 WHATWG Fetch API 标准，与浏览器和 Deno 行为一致
3. **更灵活**: 业务代码可根据需要选择 `.json()`, `.text()`, `.blob()`, `.arrayBuffer()` 等
4. **社区趋势**: 现代 HTTP 客户端（ky, ofetch, undici）都采用 fetch 风格

```typescript
// Fetch 风格 - 灵活且原生支持流
const response = await client.get('/data');
const data = await response.json();  // 或 .text(), .blob(), .arrayBuffer()

// 流式读取（Axios 风格无法原生支持）
const reader = response.body!.getReader();
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  console.log(value); // 实时处理数据块
}
```

### 为什么遵循 Axios 的拦截器模式？

1. **最广泛使用**: Axios 是最流行的 HTTP 客户端，几乎所有开发者都熟悉
2. **AI 模型友好**: 训练数据中有大量 Axios 使用示例，模型更容易生成正确的代码
3. **清晰的职责分离**: 请求拦截器、响应拦截器、错误处理分别处理
4. **成熟可靠**: 经过多年生产环境验证的模式
5. **可预测**: 开发者知道拦截器的执行顺序和行为

### 为什么基于 fetch 而不是 axios？

1. **零依赖**: 使用 Node.js 原生 API，无需额外依赖
2. **更小体积**: 代码体积小，打包后更轻量
3. **技术栈无关**: 不依赖特定框架，可在任何环境使用
4. **现代化**: fetch 是 Web 标准，未来趋势

### 与社区其他方案的对比

| 方案 | 响应风格 | 流式支持 | 拦截器模式 | 依赖 | 体积 | 熟悉度 |
|------|---------|---------|-----------|------|------|--------|
| @lark-apaas/http-client | Fetch | ✅ 原生 | Axios 风格 | 零依赖 | 小 | 高（Axios API） |
| axios | Axios | ❌ 需封装 | Axios 风格 | 有依赖 | 35.6KB | 最高 |
| ky | Fetch | ✅ 原生 | Hooks 风格 | 零依赖 | ~6KB | 中 |
| ofetch | Fetch | ✅ 原生 | 简单 | 零依赖 | ~4KB | 中 |
| undici | Fetch | ✅ 原生 | 简单 | Node专用 | 大 | 中 |
| wretch | Fetch | ✅ 原生 | 中间件风格 | 零依赖 | ~2KB | 低 |
| got | 自定义 | ⚠️ 部分 | Hooks 风格 | 有依赖 | 大 | 中 |

## 最佳实践

### 1. 创建专用的客户端实例

不要在每个请求中重新配置拦截器：

```typescript
// ❌ 不好
async function fetchUser() {
  const client = new HttpClient();
  client.interceptors.request.use(/* ... */);
  const response = await client.get('/user');
  return response.json();
}

// ✅ 好
const apiClient = new HttpClient({
  baseURL: process.env.API_URL,
});

apiClient.interceptors.request.use(/* ... */);

async function fetchUser() {
  const response = await apiClient.get('/user');
  return response.json();
}
```

### 2. 统一错误处理

在响应拦截器中统一处理错误：

```typescript
client.interceptors.response.use(
  (response) => response,
  async (error) => {
    // 统一错误日志
    logger.error('HTTP Error', {
      url: error.config?.url,
      status: error.response?.status,
      message: error.message,
    });

    // 统一错误转换
    throw new ApplicationError(error);
  }
);
```

### 3. 请求重试

使用拦截器实现请求重试：

```typescript
const MAX_RETRIES = 3;

client.interceptors.response.use(
  (response) => response,
  async (error) => {
    const config = error.config;
    config.retryCount = config.retryCount || 0;

    if (config.retryCount < MAX_RETRIES && shouldRetry(error)) {
      config.retryCount += 1;
      await sleep(1000 * config.retryCount);
      return client.request(config);
    }

    return Promise.reject(error);
  }
);
```

### 4. 处理流式响应时的错误

流式响应需要特殊的错误处理：

```typescript
async function streamData() {
  const response = await client.post('/stream', data);

  if (!response.ok) {
    // 即使是错误响应，也可能是流式的
    const errorText = await response.text();
    throw new Error(`Stream failed: ${errorText}`);
  }

  const reader = response.body!.getReader();
  const decoder = new TextDecoder();

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      const chunk = decoder.decode(value);
      processChunk(chunk);
    }
  } catch (error) {
    // 处理流读取错误
    reader.cancel();
    throw error;
  }
}
```

## 从 Axios 风格迁移

如果你之前使用的是 `response.data` 风格的 API，迁移很简单：

```typescript
// 旧代码（Axios 风格）
const response = await client.get('/users');
console.log(response.data);
console.log(response.status);

// 新代码（Fetch 风格）
const response = await client.get('/users');
const data = await response.json();  // 手动解析 JSON
console.log(data);
console.log(response.status);
```

拦截器中的变化：

```typescript
// 旧代码（Axios 风格）
client.interceptors.response.use((response) => {
  console.log(response.data);  // data 已解析
  return response;
});

// 新代码（Fetch 风格）
client.interceptors.response.use((response) => {
  // response 是原生 Response 对象，未解析
  // 拦截器中通常不解析，让业务代码决定如何解析
  return response;
});
```

详见 [TEST_MIGRATION.md](./docs/TEST_MIGRATION.md) 获取完整迁移指南。

## 许可证

MIT

## 关键字

- http-client
- fetch
- axios
- interceptor
- typescript
- technology-agnostic