@lark-apaas/http-client
v0.1.2
Published
HTTP client with Axios-style interceptors, built on native fetch API
Keywords
Readme
@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
- ✅ JWT 缓存: 自动缓存 token,在过期前自动刷新,支持多租户和多用户
- ✅ 安全特性: 敏感信息脱敏、协议白名单、响应体大小限制
安装
npm install @lark-apaas/http-client或使用 yarn:
yarn add @lark-apaas/http-client环境要求
- Node.js >= 18.0.0 (需要原生 fetch 支持)
快速开始
基础使用
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: '[email protected]',
});
const newUser = await newUserResponse.json();
console.log(newUser);响应格式
所有请求都返回标准的 Response 对象(原生 fetch API):
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、大文件下载等场景:
// 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); // 实时输出每个数据块
}// 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);
}
}
}// 大文件下载进度示例
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 被触发时,请求会被中断。
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 对象:
// 添加请求拦截器
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 对象:
// 添加响应拦截器
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 中使用
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 中使用
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 分钟自动刷新(可配置)
启用方式
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,用于注入当前用户等动态信息:
await client.post('/secure-resource', body, {
platformAuth: {
customClaims: {
user_id: currentUserId,
},
},
});⚠️ 默认情况下该能力关闭;若 AK/SK 缺失或 JWT 生成失败,会直接抛错阻断请求。 注意:平台默认/请求级
customClaims禁止设置access_key,该字段始终由 SDK 根据 AK 自动填充。
性能优化:Token 缓存
平台插件会自动缓存生成的 JWT token,避免每次请求都重新生成:
- 缓存策略:基于
claims的组合(支持多租户和多用户场景) - 自动刷新:在 token 过期前 5 分钟自动刷新(可通过
refreshBeforeMs配置) - 多租户支持:不同
tenant_id或user_id会生成不同的 token 并分别缓存
const client = new HttpClient({
platform: {
enabled: true,
refreshBeforeMs: 3 * 60 * 1000, // 过期前 3 分钟刷新
},
});安全特性
严格模式
启用严格模式后,会自动开启所有安全检查:
const client = new HttpClient({
security: {
strictMode: true, // 启用严格模式
},
});严格模式包含:
- ✅ URL 协议限制:仅允许
http:和https:,防止 SSRF 攻击 - ✅ 响应体大小限制:默认 50MB,防止内存耗尽攻击
- ✅ 敏感信息自动脱敏:错误对象中的敏感 headers 会被替换为
[REDACTED]
自定义安全配置
您也可以单独配置各项安全特性:
const client = new HttpClient({
security: {
allowedProtocols: ['https:'], // 仅允许 HTTPS
maxResponseSize: 10 * 1024 * 1024, // 限制 10MB
},
});敏感信息保护
HttpError 对象会自动清理敏感的 headers,保护您的凭证安全:
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 会被自动脱敏:
Authorizationx-api-keyCookiex-secret
API 文档
HttpClient
构造函数
new HttpClient(config?: HttpClientConfig)请求方法
所有请求方法都返回 Promise<Response>(标准 fetch Response 对象):
// 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>拦截器
// 请求拦截器
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): voidRequestConfig
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
class HttpError extends Error {
response: Response | undefined; // 响应对象(如果有)
config: SafeRequestConfig; // 已清理敏感信息的请求配置
}注意:为了安全考虑,error.config 中的敏感 headers(如 Authorization, x-api-key, Cookie)会被自动替换为 [REDACTED]。
设计理念
为什么采用 Fetch 风格的响应格式?
- 原生支持流式响应:
Response.body是ReadableStream,完美支持 SSE、实时 API、大文件下载 - Web 标准: 遵循 WHATWG Fetch API 标准,与浏览器和 Deno 行为一致
- 更灵活: 业务代码可根据需要选择
.json(),.text(),.blob(),.arrayBuffer()等 - 社区趋势: 现代 HTTP 客户端(ky, ofetch, undici)都采用 fetch 风格
// 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 的拦截器模式?
- 最广泛使用: Axios 是最流行的 HTTP 客户端,几乎所有开发者都熟悉
- AI 模型友好: 训练数据中有大量 Axios 使用示例,模型更容易生成正确的代码
- 清晰的职责分离: 请求拦截器、响应拦截器、错误处理分别处理
- 成熟可靠: 经过多年生产环境验证的模式
- 可预测: 开发者知道拦截器的执行顺序和行为
为什么基于 fetch 而不是 axios?
- 零依赖: 使用 Node.js 原生 API,无需额外依赖
- 更小体积: 代码体积小,打包后更轻量
- 技术栈无关: 不依赖特定框架,可在任何环境使用
- 现代化: 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. 创建专用的客户端实例
不要在每个请求中重新配置拦截器:
// ❌ 不好
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. 统一错误处理
在响应拦截器中统一处理错误:
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. 请求重试
使用拦截器实现请求重试:
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. 处理流式响应时的错误
流式响应需要特殊的错误处理:
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,迁移很简单:
// 旧代码(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);拦截器中的变化:
// 旧代码(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 获取完整迁移指南。
许可证
MIT
关键字
- http-client
- fetch
- axios
- interceptor
- typescript
- technology-agnostic
