Redirections
Integration Guides

HAProxy

Integrate redirect lookups with HAProxy using Lua scripts and map files with the Edge Query API

HAProxy Integration

Integrate redirect lookups with HAProxy using Lua scripts for real-time API queries or map files for high-performance local lookups.

Overview

This guide shows you how to integrate redirect lookups into HAProxy, the reliable, high-performance load balancer. You'll learn two integration patterns:

  1. Map Files (Recommended): Periodic sync to local map files for O(1) lookups with zero latency overhead
  2. Lua Scripts (Advanced): Real-time API queries for always-fresh redirect data

Estimated time: 30 minutes

Prerequisites:

  • HAProxy 2.9+ with Lua support (for real-time pattern)
  • curl command-line tool
  • API key for the Edge Query API

Check Lua support:

haproxy -vv | grep Lua

If you see "Built with Lua version", you have Lua support enabled.

Request Flow

flowchart TD
    A[Client Request] --> B[HAProxy Frontend]
    B --> C{Redirect Lookup}
    C -->|Match Found| D[Return 301 Redirect]
    C -->|No Match| E[Pass to Default Backend]
    D --> F[Client]
    E --> G[Origin Server]

HAProxy's map files provide O(1) lookup performance with zero network overhead. This is the recommended approach for most use cases.

How It Works

  1. A sync script downloads redirects from the Edge Query API
  2. Redirects are written to a HAProxy map file
  3. HAProxy uses the map() function to perform lookups
  4. A cron job keeps the map file up to date

Step 1: Create the Sync Script

Create /usr/local/bin/sync-redirects.sh:

#!/bin/bash
set -euo pipefail

API_URL="https://api.3xx.app"
PROJECT_ID="YOUR_PROJECT_ID"
API_KEY="YOUR_API_KEY"
MAP_FILE="/etc/haproxy/redirects.map"
TEMP_FILE="/tmp/redirects.map.$$"

# Download map file from API
curl -sf \
  -H "X-API-Key: ${API_KEY}" \
  "${API_URL}/v1/export?projectId=${PROJECT_ID}&format=haproxy-map-exact" \
  -o "${TEMP_FILE}"

# Atomic replacement
mv "${TEMP_FILE}" "${MAP_FILE}"

# Reload HAProxy configuration
systemctl reload haproxy

echo "Redirects synced: $(wc -l < ${MAP_FILE}) entries"

Make the script executable:

chmod +x /usr/local/bin/sync-redirects.sh

Inline verification: Run the sync script manually:

/usr/local/bin/sync-redirects.sh

You should see output like "Redirects synced: 42 entries". Check the map file:

head /etc/haproxy/redirects.map

You should see lines like:

/old-path https://example.com/new-path
/another-old-path https://example.com/another-new-path

Step 2: Configure HAProxy

Edit /etc/haproxy/haproxy.cfg:

global
    log /dev/log local0
    maxconn 2000
    user haproxy
    group haproxy
    daemon

defaults
    log     global
    mode    http
    option  httplog
    timeout connect 5000
    timeout client  50000
    timeout server  50000

frontend http-in
    bind *:80

    # Check if path exists in redirect map
    # If found, redirect with 301 and the mapped destination
    http-request redirect code 301 location %[path,map(/etc/haproxy/redirects.map)] if { path,map(/etc/haproxy/redirects.map) -m found }

    # If no redirect found, pass to default backend
    default_backend web_servers

backend web_servers
    balance roundrobin
    server web1 192.168.1.10:8080 check
    server web2 192.168.1.11:8080 check

Inline verification: Test the configuration:

haproxy -c -f /etc/haproxy/haproxy.cfg

You should see "Configuration file is valid".

Reload HAProxy:

systemctl reload haproxy

Test a redirect:

curl -I http://localhost/old-path

You should see:

HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-path

Step 3: Schedule Periodic Sync

Add a cron job to sync redirects every 5 minutes. Run crontab -e:

*/5 * * * * /usr/local/bin/sync-redirects.sh >> /var/log/redirect-sync.log 2>&1

Advanced: Runtime Map API

HAProxy provides a runtime API for live map updates without reloading:

View current map entries:

echo "show map /etc/haproxy/redirects.map" | socat stdio /var/run/haproxy.sock

Add a redirect:

echo "add map /etc/haproxy/redirects.map /new-path https://example.com/destination" | socat stdio /var/run/haproxy.sock

Update a redirect:

