You can try to use map istead of the first if block:
map $http_origin $allow_origin {
~^http://(www\.)?example.com$ $http_origin;
}
map $http_origin $allow_methods {
~^http://(www\.)?example.com$ "OPTIONS, HEAD, GET";
}
server {
listen 80 default_server;
root /var/www;
location / {
add_header Access-Control-Allow-Origin $allow_origin;
add_header Access-Control-Allow-Methods $allow_methods;
# Handling preflight requests
if ($request_method = OPTIONS) {
add_header Content-Type text/plain;
add_header Content-Length 0;
return 204;
}
}
}
nginx will refuse to add an empty HTTP headers, so they will be added only if Origin header is present in request and matched this regex.
Important update @ 2024.10.19
It’s funny, but this answer (one of my first answers on SO), although not entirely correct, has gained the most points so far. I’ve been meaning to make corrections to it for a long time, and I’ve finally gotten around to it.
The most significant mistake I made in this answer is the following.
Generally, nginx configuration directives are declarative (for example, there is no difference where you place a directive like proxy_pass; within the same location, it can be placed anywhere). While, in general, directives from the nginx rewrite module are the only ones that can be considered imperative (see the internal implementation chapter from the module documentation), the if directive is a special case. After reading the aforementioned implementation description, I thought for a long time that using only directives from the rewrite module inside an if block would make the block entirely safe, as it would be executed during the HTTP_REWRITE phase. Unfortunately, this is not true.
One of the best explanations of how this directive actually works was provided by Yichun Zhang, the author of the famous lua-nginx-module and the OpenResty bundle. In fact, every if directive implicitly creates a nested location that tries to inherit all the declarations from the parent location.
Aside from the fact that not all directives can be inherited into such a 'virtual' location (see, for example, this nginx trac ticket), all directives that are documented as "these directives are inherited from the previous configuration level if and only if there are no identical directives defined at the current level" (e.g. add_header or proxy_set_header) will behave accordingly. Thus, in the previous example, the directives
add_header Access-Control-Allow-Origin $allow_origin;
add_header Access-Control-Allow-Methods $allow_methods;
will not be applied if the condition checked by the if directive turns out to be true. Therefore, to ensure that the Access-Control-Allow-Origin and Access-Control-Allow-Methods headers are included in the response to an OPTIONS request, the main location should be modified as follows:
location / {
add_header Access-Control-Allow-Origin $allow_origin;
add_header Access-Control-Allow-Methods $allow_methods;
# Handling preflight requests
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin $allow_origin;
add_header Access-Control-Allow-Methods $allow_methods;
add_header Content-Type text/plain;
add_header Content-Length 0;
return 204;
}
}
From the comments:
Q: Is there a way to deduplicate the regex though?
A: The only way I see is to use an additional variable with a third map block, ... but I don't sure if this would be any performance improvement.
Actually, there will be a performance improvement because each map block is evaluated only once per request (unless the volatile keyword is used):
map $http_origin $origin_passed {
~^http://(www\.)?example.com$ $http_origin;
}
map $origin_passed $allow_origin {
1 $http_origin;
}
map $origin_passed $allow_methods {
1 "OPTIONS, HEAD, GET";
}
However, for performance reasons, nowadays I would rather avoid using regex entirely and use the following map block instead:
map $http_origin $origin_passed {
http://example.com 1;
https://example.com 1;
http://www.example.com 1;
https://www.example.com 1;
}
...
And if, for example, you need to allow requests from all subdomains of example.com domain, even something like this would work:
map $http_origin $origin_passed {
hostnames;
http://example.com 1;
https://example.com 1;
*.example.com 1;
}
...
(In this case, the substrings http://example and https://example will be treated by nginx as second-level domains, but, as you can guess, it won't affect the logic of how this map block works.)
The reason this is more performant rather than using regex(es) can be found in Igor Sysoev's response regarding the performance of the map directive.
When using a long list of allowed Origin values, you may need to increase the value of the map_hash_bucket_size directive.
If you want to respond to OPTIONS requests from disallowed Origin's with the standard nginx response code 405 Not Allowed, you can add another map block:
map "$origin_passed$request_method" $options {
1OPTIONS 1;
}
and change the condition inside the if block to the following one:
# Handling preflight requests
if ($options) {
...
}
add_headers inside lastifworks in this case