1.0.1 β€’ Published 5 months ago

@reedchan/koa-http-proxy v1.0.1

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

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 with significant enhancements.

🌍 Languages: English | δΈ­ζ–‡

πŸ“‹ Table of Contents

✨ Features

FeatureDescriptionStatus
πŸ”„ Smart RetryAutomatic retry with exponential backoff and custom logicβœ…
🌊 Streaming SupportTrue streaming for large files and real-time dataβœ…
πŸ”§ Request/Response TransformationModify headers, body, and paths with full async supportβœ…
πŸ“Š Memory ManagementAutomatic protection against OOM with large filesβœ…
⏱️ Timeout ControlConfigurable connection and request timeoutsβœ…
πŸ›‘οΈ Circuit BreakerBuilt-in patterns for fault toleranceβœ…
πŸ“ TypeScript SupportFull type definitions includedβœ…
πŸ” Conditional ProxyFilter requests with custom logicβœ…
πŸ—œοΈ CompressionAutomatic gzip/deflate handlingβœ…
πŸ” Session PreservationMaintain user sessions across proxiesβœ…

πŸ“¦ Installation

npm install @reedchan/koa-http-proxy --save

πŸš€ Quick Start

Basic Proxy

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

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

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:

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:

// βœ… 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:

// ❌ 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):

// βœ… 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:

// 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:

// 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:

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

Core Options

agent

Use a custom http.Agent for proxy requests.

const agent = new http.Agent({ keepAlive: true });
app.use(proxy('api.backend.com', { agent }));

port

The port to use for the proxied host.

app.use(proxy('api.backend.com', { port: 8080 }));

https

Force HTTPS for the proxy request.

app.use(proxy('api.backend.com', { https: true }));

headers

Additional headers to send to the proxied host.

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.

app.use(proxy('api.backend.com', {
  strippedHeaders: ['set-cookie', 'x-internal-header']
}));

Request Processing

filter

Filter which requests should be proxied.

app.use(proxy('api.backend.com', {
  filter: (ctx) => {
    return ctx.method === 'GET' && ctx.path.startsWith('/api');
  }
}));

proxyReqPathResolver

Transform the request path before proxying.

app.use(proxy('api.backend.com', {
  proxyReqPathResolver: (ctx) => {
    return ctx.path.replace(/^\/api/, '');
  }
}));

proxyReqOptDecorator

Modify request options before sending.

app.use(proxy('api.backend.com', {
  proxyReqOptDecorator: (proxyReqOpts, ctx) => {
    proxyReqOpts.headers['X-Forwarded-For'] = ctx.ip;
    return proxyReqOpts;
  }
}));

proxyReqBodyDecorator

Transform request body before sending.

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.

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.

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).

// 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.

app.use(proxy('api.backend.com', { reqAsBuffer: true }));

reqBodyEncoding

Encoding for request body (default: 'utf-8').

app.use(proxy('api.backend.com', { reqBodyEncoding: 'binary' }));

limit

Body size limit (default: '1mb').

app.use(proxy('api.backend.com', { limit: '50mb' }));

Session & Security

preserveReqSession

Pass session along to proxied request.

app.use(proxy('api.backend.com', { preserveReqSession: true }));

preserveHostHdr

Copy the host HTTP header to proxied request.

app.use(proxy('api.backend.com', { preserveHostHdr: true }));

Timeout Configuration

connectTimeout

Timeout for initial connection.

app.use(proxy('api.backend.com', { connectTimeout: 5000 }));

timeout

Overall request timeout.

app.use(proxy('api.backend.com', { timeout: 30000 }));

Debug Options

debug

Enable detailed request logging for debugging and monitoring.

// 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

app.use(proxy('api.backend.com', { retry: true }));

Advanced Retry

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

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.

πŸ”— Related Resources


Made with ❀️ by the community | Report Issues | Contribute

1.0.1

5 months ago

1.0.0

5 months ago

0.5.1

5 months ago

0.5.0

5 months ago

0.4.1

5 months ago

0.3.0

5 months ago