UNPKG

@reedchan/koa-http-proxy

Version:
546 lines (429 loc) 12.7 kB
# koa-http-proxy 一个强大的 Koa 中间件,用于将 HTTP 请求代理到另一个主机,具有重试机制、流式传输支持和请求/响应转换等高级功能。 > 本仓库从 [koa-better-http-proxy](https://github.com/nsimmons/koa-better-http-proxy) fork 而来,并进行了重大改进。 **🌍 Languages:** [English](README.md) | [中文](README_ZH.md) ## 📋 目录 - [✨ 功能特性](#-功能特性) - [📦 安装](#-安装) - [🚀 快速开始](#-快速开始) - [📚 使用指南](#-使用指南) - [基础代理](#基础代理) - [流式传输模式](#流式传输模式) - [重试配置](#重试配置) - [请求/响应转换](#请求响应转换) - [⚙️ 配置参考](#️-配置参考) - [🔗 相关资源](#-相关资源) ## ✨ 功能特性 | 功能 | 描述 | 状态 | |------|------|------| | **🔄 智能重试** | 指数退避和自定义逻辑的自动重试 | ✅ | | **🌊 流式传输支持** | 大文件和实时数据的真正流式传输 | ✅ | | **🔧 请求/响应转换** | 修改请求头、正文和路径,完全支持异步 | ✅ | | **📊 内存管理** | 自动防止大文件导致的内存溢出 | ✅ | | **⏱️ 超时控制** | 可配置的连接和请求超时 | ✅ | | **🛡️ 熔断器** | 内置容错模式 | ✅ | | **📝 TypeScript 支持** | 包含完整的类型定义 | ✅ | | **🔍 条件代理** | 使用自定义逻辑过滤请求 | ✅ | | **🗜️ 压缩** | 自动 gzip/deflate 处理 | ✅ | | **🔐 会话保持** | 跨代理维护用户会话 | ✅ | ## 📦 安装 ```bash npm install @reedchan/koa-http-proxy --save ``` ## 🚀 快速开始 ### 基础代理 ```js const Koa = require('koa'); const proxy = require('@reedchan/koa-http-proxy'); const app = new Koa(); // 简单代理到另一个主机 app.use(proxy('api.example.com')); app.listen(3000); ``` ### API 网关模式 ```js const app = new Koa(); // 将不同路径路由到不同服务 app.use('/api/users', proxy('user-service.internal')); app.use('/api/orders', proxy('order-service.internal')); app.use('/api/auth', proxy('auth-service.internal')); app.listen(3000); ``` ### 负载均衡模式 ```js const servers = ['server1.com', 'server2.com', 'server3.com']; let currentServer = 0; app.use(proxy(() => { const server = servers[currentServer]; currentServer = (currentServer + 1) % servers.length; return server; })); ``` ## 📚 使用指南 ### 基础代理 最简单的用例 - 将所有请求代理到另一个主机: ```js app.use(async (ctx) => { // 代理后停止(不继续执行下一个中间件) await proxy('api.backend.com', { port: 443, https: true })(ctx); }); ``` ### Koa 原生中间件模式 ⭐ 遵循 Koa 的原生设计哲学,你可以完全控制中间件的执行流程: ```js // ✅ 代理后停止 - 不传递 next app.use(async (ctx) => { await proxy('api.backend.com')(ctx); // 不调用 next(),执行在此停止 }); // ✅ 代理后继续 - 传递 next app.use(async (ctx, next) => { await proxy('api.backend.com')(ctx, next); // 会调用 next(),继续执行下一个中间件 }); // ✅ 基于条件的动态控制 app.use(async (ctx, next) => { const shouldContinue = ctx.path.startsWith('/api/'); if (shouldContinue) { await proxy('api.backend.com')(ctx, next); // 继续执行 } else { await proxy('api.backend.com')(ctx); // 停止执行 } }); ``` ### 解决路由重复匹配问题 **问题现象:** ```js // ❌ 两个路由都会对同一请求执行 router.post('/upload/:version/files', proxy('api.com')); // 执行 router.all('/(.*)', proxy('api.com')); // 也会执行! ``` **解决方案(Koa 原生方式):** ```js // ✅ 清晰、明确的控制 router.post('/upload/:version/files', async (ctx) => { await proxy('api.com')(ctx); // 不传 next - 在此停止 }); router.all('/(.*)', async (ctx, next) => { await proxy('api.com')(ctx, next); // 需要时继续执行 }); ``` ### 流式传输模式 适用于文件上传、下载和实时数据: ```js // 为文件上传启用流式传输 app.use('/upload', async (ctx) => { await proxy('fileserver.com', { parseReqBody: false, // 启用流式传输 limit: '500mb', // 支持大文件 timeout: 300000 // 5分钟超时 })(ctx); // 上传后停止 }); // 智能条件流式传输 app.use(async (ctx, next) => { const proxy_fn = proxy('backend.com', { parseReqBody: (ctx) => { const size = parseInt(ctx.headers['content-length'] || '0'); return size < 20 * 1024 * 1024; // 大于20MB的文件使用流式传输 } }); if (ctx.path.includes('/upload')) { await proxy_fn(ctx); // 上传后停止 } else { await proxy_fn(ctx, next); // 其他请求继续执行 } }); ``` ### 重试配置 针对不稳定网络的强大重试机制: ```js // 使用默认设置的简单重试 app.use(proxy('api.backend.com', { retry: true // 3次重试,指数退避 })); // 自定义重试配置 app.use(proxy('api.backend.com', { retry: { retries: 5, maxRetryTime: 30000, minTimeout: 500, maxTimeout: 5000 } })); // 高级自定义重试逻辑 app.use(proxy('api.backend.com', { retry: async (handle, ctx) => { for (let attempt = 1; attempt <= 3; attempt++) { try { const result = await handle(); if (result.proxy.res.statusCode < 500) return result; if (attempt < 3) { await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); } } catch (error) { if (attempt === 3) throw error; await new Promise(resolve => setTimeout(resolve, 1000 * attempt)); } } } })); ``` ### 请求/响应转换 动态修改请求和响应: ```js app.use(proxy('api.backend.com', { // 转换请求路径 proxyReqPathResolver: (ctx) => { return ctx.path.replace('/api/v1', '/api/v2'); }, // 添加认证头 proxyReqOptDecorator: (proxyReqOpts, ctx) => { proxyReqOpts.headers['Authorization'] = ctx.headers['authorization']; return proxyReqOpts; }, // 转换响应 userResDecorator: (proxyRes, proxyResData, ctx) => { const data = JSON.parse(proxyResData.toString()); data.timestamp = new Date().toISOString(); return JSON.stringify(data); } })); ``` ## ⚙️ 配置参考 <details> <summary><strong>点击展开详细配置选项</strong></summary> ### 核心选项 #### `agent` 使用自定义的 `http.Agent` 进行代理请求。 ```js const agent = new http.Agent({ keepAlive: true }); app.use(proxy('api.backend.com', { agent })); ``` #### `port` 代理主机使用的端口。 ```js app.use(proxy('api.backend.com', { port: 8080 })); ``` #### `https` 强制代理请求使用 HTTPS。 ```js app.use(proxy('api.backend.com', { https: true })); ``` #### `headers` 发送到代理主机的额外请求头。 ```js app.use(proxy('api.backend.com', { headers: { 'X-API-Key': 'your-api-key', 'User-Agent': 'MyApp/1.0' } })); ``` #### `strippedHeaders` 从代理响应中移除的请求头。 ```js app.use(proxy('api.backend.com', { strippedHeaders: ['set-cookie', 'x-internal-header'] })); ``` ### 请求处理 #### `filter` 过滤需要代理的请求。 ```js app.use(proxy('api.backend.com', { filter: (ctx) => { return ctx.method === 'GET' && ctx.path.startsWith('/api'); } })); ``` #### `proxyReqPathResolver` 在代理前转换请求路径。 ```js app.use(proxy('api.backend.com', { proxyReqPathResolver: (ctx) => { return ctx.path.replace(/^\/api/, ''); } })); ``` #### `proxyReqOptDecorator` 在发送前修改请求选项。 ```js app.use(proxy('api.backend.com', { proxyReqOptDecorator: (proxyReqOpts, ctx) => { proxyReqOpts.headers['X-Forwarded-For'] = ctx.ip; return proxyReqOpts; } })); ``` #### `proxyReqBodyDecorator` 在发送前转换请求正文。 ```js app.use(proxy('api.backend.com', { proxyReqBodyDecorator: (bodyContent, ctx) => { const data = JSON.parse(bodyContent); data.clientInfo = { ip: ctx.ip, userAgent: ctx.get('User-Agent') }; return JSON.stringify(data); } })); ``` ### 响应处理 #### `userResDecorator` 在发送给客户端前转换响应数据。 ```js app.use(proxy('api.backend.com', { userResDecorator: (proxyRes, proxyResData, ctx) => { const data = JSON.parse(proxyResData.toString()); data.processedAt = new Date().toISOString(); return JSON.stringify(data); } })); ``` #### `userResHeadersDecorator` 转换响应头。 ```js app.use(proxy('api.backend.com', { userResHeadersDecorator: (headers) => { headers['X-Proxy-By'] = 'koa-http-proxy'; delete headers['x-internal-header']; return headers; } })); ``` ### 正文处理 #### `parseReqBody` 控制请求正文解析(布尔值或函数)。 ```js // 禁用以启用流式传输 app.use(proxy('api.backend.com', { parseReqBody: false })); // 条件解析 app.use(proxy('api.backend.com', { parseReqBody: (ctx) => { return !ctx.path.includes('/upload'); } })); ``` #### `reqAsBuffer` 确保请求正文编码为 Buffer。 ```js app.use(proxy('api.backend.com', { reqAsBuffer: true })); ``` #### `reqBodyEncoding` 请求正文的编码(默认: 'utf-8')。 ```js app.use(proxy('api.backend.com', { reqBodyEncoding: 'binary' })); ``` #### `limit` 正文大小限制(默认: '1mb')。 ```js app.use(proxy('api.backend.com', { limit: '50mb' })); ``` ### 会话与安全 #### `preserveReqSession` 将会话传递给代理请求。 ```js app.use(proxy('api.backend.com', { preserveReqSession: true })); ``` #### `preserveHostHdr` 将 host HTTP 头复制到代理请求。 ```js app.use(proxy('api.backend.com', { preserveHostHdr: true })); ``` ### 超时配置 #### `connectTimeout` 初始连接的超时时间。 ```js app.use(proxy('api.backend.com', { connectTimeout: 5000 })); ``` #### `timeout` 整体请求超时时间。 ```js app.use(proxy('api.backend.com', { timeout: 30000 })); ``` ### 调试选项 #### `debug` 启用详细的请求日志记录,用于调试和监控。 ```js // 启用基础调试日志 app.use(proxy('api.backend.com', { debug: true })); // 使用对象配置调试选项 app.use(proxy('api.backend.com', { debug: { enabled: true, includeBody: true // 包含请求体内容 } })); ``` **示例输出:** ``` ======================================= KOA-HTTP-PROXY DEBUG ======================================= POST https://api.backend.com/users Payload Size: 256 B Headers: { "content-type": "application/json", "authorization": "Bearer token123", "user-agent": "MyApp/1.0", "content-length": 256 } Request Body: { "name": "张三", "email": "zhangsan@example.com" } ==================================================================================================== ``` **配置选项:** - `enabled`: 是否启用调试日志(默认: false) - `includeBody`: 是否在日志中包含请求体内容(默认: false) **功能特性:** - 自动隐藏标准端口号(HTTP 80, HTTPS 443) - 智能 JSON 格式化 - 对 GET/HEAD/DELETE/OPTIONS 请求不解析请求体 - 精确的文件大小显示 **使用场景:** - 开发环境调试 - API 请求监控 - 性能分析 - 故障排查 ### 重试配置 #### 简单重试 ```js app.use(proxy('api.backend.com', { retry: true })); ``` #### 高级重试 ```js app.use(proxy('api.backend.com', { retry: { retries: 5, // 最大重试次数 maxRetryTime: 30000, // 总重试时间限制 minTimeout: 1000, // 初始延迟 maxTimeout: 10000 // 最大延迟 } })); ``` #### 自定义重试函数 ```js app.use(proxy('api.backend.com', { retry: async (handle, ctx) => { // 自定义重试逻辑 let result; for (let i = 0; i < 3; i++) { result = await handle(); if (result.proxy.res.statusCode < 500) break; await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); } return result; } })); ``` > ⚠️ **内存警告**:重试功能会在内存中缓存请求正文。对于大文件(>20MB),重试会自动禁用。使用 `parseReqBody: false` 启用流式传输模式。 </details> ## 🔗 相关资源 - **[流式传输指南](STREAMING.md)** - 处理大文件和实时流的综合指南 - **[示例](examples/)** - 不同用例的工作示例 - **[TypeScript 定义](types.d.ts)** - TypeScript 用户的完整类型定义 --- **由社区用心制作 ❤️** | [报告问题](https://github.com/reedchan7/koa-http-proxy/issues) | [贡献代码](https://github.com/reedchan7/koa-http-proxy/pulls)