How Rails, Nginx and X-Accel-Redirect work together
TL;DR - To use X-Accel-Redirect to serve static files from e.g /mnt/filestorage using send_file, put this in your nginx server configuration.
proxy_set_header X-Accel-Mapping /mnt/filestorage/=/private_files/;
location /private_files/ {
internal;
alias /mnt/filestorage/;
}The need for X-Accel-Redirect (and it’s sibling X-Sendfile) comes from two distinct requirements
- The need to deliver large files.
- The need for those files to not be available to the public.
Here we are going to see how to set up X-Accel-Redirect and Rails. This is a bit complex so I’m going to run through a specific example of downloading a file. Along the way we’ll see the configuration, code and HTTP headers that are used. This assumes you already have Rails and Nginx installed and working.
1. Browser makes a request for a file
# HTTP Headers
GET /download/SecretSquirrel.zip HTTP/1.12. Nginx receives this request. It adds on a header with configuration data that will be required by rails.
#Nginx configuration
proxy_set_header X-Accel-Mapping /mnt/filestorage/=/private_files/;
# Example HTTP Headers with additional header added by nginx
GET /download/SecretSquirrel.zip HTTP/1.1
X-Accel-Mapping: /mnt/filestorage/=/private_files/3. Nginx passes the request onto Rails and it invokes the relevant controller.
4. The controller makes its authorization checks and calls send_file. Use the absolute path to the file.
# controller code (e.g. app/controllers/downloads_controller.rb)
send_file('/mnt/filestorage/SecretSquirrel.zip')5. Rails (Rack to be precise) then decides what to with the file. We need to tell rails to use X-Accel-Redirect in its configuration as shown below. Instead of using the file as the body of the request, it will add a header to the response. It uses the X-Accel-Mapping that nginx added earlier to change the file path.
# Rails configuration (e.g. config/environments/production.rb)
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
# HTTP Response
HTTP/1.1 200 OK
X-Accel-Redirect: /private_files/SecretSquirrel.zip
Content-Type: application/octet-stream
Content-length: ...
Content-Disposition: attachment; filename="SecretSquirrel.zip"
<empty body>6. Nginx receives this header from rails and interprets it. It finds the location directive and reverses the changes to the path that rails made in step 5.
#Nginx configuration
location /private_files/ {
internal;
alias /mnt/filestorage/;
}
# HTTP Response
HTTP/1.1 200 OK
Content-Type: application/octet-stream
Content-Length: ...
Content-Disposition: attachment; filename="SecretSquirrel.zip"
<contents of /mnt/filestorage/SecretSquirrel.zip>7. Browser receives the file as if it was a normal download.
Also, if you have compiled passenger into nginx, remember to use passengersetcgiparam instead of proxyset_header
Software Versions used
- Rails 3.0.7
- Passenger 3.0.5
- Nginx 0.8.54
- on Ubuntu 10.04.2 LTS
Resources
Adding headers to requests using Nginx and Passenger
TL;DR If you have compiled Passenger into Nginx, use passenger_set_cgi_param instead of proxy_set_header to set HTTP headers required by the rails process.
There are two ways of using Passenger and Nginx together. The first is when the two are compiled into one daemon that creates many processes as required. The second is using Nginx as a proxy in front of Passenger/Apache/Nginx processes running either on the same machine or on a different server altogether.
When using Nginx as a proxy, it can be configured to add or redefine http headers in the request. This is done using the proxy_set_header directive.
proxy_set_header X-Forwarded-For 127.0.0.1;If you have compiled Passenger into Nginx, the above configuration will have no effect. This is because the request is not considered to be proxied by Nginx. To have the same effect you need to use the passenger_set_cgi_param directive.
passenger_set_cgi_param HTTP_X_FORWARDED_FOR 127.0.0.1;Notice that the header name is all upper case, and has HTTP_ prefixed.
Differences between PHP Arrays and Ruby Hashes
We've looked at the diferences between Ruby and PHP arrays before. Now lets compare PHP arrays and Ruby hashes. They should be similar as all PHP arrays are implemented as hashes. There is however a cruicial difference that can trip you up if you are switching from one language to another.
Referencing an index that hasn't been set is an error in PHP, but not in Ruby.
In PHP, retreiving a value for an index that you haven't used is a warning. Although warnings can be turned off it is best practice to develop with them turned on.
<?php
$a['key1'] = 'value1';
$a['key2'] = 'value2';
print $a['key5'];
/*
lang@virtualbox1:~/$ php hash.php
PHP Notice: Undefined index: key5 in /home/lang/hash.php on line 6
*/
?>
In Ruby you can set a default value for all keys that have not set a value. By default this value is nil.
a = Hash.new
a['key1'] = 'value1'
a['key2'] = 'value2'
puts a['key5']
=begin
lang@virtualbox1:~/$ ruby hash.rb
nil
=end
Ruby hashes are unordered (up to version 1.8). PHP arrays have an internal order seperate from their index.
When you loop through a PHP array, the values will be returned in the order they were added.
<?php
$a['key1'] = 'one';
$a['key0'] = 'zero';
foreach($a as $value) {
print $value . "\n";
}
/*
lang@virtualbox1:~/$ php hash.php
one
zero
*/
?>In Ruby 1.8 hashes are unordered, they could be retreived in any order that ruby decides is best. Ruby 1.9 changes this so that it works in the same way as the PHP example above.
a = Hash.new
a['key1'] = 'one'
a['key0'] = 'zero'
a.each_value do |value|
puts value
end
=begin
lang@virtualbox1:~/$ ruby hash.rb
zero
one
=end
Is there anything that has been missed? Add a comment below.
Major differences between PHP and Ruby Arrays
PHP has a single array type, where as Ruby has Array and Hash, which covers the same functionality. In this article i'll go though the three major logical differences between PHPs array type and Rubys Array classs. We compare PHP arrays and Ruby Hashes in seperate post.
Firstly,
PHP arrays have an internal order seperate from their index.
Lets look at a simple example. In php the order you insert values into an array
determines the order that php will return them using foreach
<?php
$a[1] = 'one';
$a[0] = 'zero';
foreach($a as $value) {
print $value . "\n";
}
/*
lang@virtualbox1:~/arrays$ php array.php
one
zero
*/
?>Ruby on the other hand uses the key or index of the array to determine the order, regardless of order in which the values are inserted.
a = Array.new
a[1] = 'one'
a[0] = 'zero'
a.each do |value|
puts value
end
=begin
lang@virtualbox1:~/arrays$ ruby array.rb
zero
one
=endSecondly,
Ruby arrays always start at 0 and end at the highest index. PHP arrays only contain the keys that have been entered.
In the example below, PHP will output the two array values that are set.
<?php
$a[2] = 'two';
$a[4] = 'four';
foreach($a as $value) {
print $value . "\n";
}
/*
lang@virtualbox1:~/arrays$ php array.php
two
four
*/
?>In the ruby example, not only are the two values output, but an attempt is made to get the value from every index between 0 and the highest index set.
a = Array.new
a[2] = 'two'
a[4] = 'four'
a.each do |value|
puts value
end
=begin
lang@virtualbox1:~/arrays$ ruby array.rb
nil
nil
two
nil
four
=endThis leads to another difference…
Referencing an array index that hasn't been set is an error in PHP, but not in Ruby.
The following code produces a PHP notice. Notices are a low severity error, and are usually turned off on production servers. However it is considered good practice to develop with notices turned on.
<?php
$a[2] = 'two';
$a[4] = 'four';
print $a[5];
/*
lang@virtualbox1:~/arrays$ php array.php
PHP Notice: Undefined offset: 5 in /home/lang/arrays/array.php on line 6
*/
?>Ruby on the other hand does not consider this an error. It will return the nil value which is 'kind of' like PHPs null.
a = Array.new
a[2] = 'two'
a[4] = 'four'
puts a[5]
=begin
lang@virtualbox1:~/arrays$ ruby array3.rb
nil
=end
So what are your experiences switching between PHP and Ruby. Let me know in the comments below.
Creating a static class variable in ruby
One of the patterns I (over)use in php is having a class with only static variables and static functions. To do this in ruby isn't as straight forward as it could be, so here is how to do it.
class StaticKlass
# ruby class variables are prefixed with "@@" .
@@variable = nil
# If you want to provide getters and setters you cant use the attr_accessor
# keyword, you have to create them manually. By adding the class name to
# the method definition, we are indicating that this is a method on the
# class, not in instances of this class
def StaticKlass.variable= (x)
@@variable = x
end
# the getter is defined to return the class variable when called
def StaticKlass.variable
return @@variable
end
end