@reedchan/koa-http-proxy
Version:
http proxy middleware for koa
547 lines (429 loc) β’ 13.2 kB
Markdown
# koa-http-proxy
A powerful Koa middleware for proxying HTTP requests to another host with advanced features like retry mechanisms, streaming support, and request/response transformation.
> This repository is forked from [koa-better-http-proxy](https://github.com/nsimmons/koa-better-http-proxy) with significant enhancements.
**π Languages:** [English](README.md) | [δΈζ](README_ZH.md)
## π Table of Contents
- [β¨ Features](#-features)
- [π¦ Installation](#-installation)
- [π Quick Start](#-quick-start)
- [π Usage Guide](#-usage-guide)
- [Basic Proxy](#basic-proxy)
- [Streaming Mode](#streaming-mode)
- [Retry Configuration](#retry-configuration)
- [Request/Response Transformation](#requestresponse-transformation)
- [βοΈ Configuration Reference](#οΈ-configuration-reference)
- [π Related Resources](#-related-resources)
## β¨ Features
| Feature | Description | Status |
|---------|-------------|---------|
| **π Smart Retry** | Automatic retry with exponential backoff and custom logic | β
|
| **π Streaming Support** | True streaming for large files and real-time data | β
|
| **π§ Request/Response Transformation** | Modify headers, body, and paths with full async support | β
|
| **π Memory Management** | Automatic protection against OOM with large files | β
|
| **β±οΈ Timeout Control** | Configurable connection and request timeouts | β
|
| **π‘οΈ Circuit Breaker** | Built-in patterns for fault tolerance | β
|
| **π TypeScript Support** | Full type definitions included | β
|
| **π Conditional Proxy** | Filter requests with custom logic | β
|
| **ποΈ Compression** | Automatic gzip/deflate handling | β
|
| **π Session Preservation** | Maintain user sessions across proxies | β
|
## π¦ Installation
```bash
npm install @reedchan/koa-http-proxy --save
```
## π Quick Start
### Basic Proxy
```js
const Koa = require('koa');
const proxy = require('@reedchan/koa-http-proxy');
const app = new Koa();
// Simple proxy to another host
app.use(proxy('api.example.com'));
app.listen(3000);
```
### API Gateway Pattern
```js
const app = new Koa();
// Route different paths to different services
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);
```
### Load Balancer Pattern
```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;
}));
```
## π Usage Guide
### Basic Proxy
The simplest use case - proxy all requests to another host:
```js
app.use(async (ctx) => {
// Stop after proxy (don't continue to next middleware)
await proxy('api.backend.com', {
port: 443,
https: true
})(ctx);
});
```
### Koa Native Middleware Pattern β
Following Koa's native design philosophy, you have complete control over middleware execution:
```js
// β
Stop after proxy - don't pass next
app.use(async (ctx) => {
await proxy('api.backend.com')(ctx);
// No next() called, execution stops here
});
// β
Continue after proxy - pass next
app.use(async (ctx, next) => {
await proxy('api.backend.com')(ctx, next);
// next() will be called, continues to next middleware
});
// β
Dynamic control based on conditions
app.use(async (ctx, next) => {
const shouldContinue = ctx.path.startsWith('/api/');
if (shouldContinue) {
await proxy('api.backend.com')(ctx, next); // Continue
} else {
await proxy('api.backend.com')(ctx); // Stop
}
});
```
### Solving Route Duplication Issues
**The Problem:**
```js
// β Both routes execute for the same request
router.post('/upload/:version/files', proxy('api.com')); // Executes
router.all('/(.*)', proxy('api.com')); // Also executes!
```
**The Solution (Koa Native Way):**
```js
// β
Clean, explicit control
router.post('/upload/:version/files', async (ctx) => {
await proxy('api.com')(ctx); // Don't pass next - stops here
});
router.all('/(.*)', async (ctx, next) => {
await proxy('api.com')(ctx, next); // Continue if needed
});
```
### Streaming Mode
Perfect for file uploads, downloads, and real-time data:
```js
// Enable streaming for file uploads
app.use('/upload', async (ctx) => {
await proxy('fileserver.com', {
parseReqBody: false, // Enable streaming
limit: '500mb', // Support large files
timeout: 300000 // 5 minute timeout
})(ctx); // Stop after upload
});
// Smart conditional streaming
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; // Stream files >20MB
}
});
if (ctx.path.includes('/upload')) {
await proxy_fn(ctx); // Stop after upload
} else {
await proxy_fn(ctx, next); // Continue for other requests
}
});
```
### Retry Configuration
Robust retry mechanisms for unreliable networks:
```js
// Simple retry with defaults
app.use(proxy('api.backend.com', {
retry: true // 3 retries with exponential backoff
}));
// Custom retry configuration
app.use(proxy('api.backend.com', {
retry: {
retries: 5,
maxRetryTime: 30000,
minTimeout: 500,
maxTimeout: 5000
}
}));
// Advanced custom retry logic
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));
}
}
}
}));
```
### Request/Response Transformation
Modify requests and responses on the fly:
```js
app.use(proxy('api.backend.com', {
// Transform request path
proxyReqPathResolver: (ctx) => {
return ctx.path.replace('/api/v1', '/api/v2');
},
// Add authentication headers
proxyReqOptDecorator: (proxyReqOpts, ctx) => {
proxyReqOpts.headers['Authorization'] = ctx.headers['authorization'];
return proxyReqOpts;
},
// Transform response
userResDecorator: (proxyRes, proxyResData, ctx) => {
const data = JSON.parse(proxyResData.toString());
data.timestamp = new Date().toISOString();
return JSON.stringify(data);
}
}));
```
## βοΈ Configuration Reference
<details>
<summary><strong>Click to expand detailed configuration options</strong></summary>
### Core Options
#### `agent`
Use a custom `http.Agent` for proxy requests.
```js
const agent = new http.Agent({ keepAlive: true });
app.use(proxy('api.backend.com', { agent }));
```
#### `port`
The port to use for the proxied host.
```js
app.use(proxy('api.backend.com', { port: 8080 }));
```
#### `https`
Force HTTPS for the proxy request.
```js
app.use(proxy('api.backend.com', { https: true }));
```
#### `headers`
Additional headers to send to the proxied host.
```js
app.use(proxy('api.backend.com', {
headers: {
'X-API-Key': 'your-api-key',
'User-Agent': 'MyApp/1.0'
}
}));
```
#### `strippedHeaders`
Headers to remove from proxy response.
```js
app.use(proxy('api.backend.com', {
strippedHeaders: ['set-cookie', 'x-internal-header']
}));
```
### Request Processing
#### `filter`
Filter which requests should be proxied.
```js
app.use(proxy('api.backend.com', {
filter: (ctx) => {
return ctx.method === 'GET' && ctx.path.startsWith('/api');
}
}));
```
#### `proxyReqPathResolver`
Transform the request path before proxying.
```js
app.use(proxy('api.backend.com', {
proxyReqPathResolver: (ctx) => {
return ctx.path.replace(/^\/api/, '');
}
}));
```
#### `proxyReqOptDecorator`
Modify request options before sending.
```js
app.use(proxy('api.backend.com', {
proxyReqOptDecorator: (proxyReqOpts, ctx) => {
proxyReqOpts.headers['X-Forwarded-For'] = ctx.ip;
return proxyReqOpts;
}
}));
```
#### `proxyReqBodyDecorator`
Transform request body before sending.
```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);
}
}));
```
### Response Processing
#### `userResDecorator`
Transform response data before sending to client.
```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`
Transform response headers.
```js
app.use(proxy('api.backend.com', {
userResHeadersDecorator: (headers) => {
headers['X-Proxy-By'] = 'koa-http-proxy';
delete headers['x-internal-header'];
return headers;
}
}));
```
### Body Processing
#### `parseReqBody`
Control request body parsing (boolean or function).
```js
// Disable for streaming
app.use(proxy('api.backend.com', { parseReqBody: false }));
// Conditional parsing
app.use(proxy('api.backend.com', {
parseReqBody: (ctx) => {
return !ctx.path.includes('/upload');
}
}));
```
#### `reqAsBuffer`
Ensure request body is encoded as Buffer.
```js
app.use(proxy('api.backend.com', { reqAsBuffer: true }));
```
#### `reqBodyEncoding`
Encoding for request body (default: 'utf-8').
```js
app.use(proxy('api.backend.com', { reqBodyEncoding: 'binary' }));
```
#### `limit`
Body size limit (default: '1mb').
```js
app.use(proxy('api.backend.com', { limit: '50mb' }));
```
### Session & Security
#### `preserveReqSession`
Pass session along to proxied request.
```js
app.use(proxy('api.backend.com', { preserveReqSession: true }));
```
#### `preserveHostHdr`
Copy the host HTTP header to proxied request.
```js
app.use(proxy('api.backend.com', { preserveHostHdr: true }));
```
### Timeout Configuration
#### `connectTimeout`
Timeout for initial connection.
```js
app.use(proxy('api.backend.com', { connectTimeout: 5000 }));
```
#### `timeout`
Overall request timeout.
```js
app.use(proxy('api.backend.com', { timeout: 30000 }));
```
### Debug Options
#### `debug`
Enable detailed request logging for debugging and monitoring.
```js
// Enable basic debug logging
app.use(proxy('api.backend.com', { debug: true }));
// Use object configuration for debug options
app.use(proxy('api.backend.com', {
debug: {
enabled: true,
includeBody: true // Include request body content
}
}));
```
**Example Output:**
```
======================================= 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": "John Doe",
"email": "john@example.com"
}
====================================================================================================
```
**Configuration Options:**
- `enabled`: Whether to enable debug logging (default: false)
- `includeBody`: Whether to include request body content in logs (default: false)
**Features:**
- Automatically hide standard ports (HTTP 80, HTTPS 443)
- Smart JSON formatting
- No body parsing for GET/HEAD/DELETE/OPTIONS requests
- Precise file size display
**Use Cases:**
- Development debugging
- API monitoring
- Performance analysis
- Request troubleshooting
### Retry Configuration
#### Simple Retry
```js
app.use(proxy('api.backend.com', { retry: true }));
```
#### Advanced Retry
```js
app.use(proxy('api.backend.com', {
retry: {
retries: 5, // Max retry attempts
maxRetryTime: 30000, // Total retry time limit
minTimeout: 1000, // Initial delay
maxTimeout: 10000 // Maximum delay
}
}));
```
#### Custom Retry Function
```js
app.use(proxy('api.backend.com', {
retry: async (handle, ctx) => {
// Custom retry logic
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;
}
}));
```
> β οΈ **Memory Warning**: Retry caches request body in memory. For large files (>20MB), retry is automatically disabled. Use `parseReqBody: false` for streaming mode.
</details>
## π Related Resources
- **[Streaming Guide](STREAMING.md)** - Comprehensive guide for handling large files and real-time streams
- **[Examples](examples/)** - Working examples for different use cases
- **[TypeScript Definitions](types.d.ts)** - Full type definitions for TypeScript users
---
**Made with β€οΈ by the community** | [Report Issues](https://github.com/reedchan7/koa-http-proxy/issues) | [Contribute](https://github.com/reedchan7/koa-http-proxy/pulls)