31

Is it possible/how can I configure an Nginx location block to proxy to different backends depending on the request method (ie. GET/POST)?

The reason is, I am currently handling the 2 methods at 2 different URLs (one via http proxy and the other via fcgi) and am trying to make it more "REST"ful so, would ideally like the GETting the resource to return the list, while POSTing to the same resource should add to the list.

Brenton Alker
  • 470
  • 1
  • 4
  • 7
  • https://stackoverflow.com/a/72181572/308851 could be easily adjusted to do this. – chx May 10 '22 at 07:22

6 Answers6

43

I don't use this configuration, but based on the examples here:

location /service  {
  if ($request_method = POST ) {
    fastcgi_pass 127.0.0.1:1234;
  }

  if ($request_method = GET ) {
     alias /path/to/files;
  }
}

If your writing your own application, you can also consider checking GET/POST in it, and sending X-Accel-Redirect headers to hand off transport of the files to nginx.

Cans
  • 103
  • 2
Jason
  • 1,885
  • 1
  • 13
  • 12
  • The GET block is a proxy_pass in my case, but otherwise that works. At the moment I'm not using the second if block, nginx *appears* to be stopping "processing" when the fastcgi_pass directive is reached (ie. not falling through and running the proxy pass as well) because I want anything other than POST to revert to the proxy. – Brenton Alker Jun 20 '10 at 11:51
  • 4
    Note that `if` is generally discouraged by the Nginx documentation: https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/ – vog Dec 28 '16 at 17:07
  • 1
    So, what is the alternative? – W.M. Sep 13 '17 at 09:51
  • 1
    @W.M. See my answer: https://serverfault.com/a/823053/175421 – vog Dec 14 '17 at 08:45
  • @vog, Interesting. Pretty smart way to do it. Thanks for sharing. – W.M. Dec 14 '17 at 12:53
30

Although you could achieve this with if, this is generally discouraged by the Nginx documentation, because if doesn't play well with other directives. For example, assume that GET should be open for everyone, while POST is only for authenticated users, using HTTP Basic Auth. That would require if to be combined with auth_basic, which doesn't work properly.

Here is an alternative that works without if. The trick is to use "GET" and "POST" as part of the upstream names, so these can be addressed by variable substitution:

http {
  upstream other_GET {
    server ...;
  }
  upstream other_POST {
    server ...;
  }
  server {
    location /service {
      proxy_pass http://other_$request_method;
    }
  }
}

To combine this with HTTP Basic Auth for everything but GET, just add a limit_except block:

  ...
    location /service {
      proxy_pass http://other_$request_method;
      limit_except GET {
        auth_basic ...;
      }
    }
  ...
vog
  • 558
  • 6
  • 11
  • 1
    The problem with this approach is now we will return `502 gateway error` because of `no resolver defined to resolve other_HEAD` (or whatever your missing upstream is). It will be more semantic to return something like `405 method not allowed`. Is there a way to accomplish this? – James Nov 28 '19 at 19:20
  • 1
    @James: This sould perhaps be phrased as a new question, referring to this one. I don't have an answer for this detail, but maybe others to. – vog Dec 02 '19 at 15:30
  • @James: Thinking more of it, repeating `upstream other_GET {...}` as `upstream other_HEAD {...}` should do the trick, even though this means some duplication in your nginx configuration. – vog Jan 16 '20 at 15:52
7

I couldn't get the answer from @timmmmmy to work, but it pointed me to the map documentation and this worked for me:

map $request_method $upstream_location {
   PUT     example.com:8081;
   POST    example.com:8081;
   PATCH   example.com:8081;
   default example.com:8082;
}
server {
   location / {
      proxy_pass https://$upstream_location;
   }
}
rik harris
  • 71
  • 1
  • 2
0

@timmmmmy 's answer with the fixed syntax errors (without edit because other person refers to the original answer)

        upstream webdav_default {
                server ...;
        }
        upstream webdav_upload {
                server ...;
        }
        upstream webdav_download {
                server ...;
        }
        map $request_method $upstream_location {
            GET     webdav_download;
            HEAD    webdav_download;
            PUT     webdav_upload;
            LOCK    webdav_upload;
            default webdav_default;
        }
        server {
            location / {
                proxy_pass https://$upstream_location;
            }
        }
Nick Vee
  • 101
  • 3
0

This is what i did to make things working for me

add_header Allow "GET, POST, HEAD" always;
if ( $request_method !~ ^(GET|POST|HEAD)$ ) {
    proxy_pass http://back-end;
}
Mansur Ul Hasan
  • 262
  • 3
  • 9
0

Slight change to vog's answer to include a default handler for other methods like OPTIONS, PUT, etc.

    upstream webdav_default {
            server example.com;
    }
    upstream webdav_upload {
            server example.com:8081;
    }
    upstream webdav_download {
            server example.com:8082;
    }
    server {
            map upstream_location $request_method {
                    GET     webdav_download;
                    HEAD    webdav_download;
                    PUT     webdav_upload;
                    LOCK    webdav_upload;
                    default webdav_default;
            }
            location / {
                    proxy_pass https://$upstream_location;
            }
    }