22

My proxy server runs on ip A and this is how people access my web service. The nginx configuration will redirect to a virtual machine on ip B.

For the proxy server on IP A, I have this in my sites-available

server {
        listen 443;
        ssl on;
        ssl_certificate nginx.pem;
        ssl_certificate_key nginx.key;

        client_max_body_size 200M;
        server_name localhost 127.0.0.1;
        server_name_in_redirect off;

        location / {
                proxy_pass http://10.10.0.59:80;
                proxy_redirect http://10.10.0.59:80/ /;

                proxy_set_header Host $http_host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }

}

server {
        listen 80;
        rewrite     ^(.*)   https://$http_host$1 permanent;
        server_name localhost 127.0.0.1;
        server_name_in_redirect off;
        location / {
                proxy_pass http://10.10.0.59:80;
                proxy_redirect http://10.10.0.59:80/ /;
                proxy_set_header Host $http_host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        }
}

The proxy_redirect was taken from how do I get nginx to forward HTTP POST requests via rewrite?

Everything that hits the public IP will hit 443 because of the rewrite. Internally, we are forwarding to 80 on the virtual machine.

But when I run a python script such as the one below to test our configuration

import requests

data = {'username': '....', 'password': '.....'}
url = 'http://IP_A/api/service/signup'

res  = requests.post(url, data=data, verify=False)
print res
print res.json
print res.status_code
print res.headers

I am getting a 405 Method Not Allowed. In nginx we found that when it hit the internal server, the internal nginx was getting a GET request, even though in the original header we did a POST (this was shown in the Python script).

So it seems like rewrite has problem. Any idea how to fix this? When I commented out the rewrite, it hits 80 for sure, and it went through. Since rewrite was able to talk to our internal server, so rewrite itself has no issue. It's just the rewrite dropped POST to GET.

Thank you!

(This will also be asked on Nginx forum because this is a critical blocker...)

CppLearner
  • 767
  • 3
  • 10
  • 24

4 Answers4

12

It's not Nginx, it's your browser.

Note from RFC2616:

RFC 1945 and RFC 2068 specify that the client is not allowed to change the method on the redirected request. However, most existing user agent implementations treat 302 as if it were a 303 response, performing a GET on the Location [..]

This is true for all popular browsers and there is nothing you can do about it.

c2h5oh
  • 1,489
  • 10
  • 13
  • @c2h50h I understand that the HTTP spec stated something similar like that. But what can I do in Nginx? I mean this is a trivial setup where people forward 443 to an internal 80 port, but they can still do `PUT`, `POST`, `DELETE`, `GET`. In my previous setup I didn't have this extra proxy at the front serving the crowd. I had the same configuration on the same internal server (our test server). That works fine. – CppLearner Oct 02 '12 at 20:51
  • Nothing. It's 100% client side. If a webserver, any webserver, returns a 301 or 302 redirect then browser, on client side, will replace any type of request to `GET`. No server-side configuration or any http headers returned will not change that. It's like that for historical reasons (early browsers behaved that way due to misunderstanding and it became a de facto standard). – c2h5oh Oct 02 '12 at 20:54
  • Well for one this is not really a browser. Well you can say it's browser because it uses HTTP protocol..okay.. thats fine. But then again, it looks like this only happens if I were doing over this two-layer configuration. If I were to put the same configuration directly on the internal machine, and running the test there, it will not complain. But then again, how do people do it in their production? I would assume some people are doing similar thing, reverse 443 to some VM that may be running 80 only. If there is a better practice, I'd like to learn it and hear about it. – CppLearner Oct 02 '12 at 20:58
  • 2
    By browser I meant HTTP client and with all popular clients `POST` will become `GET` if it's 301 or 302 redirected. POST will remain POST on proxy redirect, but not on rewrite. – c2h5oh Oct 02 '12 at 21:22
  • @c2h50h Thanks. But any way to go around this problem? Any better way to route from proxy to internal (which is 80 by default)? I insist that this is not a common issue so my method is probably really bad so if there's a more practical setup please share :) – CppLearner Oct 02 '12 at 21:30
  • I'd say this applies to all major browsers and 99% of other http clients. See http://trac.tools.ietf.org/wg/httpbis/trac/ticket/160 and http://ftp.ics.uci.edu/pub/ietf/http/hypermail/1997q3/0611.html – c2h5oh Oct 02 '12 at 21:37
  • See also: http://stackoverflow.com/questions/5405718/jsf-redirect-to-url-as-post-not-as-get http://stackoverflow.com/questions/985596/redirect-to-using-post-in-rails http://stackoverflow.com/questions/12327089/rails-redirect-to-post http://stackoverflow.com/questions/5576619/php-redirect-with-post-data – c2h5oh Oct 02 '12 at 21:43
  • @c2h50h Thanks. Doing `rewrite ^(.*) https://$http_host$1;` and then on the next line `return 307;` has no effect. From what I understand as you pointed out in wiki, 307 will continue to serve `POST`. But it seems like the redirect will still result in 301/302 because of rewrite... – CppLearner Oct 02 '12 at 21:56
  • 1
    RFC2616 again: `If the 307 status code is received in response to a request other than GET or HEAD, the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user, since this might change the conditions under which the request was issued.` So most browsers will pop up a warning message, as for other HTTP clients I can't even guess what their behavior will be. – c2h5oh Oct 02 '12 at 22:02
  • @c2hoh My bad. I apologize for taking this so long. I understand where my stupidy lies. You are correct. When a user hits it with http it will always be a `GET` (getting resource e.g. display the webpage). The script I wrote was wrong with that http because the old configuration was special. Thanks! – CppLearner Oct 02 '12 at 22:06
  • No worries - I had the same problem just weeks ago, so I didn't even have to search for all that stuff - all I had to check was my browser history. – c2h5oh Oct 02 '12 at 22:08
