Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
nginx rails send_file madness

Goal: Have some static content, served by Nginx, but it requires an authenticated user. User auth is through Rails.

Project structure: The actual static files are in /home/vagrant/docs/ The browser will use the URL /help/* to access this content

The Ruby bits: RAILS_ENV=production rails server is used to start a WEBrick on port 3000.

config/environments/production.rb

  config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
  config.middleware.insert(0, Rack::Sendfile, config.action_dispatch.x_sendfile_header)

config/routes.rb

  get 'help/*file(.:extension)' => 'docs#show'

controllers/docs_controller.rb

class DocsController < ActionController::Base
  before_filter :validate_user

  def validate_user
    true
  end

  def show
    file_path = "/home/vagrant/docs/" + params[:file].to_s
    file_path += "." + params[:extension].to_s if params[:extension]

    send_file(file_path, disposition: 'inline', status: 200, x_sendfile: true)
  end
end

Nginx config:

server {
        listen 8888 default_server;
        listen [::]:8888 default_server ipv6only=on;

        root /home/vagrant/Projects/seabass/management/public;
        index index.html index.htm;

        # Make site accessible from http://localhost/
        server_name localhost;

        location ~ /docs/(.*) {
                alias /home/vagrant/docs/$1;
                internal;
        }

        location /help {
                proxy_redirect    off;

                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;

                proxy_set_header  X-Sendfile-Type  X-Accel-Redirect;
                proxy_set_header  X-Accel-Mapping  /docs/=/home/vagrant/docs/;

                proxy_pass http://rails;
        }

Result from all this:

  • Making a browser request to http://server:8888/docs/index.html
  • The browser receives an Nginx 404

This is as expected because the /docs/ location is marked internal, so we are successfully preventing the users from directly accessing the content.

  • Making a browser request to http://server:8888/help/index.html
  • succesfully hits Rails on port 3000 with the X-Accel headers.
  • Rails send_file() is called with the full file system path.
  • I THINK this redirects back to Nginx and Nginx serves the file from /home/vagrant/docs/index.html.
  • The browser gets the contents of the file that is physically at /home/vagrant/docs/index.html.

That is working as expected. (yay!) However:

  • Making a browser request to http://server:8888/help/not_exist.html
  • succesfully hits Rails on port 3000 with the X-Accel headers.
  • Rails send_file() is called with the full file system path.
  • The browser gets HTTP 500 with the Rails default 500 error HTML page.
  • Rails logs from logs/production.log:
I, [2016-03-02T16:18:07.923144 #6439]  INFO -- : Started GET "/help/not_exist.html" for 127.0.0.1 at 2016-03-02 16:18:07 +0000
I, [2016-03-02T16:18:07.924338 #6439]  INFO -- : Processing by DocsController#show as HTML
I, [2016-03-02T16:18:07.924542 #6439]  INFO -- :   Parameters: {"file"=>"not_exist", "extension"=>"html"}
I, [2016-03-02T16:18:07.926543 #6439]  INFO -- : Sent file /home/vagrant/docs/not_exist.html (1.5ms)
I, [2016-03-02T16:18:07.926829 #6439]  INFO -- : Completed 500 Internal Server Error in 2ms (ActiveRecord: 0.0ms)
F, [2016-03-02T16:18:07.927604 #6439] FATAL -- : 
ActionController::MissingFile (Cannot read file /home/vagrant/docs/not_exist.html):
  app/controllers/docs_controller.rb:15:in `show'

Maybe this is all OK and working as expected, but it is unclear from any docs I can find how send_file() gets back in contact with Nginx to serve the file.

What I thought would happen is that send_file() would just tell Nginx to serve the static content file, and Nginx would then find the file missing and give the standard 404 response.

However maybe something internal to send_file() checks the file existance prior to doing that? Maybe Rails is actually piping the file back to the browser and not Nginx? I don't know how to tell...

I'd like to get a 404 not a 500, but adding a file system check to the rails controller seems like a lot of overhead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.