How to Conditionally Add Response Headers in HAproxy 1.6 Based on Request URI Patterns


2 views

When working with HAproxy 1.6 as a load balancer, you might encounter scenarios where you need to dynamically add response headers based on the incoming request URI. A common use case is adding cache-control headers for specific API endpoints while leaving other routes unaffected.

The initial approach many try is using ACLs with path matching for response headers:

acl api path_reg ^/api/(.*)$
http-response add-header Cache-Control public,max-age="600" if api

However, HAproxy 1.6 will reject this with the warning:

[WARNING] acl 'api' will never match because it only involves keywords 
that are incompatible with 'backend http-response header rule'

Here's an effective method that works with HAproxy 1.6:

frontend http-in
    bind *:80
    acl is_api path_beg /api
    reqrep ^([^\ ]*\ /api) \1 if is_api
    use_backend api_servers if is_api
    default_backend other_servers

backend api_servers
    server api1 10.0.0.1:8080 check
    http-response set-header Cache-Control "public, max-age=600"

backend other_servers
    server app1 10.0.0.2:8080 check

For more complex URI patterns, consider this method:

frontend http-in
    bind *:80
    acl is_api path_beg /api
    capture request header Host len 128
    capture request header X-Forwarded-For len 128
    use_backend api_servers if is_api
    default_backend other_servers

backend api_servers
    server api1 10.0.0.1:8080 check
    http-response set-header Cache-Control "public, max-age=600"
    http-response set-header Vary "Accept-Encoding"

When implementing these solutions:

  • Test header modifications thoroughly in staging before production
  • Be aware of HAproxy's processing order for requests and responses
  • Consider upgrading to newer HAproxy versions for more flexible header manipulation
  • Monitor performance impact when adding multiple conditional headers

Here's a complete example for a production-like scenario:

global
    log /dev/log local0
    log /dev/log local1 notice
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

frontend http-in
    bind *:80
    bind *:443 ssl crt /etc/ssl/certs/example.pem
    redirect scheme https if !{ ssl_fc }
    
    acl is_static path_beg /static/
    acl is_api path_beg /api/
    
    use_backend static_servers if is_static
    use_backend api_servers if is_api
    default_backend app_servers

backend static_servers
    balance roundrobin
    option forwardfor
    http-response set-header Cache-Control "public, max-age=31536000"
    server static1 10.0.0.10:80 check
    server static2 10.0.0.11:80 check

backend api_servers
    balance roundrobin
    option forwardfor
    http-response set-header Cache-Control "public, max-age=600"
    http-response set-header X-API-Version "1.0"
    server api1 10.0.0.20:8080 check
    server api2 10.0.0.21:8080 check

backend app_servers
    balance roundrobin
    option forwardfor
    server app1 10.0.0.30:8080 check
    server app2 10.0.0.31:8080 check

When working with HAProxy 1.6 as a load balancer for Tomcat servers, a common requirement is to dynamically set response headers based on the incoming request URI. The specific case we're addressing involves adding a Cache-Control header for /api endpoints while excluding other paths.

The initial approach using path-based ACLs with http-response fails because:

acl api path_reg ^/api/(.*)$
http-response add-header Cache-Control public,max-age=\"600\" if api

This triggers a warning because path_reg fetch method isn't compatible with response-time processing in HAProxy 1.6.

Here's the proper way to implement this in HAProxy 1.6:

frontend http-in
    bind *:80
    # Capture the request URI
    capture request header Host len 128
    capture request header User-Agent len 128
    capture request uri len 128
    
    # Define ACL based on captured URI
    acl is_api_path capture.req.uri -m beg /api/
    
    # Set response header conditionally
    http-response set-header Cache-Control \"public, max-age=600\" if is_api_path
    
    default_backend tomcat_servers

For more complex matching patterns, you can use transaction variables:

frontend http-in
    bind *:80
    
    # Store URI in transaction variable
    http-request set-var(txn.request_uri) path
    
    # Define ACL using the variable
    acl is_api_path var(txn.request_uri) -m beg /api/
    
    # Set response header
    http-response set-header Cache-Control \"public, max-age=600\" if is_api_path
    
    default_backend tomcat_servers

When implementing this solution, keep in mind:

  • The capture method has slightly better performance than transaction variables
  • For simple prefix matches (/api/), use -m beg instead of regex
  • In HAProxy 2.0+, you can use path directly with http-response

After implementing the changes:

  1. Test syntax with: haproxy -f /etc/haproxy/haproxy.cfg -c
  2. Verify with curl: curl -I http://yourdomain.com/api/test
  3. Check for the Cache-Control header in responses