4

TL;DR If you want to completely redirect to a new resource and method and body of the requests should not be changed, use 308 instead of 301 or 302.

301 is permanent redirect, but 302 is temporary so search engines don't change urls associated with that website when 302 is used. 301 and 302 indicate method and body should not be altered, but not all user agents align with that. Read this explanation from Mozilla:

The HyperText Transfer Protocol (HTTP) 302 Found redirect status response code indicates that the resource requested has been temporarily moved to the URL given by the Location header. A browser redirects to this page but search engines don't update their links to the resource (in 'SEO-speak', it is said that the 'link-juice' is not sent to the new URL). Even if the specification requires the method (and the body) not to be altered when the redirection is performed, not all user-agents conform here - you can still find this type of bugged software out there. It is therefore recommended to set the 302 code only as a response for GET or HEAD methods and to use 307 Temporary Redirect instead, as the method change is explicitly prohibited in that case. In the cases where you want the method used to be changed to GET, use 303 See Other instead. This is useful when you want to give a response to a PUT method that is not the uploaded resource but a confirmation message such as: 'you successfully uploaded XYZ'.

308 and 307 both redirect to a new resource but they guarantee body and method of request won't be altered. the difference is that 308 is permanent and 307 is temporary, so 308 will signal search engines to change urls. See this:

The only difference between 307 and 302 is that 307 guarantees that the method and the body will not be changed when the redirected request is made. With 302, some old clients were incorrectly changing the method to GET: the behavior with non-GET methods and 302 is then unpredictable on the Web, whereas the behavior with 307 is predictable. For GET requests, their behavior is identical.

Drifter104
  • 3,693
  • 2
  • 22
  • 39
Miad Abdi
  • 141
  • 1
4

I found POST /api/brand was being turned into GET /api/brand because the web application I was using (flask-restful) was making an "invalid" request. If I used POST /api/brand/ (notice the trailing /), it was successful.

Anon
  • 1,210
  • 10
  • 23
gaozhidf
  • 141
  • 3
  • 1
    I was using Postman to test django rest-auth login and was seeing the same issue described. The key was that I had neglected the trailing '/' in the POST request. – Steve L Mar 13 '19 at 20:43
-1

My problem with this was that I was requesting an HTTP request, which was getting 301 redirected to HTTPS. Using HTTPS for the requests solves this issue.