echo "set map /etc/haproxy/redirects.map /old-path https://example.com/updated-destination" | socat stdio /var/run/haproxy.sock

Clear all redirects:

echo "clear map /etc/haproxy/redirects.map" | socat stdio /var/run/haproxy.sock

Note: Runtime changes are not persisted. Use the sync script for permanent updates.

Pattern 2: Real-Time API Lookup (Lua)

For scenarios requiring always-fresh redirect data, HAProxy can use Lua scripts to query the Edge Query API in real-time.

Trade-offs:

  • ✅ Always fresh (no sync delay)
  • ✅ No local storage required
  • ❌ Adds network latency to each lookup
  • ❌ Requires Lua support in HAProxy
  • ❌ More complex error handling

Step 1: Create Lua Redirect Script

Create /etc/haproxy/redirect-lookup.lua:

local http = require("socket.http")
local json = require("json")

-- Configuration
local API_URL = "https://api.3xx.app"
local PROJECT_ID = os.getenv("REDIRECT_PROJECT_ID") or "YOUR_PROJECT_ID"
local API_KEY = os.getenv("REDIRECT_API_KEY") or "YOUR_API_KEY"

function redirect_lookup(txn)
    local path = txn.sf:path()

    -- Build API URL
    local lookup_url = string.format(
        "%s/v1/lookup?projectId=%s&path=%s",
        API_URL,
        PROJECT_ID,
        path
    )

    -- Make HTTP request with error handling
    local success, response_body, response_code = pcall(function()
        local body, code = http.request({
            url = lookup_url,
            method = "GET",
            headers = {
                ["X-API-Key"] = API_KEY,
                ["Accept"] = "application/json"
            }
        })
        return body, code
    end)

    -- Handle errors
    if not success or response_code ~= 200 then
        core.Debug("Redirect lookup failed: " .. tostring(response_code))
        return nil
    end

    -- Parse JSON response
    local ok, data = pcall(json.decode, response_body)
    if not ok then
        core.Debug("Failed to parse JSON response")
        return nil
    end

    -- Check if redirect exists
    if data.redirect and data.redirect.destination then
        core.Info("Redirect found: " .. path .. " -> " .. data.redirect.destination)
        return data.redirect.destination
    end

    return nil
end

-- Register the function
core.register_fetches("redirect_lookup", redirect_lookup)

Note: HAProxy's Lua environment has limitations. The above example uses LuaSocket which may require additional configuration. For production use, consider using the map file pattern or HAProxy's native HTTP client capabilities.

Step 2: Configure HAProxy with Lua

Edit /etc/haproxy/haproxy.cfg:

global
    log /dev/log local0
    maxconn 2000
    user haproxy
    group haproxy
    daemon

    # Load Lua script
    lua-load /etc/haproxy/redirect-lookup.lua

    # Set environment variables for API credentials
    setenv REDIRECT_PROJECT_ID YOUR_PROJECT_ID
    setenv REDIRECT_API_KEY YOUR_API_KEY

defaults
    log     global
    mode    http
    option  httplog
    timeout connect 5000
    timeout client  50000
    timeout server  50000

frontend http-in
    bind *:80

    # Perform redirect lookup via Lua
    http-request set-var(txn.redirect_dest) lua.redirect_lookup

    # If redirect found, return 301
    http-request redirect code 301 location %[var(txn.redirect_dest)] if { var(txn.redirect_dest) -m found }

    # Otherwise, pass to backend
    default_backend web_servers

backend web_servers
    balance roundrobin
    server web1 192.168.1.10:8080 check
    server web2 192.168.1.11:8080 check

Inline verification: Test the configuration:

haproxy -c -f /etc/haproxy/haproxy.cfg

You should see "Configuration file is valid".

Reload HAProxy:

systemctl reload haproxy

Test a redirect:

curl -I http://localhost/test-path

Alternative: External Environment File

For better security, store credentials in a separate file:

Create /etc/haproxy/redirect-env:

REDIRECT_PROJECT_ID=your_project_id_here
REDIRECT_API_KEY=your_api_key_here

Restrict permissions:

chmod 600 /etc/haproxy/redirect-env
chown haproxy:haproxy /etc/haproxy/redirect-env

Update your systemd service to load the environment file by creating /etc/systemd/system/haproxy.service.d/override.conf:

[Service]
EnvironmentFile=/etc/haproxy/redirect-env

Reload systemd and restart HAProxy:

systemctl daemon-reload
systemctl restart haproxy

Fallback Behavior

Map File Pattern

