Score:3

Is it impossible to have conditional headers in Nginx?

cn flag

I am currently trying to only return a set of CORS headers conditionally using Nginx. At first it seemed like a simple task, as I already had this working config:

upstream api-app {
  server unix:/tmp/api-app.sock fail_timeout=0;
}

server {
  listen 80;
  # [other server stuff...]

  # CORS headers added to all ALL responses of this server (needed for all requests)
  more_set_headers 'Access-Control-Allow-Methods: GET,POST,PATCH,PUT,DELETE,OPTIONS';
  more_set_headers 'Access-Control-Allow-Origin: *';
  more_set_headers 'Access-Control-Allow-Credentials: true';
  more_set_headers 'Access-Control-Request-Method: GET,POST,PATCH,PUT,DELETE,OPTIONS';
  more_set_headers 'Access-Control-Request-Headers: Content-Type';
  more_set_headers 'Access-Control-Allow-Headers: Origin,X-Requested-With,Content-Type,Accept,Session-Id,Role-Id,Visitor-Id,X-Window-Location';
  more_set_headers 'Access-Control-Expose-Headers: X-Total-Entries,X-Total-Pages,X-Page,X-Per-Page,X-Folder-Hierarchy';

  location / {
    # For OPTIONS request only return above headers and no content (also allow caching them)
    if ($request_method = 'OPTIONS') {
      # cache above (Access-Control) headers
      add_header 'Access-Control-Max-Age' 600;

      # set content length and type just for good measure:
      add_header 'Content-Length' 0;
      add_header 'Content-Type' 'text/plain charset=UTF-8';

      # return 204 - no content
      return 204;
    }

    # Forward requests to actual app (if all above conditions did not match)
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_read_timeout 35;
    proxy_send_timeout 35;

    proxy_pass http://api-app;
  }
}

So I just wanted to change the CORS header section into s.th. like this

# ...
  set $cors '';

  if ($http_origin ~ '^https?://(some.domain|some.other.domain|other-allows.domain)') {
      set $cors 'true';
  }

  if ($cors = 'true') {
      more_set_headers 'Access-Control-Allow-Methods: GET,POST,PATCH,PUT,DELETE,OPTIONS';
      more_set_headers 'Access-Control-Allow-Origin: $http_origin';
      more_set_headers 'Access-Control-Allow-Credentials: true';
      more_set_headers 'Access-Control-Request-Method: GET,POST,PATCH,PUT,DELETE,OPTIONS';
      more_set_headers 'Access-Control-Request-Headers: Content-Type';
      more_set_headers 'Access-Control-Allow-Headers: Origin,X-Requested-With,Content-Type,Accept,Session-Id,Role-Id,Visitor-Id,X-Window-Location';
      more_set_headers 'Access-Control-Expose-Headers: X-Total-Entries,X-Total-Pages,X-Page,X-Per-Page,X-Folder-Hierarchy';
  }
  

  location / {
# ...

However nginx -t quickly told me more_set_headers can't be used in an if statement. Neither can add_header.

I figured out that in the location section it would work. But I already knew this won't be good solution as we all know if is evil in nginx location sections. And I was right as then CORS headers would be lost for the OPTIONS request, I already had an if case for (an OK one, as it returns).

I also came accross the option to use Nginxs map function. But that only works if I want to alter header values, not if I want to add an entire block of headers.

So my question is the following: Is there a way to add headers conditionally (not using map) and outside the location block? Ideally allowing for an include of the CORS section so I can use it in multiple servers (i.e. nginx sites).

cn flag
Just for completeness I found a hacky way to make this work by copying the CORS section into the location section (where if is allows) and again into the OTPIONS conditional too to return the CORS headers in that case too, but I don't want to use this in production :P
Score:3
us flag

I don't know if more_set_headers accepts empty strings. If it accepts, then you can define multiple map statements:

map $http_origin $cors_methods {
    default "";
    ~^https?://(some.domain|some.other.domain|other-allows.domain) Access-Control-Allow-Methods: GET,POST,PATCH,PUT,DELETE,OPTIONS;
}

map $http_origin $cors_origin {
    default "";
    ~^https?://(some.domain|some.other.domain|other-allows.domain) Access-Control-Allow-Origin: $http_origin;
}

And then use this:

more_set_headers $cors_methods;
more_set_headers $cors_origin;
cn flag
Yes, that's what I try to mention with the map function. However I hoped for an easier way to enable/disable all headers at once, but probbly there is none.
cn flag
Also I jus realized that the map function can't be used in the server block, only in the http block.
us flag
Yes, `map` has to be defined in the `http` level, but that doesn't affect its usefulness.
mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.