A modern, fetch-based HTTP proxy library optimized for Bun runtime with advanced features like hooks, circuit breakers, and comprehensive security protections.
- 🚀 Bun Optimized: Built specifically for Bun runtime with modern fetch API
- 🔄 Circuit Breaker: Automatic failure detection and recovery
- ⏰ Timeouts: Configurable request and circuit breaker timeouts
- 🪝 Enhanced Hooks: Descriptive lifecycle hooks with circuit breaker monitoring
- 🗄️ URL Caching: LRU-based URL caching for performance
- 📦 TypeScript: Full TypeScript support with comprehensive types
- 🔀 Redirect Control: Manual redirect handling support
- 🛡️ Security Hardened: Protection against SSRF, injection attacks, path traversal, and more
- 📝 Comprehensive Logging: Structured logging with Pino for monitoring and debugging
- ✅ Comprehensive Testing: High test coverage with Bun's test runner
- 📈 Performance Optimized: Designed for high throughput and low latency
bun add fetch-gateimport createFetchGate from "fetch-gate" // Create proxy instance const { proxy } = createFetchGate({ base: "https://api.example.com", }) // Use with Bun's HTTP server const server = Bun.serve({ port: 3000, async fetch(req) { // Proxy all requests to the base URL return proxy(req) }, }) console.log("Proxy server running on http://localhost:3000")Backend server can be proxied through a gateway:
// Backend server const backendServer = Bun.serve({ port: 3001, hostname: "localhost", async fetch(req: Request): Promise<Response> { const url = new URL(req.url) if (url.pathname === "/users") { return new Response(JSON.stringify([]), { headers: { "content-type": "application/json" }, }) } return new Response("Not Found", { status: 404 }) }, }) console.log(`Backend server running on http://localhost:${backendServer.port}`)Gateway server that proxies requests to the backend:
import createFetchGate from "fetch-gate" // Create proxy const { proxy } = createFetchGate({ base: "http://localhost:3001", }) // Gateway server const gatewayServer = Bun.serve({ port: 3000, hostname: "localhost", async fetch(req: Request): Promise<Response> { const url = new URL(req.url) if (url.pathname === "/api/users") { return proxy(req, "/users") } return new Response("Not Found", { status: 404 }) }, }) console.log(`Gateway server running on http://localhost:${gatewayServer.port}`) console.log(`Try: curl http://localhost:3000/api/users`)Creates a new proxy instance with the specified options.
interface ProxyOptions { base?: string // Base URL for all requests timeout?: number // Request timeout (default: 30000ms) circuitBreaker?: CircuitBreakerOptions cacheURLs?: number // URL cache size (default: 100, 0 to disable) headers?: Record<string, string> // Default headers logger?: Logger // Pino logger instance for comprehensive logging followRedirects?: boolean // Follow redirects (default: false) maxRedirects?: number // Max redirects (default: 5) } interface CircuitBreakerOptions { failureThreshold?: number // Failures to open circuit (default: 5) resetTimeout?: number // Reset timeout (default: 60000ms) timeout?: number // Circuit breaker timeout (default: 5000ms) enabled?: boolean // Enable circuit breaker (default: true) } interface CircuitBreakerResult { success: boolean // Whether the circuit breaker execution was successful error?: Error // Error object if execution failed state: CircuitState // Current circuit breaker state failureCount: number // Current failure count executionTimeMs: number // Execution time in milliseconds fallbackResponseProvided?: boolean // Whether a fallback response was provided }{ proxy: (req: Request, source?: string, opts?: ProxyRequestOptions) => Promise<Response>; close: () => void; getCircuitBreakerState: () => CircuitState; getCircuitBreakerFailures: () => number; clearURLCache: () => void; }Proxies an HTTP request to the target server.
req: Request- The incoming request objectsource?: string- Target URL or path (optional if base is set)opts?: ProxyRequestOptions- Per-request options
interface ProxyRequestOptions { base?: string // Override base URL timeout?: number // Override timeout headers?: Record<string, string> // Additional headers queryString?: Record<string, any> | string // Query parameters request?: RequestInit // Custom fetch options logger?: Logger // Override proxy logger for this request // Lifecycle Hooks beforeRequest?: ( req: Request, opts: ProxyRequestOptions, ) => void | Promise<void> afterResponse?: ( req: Request, res: Response, body?: ReadableStream | null, ) => void | Promise<void> onError?: ( req: Request, error: Error, ) => void | Promise<void> | Promise<Response> beforeCircuitBreakerExecution?: ( req: Request, opts: ProxyRequestOptions, ) => void | Promise<void> afterCircuitBreakerExecution?: ( req: Request, result: CircuitBreakerResult, ) => void | Promise<void> }fetch-gate includes comprehensive logging capabilities using Pino, providing structured logging for request lifecycle, security events, performance metrics, and circuit breaker operations.
import createFetchGate from "fetch-gate" import pino from "pino" // Use default logger (automatically configured) const { proxy } = createFetchGate({ base: "https://api.example.com", // Default logger is created automatically }) // Or provide custom logger const logger = pino({ level: "info", transport: { target: "pino-pretty", options: { colorize: true }, }, }) const { proxy: customProxy } = createFetchGate({ base: "https://api.example.com", logger: logger, })const productionLogger = pino({ level: "warn", timestamp: pino.stdTimeFunctions.isoTime, formatters: { level: (label) => ({ level: label }), log: (object) => ({ ...object, service: "fetch-gate", environment: "production", }), }, redact: ["authorization", "cookie", "password"], transport: { target: "pino/file", options: { destination: "./logs/proxy.log" }, }, }) const { proxy } = createFetchGate({ base: "https://api.example.com", logger: productionLogger, })// Override proxy logger for specific requests const response = await proxy(request, undefined, { logger: customRequestLogger, headers: { "X-Debug": "true" }, })The library logs various structured events:
- Request Lifecycle: Start, success, error, timeout events
- Security Events: Protocol validation, injection attempts, SSRF prevention
- Circuit Breaker: State changes, error thresholds, recovery events
- Performance: Response times, cache hits/misses, timing metrics
- Cache Operations: URL cache hits, misses, and evictions
Example log output:
{ "level": 30, "time": "2025-05-31T12:00:00.000Z", "event": "request_start", "requestId": "req-abc123", "method": "GET", "url": "https://api.example.com/users" } { "level": 40, "time": "2025-05-31T12:00:01.000Z", "event": "security_header_validation", "requestId": "req-abc123", "message": "Header validation failed", "headerName": "X-Custom", "issue": "CRLF injection attempt" }For detailed logging configuration examples, see the Logging Guide.
const { proxy } = createFetchGate({ base: "https://api.example.com", }) Bun.serve({ async fetch(req) { return proxy(req, undefined, { beforeRequest: async (req, opts) => { console.log(`Proxying ${req.method} ${req.url}`) }, afterResponse: async (req, res, body) => { console.log(`Response: ${res.status} ${res.statusText}`) }, onError: async (req, error) => { console.error(`Proxy error for ${req.url}:`, error.message) }, }) }, })The enhanced hook naming conventions provide more descriptive and semantically meaningful hook names:
const { proxy } = createFetchGate({ base: "https://api.example.com", }) Bun.serve({ async fetch(req) { return proxy(req, undefined, { beforeRequest: async (req, opts) => { console.log(`🔄 Starting request: ${req.method} ${req.url}`) console.log(`Request timeout: ${opts.timeout}ms`) }, afterResponse: async (req, res, body) => { console.log(`✅ Request completed: ${res.status} ${res.statusText}`) }, beforeCircuitBreakerExecution: async (req, opts) => { console.log(`⚡ Circuit breaker executing request`) }, afterCircuitBreakerExecution: async (req, result) => { const { success, state, failureCount, executionTimeMs } = result console.log(`⚡ Circuit breaker result:`, { success, state, failureCount, executionTime: `${executionTimeMs}ms`, }) if (state === "OPEN") { console.warn(`🚨 Circuit breaker is OPEN!`) } }, onError: async (req, error) => { console.error(`💥 Request failed: ${error.message}`) }, }) }, })The hooks are executed in a specific order to provide predictable lifecycle management:
beforeRequest- Called before the request is sent to the target serverbeforeCircuitBreakerExecution- Called before the circuit breaker executes the request- Circuit Breaker Execution - The actual fetch request is executed within the circuit breaker
afterResponse- Called after a successful response is received (only on success)afterCircuitBreakerExecution- Called after the circuit breaker completes (success or failure)onError- Called if any error occurs during the request lifecycle
const { proxy } = createFetchGate({ base: "https://api.example.com", }) const executionOrder: string[] = [] await proxy(req, undefined, { beforeRequest: async () => { executionOrder.push("beforeRequest") // 1st }, beforeCircuitBreakerExecution: async () => { executionOrder.push("beforeCircuitBreaker") // 2nd }, afterResponse: async () => { executionOrder.push("afterResponse") // 3rd (success only) }, afterCircuitBreakerExecution: async () => { executionOrder.push("afterCircuitBreaker") // 4th }, onError: async () => { executionOrder.push("onError") // Called on any error }, }) // Result: ["beforeRequest", "beforeCircuitBreaker", "afterResponse", "afterCircuitBreaker"]const { proxy } = createFetchGate({ base: "https://api.example.com", }) Bun.serve({ async fetch(req) { return proxy(req, undefined, { beforeRequest: async (req, opts) => { // Add authentication header req.headers.set("authorization", "Bearer " + process.env.API_TOKEN) // Remove sensitive headers req.headers.delete("x-internal-key") // Add custom headers via opts.headers if (!opts.headers) opts.headers = {} opts.headers["x-proxy-timestamp"] = new Date().toISOString() }, afterResponse: async (req, res, body) => { // Modify response headers (create new response with modified headers) const headers = new Headers(res.headers) // Add CORS headers headers.set("access-control-allow-origin", "*") headers.set("access-control-allow-methods", "GET, POST, PUT, DELETE") // Remove server information headers.delete("server") headers.delete("x-powered-by") // Replace the response with modified headers return new Response(res.body, { status: res.status, statusText: res.statusText, headers: headers, }) }, }) }, })const { proxy, getCircuitBreakerState, getCircuitBreakerFailures } = createFetchGate({ base: "https://api.example.com", circuitBreaker: { failureThreshold: 3, resetTimeout: 30000, }, }) // Monitor circuit breaker status setInterval(() => { const state = getCircuitBreakerState() const failures = getCircuitBreakerFailures() console.log(`Circuit breaker: ${state}, failures: ${failures}`) }, 5000) Bun.serve({ async fetch(req) { const response = await proxy(req) // Add circuit breaker status to response headers response.headers.set("x-circuit-breaker", getCircuitBreakerState()) return response }, })const services = [ "https://api1.example.com", "https://api2.example.com", "https://api3.example.com", ] let currentIndex = 0 const { proxy } = createFetchGate({ timeout: 5000, circuitBreaker: { enabled: true }, }) Bun.serve({ async fetch(req) { // Simple round-robin load balancing const targetBase = services[currentIndex] currentIndex = (currentIndex + 1) % services.length return proxy(req, undefined, { base: targetBase, onError: async (req, error) => { console.log(`Failed request to ${targetBase}: ${error.message}`) }, }) }, })Bun's fetch API automatically handles connection pooling, so you don't need to manage connections manually. Each request will reuse existing connections when possible, improving performance and reducing latency.
Read more about Bun's fetch connection pooling.
The library automatically handles common error scenarios:
- 503 Service Unavailable: When circuit breaker is open
- 504 Gateway Timeout: When requests exceed timeout
- 502 Bad Gateway: For other proxy errors
You can customize error handling using the onError hook:
proxy(req, undefined, { onError: async (req, error) => { // Log error console.error("Proxy error:", error) // Custom metrics metrics.increment("proxy.errors", { error_type: error.message.includes("timeout") ? "timeout" : "other", }) }, })You can return a fallback response from the onError hook by resolving the hook with a Response object. This allows you to customize the error response sent to the client.
proxy(req, undefined, { onError: async (req, error) => { // Log error console.error("Proxy error:", error) // Return a fallback response console.log("Returning fallback response for:", req.url) return new Response("Fallback response", { status: 200 }) }, })- URL Caching: Keep
cacheURLsenabled (default 100) for better performance - Circuit Breaker: Tune thresholds based on your service characteristics
- Timeouts: Set appropriate timeouts for your use case
- Connection Reuse: Bun's fetch automatically handles connection pooling
MIT
To install dependencies:
bun installTo run tests:
bun testTo run examples:
# Debug example bun run example:debug # Gateway server example bun run example:gateway # Load balancer example bun run example:loadbalancer # Performance benchmark example bun run example:benchmarkTo build the library:
bun run buildThe library includes comprehensive tests covering all major functionality:
- Proxy operations
- Circuit breaker behavior
- Error handling
- Header transformations
- Timeout scenarios
- Security protections and attack prevention
Run the test suite with:
bun testRun tests with coverage:
bun test --coverageThis library includes comprehensive security protections against common web vulnerabilities:
- SSRF Protection: Protocol validation and domain restrictions
- Header Injection Prevention: CRLF injection and response splitting protection
- Query String Injection Protection: Parameter validation and encoding safety
- Path Traversal Prevention: Secure path normalization utilities
- HTTP Method Validation: Whitelist-based method validation
- DoS Prevention Guidelines: Resource exhaustion protection recommendations
Contributions are welcome! Please feel free to submit a Pull Request.