Skip to main content

Request Forwarding

The CLI's forwarding feature allows you to send webhook events to your local development environment in real-time. This is essential for testing webhook integrations during development.

How Forwarding Works

When forwarding is enabled, the CLI acts as a proxy:

  1. Webhook arrives at your endpoint
  2. CLI receives the event via WebSocket
  3. CLI forwards the request to your local server
  4. Your handler processes the webhook
  5. Response flows back through the CLI
Webhook Source → Your Endpoint → CLI → Local Server

Response Status

Basic Forwarding

Start Forwarding

Forward all webhook events to your local development server:

hookvm listen https://hookvm.com/hooks/your-endpoint-id \
--target http://localhost:3000/webhooks

Short form using -t:

hookvm listen endpoint-url -t http://localhost:3000/webhooks

Target URL Format

The target URL should point to your local webhook handler:

# Different local development setups
-t http://localhost:3000/webhooks # Express.js
-t http://localhost:8000/api/webhooks # Django
-t http://localhost:3000/api/webhooks # Next.js
-t http://localhost:4567/webhook-handler # Sinatra
-t https://localhost:3443/webhooks # HTTPS local

Forwarding Indicators

Visual Status

In the interactive UI, you'll see forwarding status:

🟢 POST / [12:34:56] → ✅ 200
├── 📥 Received (192.168.1.100)
└── 📤 Forwarded → localhost:3000 (✅ 200 OK)

Status Icons

  • 📤 - Forwarding in progress or completed
  • - Successful forwarding (2xx response)
  • - Failed forwarding (error or non-2xx response)
  • - Forwarding in progress

Response Codes

The CLI shows the response from your local handler:

  • ✅ 200 OK - Handler processed successfully
  • ❌ 404 Not Found - Handler route doesn't exist
  • ❌ 500 Internal Server Error - Handler had an error
  • ❌ Connection refused - Local server not running

Advanced Forwarding

Multiple Endpoints

Forward different endpoints to different local handlers:

# Terminal 1 - Payment webhooks
hookvm listen https://hookvm.com/hooks/payment-endpoint \
--target http://localhost:3000/webhooks/payments

# Terminal 2 - User webhooks
hookvm listen https://hookvm.com/hooks/user-endpoint \
--target http://localhost:3000/webhooks/users

# Terminal 3 - Order webhooks
hookvm listen https://hookvm.com/hooks/order-endpoint \
--target http://localhost:3000/webhooks/orders

Custom Headers

Add headers to forwarded requests:

hookvm listen endpoint-url \
--target http://localhost:3000/webhooks \
--header "Authorization: Bearer dev-token-123" \
--header "X-Environment: development" \
--header "X-Source: webhook-cli"

HTTPS Local Servers

Forward to HTTPS local development servers:

hookvm listen endpoint-url \
--target https://localhost:3443/webhooks

Note: You may need to configure your local server with SSL certificates.

Request Details

What Gets Forwarded

The CLI forwards the complete webhook request:

HTTP Method

  • Always POST (webhooks are POST requests)

Headers

Essential headers are forwarded:

  • Content-Type
  • User-Agent
  • Authorization (if present)
  • Custom headers from the webhook source
  • Any headers added with --header

Body

  • Complete request body (JSON, form data, etc.)
  • Preserved exactly as received

Query Parameters

  • Any query parameters from the original request

What's Added

The CLI adds these headers to forwarded requests:

X-Forwarded-By: webhook-cli
X-Original-Host: hookvm.com
X-Forwarded-For: [original-ip]

Local Handler Setup

Basic Handler

Your local server needs to handle POST requests:

Node.js/Express

const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks', (req, res) => {
console.log('Webhook received:', {
headers: req.headers,
body: req.body
});

// Process the webhook

// Always respond with success
res.status(200).json({ received: true });
});

app.listen(3000, () => {
console.log('Server running on http://localhost:3000');
});

Python/Flask

from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhooks', methods=['POST'])
def handle_webhook():
print(f"Webhook received: {request.get_json()}")

