Configure HAProxy to Accept Preflight Requests (CORS)
Recently I developed an API with Spring Boot. All the tests on the local environment went well with Postman. However, when deploying to prod through HAProxy, I got this CORS error:

There are also other errors regarding missing headers, origin…
There are some solutions available, such as adding code to my Spring Boot code base. I don’t like this method since it’s not pure.
Why add non-business code in API?
That’s why I need to find another solution.
It turned out, that the solution is simple.
If you prefer to watch the video, here it is:
Enable CORS for OPTIONS request in HAProxy
First, add a new backend only for serving CORS in HAProxy:
backend cors_backend
http-after-response set-header Access-Control-Allow-Origin "*"
http-after-response set-header Access-Control-Allow-Headers "*"
http-after-response set-header Access-Control-Max-Age "31536000"
http-request return status 200
Now, in the frontend part, add a check for OPTIONS request and direct that request to this backend:
frontend your_fe
...
acl is_options method OPTIONS
...
use_backend cors_backend if is_options
# put your actual backend below this
Of course, in your actual backend, you need to set the CORS headers:
backend your_main_backend
...
# START CORS
http-response add-header Access-Control-Allow-Origin "*"
http-response add-header Access-Control-Allow-Headers "*"
http-response add-header Access-Control-Max-Age 3600
http-response add-header Access-Control-Allow-Methods "GET, DELETE, OPTIONS, POST, PUT, PATCH"
# END CORS
server w1 server:8080
And that’s all you need. Now you can work with your API CORS-free
The Complete configuration
Here are the complete configuration files:
- docker-compose.yml
version: '3.8'
services:
haproxy_license_server:
container_name: haproxy_license_server
image: haproxy:2.4.0
restart: always
ports:
- "7088:7088"
- "8789:8788"
volumes:
- ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg
- haproxy.cfg
global
stats timeout 30s
defaults
log global
mode http
option httplog
option dontlognull
timeout connect 5000
timeout client 50000
timeout server 50000
option forwardfor
option http-server-close
frontend ukata
bind :8788
mode tcp
acl is_options method OPTIONS
use_backend cors_backend if is_options
default_backend ukata_backend
backend cors_backend
http-after-response set-header Access-Control-Allow-Origin "*"
http-after-response set-header Access-Control-Allow-Headers "*"
http-after-response set-header Access-Control-Allow-Credentials "true"
http-after-response set-header Access-Control-Allow-Methods "GET, DELETE, OPTIONS, POST, PUT, PATCH"
http-after-response set-header Access-Control-Max-Age "31536000"
http-request return status 200
backend ukata_backend
mode http
balance roundrobin
# START CORS
http-response add-header Access-Control-Allow-Origin "http://localhost:4200"
http-response add-header Access-Control-Allow-Headers "*"
http-response add-header Access-Control-Max-Age 3600
http-after-response set-header Access-Control-Allow-Credentials "true"
http-response add-header Access-Control-Allow-Methods "GET, DELETE, OPTIONS, POST, PUT, PATCH"
# END CORS
# Don't set IP address of the server, use the hostname instead
# set local-dev-1, 2 to your local ip address (192.168.x.x)
server w1 192.168.100.6:8788 check
Caution
On a production server, you may need to replace the “*” with only the hosts you allowed (from your frontend, for example). In that case, you can replace * with your client URL (starts with http/https).