Why am I getting infinite redirect loop with force_ssl in my Rails app?
Solution 1
You're not forwarding any information about whether this request was an HTTPS-terminated request or not. Normally, in a server, the "ssl on;" directive will set these headers, but you're using a combined block.
Rack (and force_ssl) determines SSL by:
- If the request came in on port 443 (this is likely not being passed back to Unicorn from nginx)
- If ENV['HTTPS'] == "on"
- If the X-Forwarded-Proto header == "HTTPS"
See the force_ssl source for the full story.
Since you're using a combined block, you want to use the third form. Try:
proxy_set_header X-Forwarded-Proto $scheme;
in your server or location block per the nginx documentation.
This will set the header to "http" when you come in on a port 80 request, and set it to "https" when you come in on a 443 request.
Solution 2
Try setting this directive in your nginx location @unicorn
block:
proxy_set_header X-Forwarded-Proto https;
I had this same issue and investigating the Rack middleware handler (not force_ssl
but similar) I could see that it was expecting that header to be set to determine if the request was already processed as being SSL by nginx.
Jakub Arnold
Experienced software engineer with a background in machine learning and computer science and over 7 years of commercial practice of software development, looking to work on production quality software.
Updated on June 02, 2022Comments
-
Jakub Arnold almost 2 years
I want to have my API controller use SSL, so I added another listen directive to my nginx.conf
upstream unicorn { server unix:/tmp/unicorn.foo.sock fail_timeout=0; } server { listen 80 default deferred; listen 443 ssl default; ssl_certificate /etc/ssl/certs/foo.crt; ssl_certificate_key /etc/ssl/private/foo.key; server_name foo; root /var/apps/foo/current/public; try_files $uri/system/maintenance.html $uri/index.html $uri @unicorn; location @unicorn { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://unicorn; } error_page 502 503 /maintenance.html; error_page 500 504 /500.html; keepalive_timeout 5; }
which passes the nginx conftest without any problems. I also added a
force_ssl
directive to my ApiControllerclass ApiController < ApplicationController force_ssl if Rails.env.production? def auth user = User.authenticate(params[:username], params[:password]) respond_to do |format| format.json do if user user.generate_api_key! unless user.api_key.present? render json: { key: user.api_key } else render json: { error: 401 }, status: 401 end end end end def check user = User.find_by_api_key(params[:api_key]) respond_to do |format| format.json do if user render json: { status: 'ok' } else render json: { status: 'failure' }, status: 401 end end end end end
which worked just fine when I wasn't using SSL, but now when I try to
curl --LI http://foo/api/auth.json
, I get properly redirected tohttps
, but then I keep on getting redirected tohttp://foo/api/auth
ending in an infinite redirect loop.My routes simply have
get "api/auth" get "api/check"
I'm using Rails 3.2.1 on Ruby 1.9.2 with nginx 0.7.65
-
Chris Heald about 12 yearsThis will mark all requests as SSL requests, even port 80 requests, since it's a combined server block.
-
A Fader Darkly about 8 yearsPure magic. :) In my case I was moving a Rails site over to using Varnish. The site was pure HTTPS, and as Varnish doesn't support SSL and Passenger exposes a socket rather than a port, two Nginx server configs were required, one either side of Varnish. Distancing the Passenger socket connection from the HTTPS config on port 443 caused a redirect loop. And this fixed it. Thank you!
-
Cheyne over 4 yearsSpent hours looking for this answer. Thank you!