HAProxy doesn’t actually add CORS headers; it rewrites existing headers to include CORS information, and it does this by inspecting the response before it’s sent to the client.

Let’s see it in action. Imagine a simple backend service that serves up some data.

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 18
Access-Control-Allow-Origin: *

{"message": "hello"}

This backend is not CORS-enabled. If a JavaScript application running on http://frontend.com tries to fetch this data, the browser will block it with a CORS error because the Access-Control-Allow-Origin header is missing or doesn’t match the frontend’s origin.

Now, let’s put HAProxy in front of it and configure it to handle CORS.

Here’s a basic HAProxy configuration snippet for a frontend and backend:

frontend http_in
    bind *:80
    default_backend web_app

backend web_app
    server app1 192.168.1.100:8080 check

To add CORS headers, we’ll add a http-response set-header directive within the http-response section of our backend. This directive allows us to add or modify headers on outgoing responses.

frontend http_in
    bind *:80
    default_backend web_app

backend web_app
    http-response set-header Access-Control-Allow-Origin *
    http-response set-header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS
    http-response set-header Access-Control-Allow-Headers Content-Type,Authorization
    http-response set-header Access-Control-Max-Age 86400
    server app1 192.168.1.100:8080 check

With this configuration, HAProxy intercepts the response from 192.168.1.100:8080. If the response doesn’t already have Access-Control-Allow-Origin, HAProxy adds it. If it does have it, HAProxy will overwrite it with the value specified in the configuration (in this case, *). The same logic applies to the other CORS headers we’ve defined.

The Access-Control-Allow-Origin * header tells the browser that the resource can be requested from any origin. This is the simplest, but least secure, option. For more security, you’d typically want to specify the exact origin of your frontend application, like http://frontend.com. HAProxy can do this dynamically by inspecting the Origin header of the incoming request.

frontend http_in
    bind *:80
    acl is_cors_request hdr(Origin) -m found
    default_backend web_app

backend web_app
    http-response set-header Access-Control-Allow-Origin %[hdr(Origin)] if is_cors_request
    http-response set-header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS if is_cors_request
    http-response set-header Access-Control-Allow-Headers Content-Type,Authorization if is_cors_request
    http-response set-header Access-Control-Max-Age 86400 if is_cors_request
    server app1 192.168.1.100:8080 check

Here, acl is_cors_request hdr(Origin) -m found creates a condition that is true if the incoming request has an Origin header. Then, the if is_cors_request clause on the set-header directives ensures that these headers are only added if it’s a CORS-related request. The Access-Control-Allow-Origin %[hdr(Origin)] part dynamically injects the value from the incoming Origin header into the response.

This setup effectively allows your frontend application, even if it’s on a different domain or port, to successfully make requests to your backend service.

A common pitfall is forgetting to handle OPTIONS requests. Browsers send an OPTIONS request (a "preflight" request) before the actual request (like GET or POST) to check if the server allows the requested method and headers. If your HAProxy configuration doesn’t properly respond to OPTIONS requests with the correct CORS headers, the actual request will never be sent.

To handle OPTIONS requests specifically, you can add a rule to respond directly from HAProxy without even hitting the backend.

frontend http_in
    bind *:80
    acl is_cors_request hdr(Origin) -m found
    acl is_preflight method OPTIONS

    http-request allow if !is_preflight
    http-request deny if is_preflight !is_cors_request # Deny preflight from non-origin

    # Respond to OPTIONS requests directly
    http-response set-header Access-Control-Allow-Origin %[hdr(Origin)] if is_preflight is_cors_request
    http-response set-header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS if is_preflight is_cors_request
    http-response set-header Access-Control-Allow-Headers Content-Type,Authorization if is_preflight is_cors_request
    http-response set-header Access-Control-Max-Age 86400 if is_preflight is_cors_request
    http-response set-header Content-Length 0 if is_preflight # Standard for OPTIONS

    default_backend web_app

backend web_app
    # These are for non-OPTIONS requests
    http-response set-header Access-Control-Allow-Origin %[hdr(Origin)] if is_cors_request
    http-response set-header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS if is_cors_request
    http-response set-header Access-Control-Allow-Headers Content-Type,Authorization if is_cors_request
    http-response set-header Access-Control-Max-Age 86400 if is_cors_request
    server app1 192.168.1.100:8080 check

In this enhanced configuration, we first define an is_preflight ACL for OPTIONS requests. Then, we have rules to allow non-preflight requests and deny preflight requests that don’t have an Origin header. Crucially, we have a block of http-response directives that only apply if is_preflight is_cors_request. These directives set the appropriate CORS headers and a Content-Length 0 for the OPTIONS response, effectively terminating the request at HAProxy. The original http-response directives for the backend now only apply to non-OPTIONS requests.

The http-response set-header command in HAProxy works by inspecting the response after it has been generated by the backend but before it’s sent to the client. It then applies the header modification. This means HAProxy can intelligently add or overwrite headers based on the context of the request and the response.

The next challenge is often handling specific headers that might be sensitive or require more nuanced configuration, such as Access-Control-Allow-Credentials.

Want structured learning?

Take the full Haproxy course →