# Process the webhook

return jsonify({"received": True}), 200

if __name__ == '__main__':
app.run(port=3000, debug=True)

Next.js API Route

// pages/api/webhooks.js
export default function handler(req, res) {
if (req.method === 'POST') {
console.log('Webhook received:', req.body);

// Process the webhook

res.status(200).json({ received: true });
} else {
res.setHeader('Allow', ['POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}

Debugging Forwarded Requests

Response Time Monitoring

The CLI shows how long your handler takes to respond:

🟢 POST / [12:34:56] → ✅ 200 (142ms)
├── 📥 Received (webhook-source)
└── 📤 Forwarded → localhost:3000 (✅ 200 OK, 142ms)

Error Handling

When your handler returns errors:

🔴 POST / [12:34:56] → ❌ 500 Internal Server Error
├── 📥 Received (webhook-source)
└── 📤 Forwarded → localhost:3000 (❌ 500 Internal Server Error)

Connection Issues

When your local server isn't available:

🔴 POST / [12:34:56] → ❌ Connection refused
├── 📥 Received (webhook-source)
└── 📤 Forwarded → localhost:3000 (❌ Connection refused)

Testing Your Handler

Manual Testing

Test your handler directly:

# Test with curl
curl -X POST http://localhost:3000/webhooks \
-H "Content-Type: application/json" \
-d '{
"event": "test.event",
"data": {
"message": "Hello from manual test"
}
}'

Webhook Test Events

Use your webhook source's test feature:

  1. Stripe: Dashboard → Webhooks → Send test webhook
  2. GitHub: Settings → Webhooks → Redeliver
  3. Shopify: Settings → Notifications → Test webhook

CLI Test Mode

Send test events through the CLI:

# Send a test event to your endpoint
curl -X POST https://hookvm.com/hooks/your-endpoint-id \
-H "Content-Type: application/json" \
-d '{
"test": true,
"message": "Test from CLI"
}'

Performance Considerations

Handler Response Time

Webhook sources expect quick responses:

  • < 5 seconds: Excellent
  • < 10 seconds: Good
  • < 30 seconds: Acceptable
  • > 30 seconds: May cause timeouts

Async Processing

For heavy processing, use async patterns:

app.post('/webhooks', async (req, res) => {
// Respond immediately
res.status(200).json({ received: true });

// Process asynchronously
setTimeout(() => {
processWebhookData(req.body);
}, 0);
});

Error Recovery

Handle errors gracefully:

app.post('/webhooks', (req, res) => {
try {
processWebhook(req.body);
res.status(200).json({ success: true });
} catch (error) {
console.error('Webhook processing error:', error);
// Still return 200 to avoid retries for permanent errors
res.status(200).json({
success: false,
error: 'Processing failed'
});
}
});

Security Best Practices

Verify Forwarded Requests

Add verification to your handler:

app.post('/webhooks', (req, res) => {
// Check if request came from webhook-cli
if (req.headers['x-forwarded-by'] !== 'webhook-cli') {
return res.status(401).json({ error: 'Invalid source' });
}

// Process the webhook
});

Local Development Only

Never use forwarding in production:

if (process.env.NODE_ENV === 'production') {
// Direct webhook handling in production
} else {
// Accept forwarded requests in development
}

Troubleshooting Forwarding

Common Issues

"Connection refused"

  • Check: Local server is running
  • Check: Port number is correct
  • Check: No firewall blocking connections

"404 Not Found"

  • Check: Handler route exists (/webhooks)
  • Check: Route accepts POST requests
  • Check: No typos in the path

"Handler not called"

  • Check: Handler is properly configured
  • Check: No middleware blocking requests
  • Check: Application logs for errors

Debug Commands

# Check if local server is running
curl http://localhost:3000/webhooks

# Check specific handler
curl -X POST http://localhost:3000/webhooks \
-H "Content-Type: application/json" \
-d '{"test": true}'

# Check what's listening on port
lsof -i :3000

Ready for production? Learn about webhook security and deployment best practices.