Enmanuel Duran
Enmanuel Duran

Reputation: 5118

Nginx returns CORS error instead of 429 when rate limiting

I have been trying to do rate limiting in some api endpoints that I am hitting asynchronously, requests return 200 ok as expected before rate limiting, but when the limit is reached instead of returning a 429 error my api endpoints come back with CORS Error in the browser, I read here that it might be due to some headers, but I tried that and still I get the same cors error.

Sharing my humble nginx config below, in this example I would like to get a 429 when rate limit is reached for endpoint api.domain.com/custom/url

http {
    limit_req_zone $binary_remote_addr zone=mylimit:10m rate=5r/m;

    server {
        listen 80;
        server_name domain.com api.domain.com;

        location .../my-acme-challenge/ {
            root .../my-certbot;
        }

        location / {
            return 301 https://$server_name$request_uri;
        }
    }

    server {
        listen 443 ssl;
        server_name api.domain.com;

        .../mychain.pem;
        .../myprivkey.pem;
        include .../my-options-ssl-nginx.conf;
        ssl_dhparam .../my-ssl-dhparams.pem;


        Relevant Piece !!!!!!
        location /custom/url {
            add_header Access-Control-Allow-Credentials 'true' always;
            add_header Access-Control-Allow-Origin '*' always;
            limit_req zone=mylimit burst=5 nodelay;
            limit_req_log_level error;
            limit_req_status 429;
            proxy_pass http://myip:myport/custom/url;
        }

        location / {
            proxy_pass http://myip:myport;
        }
    }

    ...

}

Any idea of what might be going on? Tried to simplify my config here as much as I could.

Screenshot of my network: enter image description here

My nginx logs:

[error] 9#9: *51 limiting requests, excess: 5.697 by zone "mylimit", client: myip, server: api.example.com, request: "OPTIONS /custom/url HTTP/1.1", host: "api.example.com", referrer: "https://example.com/"

myip - - [23/Nov/2021:01:10:32 +0000] "OPTIONS /custom/url HTTP/1.1" 429 571 "https://example.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36"

Update:

Curiously enough, when I copy the curl request from the network (right click > Copy > Copy as curl) I get this html page which appears to be an 429, but instead I should be getting this error in the browser when I hit with an async request not in curl. enter image description here

Upvotes: 4

Views: 1665

Answers (2)

Enmanuel Duran
Enmanuel Duran

Reputation: 5118

Answering in case someone else goes through the same situation, the problem was actually a composition of multiple things:

  1. Preflight requests needs to be especially handled or you will get an error related to it, I followed the approach from this answer:
if ($request_method = 'OPTIONS') {
    add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;
    add_header 'Access-Control-Allow-Methods' 'GET, HEAD, OPTIONS, POST, PUT, DELETE' always;
    add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, contentType, Content-Type, Accept, Authorization, Pragma' always;
    add_header 'Access-Control-Max-Age' 1728000;
    add_header 'Content-Type' 'text/plain charset=UTF-8';
    add_header 'Content-Length' 0;
    return 204;
}
  1. I found that there was a conflict between the headers being set by my upstream server and nginx so I removed all the headers from upstream and just kept the ones from nginx, adding now my headers to all requests in the server {} level (notice the always keyword, this is pivotal for adding the headers to error responses) i.e:
    add_header 'Access-Control-Allow-Origin' 'https://example.com' always;
    add_header 'Access-Control-Allow-Credentials' 'true' always;
    add_header 'Access-Control-Allow-Methods' 'GET, HEAD, OPTIONS, POST, PUT, DELETE' always;
    add_header 'Access-Control-Allow-Headers' 'Origin, X-Requested-With, contentType, Content-Type, Accept, Authorization, Pragma' always;
  1. Since the response from nginx is not JSON, I had to add some manual validations to my requests handler, to validate for error codes before trying to parse any async request's response.

Upvotes: 2

anthumchris
anthumchris

Reputation: 9072

Adding your headers outside of the location block should ensure they are included in the rate limiting response too.

server {
  add_header Access-Control-Allow-Origin * always;

  location / {
    add_header Access-Control-Allow-Origin * always;
    ...
  }

  ...
}

Upvotes: 0

Related Questions