Missing map file: If the map file doesn't exist, HAProxy will log a warning but continue to operate. No redirects will be processed, and all requests will pass to the default backend.

Empty map file: Same behavior as missing file - requests pass through to backend.

Invalid map file format: HAProxy will log errors and may fail to reload. Use haproxy -c to validate before reloading.

Lua Pattern

API unreachable: The Lua script wraps HTTP calls in pcall() to catch errors. Failed lookups return nil, causing requests to pass through to the default backend.

Timeout: Configure socket timeouts in the Lua script to prevent hanging requests.

Invalid response: JSON parse errors are caught and logged. Request passes to backend.

Testing

Test Exact Match Redirect

curl -I http://localhost/old-path

Expected:

HTTP/1.1 301 Moved Permanently
Location: https://example.com/new-path

Test Non-Existent Path

curl -I http://localhost/does-not-exist

Expected:

HTTP/1.1 200 OK
(response from backend)

Verify Map File Reload

  1. Update a redirect in your dashboard
  2. Wait for sync (or run /usr/local/bin/sync-redirects.sh manually)
  3. Check map file: grep "updated-path" /etc/haproxy/redirects.map
  4. Test redirect: curl -I http://localhost/updated-path

Debug Map Lookups

Add logging to see which redirects are matched:

frontend http-in
    bind *:80

    # Log the redirect destination if found
    http-request set-var(txn.redirect_dest) path,map(/etc/haproxy/redirects.map)
    http-request capture var(txn.redirect_dest) len 256 if { var(txn.redirect_dest) -m found }

    http-request redirect code 301 location %[var(txn.redirect_dest)] if { var(txn.redirect_dest) -m found }

    default_backend web_servers

Check logs:

tail -f /var/log/haproxy.log

Troubleshooting

Configuration Validation Fails

Problem: haproxy -c -f /etc/haproxy/haproxy.cfg shows errors

Solution:

  1. Check syntax carefully - HAProxy config is whitespace-sensitive
  2. Verify map file exists before referencing it
  3. For Lua issues, check that Lua support is compiled in: haproxy -vv | grep Lua

Map File Format Errors

Problem: Redirects not working after sync

Solution:

  1. Check map file format: cat /etc/haproxy/redirects.map
  2. Format should be: source_path destination_url (space-separated, one per line)
  3. No empty lines or comments in map file
  4. Verify file permissions: ls -l /etc/haproxy/redirects.map

Lua Script Not Working

Problem: Lua redirects not executing

Possible causes:

  1. Lua not compiled in: Run haproxy -vv | grep Lua - if no output, rebuild HAProxy with Lua support
  2. LuaSocket not available: HAProxy's Lua environment may not include socket libraries by default
  3. Environment variables not set: Check systemctl status haproxy and verify environment variables are loaded
  4. HTTP client limitations: HAProxy's Lua HTTP client has limitations. Consider using the map file pattern instead.

Runtime Map API Not Working

Problem: socat commands fail

Solution:

  1. Verify stats socket is configured in HAProxy config:
    global
        stats socket /var/run/haproxy.sock mode 660 level admin
  2. Check socket permissions: ls -l /var/run/haproxy.sock
  3. Ensure user is in haproxy group: groups

Redirects Not Updating

Problem: New redirects don't appear

Solution:

  1. Check sync script logs: tail -f /var/log/redirect-sync.log
  2. Verify cron job is running: systemctl status cron
  3. Test sync manually: /usr/local/bin/sync-redirects.sh
  4. Check API credentials in sync script
  5. Verify HAProxy was reloaded: systemctl status haproxy

High Latency with Lua Pattern

Problem: Requests are slow with real-time lookups

Solution:

  1. Add timeout to Lua HTTP requests
  2. Consider switching to map file pattern for better performance
  3. Implement caching in Lua script for frequently accessed paths
  4. Monitor API response times

For more troubleshooting tips, see the Troubleshooting Guide.

Performance Considerations

Map File Pattern:

  • Lookup time: O(1) constant time
  • Memory usage: ~100 bytes per redirect
  • Network overhead: Zero (local lookups only)
  • Sync delay: 5 minutes (configurable)

Lua Pattern:

  • Lookup time: Network RTT + API response time (~50ms)
  • Memory usage: Minimal
  • Network overhead: One API call per unique path lookup
  • Freshness: Real-time

Recommendation: Use map files for production. The sync delay is acceptable for most use cases, and the performance benefits are significant.

Next Steps